From 9c6dba99d46dc00be0f6672cfc6f6faa95285552 Mon Sep 17 00:00:00 2001 From: snipe <72265661+notsniped@users.noreply.github.com> Date: Sun, 7 Apr 2024 19:08:48 +0530 Subject: [PATCH] Add direct OpenAI library source to isobot directory --- openai/__init__.py | 339 +++ openai/__main__.py | 3 + openai/_base_client.py | 1972 +++++++++++++++ openai/_client.py | 503 ++++ openai/_compat.py | 222 ++ openai/_constants.py | 14 + openai/_exceptions.py | 125 + openai/_extras/__init__.py | 2 + openai/_extras/_common.py | 21 + openai/_extras/numpy_proxy.py | 37 + openai/_extras/pandas_proxy.py | 28 + openai/_files.py | 127 + openai/_legacy_response.py | 456 ++++ openai/_models.py | 654 +++++ openai/_module_client.py | 78 + openai/_qs.py | 150 ++ openai/_resource.py | 43 + openai/_response.py | 824 ++++++ openai/_streaming.py | 410 +++ openai/_types.py | 222 ++ openai/_utils/__init__.py | 50 + openai/_utils/_logs.py | 25 + openai/_utils/_proxy.py | 63 + openai/_utils/_streams.py | 12 + openai/_utils/_sync.py | 64 + openai/_utils/_transform.py | 382 +++ openai/_utils/_typing.py | 120 + openai/_utils/_utils.py | 391 +++ openai/_version.py | 4 + openai/cli/__init__.py | 1 + openai/cli/_api/__init__.py | 1 + openai/cli/_api/_main.py | 16 + openai/cli/_api/audio.py | 94 + openai/cli/_api/chat/__init__.py | 13 + openai/cli/_api/chat/completions.py | 156 ++ openai/cli/_api/completions.py | 173 ++ openai/cli/_api/files.py | 80 + openai/cli/_api/image.py | 139 ++ openai/cli/_api/models.py | 45 + openai/cli/_cli.py | 234 ++ openai/cli/_errors.py | 23 + openai/cli/_models.py | 17 + openai/cli/_progress.py | 59 + openai/cli/_tools/__init__.py | 1 + openai/cli/_tools/_main.py | 17 + openai/cli/_tools/fine_tunes.py | 63 + openai/cli/_tools/migrate.py | 181 ++ openai/cli/_utils.py | 45 + openai/lib/.keep | 4 + openai/lib/_old_api.py | 72 + openai/lib/_validators.py | 805 ++++++ openai/lib/azure.py | 529 ++++ openai/lib/streaming/__init__.py | 8 + openai/lib/streaming/_assistants.py | 1035 ++++++++ openai/pagination.py | 107 + openai/py.typed | 0 openai/resources/__init__.py | 145 ++ openai/resources/audio/__init__.py | 61 + openai/resources/audio/audio.py | 144 ++ openai/resources/audio/speech.py | 213 ++ openai/resources/audio/transcriptions.py | 256 ++ openai/resources/audio/translations.py | 226 ++ openai/resources/beta/__init__.py | 47 + openai/resources/beta/assistants/__init__.py | 33 + .../resources/beta/assistants/assistants.py | 747 ++++++ openai/resources/beta/assistants/files.py | 483 ++++ openai/resources/beta/beta.py | 114 + openai/resources/beta/threads/__init__.py | 47 + .../beta/threads/messages/__init__.py | 33 + .../resources/beta/threads/messages/files.py | 312 +++ .../beta/threads/messages/messages.py | 588 +++++ .../resources/beta/threads/runs/__init__.py | 33 + openai/resources/beta/threads/runs/runs.py | 2223 +++++++++++++++++ openai/resources/beta/threads/runs/steps.py | 310 +++ openai/resources/beta/threads/threads.py | 1259 ++++++++++ openai/resources/chat/__init__.py | 33 + openai/resources/chat/chat.py | 80 + openai/resources/chat/completions.py | 1403 +++++++++++ openai/resources/completions.py | 1102 ++++++++ openai/resources/embeddings.py | 261 ++ openai/resources/files.py | 689 +++++ openai/resources/fine_tuning/__init__.py | 33 + openai/resources/fine_tuning/fine_tuning.py | 80 + openai/resources/fine_tuning/jobs.py | 638 +++++ openai/resources/images.py | 587 +++++ openai/resources/models.py | 283 +++ openai/resources/moderations.py | 180 ++ openai/types/__init__.py | 31 + openai/types/audio/__init__.py | 9 + openai/types/audio/speech_create_params.py | 39 + openai/types/audio/transcription.py | 10 + .../audio/transcription_create_params.py | 65 + openai/types/audio/translation.py | 9 + .../types/audio/translation_create_params.py | 48 + openai/types/beta/__init__.py | 23 + openai/types/beta/assistant.py | 64 + openai/types/beta/assistant_create_params.py | 56 + openai/types/beta/assistant_deleted.py | 15 + openai/types/beta/assistant_list_params.py | 39 + openai/types/beta/assistant_stream_event.py | 276 ++ openai/types/beta/assistant_tool.py | 13 + openai/types/beta/assistant_tool_param.py | 13 + openai/types/beta/assistant_update_params.py | 58 + openai/types/beta/assistants/__init__.py | 8 + .../types/beta/assistants/assistant_file.py | 21 + .../beta/assistants/file_create_params.py | 16 + .../beta/assistants/file_delete_response.py | 15 + .../types/beta/assistants/file_list_params.py | 39 + openai/types/beta/chat/__init__.py | 3 + openai/types/beta/code_interpreter_tool.py | 12 + .../types/beta/code_interpreter_tool_param.py | 12 + openai/types/beta/function_tool.py | 15 + openai/types/beta/function_tool_param.py | 16 + openai/types/beta/retrieval_tool.py | 12 + openai/types/beta/retrieval_tool_param.py | 12 + openai/types/beta/thread.py | 27 + .../beta/thread_create_and_run_params.py | 136 + openai/types/beta/thread_create_params.py | 54 + openai/types/beta/thread_deleted.py | 15 + openai/types/beta/thread_update_params.py | 18 + openai/types/beta/threads/__init__.py | 33 + openai/types/beta/threads/annotation.py | 12 + openai/types/beta/threads/annotation_delta.py | 14 + .../beta/threads/file_citation_annotation.py | 29 + .../threads/file_citation_delta_annotation.py | 33 + .../beta/threads/file_path_annotation.py | 26 + .../threads/file_path_delta_annotation.py | 30 + openai/types/beta/threads/image_file.py | 13 + .../beta/threads/image_file_content_block.py | 15 + openai/types/beta/threads/image_file_delta.py | 15 + .../beta/threads/image_file_delta_block.py | 19 + openai/types/beta/threads/message.py | 81 + openai/types/beta/threads/message_content.py | 12 + .../beta/threads/message_content_delta.py | 12 + .../beta/threads/message_create_params.py | 38 + openai/types/beta/threads/message_delta.py | 24 + .../types/beta/threads/message_delta_event.py | 19 + .../types/beta/threads/message_list_params.py | 42 + .../beta/threads/message_update_params.py | 20 + .../types/beta/threads/messages/__init__.py | 6 + .../beta/threads/messages/file_list_params.py | 41 + .../beta/threads/messages/message_file.py | 25 + .../required_action_function_tool_call.py | 34 + openai/types/beta/threads/run.py | 144 ++ .../types/beta/threads/run_create_params.py | 83 + openai/types/beta/threads/run_list_params.py | 39 + openai/types/beta/threads/run_status.py | 9 + .../threads/run_submit_tool_outputs_params.py | 52 + .../types/beta/threads/run_update_params.py | 20 + openai/types/beta/threads/runs/__init__.py | 22 + .../threads/runs/code_interpreter_logs.py | 19 + .../runs/code_interpreter_output_image.py | 26 + .../runs/code_interpreter_tool_call.py | 70 + .../runs/code_interpreter_tool_call_delta.py | 44 + .../beta/threads/runs/function_tool_call.py | 38 + .../threads/runs/function_tool_call_delta.py | 41 + .../runs/message_creation_step_details.py | 19 + .../beta/threads/runs/retrieval_tool_call.py | 21 + .../threads/runs/retrieval_tool_call_delta.py | 25 + openai/types/beta/threads/runs/run_step.py | 110 + .../types/beta/threads/runs/run_step_delta.py | 18 + .../beta/threads/runs/run_step_delta_event.py | 19 + .../runs/run_step_delta_message_delta.py | 20 + .../beta/threads/runs/step_list_params.py | 41 + openai/types/beta/threads/runs/tool_call.py | 15 + .../beta/threads/runs/tool_call_delta.py | 16 + .../threads/runs/tool_call_delta_object.py | 21 + .../threads/runs/tool_calls_step_details.py | 21 + openai/types/beta/threads/text.py | 15 + .../types/beta/threads/text_content_block.py | 15 + openai/types/beta/threads/text_delta.py | 15 + openai/types/beta/threads/text_delta_block.py | 19 + openai/types/chat/__init__.py | 41 + openai/types/chat/chat_completion.py | 67 + ...chat_completion_assistant_message_param.py | 51 + openai/types/chat/chat_completion_chunk.py | 128 + ...hat_completion_content_part_image_param.py | 26 + .../chat_completion_content_part_param.py | 12 + ...chat_completion_content_part_text_param.py | 15 + ...t_completion_function_call_option_param.py | 12 + .../chat_completion_function_message_param.py | 19 + openai/types/chat/chat_completion_message.py | 40 + .../chat/chat_completion_message_param.py | 21 + .../chat/chat_completion_message_tool_call.py | 31 + ...chat_completion_message_tool_call_param.py | 31 + ...chat_completion_named_tool_choice_param.py | 19 + openai/types/chat/chat_completion_role.py | 7 + .../chat_completion_system_message_param.py | 22 + .../chat/chat_completion_token_logprob.py | 57 + ...hat_completion_tool_choice_option_param.py | 12 + .../chat_completion_tool_message_param.py | 18 + .../types/chat/chat_completion_tool_param.py | 16 + .../chat_completion_user_message_param.py | 25 + openai/types/chat/completion_create_params.py | 280 +++ openai/types/completion.py | 37 + openai/types/completion_choice.py | 35 + openai/types/completion_create_params.py | 182 ++ openai/types/completion_usage.py | 16 + openai/types/create_embedding_response.py | 31 + openai/types/embedding.py | 23 + openai/types/embedding_create_params.py | 50 + openai/types/file_content.py | 6 + openai/types/file_create_params.py | 25 + openai/types/file_deleted.py | 15 + openai/types/file_list_params.py | 12 + openai/types/file_object.py | 46 + openai/types/fine_tuning/__init__.py | 9 + openai/types/fine_tuning/fine_tuning_job.py | 107 + .../fine_tuning/fine_tuning_job_event.py | 19 + openai/types/fine_tuning/job_create_params.py | 78 + .../fine_tuning/job_list_events_params.py | 15 + openai/types/fine_tuning/job_list_params.py | 15 + openai/types/image.py | 24 + openai/types/image_create_variation_params.py | 50 + openai/types/image_edit_params.py | 61 + openai/types/image_generate_params.py | 63 + openai/types/images_response.py | 14 + openai/types/model.py | 21 + openai/types/model_deleted.py | 13 + openai/types/moderation.py | 117 + openai/types/moderation_create_params.py | 25 + openai/types/moderation_create_response.py | 19 + openai/types/shared/__init__.py | 5 + openai/types/shared/error_object.py | 17 + openai/types/shared/function_definition.py | 35 + openai/types/shared/function_parameters.py | 7 + openai/types/shared_params/__init__.py | 4 + .../shared_params/function_definition.py | 36 + .../shared_params/function_parameters.py | 9 + openai/version.py | 3 + 230 files changed, 28905 insertions(+) create mode 100644 openai/__init__.py create mode 100644 openai/__main__.py create mode 100644 openai/_base_client.py create mode 100644 openai/_client.py create mode 100644 openai/_compat.py create mode 100644 openai/_constants.py create mode 100644 openai/_exceptions.py create mode 100644 openai/_extras/__init__.py create mode 100644 openai/_extras/_common.py create mode 100644 openai/_extras/numpy_proxy.py create mode 100644 openai/_extras/pandas_proxy.py create mode 100644 openai/_files.py create mode 100644 openai/_legacy_response.py create mode 100644 openai/_models.py create mode 100644 openai/_module_client.py create mode 100644 openai/_qs.py create mode 100644 openai/_resource.py create mode 100644 openai/_response.py create mode 100644 openai/_streaming.py create mode 100644 openai/_types.py create mode 100644 openai/_utils/__init__.py create mode 100644 openai/_utils/_logs.py create mode 100644 openai/_utils/_proxy.py create mode 100644 openai/_utils/_streams.py create mode 100644 openai/_utils/_sync.py create mode 100644 openai/_utils/_transform.py create mode 100644 openai/_utils/_typing.py create mode 100644 openai/_utils/_utils.py create mode 100644 openai/_version.py create mode 100644 openai/cli/__init__.py create mode 100644 openai/cli/_api/__init__.py create mode 100644 openai/cli/_api/_main.py create mode 100644 openai/cli/_api/audio.py create mode 100644 openai/cli/_api/chat/__init__.py create mode 100644 openai/cli/_api/chat/completions.py create mode 100644 openai/cli/_api/completions.py create mode 100644 openai/cli/_api/files.py create mode 100644 openai/cli/_api/image.py create mode 100644 openai/cli/_api/models.py create mode 100644 openai/cli/_cli.py create mode 100644 openai/cli/_errors.py create mode 100644 openai/cli/_models.py create mode 100644 openai/cli/_progress.py create mode 100644 openai/cli/_tools/__init__.py create mode 100644 openai/cli/_tools/_main.py create mode 100644 openai/cli/_tools/fine_tunes.py create mode 100644 openai/cli/_tools/migrate.py create mode 100644 openai/cli/_utils.py create mode 100644 openai/lib/.keep create mode 100644 openai/lib/_old_api.py create mode 100644 openai/lib/_validators.py create mode 100644 openai/lib/azure.py create mode 100644 openai/lib/streaming/__init__.py create mode 100644 openai/lib/streaming/_assistants.py create mode 100644 openai/pagination.py create mode 100644 openai/py.typed create mode 100644 openai/resources/__init__.py create mode 100644 openai/resources/audio/__init__.py create mode 100644 openai/resources/audio/audio.py create mode 100644 openai/resources/audio/speech.py create mode 100644 openai/resources/audio/transcriptions.py create mode 100644 openai/resources/audio/translations.py create mode 100644 openai/resources/beta/__init__.py create mode 100644 openai/resources/beta/assistants/__init__.py create mode 100644 openai/resources/beta/assistants/assistants.py create mode 100644 openai/resources/beta/assistants/files.py create mode 100644 openai/resources/beta/beta.py create mode 100644 openai/resources/beta/threads/__init__.py create mode 100644 openai/resources/beta/threads/messages/__init__.py create mode 100644 openai/resources/beta/threads/messages/files.py create mode 100644 openai/resources/beta/threads/messages/messages.py create mode 100644 openai/resources/beta/threads/runs/__init__.py create mode 100644 openai/resources/beta/threads/runs/runs.py create mode 100644 openai/resources/beta/threads/runs/steps.py create mode 100644 openai/resources/beta/threads/threads.py create mode 100644 openai/resources/chat/__init__.py create mode 100644 openai/resources/chat/chat.py create mode 100644 openai/resources/chat/completions.py create mode 100644 openai/resources/completions.py create mode 100644 openai/resources/embeddings.py create mode 100644 openai/resources/files.py create mode 100644 openai/resources/fine_tuning/__init__.py create mode 100644 openai/resources/fine_tuning/fine_tuning.py create mode 100644 openai/resources/fine_tuning/jobs.py create mode 100644 openai/resources/images.py create mode 100644 openai/resources/models.py create mode 100644 openai/resources/moderations.py create mode 100644 openai/types/__init__.py create mode 100644 openai/types/audio/__init__.py create mode 100644 openai/types/audio/speech_create_params.py create mode 100644 openai/types/audio/transcription.py create mode 100644 openai/types/audio/transcription_create_params.py create mode 100644 openai/types/audio/translation.py create mode 100644 openai/types/audio/translation_create_params.py create mode 100644 openai/types/beta/__init__.py create mode 100644 openai/types/beta/assistant.py create mode 100644 openai/types/beta/assistant_create_params.py create mode 100644 openai/types/beta/assistant_deleted.py create mode 100644 openai/types/beta/assistant_list_params.py create mode 100644 openai/types/beta/assistant_stream_event.py create mode 100644 openai/types/beta/assistant_tool.py create mode 100644 openai/types/beta/assistant_tool_param.py create mode 100644 openai/types/beta/assistant_update_params.py create mode 100644 openai/types/beta/assistants/__init__.py create mode 100644 openai/types/beta/assistants/assistant_file.py create mode 100644 openai/types/beta/assistants/file_create_params.py create mode 100644 openai/types/beta/assistants/file_delete_response.py create mode 100644 openai/types/beta/assistants/file_list_params.py create mode 100644 openai/types/beta/chat/__init__.py create mode 100644 openai/types/beta/code_interpreter_tool.py create mode 100644 openai/types/beta/code_interpreter_tool_param.py create mode 100644 openai/types/beta/function_tool.py create mode 100644 openai/types/beta/function_tool_param.py create mode 100644 openai/types/beta/retrieval_tool.py create mode 100644 openai/types/beta/retrieval_tool_param.py create mode 100644 openai/types/beta/thread.py create mode 100644 openai/types/beta/thread_create_and_run_params.py create mode 100644 openai/types/beta/thread_create_params.py create mode 100644 openai/types/beta/thread_deleted.py create mode 100644 openai/types/beta/thread_update_params.py create mode 100644 openai/types/beta/threads/__init__.py create mode 100644 openai/types/beta/threads/annotation.py create mode 100644 openai/types/beta/threads/annotation_delta.py create mode 100644 openai/types/beta/threads/file_citation_annotation.py create mode 100644 openai/types/beta/threads/file_citation_delta_annotation.py create mode 100644 openai/types/beta/threads/file_path_annotation.py create mode 100644 openai/types/beta/threads/file_path_delta_annotation.py create mode 100644 openai/types/beta/threads/image_file.py create mode 100644 openai/types/beta/threads/image_file_content_block.py create mode 100644 openai/types/beta/threads/image_file_delta.py create mode 100644 openai/types/beta/threads/image_file_delta_block.py create mode 100644 openai/types/beta/threads/message.py create mode 100644 openai/types/beta/threads/message_content.py create mode 100644 openai/types/beta/threads/message_content_delta.py create mode 100644 openai/types/beta/threads/message_create_params.py create mode 100644 openai/types/beta/threads/message_delta.py create mode 100644 openai/types/beta/threads/message_delta_event.py create mode 100644 openai/types/beta/threads/message_list_params.py create mode 100644 openai/types/beta/threads/message_update_params.py create mode 100644 openai/types/beta/threads/messages/__init__.py create mode 100644 openai/types/beta/threads/messages/file_list_params.py create mode 100644 openai/types/beta/threads/messages/message_file.py create mode 100644 openai/types/beta/threads/required_action_function_tool_call.py create mode 100644 openai/types/beta/threads/run.py create mode 100644 openai/types/beta/threads/run_create_params.py create mode 100644 openai/types/beta/threads/run_list_params.py create mode 100644 openai/types/beta/threads/run_status.py create mode 100644 openai/types/beta/threads/run_submit_tool_outputs_params.py create mode 100644 openai/types/beta/threads/run_update_params.py create mode 100644 openai/types/beta/threads/runs/__init__.py create mode 100644 openai/types/beta/threads/runs/code_interpreter_logs.py create mode 100644 openai/types/beta/threads/runs/code_interpreter_output_image.py create mode 100644 openai/types/beta/threads/runs/code_interpreter_tool_call.py create mode 100644 openai/types/beta/threads/runs/code_interpreter_tool_call_delta.py create mode 100644 openai/types/beta/threads/runs/function_tool_call.py create mode 100644 openai/types/beta/threads/runs/function_tool_call_delta.py create mode 100644 openai/types/beta/threads/runs/message_creation_step_details.py create mode 100644 openai/types/beta/threads/runs/retrieval_tool_call.py create mode 100644 openai/types/beta/threads/runs/retrieval_tool_call_delta.py create mode 100644 openai/types/beta/threads/runs/run_step.py create mode 100644 openai/types/beta/threads/runs/run_step_delta.py create mode 100644 openai/types/beta/threads/runs/run_step_delta_event.py create mode 100644 openai/types/beta/threads/runs/run_step_delta_message_delta.py create mode 100644 openai/types/beta/threads/runs/step_list_params.py create mode 100644 openai/types/beta/threads/runs/tool_call.py create mode 100644 openai/types/beta/threads/runs/tool_call_delta.py create mode 100644 openai/types/beta/threads/runs/tool_call_delta_object.py create mode 100644 openai/types/beta/threads/runs/tool_calls_step_details.py create mode 100644 openai/types/beta/threads/text.py create mode 100644 openai/types/beta/threads/text_content_block.py create mode 100644 openai/types/beta/threads/text_delta.py create mode 100644 openai/types/beta/threads/text_delta_block.py create mode 100644 openai/types/chat/__init__.py create mode 100644 openai/types/chat/chat_completion.py create mode 100644 openai/types/chat/chat_completion_assistant_message_param.py create mode 100644 openai/types/chat/chat_completion_chunk.py create mode 100644 openai/types/chat/chat_completion_content_part_image_param.py create mode 100644 openai/types/chat/chat_completion_content_part_param.py create mode 100644 openai/types/chat/chat_completion_content_part_text_param.py create mode 100644 openai/types/chat/chat_completion_function_call_option_param.py create mode 100644 openai/types/chat/chat_completion_function_message_param.py create mode 100644 openai/types/chat/chat_completion_message.py create mode 100644 openai/types/chat/chat_completion_message_param.py create mode 100644 openai/types/chat/chat_completion_message_tool_call.py create mode 100644 openai/types/chat/chat_completion_message_tool_call_param.py create mode 100644 openai/types/chat/chat_completion_named_tool_choice_param.py create mode 100644 openai/types/chat/chat_completion_role.py create mode 100644 openai/types/chat/chat_completion_system_message_param.py create mode 100644 openai/types/chat/chat_completion_token_logprob.py create mode 100644 openai/types/chat/chat_completion_tool_choice_option_param.py create mode 100644 openai/types/chat/chat_completion_tool_message_param.py create mode 100644 openai/types/chat/chat_completion_tool_param.py create mode 100644 openai/types/chat/chat_completion_user_message_param.py create mode 100644 openai/types/chat/completion_create_params.py create mode 100644 openai/types/completion.py create mode 100644 openai/types/completion_choice.py create mode 100644 openai/types/completion_create_params.py create mode 100644 openai/types/completion_usage.py create mode 100644 openai/types/create_embedding_response.py create mode 100644 openai/types/embedding.py create mode 100644 openai/types/embedding_create_params.py create mode 100644 openai/types/file_content.py create mode 100644 openai/types/file_create_params.py create mode 100644 openai/types/file_deleted.py create mode 100644 openai/types/file_list_params.py create mode 100644 openai/types/file_object.py create mode 100644 openai/types/fine_tuning/__init__.py create mode 100644 openai/types/fine_tuning/fine_tuning_job.py create mode 100644 openai/types/fine_tuning/fine_tuning_job_event.py create mode 100644 openai/types/fine_tuning/job_create_params.py create mode 100644 openai/types/fine_tuning/job_list_events_params.py create mode 100644 openai/types/fine_tuning/job_list_params.py create mode 100644 openai/types/image.py create mode 100644 openai/types/image_create_variation_params.py create mode 100644 openai/types/image_edit_params.py create mode 100644 openai/types/image_generate_params.py create mode 100644 openai/types/images_response.py create mode 100644 openai/types/model.py create mode 100644 openai/types/model_deleted.py create mode 100644 openai/types/moderation.py create mode 100644 openai/types/moderation_create_params.py create mode 100644 openai/types/moderation_create_response.py create mode 100644 openai/types/shared/__init__.py create mode 100644 openai/types/shared/error_object.py create mode 100644 openai/types/shared/function_definition.py create mode 100644 openai/types/shared/function_parameters.py create mode 100644 openai/types/shared_params/__init__.py create mode 100644 openai/types/shared_params/function_definition.py create mode 100644 openai/types/shared_params/function_parameters.py create mode 100644 openai/version.py diff --git a/openai/__init__.py b/openai/__init__.py new file mode 100644 index 00000000..cd05a749 --- /dev/null +++ b/openai/__init__.py @@ -0,0 +1,339 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os as _os +from typing_extensions import override + +from . import types +from ._types import NOT_GIVEN, NoneType, NotGiven, Transport, ProxiesTypes +from ._utils import file_from_path +from ._client import Client, OpenAI, Stream, Timeout, Transport, AsyncClient, AsyncOpenAI, AsyncStream, RequestOptions +from ._models import BaseModel +from ._version import __title__, __version__ +from ._response import APIResponse as APIResponse, AsyncAPIResponse as AsyncAPIResponse +from ._constants import DEFAULT_TIMEOUT, DEFAULT_MAX_RETRIES, DEFAULT_CONNECTION_LIMITS +from ._exceptions import ( + APIError, + OpenAIError, + ConflictError, + NotFoundError, + APIStatusError, + RateLimitError, + APITimeoutError, + BadRequestError, + APIConnectionError, + AuthenticationError, + InternalServerError, + PermissionDeniedError, + UnprocessableEntityError, + APIResponseValidationError, +) +from ._utils._logs import setup_logging as _setup_logging + +__all__ = [ + "types", + "__version__", + "__title__", + "NoneType", + "Transport", + "ProxiesTypes", + "NotGiven", + "NOT_GIVEN", + "OpenAIError", + "APIError", + "APIStatusError", + "APITimeoutError", + "APIConnectionError", + "APIResponseValidationError", + "BadRequestError", + "AuthenticationError", + "PermissionDeniedError", + "NotFoundError", + "ConflictError", + "UnprocessableEntityError", + "RateLimitError", + "InternalServerError", + "Timeout", + "RequestOptions", + "Client", + "AsyncClient", + "Stream", + "AsyncStream", + "OpenAI", + "AsyncOpenAI", + "file_from_path", + "BaseModel", + "DEFAULT_TIMEOUT", + "DEFAULT_MAX_RETRIES", + "DEFAULT_CONNECTION_LIMITS", +] + +from .lib import azure as _azure +from .version import VERSION as VERSION +from .lib.azure import AzureOpenAI as AzureOpenAI, AsyncAzureOpenAI as AsyncAzureOpenAI +from .lib._old_api import * +from .lib.streaming import ( + AssistantEventHandler as AssistantEventHandler, + AsyncAssistantEventHandler as AsyncAssistantEventHandler, +) + +_setup_logging() + +# Update the __module__ attribute for exported symbols so that +# error messages point to this module instead of the module +# it was originally defined in, e.g. +# openai._exceptions.NotFoundError -> openai.NotFoundError +__locals = locals() +for __name in __all__: + if not __name.startswith("__"): + try: + __locals[__name].__module__ = "openai" + except (TypeError, AttributeError): + # Some of our exported symbols are builtins which we can't set attributes for. + pass + +# ------ Module level client ------ +import typing as _t +import typing_extensions as _te + +import httpx as _httpx + +from ._base_client import DEFAULT_TIMEOUT, DEFAULT_MAX_RETRIES + +api_key: str | None = None + +organization: str | None = None + +base_url: str | _httpx.URL | None = None + +timeout: float | Timeout | None = DEFAULT_TIMEOUT + +max_retries: int = DEFAULT_MAX_RETRIES + +default_headers: _t.Mapping[str, str] | None = None + +default_query: _t.Mapping[str, object] | None = None + +http_client: _httpx.Client | None = None + +_ApiType = _te.Literal["openai", "azure"] + +api_type: _ApiType | None = _t.cast(_ApiType, _os.environ.get("OPENAI_API_TYPE")) + +api_version: str | None = _os.environ.get("OPENAI_API_VERSION") + +azure_endpoint: str | None = _os.environ.get("AZURE_OPENAI_ENDPOINT") + +azure_ad_token: str | None = _os.environ.get("AZURE_OPENAI_AD_TOKEN") + +azure_ad_token_provider: _azure.AzureADTokenProvider | None = None + + +class _ModuleClient(OpenAI): + # Note: we have to use type: ignores here as overriding class members + # with properties is technically unsafe but it is fine for our use case + + @property # type: ignore + @override + def api_key(self) -> str | None: + return api_key + + @api_key.setter # type: ignore + def api_key(self, value: str | None) -> None: # type: ignore + global api_key + + api_key = value + + @property # type: ignore + @override + def organization(self) -> str | None: + return organization + + @organization.setter # type: ignore + def organization(self, value: str | None) -> None: # type: ignore + global organization + + organization = value + + @property + @override + def base_url(self) -> _httpx.URL: + if base_url is not None: + return _httpx.URL(base_url) + + return super().base_url + + @base_url.setter + def base_url(self, url: _httpx.URL | str) -> None: + super().base_url = url # type: ignore[misc] + + @property # type: ignore + @override + def timeout(self) -> float | Timeout | None: + return timeout + + @timeout.setter # type: ignore + def timeout(self, value: float | Timeout | None) -> None: # type: ignore + global timeout + + timeout = value + + @property # type: ignore + @override + def max_retries(self) -> int: + return max_retries + + @max_retries.setter # type: ignore + def max_retries(self, value: int) -> None: # type: ignore + global max_retries + + max_retries = value + + @property # type: ignore + @override + def _custom_headers(self) -> _t.Mapping[str, str] | None: + return default_headers + + @_custom_headers.setter # type: ignore + def _custom_headers(self, value: _t.Mapping[str, str] | None) -> None: # type: ignore + global default_headers + + default_headers = value + + @property # type: ignore + @override + def _custom_query(self) -> _t.Mapping[str, object] | None: + return default_query + + @_custom_query.setter # type: ignore + def _custom_query(self, value: _t.Mapping[str, object] | None) -> None: # type: ignore + global default_query + + default_query = value + + @property # type: ignore + @override + def _client(self) -> _httpx.Client: + return http_client or super()._client + + @_client.setter # type: ignore + def _client(self, value: _httpx.Client) -> None: # type: ignore + global http_client + + http_client = value + + +class _AzureModuleClient(_ModuleClient, AzureOpenAI): # type: ignore + ... + + +class _AmbiguousModuleClientUsageError(OpenAIError): + def __init__(self) -> None: + super().__init__( + "Ambiguous use of module client; please set `openai.api_type` or the `OPENAI_API_TYPE` environment variable to `openai` or `azure`" + ) + + +def _has_openai_credentials() -> bool: + return _os.environ.get("OPENAI_API_KEY") is not None + + +def _has_azure_credentials() -> bool: + return azure_endpoint is not None or _os.environ.get("AZURE_OPENAI_API_KEY") is not None + + +def _has_azure_ad_credentials() -> bool: + return ( + _os.environ.get("AZURE_OPENAI_AD_TOKEN") is not None + or azure_ad_token is not None + or azure_ad_token_provider is not None + ) + + +_client: OpenAI | None = None + + +def _load_client() -> OpenAI: # type: ignore[reportUnusedFunction] + global _client + + if _client is None: + global api_type, azure_endpoint, azure_ad_token, api_version + + if azure_endpoint is None: + azure_endpoint = _os.environ.get("AZURE_OPENAI_ENDPOINT") + + if azure_ad_token is None: + azure_ad_token = _os.environ.get("AZURE_OPENAI_AD_TOKEN") + + if api_version is None: + api_version = _os.environ.get("OPENAI_API_VERSION") + + if api_type is None: + has_openai = _has_openai_credentials() + has_azure = _has_azure_credentials() + has_azure_ad = _has_azure_ad_credentials() + + if has_openai and (has_azure or has_azure_ad): + raise _AmbiguousModuleClientUsageError() + + if (azure_ad_token is not None or azure_ad_token_provider is not None) and _os.environ.get( + "AZURE_OPENAI_API_KEY" + ) is not None: + raise _AmbiguousModuleClientUsageError() + + if has_azure or has_azure_ad: + api_type = "azure" + else: + api_type = "openai" + + if api_type == "azure": + _client = _AzureModuleClient( # type: ignore + api_version=api_version, + azure_endpoint=azure_endpoint, + api_key=api_key, + azure_ad_token=azure_ad_token, + azure_ad_token_provider=azure_ad_token_provider, + organization=organization, + base_url=base_url, + timeout=timeout, + max_retries=max_retries, + default_headers=default_headers, + default_query=default_query, + http_client=http_client, + ) + return _client + + _client = _ModuleClient( + api_key=api_key, + organization=organization, + base_url=base_url, + timeout=timeout, + max_retries=max_retries, + default_headers=default_headers, + default_query=default_query, + http_client=http_client, + ) + return _client + + return _client + + +def _reset_client() -> None: # type: ignore[reportUnusedFunction] + global _client + + _client = None + + +from ._module_client import ( + beta as beta, + chat as chat, + audio as audio, + files as files, + images as images, + models as models, + embeddings as embeddings, + completions as completions, + fine_tuning as fine_tuning, + moderations as moderations, +) diff --git a/openai/__main__.py b/openai/__main__.py new file mode 100644 index 00000000..4e28416e --- /dev/null +++ b/openai/__main__.py @@ -0,0 +1,3 @@ +from .cli import main + +main() diff --git a/openai/_base_client.py b/openai/_base_client.py new file mode 100644 index 00000000..502ed7c7 --- /dev/null +++ b/openai/_base_client.py @@ -0,0 +1,1972 @@ +from __future__ import annotations + +import json +import time +import uuid +import email +import asyncio +import inspect +import logging +import platform +import warnings +import email.utils +from types import TracebackType +from random import random +from typing import ( + TYPE_CHECKING, + Any, + Dict, + Type, + Union, + Generic, + Mapping, + TypeVar, + Iterable, + Iterator, + Optional, + Generator, + AsyncIterator, + cast, + overload, +) +from functools import lru_cache +from typing_extensions import Literal, override, get_origin + +import anyio +import httpx +import distro +import pydantic +from httpx import URL, Limits +from pydantic import PrivateAttr + +from . import _exceptions +from ._qs import Querystring +from ._files import to_httpx_files, async_to_httpx_files +from ._types import ( + NOT_GIVEN, + Body, + Omit, + Query, + Headers, + Timeout, + NotGiven, + ResponseT, + Transport, + AnyMapping, + PostParser, + ProxiesTypes, + RequestFiles, + HttpxSendArgs, + AsyncTransport, + RequestOptions, + ModelBuilderProtocol, +) +from ._utils import is_dict, is_list, is_given, is_mapping +from ._compat import model_copy, model_dump +from ._models import GenericModel, FinalRequestOptions, validate_type, construct_type +from ._response import ( + APIResponse, + BaseAPIResponse, + AsyncAPIResponse, + extract_response_type, +) +from ._constants import ( + DEFAULT_TIMEOUT, + MAX_RETRY_DELAY, + DEFAULT_MAX_RETRIES, + INITIAL_RETRY_DELAY, + RAW_RESPONSE_HEADER, + OVERRIDE_CAST_TO_HEADER, + DEFAULT_CONNECTION_LIMITS, +) +from ._streaming import Stream, SSEDecoder, AsyncStream, SSEBytesDecoder +from ._exceptions import ( + APIStatusError, + APITimeoutError, + APIConnectionError, + APIResponseValidationError, +) +from ._legacy_response import LegacyAPIResponse + +log: logging.Logger = logging.getLogger(__name__) + +# TODO: make base page type vars covariant +SyncPageT = TypeVar("SyncPageT", bound="BaseSyncPage[Any]") +AsyncPageT = TypeVar("AsyncPageT", bound="BaseAsyncPage[Any]") + + +_T = TypeVar("_T") +_T_co = TypeVar("_T_co", covariant=True) + +_StreamT = TypeVar("_StreamT", bound=Stream[Any]) +_AsyncStreamT = TypeVar("_AsyncStreamT", bound=AsyncStream[Any]) + +if TYPE_CHECKING: + from httpx._config import DEFAULT_TIMEOUT_CONFIG as HTTPX_DEFAULT_TIMEOUT +else: + try: + from httpx._config import DEFAULT_TIMEOUT_CONFIG as HTTPX_DEFAULT_TIMEOUT + except ImportError: + # taken from https://github.com/encode/httpx/blob/3ba5fe0d7ac70222590e759c31442b1cab263791/httpx/_config.py#L366 + HTTPX_DEFAULT_TIMEOUT = Timeout(5.0) + + +class PageInfo: + """Stores the necessary information to build the request to retrieve the next page. + + Either `url` or `params` must be set. + """ + + url: URL | NotGiven + params: Query | NotGiven + + @overload + def __init__( + self, + *, + url: URL, + ) -> None: + ... + + @overload + def __init__( + self, + *, + params: Query, + ) -> None: + ... + + def __init__( + self, + *, + url: URL | NotGiven = NOT_GIVEN, + params: Query | NotGiven = NOT_GIVEN, + ) -> None: + self.url = url + self.params = params + + +class BasePage(GenericModel, Generic[_T]): + """ + Defines the core interface for pagination. + + Type Args: + ModelT: The pydantic model that represents an item in the response. + + Methods: + has_next_page(): Check if there is another page available + next_page_info(): Get the necessary information to make a request for the next page + """ + + _options: FinalRequestOptions = PrivateAttr() + _model: Type[_T] = PrivateAttr() + + def has_next_page(self) -> bool: + items = self._get_page_items() + if not items: + return False + return self.next_page_info() is not None + + def next_page_info(self) -> Optional[PageInfo]: + ... + + def _get_page_items(self) -> Iterable[_T]: # type: ignore[empty-body] + ... + + def _params_from_url(self, url: URL) -> httpx.QueryParams: + # TODO: do we have to preprocess params here? + return httpx.QueryParams(cast(Any, self._options.params)).merge(url.params) + + def _info_to_options(self, info: PageInfo) -> FinalRequestOptions: + options = model_copy(self._options) + options._strip_raw_response_header() + + if not isinstance(info.params, NotGiven): + options.params = {**options.params, **info.params} + return options + + if not isinstance(info.url, NotGiven): + params = self._params_from_url(info.url) + url = info.url.copy_with(params=params) + options.params = dict(url.params) + options.url = str(url) + return options + + raise ValueError("Unexpected PageInfo state") + + +class BaseSyncPage(BasePage[_T], Generic[_T]): + _client: SyncAPIClient = pydantic.PrivateAttr() + + def _set_private_attributes( + self, + client: SyncAPIClient, + model: Type[_T], + options: FinalRequestOptions, + ) -> None: + self._model = model + self._client = client + self._options = options + + # Pydantic uses a custom `__iter__` method to support casting BaseModels + # to dictionaries. e.g. dict(model). + # As we want to support `for item in page`, this is inherently incompatible + # with the default pydantic behaviour. It is not possible to support both + # use cases at once. Fortunately, this is not a big deal as all other pydantic + # methods should continue to work as expected as there is an alternative method + # to cast a model to a dictionary, model.dict(), which is used internally + # by pydantic. + def __iter__(self) -> Iterator[_T]: # type: ignore + for page in self.iter_pages(): + for item in page._get_page_items(): + yield item + + def iter_pages(self: SyncPageT) -> Iterator[SyncPageT]: + page = self + while True: + yield page + if page.has_next_page(): + page = page.get_next_page() + else: + return + + def get_next_page(self: SyncPageT) -> SyncPageT: + info = self.next_page_info() + if not info: + raise RuntimeError( + "No next page expected; please check `.has_next_page()` before calling `.get_next_page()`." + ) + + options = self._info_to_options(info) + return self._client._request_api_list(self._model, page=self.__class__, options=options) + + +class AsyncPaginator(Generic[_T, AsyncPageT]): + def __init__( + self, + client: AsyncAPIClient, + options: FinalRequestOptions, + page_cls: Type[AsyncPageT], + model: Type[_T], + ) -> None: + self._model = model + self._client = client + self._options = options + self._page_cls = page_cls + + def __await__(self) -> Generator[Any, None, AsyncPageT]: + return self._get_page().__await__() + + async def _get_page(self) -> AsyncPageT: + def _parser(resp: AsyncPageT) -> AsyncPageT: + resp._set_private_attributes( + model=self._model, + options=self._options, + client=self._client, + ) + return resp + + self._options.post_parser = _parser + + return await self._client.request(self._page_cls, self._options) + + async def __aiter__(self) -> AsyncIterator[_T]: + # https://github.com/microsoft/pyright/issues/3464 + page = cast( + AsyncPageT, + await self, # type: ignore + ) + async for item in page: + yield item + + +class BaseAsyncPage(BasePage[_T], Generic[_T]): + _client: AsyncAPIClient = pydantic.PrivateAttr() + + def _set_private_attributes( + self, + model: Type[_T], + client: AsyncAPIClient, + options: FinalRequestOptions, + ) -> None: + self._model = model + self._client = client + self._options = options + + async def __aiter__(self) -> AsyncIterator[_T]: + async for page in self.iter_pages(): + for item in page._get_page_items(): + yield item + + async def iter_pages(self: AsyncPageT) -> AsyncIterator[AsyncPageT]: + page = self + while True: + yield page + if page.has_next_page(): + page = await page.get_next_page() + else: + return + + async def get_next_page(self: AsyncPageT) -> AsyncPageT: + info = self.next_page_info() + if not info: + raise RuntimeError( + "No next page expected; please check `.has_next_page()` before calling `.get_next_page()`." + ) + + options = self._info_to_options(info) + return await self._client._request_api_list(self._model, page=self.__class__, options=options) + + +_HttpxClientT = TypeVar("_HttpxClientT", bound=Union[httpx.Client, httpx.AsyncClient]) +_DefaultStreamT = TypeVar("_DefaultStreamT", bound=Union[Stream[Any], AsyncStream[Any]]) + + +class BaseClient(Generic[_HttpxClientT, _DefaultStreamT]): + _client: _HttpxClientT + _version: str + _base_url: URL + max_retries: int + timeout: Union[float, Timeout, None] + _limits: httpx.Limits + _proxies: ProxiesTypes | None + _transport: Transport | AsyncTransport | None + _strict_response_validation: bool + _idempotency_header: str | None + _default_stream_cls: type[_DefaultStreamT] | None = None + + def __init__( + self, + *, + version: str, + base_url: str | URL, + _strict_response_validation: bool, + max_retries: int = DEFAULT_MAX_RETRIES, + timeout: float | Timeout | None = DEFAULT_TIMEOUT, + limits: httpx.Limits, + transport: Transport | AsyncTransport | None, + proxies: ProxiesTypes | None, + custom_headers: Mapping[str, str] | None = None, + custom_query: Mapping[str, object] | None = None, + ) -> None: + self._version = version + self._base_url = self._enforce_trailing_slash(URL(base_url)) + self.max_retries = max_retries + self.timeout = timeout + self._limits = limits + self._proxies = proxies + self._transport = transport + self._custom_headers = custom_headers or {} + self._custom_query = custom_query or {} + self._strict_response_validation = _strict_response_validation + self._idempotency_header = None + + if max_retries is None: # pyright: ignore[reportUnnecessaryComparison] + raise TypeError( + "max_retries cannot be None. If you want to disable retries, pass `0`; if you want unlimited retries, pass `math.inf` or a very high number; if you want the default behavior, pass `openai.DEFAULT_MAX_RETRIES`" + ) + + def _enforce_trailing_slash(self, url: URL) -> URL: + if url.raw_path.endswith(b"/"): + return url + return url.copy_with(raw_path=url.raw_path + b"/") + + def _make_status_error_from_response( + self, + response: httpx.Response, + ) -> APIStatusError: + if response.is_closed and not response.is_stream_consumed: + # We can't read the response body as it has been closed + # before it was read. This can happen if an event hook + # raises a status error. + body = None + err_msg = f"Error code: {response.status_code}" + else: + err_text = response.text.strip() + body = err_text + + try: + body = json.loads(err_text) + err_msg = f"Error code: {response.status_code} - {body}" + except Exception: + err_msg = err_text or f"Error code: {response.status_code}" + + return self._make_status_error(err_msg, body=body, response=response) + + def _make_status_error( + self, + err_msg: str, + *, + body: object, + response: httpx.Response, + ) -> _exceptions.APIStatusError: + raise NotImplementedError() + + def _remaining_retries( + self, + remaining_retries: Optional[int], + options: FinalRequestOptions, + ) -> int: + return remaining_retries if remaining_retries is not None else options.get_max_retries(self.max_retries) + + def _build_headers(self, options: FinalRequestOptions) -> httpx.Headers: + custom_headers = options.headers or {} + headers_dict = _merge_mappings(self.default_headers, custom_headers) + self._validate_headers(headers_dict, custom_headers) + + # headers are case-insensitive while dictionaries are not. + headers = httpx.Headers(headers_dict) + + idempotency_header = self._idempotency_header + if idempotency_header and options.method.lower() != "get" and idempotency_header not in headers: + headers[idempotency_header] = options.idempotency_key or self._idempotency_key() + + return headers + + def _prepare_url(self, url: str) -> URL: + """ + Merge a URL argument together with any 'base_url' on the client, + to create the URL used for the outgoing request. + """ + # Copied from httpx's `_merge_url` method. + merge_url = URL(url) + if merge_url.is_relative_url: + merge_raw_path = self.base_url.raw_path + merge_url.raw_path.lstrip(b"/") + return self.base_url.copy_with(raw_path=merge_raw_path) + + return merge_url + + def _make_sse_decoder(self) -> SSEDecoder | SSEBytesDecoder: + return SSEDecoder() + + def _build_request( + self, + options: FinalRequestOptions, + ) -> httpx.Request: + if log.isEnabledFor(logging.DEBUG): + log.debug("Request options: %s", model_dump(options, exclude_unset=True)) + + kwargs: dict[str, Any] = {} + + json_data = options.json_data + if options.extra_json is not None: + if json_data is None: + json_data = cast(Body, options.extra_json) + elif is_mapping(json_data): + json_data = _merge_mappings(json_data, options.extra_json) + else: + raise RuntimeError(f"Unexpected JSON data type, {type(json_data)}, cannot merge with `extra_body`") + + headers = self._build_headers(options) + params = _merge_mappings(self._custom_query, options.params) + content_type = headers.get("Content-Type") + + # If the given Content-Type header is multipart/form-data then it + # has to be removed so that httpx can generate the header with + # additional information for us as it has to be in this form + # for the server to be able to correctly parse the request: + # multipart/form-data; boundary=---abc-- + if content_type is not None and content_type.startswith("multipart/form-data"): + if "boundary" not in content_type: + # only remove the header if the boundary hasn't been explicitly set + # as the caller doesn't want httpx to come up with their own boundary + headers.pop("Content-Type") + + # As we are now sending multipart/form-data instead of application/json + # we need to tell httpx to use it, https://www.python-httpx.org/advanced/#multipart-file-encoding + if json_data: + if not is_dict(json_data): + raise TypeError( + f"Expected query input to be a dictionary for multipart requests but got {type(json_data)} instead." + ) + kwargs["data"] = self._serialize_multipartform(json_data) + + # TODO: report this error to httpx + return self._client.build_request( # pyright: ignore[reportUnknownMemberType] + headers=headers, + timeout=self.timeout if isinstance(options.timeout, NotGiven) else options.timeout, + method=options.method, + url=self._prepare_url(options.url), + # the `Query` type that we use is incompatible with qs' + # `Params` type as it needs to be typed as `Mapping[str, object]` + # so that passing a `TypedDict` doesn't cause an error. + # https://github.com/microsoft/pyright/issues/3526#event-6715453066 + params=self.qs.stringify(cast(Mapping[str, Any], params)) if params else None, + json=json_data, + files=options.files, + **kwargs, + ) + + def _serialize_multipartform(self, data: Mapping[object, object]) -> dict[str, object]: + items = self.qs.stringify_items( + # TODO: type ignore is required as stringify_items is well typed but we can't be + # well typed without heavy validation. + data, # type: ignore + array_format="brackets", + ) + serialized: dict[str, object] = {} + for key, value in items: + existing = serialized.get(key) + + if not existing: + serialized[key] = value + continue + + # If a value has already been set for this key then that + # means we're sending data like `array[]=[1, 2, 3]` and we + # need to tell httpx that we want to send multiple values with + # the same key which is done by using a list or a tuple. + # + # Note: 2d arrays should never result in the same key at both + # levels so it's safe to assume that if the value is a list, + # it was because we changed it to be a list. + if is_list(existing): + existing.append(value) + else: + serialized[key] = [existing, value] + + return serialized + + def _maybe_override_cast_to(self, cast_to: type[ResponseT], options: FinalRequestOptions) -> type[ResponseT]: + if not is_given(options.headers): + return cast_to + + # make a copy of the headers so we don't mutate user-input + headers = dict(options.headers) + + # we internally support defining a temporary header to override the + # default `cast_to` type for use with `.with_raw_response` and `.with_streaming_response` + # see _response.py for implementation details + override_cast_to = headers.pop(OVERRIDE_CAST_TO_HEADER, NOT_GIVEN) + if is_given(override_cast_to): + options.headers = headers + return cast(Type[ResponseT], override_cast_to) + + return cast_to + + def _should_stream_response_body(self, request: httpx.Request) -> bool: + return request.headers.get(RAW_RESPONSE_HEADER) == "stream" # type: ignore[no-any-return] + + def _process_response_data( + self, + *, + data: object, + cast_to: type[ResponseT], + response: httpx.Response, + ) -> ResponseT: + if data is None: + return cast(ResponseT, None) + + if cast_to is object: + return cast(ResponseT, data) + + try: + if inspect.isclass(cast_to) and issubclass(cast_to, ModelBuilderProtocol): + return cast(ResponseT, cast_to.build(response=response, data=data)) + + if self._strict_response_validation: + return cast(ResponseT, validate_type(type_=cast_to, value=data)) + + return cast(ResponseT, construct_type(type_=cast_to, value=data)) + except pydantic.ValidationError as err: + raise APIResponseValidationError(response=response, body=data) from err + + @property + def qs(self) -> Querystring: + return Querystring() + + @property + def custom_auth(self) -> httpx.Auth | None: + return None + + @property + def auth_headers(self) -> dict[str, str]: + return {} + + @property + def default_headers(self) -> dict[str, str | Omit]: + return { + "Accept": "application/json", + "Content-Type": "application/json", + "User-Agent": self.user_agent, + **self.platform_headers(), + **self.auth_headers, + **self._custom_headers, + } + + def _validate_headers( + self, + headers: Headers, # noqa: ARG002 + custom_headers: Headers, # noqa: ARG002 + ) -> None: + """Validate the given default headers and custom headers. + + Does nothing by default. + """ + return + + @property + def user_agent(self) -> str: + return f"{self.__class__.__name__}/Python {self._version}" + + @property + def base_url(self) -> URL: + return self._base_url + + @base_url.setter + def base_url(self, url: URL | str) -> None: + self._base_url = self._enforce_trailing_slash(url if isinstance(url, URL) else URL(url)) + + def platform_headers(self) -> Dict[str, str]: + return platform_headers(self._version) + + def _parse_retry_after_header(self, response_headers: Optional[httpx.Headers] = None) -> float | None: + """Returns a float of the number of seconds (not milliseconds) to wait after retrying, or None if unspecified. + + About the Retry-After header: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After + See also https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After#syntax + """ + if response_headers is None: + return None + + # First, try the non-standard `retry-after-ms` header for milliseconds, + # which is more precise than integer-seconds `retry-after` + try: + retry_ms_header = response_headers.get("retry-after-ms", None) + return float(retry_ms_header) / 1000 + except (TypeError, ValueError): + pass + + # Next, try parsing `retry-after` header as seconds (allowing nonstandard floats). + retry_header = response_headers.get("retry-after") + try: + # note: the spec indicates that this should only ever be an integer + # but if someone sends a float there's no reason for us to not respect it + return float(retry_header) + except (TypeError, ValueError): + pass + + # Last, try parsing `retry-after` as a date. + retry_date_tuple = email.utils.parsedate_tz(retry_header) + if retry_date_tuple is None: + return None + + retry_date = email.utils.mktime_tz(retry_date_tuple) + return float(retry_date - time.time()) + + def _calculate_retry_timeout( + self, + remaining_retries: int, + options: FinalRequestOptions, + response_headers: Optional[httpx.Headers] = None, + ) -> float: + max_retries = options.get_max_retries(self.max_retries) + + # If the API asks us to wait a certain amount of time (and it's a reasonable amount), just do what it says. + retry_after = self._parse_retry_after_header(response_headers) + if retry_after is not None and 0 < retry_after <= 60: + return retry_after + + nb_retries = max_retries - remaining_retries + + # Apply exponential backoff, but not more than the max. + sleep_seconds = min(INITIAL_RETRY_DELAY * pow(2.0, nb_retries), MAX_RETRY_DELAY) + + # Apply some jitter, plus-or-minus half a second. + jitter = 1 - 0.25 * random() + timeout = sleep_seconds * jitter + return timeout if timeout >= 0 else 0 + + def _should_retry(self, response: httpx.Response) -> bool: + # Note: this is not a standard header + should_retry_header = response.headers.get("x-should-retry") + + # If the server explicitly says whether or not to retry, obey. + if should_retry_header == "true": + log.debug("Retrying as header `x-should-retry` is set to `true`") + return True + if should_retry_header == "false": + log.debug("Not retrying as header `x-should-retry` is set to `false`") + return False + + # Retry on request timeouts. + if response.status_code == 408: + log.debug("Retrying due to status code %i", response.status_code) + return True + + # Retry on lock timeouts. + if response.status_code == 409: + log.debug("Retrying due to status code %i", response.status_code) + return True + + # Retry on rate limits. + if response.status_code == 429: + log.debug("Retrying due to status code %i", response.status_code) + return True + + # Retry internal errors. + if response.status_code >= 500: + log.debug("Retrying due to status code %i", response.status_code) + return True + + log.debug("Not retrying") + return False + + def _idempotency_key(self) -> str: + return f"stainless-python-retry-{uuid.uuid4()}" + + +class SyncHttpxClientWrapper(httpx.Client): + def __del__(self) -> None: + try: + self.close() + except Exception: + pass + + +class SyncAPIClient(BaseClient[httpx.Client, Stream[Any]]): + _client: httpx.Client + _default_stream_cls: type[Stream[Any]] | None = None + + def __init__( + self, + *, + version: str, + base_url: str | URL, + max_retries: int = DEFAULT_MAX_RETRIES, + timeout: float | Timeout | None | NotGiven = NOT_GIVEN, + transport: Transport | None = None, + proxies: ProxiesTypes | None = None, + limits: Limits | None = None, + http_client: httpx.Client | None = None, + custom_headers: Mapping[str, str] | None = None, + custom_query: Mapping[str, object] | None = None, + _strict_response_validation: bool, + ) -> None: + if limits is not None: + warnings.warn( + "The `connection_pool_limits` argument is deprecated. The `http_client` argument should be passed instead", + category=DeprecationWarning, + stacklevel=3, + ) + if http_client is not None: + raise ValueError("The `http_client` argument is mutually exclusive with `connection_pool_limits`") + else: + limits = DEFAULT_CONNECTION_LIMITS + + if transport is not None: + warnings.warn( + "The `transport` argument is deprecated. The `http_client` argument should be passed instead", + category=DeprecationWarning, + stacklevel=3, + ) + if http_client is not None: + raise ValueError("The `http_client` argument is mutually exclusive with `transport`") + + if proxies is not None: + warnings.warn( + "The `proxies` argument is deprecated. The `http_client` argument should be passed instead", + category=DeprecationWarning, + stacklevel=3, + ) + if http_client is not None: + raise ValueError("The `http_client` argument is mutually exclusive with `proxies`") + + if not is_given(timeout): + # if the user passed in a custom http client with a non-default + # timeout set then we use that timeout. + # + # note: there is an edge case here where the user passes in a client + # where they've explicitly set the timeout to match the default timeout + # as this check is structural, meaning that we'll think they didn't + # pass in a timeout and will ignore it + if http_client and http_client.timeout != HTTPX_DEFAULT_TIMEOUT: + timeout = http_client.timeout + else: + timeout = DEFAULT_TIMEOUT + + if http_client is not None and not isinstance(http_client, httpx.Client): # pyright: ignore[reportUnnecessaryIsInstance] + raise TypeError( + f"Invalid `http_client` argument; Expected an instance of `httpx.Client` but got {type(http_client)}" + ) + + super().__init__( + version=version, + limits=limits, + # cast to a valid type because mypy doesn't understand our type narrowing + timeout=cast(Timeout, timeout), + proxies=proxies, + base_url=base_url, + transport=transport, + max_retries=max_retries, + custom_query=custom_query, + custom_headers=custom_headers, + _strict_response_validation=_strict_response_validation, + ) + self._client = http_client or SyncHttpxClientWrapper( + base_url=base_url, + # cast to a valid type because mypy doesn't understand our type narrowing + timeout=cast(Timeout, timeout), + proxies=proxies, + transport=transport, + limits=limits, + follow_redirects=True, + ) + + def is_closed(self) -> bool: + return self._client.is_closed + + def close(self) -> None: + """Close the underlying HTTPX client. + + The client will *not* be usable after this. + """ + # If an error is thrown while constructing a client, self._client + # may not be present + if hasattr(self, "_client"): + self._client.close() + + def __enter__(self: _T) -> _T: + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + self.close() + + def _prepare_options( + self, + options: FinalRequestOptions, # noqa: ARG002 + ) -> None: + """Hook for mutating the given options""" + return None + + def _prepare_request( + self, + request: httpx.Request, # noqa: ARG002 + ) -> None: + """This method is used as a callback for mutating the `Request` object + after it has been constructed. + This is useful for cases where you want to add certain headers based off of + the request properties, e.g. `url`, `method` etc. + """ + return None + + @overload + def request( + self, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + remaining_retries: Optional[int] = None, + *, + stream: Literal[True], + stream_cls: Type[_StreamT], + ) -> _StreamT: + ... + + @overload + def request( + self, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + remaining_retries: Optional[int] = None, + *, + stream: Literal[False] = False, + ) -> ResponseT: + ... + + @overload + def request( + self, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + remaining_retries: Optional[int] = None, + *, + stream: bool = False, + stream_cls: Type[_StreamT] | None = None, + ) -> ResponseT | _StreamT: + ... + + def request( + self, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + remaining_retries: Optional[int] = None, + *, + stream: bool = False, + stream_cls: type[_StreamT] | None = None, + ) -> ResponseT | _StreamT: + return self._request( + cast_to=cast_to, + options=options, + stream=stream, + stream_cls=stream_cls, + remaining_retries=remaining_retries, + ) + + def _request( + self, + *, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + remaining_retries: int | None, + stream: bool, + stream_cls: type[_StreamT] | None, + ) -> ResponseT | _StreamT: + cast_to = self._maybe_override_cast_to(cast_to, options) + self._prepare_options(options) + + retries = self._remaining_retries(remaining_retries, options) + request = self._build_request(options) + self._prepare_request(request) + + kwargs: HttpxSendArgs = {} + if self.custom_auth is not None: + kwargs["auth"] = self.custom_auth + + try: + response = self._client.send( + request, + stream=stream or self._should_stream_response_body(request=request), + **kwargs, + ) + except httpx.TimeoutException as err: + log.debug("Encountered httpx.TimeoutException", exc_info=True) + + if retries > 0: + return self._retry_request( + options, + cast_to, + retries, + stream=stream, + stream_cls=stream_cls, + response_headers=None, + ) + + log.debug("Raising timeout error") + raise APITimeoutError(request=request) from err + except Exception as err: + log.debug("Encountered Exception", exc_info=True) + + if retries > 0: + return self._retry_request( + options, + cast_to, + retries, + stream=stream, + stream_cls=stream_cls, + response_headers=None, + ) + + log.debug("Raising connection error") + raise APIConnectionError(request=request) from err + + log.debug( + 'HTTP Request: %s %s "%i %s"', request.method, request.url, response.status_code, response.reason_phrase + ) + + try: + response.raise_for_status() + except httpx.HTTPStatusError as err: # thrown on 4xx and 5xx status code + log.debug("Encountered httpx.HTTPStatusError", exc_info=True) + + if retries > 0 and self._should_retry(err.response): + err.response.close() + return self._retry_request( + options, + cast_to, + retries, + err.response.headers, + stream=stream, + stream_cls=stream_cls, + ) + + # If the response is streamed then we need to explicitly read the response + # to completion before attempting to access the response text. + if not err.response.is_closed: + err.response.read() + + log.debug("Re-raising status error") + raise self._make_status_error_from_response(err.response) from None + + return self._process_response( + cast_to=cast_to, + options=options, + response=response, + stream=stream, + stream_cls=stream_cls, + ) + + def _retry_request( + self, + options: FinalRequestOptions, + cast_to: Type[ResponseT], + remaining_retries: int, + response_headers: httpx.Headers | None, + *, + stream: bool, + stream_cls: type[_StreamT] | None, + ) -> ResponseT | _StreamT: + remaining = remaining_retries - 1 + if remaining == 1: + log.debug("1 retry left") + else: + log.debug("%i retries left", remaining) + + timeout = self._calculate_retry_timeout(remaining, options, response_headers) + log.info("Retrying request to %s in %f seconds", options.url, timeout) + + # In a synchronous context we are blocking the entire thread. Up to the library user to run the client in a + # different thread if necessary. + time.sleep(timeout) + + return self._request( + options=options, + cast_to=cast_to, + remaining_retries=remaining, + stream=stream, + stream_cls=stream_cls, + ) + + def _process_response( + self, + *, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + response: httpx.Response, + stream: bool, + stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None, + ) -> ResponseT: + if response.request.headers.get(RAW_RESPONSE_HEADER) == "true": + return cast( + ResponseT, + LegacyAPIResponse( + raw=response, + client=self, + cast_to=cast_to, + stream=stream, + stream_cls=stream_cls, + options=options, + ), + ) + + origin = get_origin(cast_to) or cast_to + + if inspect.isclass(origin) and issubclass(origin, BaseAPIResponse): + if not issubclass(origin, APIResponse): + raise TypeError(f"API Response types must subclass {APIResponse}; Received {origin}") + + response_cls = cast("type[BaseAPIResponse[Any]]", cast_to) + return cast( + ResponseT, + response_cls( + raw=response, + client=self, + cast_to=extract_response_type(response_cls), + stream=stream, + stream_cls=stream_cls, + options=options, + ), + ) + + if cast_to == httpx.Response: + return cast(ResponseT, response) + + api_response = APIResponse( + raw=response, + client=self, + cast_to=cast("type[ResponseT]", cast_to), # pyright: ignore[reportUnnecessaryCast] + stream=stream, + stream_cls=stream_cls, + options=options, + ) + if bool(response.request.headers.get(RAW_RESPONSE_HEADER)): + return cast(ResponseT, api_response) + + return api_response.parse() + + def _request_api_list( + self, + model: Type[object], + page: Type[SyncPageT], + options: FinalRequestOptions, + ) -> SyncPageT: + def _parser(resp: SyncPageT) -> SyncPageT: + resp._set_private_attributes( + client=self, + model=model, + options=options, + ) + return resp + + options.post_parser = _parser + + return self.request(page, options, stream=False) + + @overload + def get( + self, + path: str, + *, + cast_to: Type[ResponseT], + options: RequestOptions = {}, + stream: Literal[False] = False, + ) -> ResponseT: + ... + + @overload + def get( + self, + path: str, + *, + cast_to: Type[ResponseT], + options: RequestOptions = {}, + stream: Literal[True], + stream_cls: type[_StreamT], + ) -> _StreamT: + ... + + @overload + def get( + self, + path: str, + *, + cast_to: Type[ResponseT], + options: RequestOptions = {}, + stream: bool, + stream_cls: type[_StreamT] | None = None, + ) -> ResponseT | _StreamT: + ... + + def get( + self, + path: str, + *, + cast_to: Type[ResponseT], + options: RequestOptions = {}, + stream: bool = False, + stream_cls: type[_StreamT] | None = None, + ) -> ResponseT | _StreamT: + opts = FinalRequestOptions.construct(method="get", url=path, **options) + # cast is required because mypy complains about returning Any even though + # it understands the type variables + return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls)) + + @overload + def post( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + options: RequestOptions = {}, + files: RequestFiles | None = None, + stream: Literal[False] = False, + ) -> ResponseT: + ... + + @overload + def post( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + options: RequestOptions = {}, + files: RequestFiles | None = None, + stream: Literal[True], + stream_cls: type[_StreamT], + ) -> _StreamT: + ... + + @overload + def post( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + options: RequestOptions = {}, + files: RequestFiles | None = None, + stream: bool, + stream_cls: type[_StreamT] | None = None, + ) -> ResponseT | _StreamT: + ... + + def post( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + options: RequestOptions = {}, + files: RequestFiles | None = None, + stream: bool = False, + stream_cls: type[_StreamT] | None = None, + ) -> ResponseT | _StreamT: + opts = FinalRequestOptions.construct( + method="post", url=path, json_data=body, files=to_httpx_files(files), **options + ) + return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls)) + + def patch( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + options: RequestOptions = {}, + ) -> ResponseT: + opts = FinalRequestOptions.construct(method="patch", url=path, json_data=body, **options) + return self.request(cast_to, opts) + + def put( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + files: RequestFiles | None = None, + options: RequestOptions = {}, + ) -> ResponseT: + opts = FinalRequestOptions.construct( + method="put", url=path, json_data=body, files=to_httpx_files(files), **options + ) + return self.request(cast_to, opts) + + def delete( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + options: RequestOptions = {}, + ) -> ResponseT: + opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, **options) + return self.request(cast_to, opts) + + def get_api_list( + self, + path: str, + *, + model: Type[object], + page: Type[SyncPageT], + body: Body | None = None, + options: RequestOptions = {}, + method: str = "get", + ) -> SyncPageT: + opts = FinalRequestOptions.construct(method=method, url=path, json_data=body, **options) + return self._request_api_list(model, page, opts) + + +class AsyncHttpxClientWrapper(httpx.AsyncClient): + def __del__(self) -> None: + try: + # TODO(someday): support non asyncio runtimes here + asyncio.get_running_loop().create_task(self.aclose()) + except Exception: + pass + + +class AsyncAPIClient(BaseClient[httpx.AsyncClient, AsyncStream[Any]]): + _client: httpx.AsyncClient + _default_stream_cls: type[AsyncStream[Any]] | None = None + + def __init__( + self, + *, + version: str, + base_url: str | URL, + _strict_response_validation: bool, + max_retries: int = DEFAULT_MAX_RETRIES, + timeout: float | Timeout | None | NotGiven = NOT_GIVEN, + transport: AsyncTransport | None = None, + proxies: ProxiesTypes | None = None, + limits: Limits | None = None, + http_client: httpx.AsyncClient | None = None, + custom_headers: Mapping[str, str] | None = None, + custom_query: Mapping[str, object] | None = None, + ) -> None: + if limits is not None: + warnings.warn( + "The `connection_pool_limits` argument is deprecated. The `http_client` argument should be passed instead", + category=DeprecationWarning, + stacklevel=3, + ) + if http_client is not None: + raise ValueError("The `http_client` argument is mutually exclusive with `connection_pool_limits`") + else: + limits = DEFAULT_CONNECTION_LIMITS + + if transport is not None: + warnings.warn( + "The `transport` argument is deprecated. The `http_client` argument should be passed instead", + category=DeprecationWarning, + stacklevel=3, + ) + if http_client is not None: + raise ValueError("The `http_client` argument is mutually exclusive with `transport`") + + if proxies is not None: + warnings.warn( + "The `proxies` argument is deprecated. The `http_client` argument should be passed instead", + category=DeprecationWarning, + stacklevel=3, + ) + if http_client is not None: + raise ValueError("The `http_client` argument is mutually exclusive with `proxies`") + + if not is_given(timeout): + # if the user passed in a custom http client with a non-default + # timeout set then we use that timeout. + # + # note: there is an edge case here where the user passes in a client + # where they've explicitly set the timeout to match the default timeout + # as this check is structural, meaning that we'll think they didn't + # pass in a timeout and will ignore it + if http_client and http_client.timeout != HTTPX_DEFAULT_TIMEOUT: + timeout = http_client.timeout + else: + timeout = DEFAULT_TIMEOUT + + if http_client is not None and not isinstance(http_client, httpx.AsyncClient): # pyright: ignore[reportUnnecessaryIsInstance] + raise TypeError( + f"Invalid `http_client` argument; Expected an instance of `httpx.AsyncClient` but got {type(http_client)}" + ) + + super().__init__( + version=version, + base_url=base_url, + limits=limits, + # cast to a valid type because mypy doesn't understand our type narrowing + timeout=cast(Timeout, timeout), + proxies=proxies, + transport=transport, + max_retries=max_retries, + custom_query=custom_query, + custom_headers=custom_headers, + _strict_response_validation=_strict_response_validation, + ) + self._client = http_client or AsyncHttpxClientWrapper( + base_url=base_url, + # cast to a valid type because mypy doesn't understand our type narrowing + timeout=cast(Timeout, timeout), + proxies=proxies, + transport=transport, + limits=limits, + follow_redirects=True, + ) + + def is_closed(self) -> bool: + return self._client.is_closed + + async def close(self) -> None: + """Close the underlying HTTPX client. + + The client will *not* be usable after this. + """ + await self._client.aclose() + + async def __aenter__(self: _T) -> _T: + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + await self.close() + + async def _prepare_options( + self, + options: FinalRequestOptions, # noqa: ARG002 + ) -> None: + """Hook for mutating the given options""" + return None + + async def _prepare_request( + self, + request: httpx.Request, # noqa: ARG002 + ) -> None: + """This method is used as a callback for mutating the `Request` object + after it has been constructed. + This is useful for cases where you want to add certain headers based off of + the request properties, e.g. `url`, `method` etc. + """ + return None + + @overload + async def request( + self, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + *, + stream: Literal[False] = False, + remaining_retries: Optional[int] = None, + ) -> ResponseT: + ... + + @overload + async def request( + self, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + *, + stream: Literal[True], + stream_cls: type[_AsyncStreamT], + remaining_retries: Optional[int] = None, + ) -> _AsyncStreamT: + ... + + @overload + async def request( + self, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + *, + stream: bool, + stream_cls: type[_AsyncStreamT] | None = None, + remaining_retries: Optional[int] = None, + ) -> ResponseT | _AsyncStreamT: + ... + + async def request( + self, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + *, + stream: bool = False, + stream_cls: type[_AsyncStreamT] | None = None, + remaining_retries: Optional[int] = None, + ) -> ResponseT | _AsyncStreamT: + return await self._request( + cast_to=cast_to, + options=options, + stream=stream, + stream_cls=stream_cls, + remaining_retries=remaining_retries, + ) + + async def _request( + self, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + *, + stream: bool, + stream_cls: type[_AsyncStreamT] | None, + remaining_retries: int | None, + ) -> ResponseT | _AsyncStreamT: + cast_to = self._maybe_override_cast_to(cast_to, options) + await self._prepare_options(options) + + retries = self._remaining_retries(remaining_retries, options) + request = self._build_request(options) + await self._prepare_request(request) + + kwargs: HttpxSendArgs = {} + if self.custom_auth is not None: + kwargs["auth"] = self.custom_auth + + try: + response = await self._client.send( + request, + stream=stream or self._should_stream_response_body(request=request), + **kwargs, + ) + except httpx.TimeoutException as err: + log.debug("Encountered httpx.TimeoutException", exc_info=True) + + if retries > 0: + return await self._retry_request( + options, + cast_to, + retries, + stream=stream, + stream_cls=stream_cls, + response_headers=None, + ) + + log.debug("Raising timeout error") + raise APITimeoutError(request=request) from err + except Exception as err: + log.debug("Encountered Exception", exc_info=True) + + if retries > 0: + return await self._retry_request( + options, + cast_to, + retries, + stream=stream, + stream_cls=stream_cls, + response_headers=None, + ) + + log.debug("Raising connection error") + raise APIConnectionError(request=request) from err + + log.debug( + 'HTTP Request: %s %s "%i %s"', request.method, request.url, response.status_code, response.reason_phrase + ) + + try: + response.raise_for_status() + except httpx.HTTPStatusError as err: # thrown on 4xx and 5xx status code + log.debug("Encountered httpx.HTTPStatusError", exc_info=True) + + if retries > 0 and self._should_retry(err.response): + await err.response.aclose() + return await self._retry_request( + options, + cast_to, + retries, + err.response.headers, + stream=stream, + stream_cls=stream_cls, + ) + + # If the response is streamed then we need to explicitly read the response + # to completion before attempting to access the response text. + if not err.response.is_closed: + await err.response.aread() + + log.debug("Re-raising status error") + raise self._make_status_error_from_response(err.response) from None + + return await self._process_response( + cast_to=cast_to, + options=options, + response=response, + stream=stream, + stream_cls=stream_cls, + ) + + async def _retry_request( + self, + options: FinalRequestOptions, + cast_to: Type[ResponseT], + remaining_retries: int, + response_headers: httpx.Headers | None, + *, + stream: bool, + stream_cls: type[_AsyncStreamT] | None, + ) -> ResponseT | _AsyncStreamT: + remaining = remaining_retries - 1 + if remaining == 1: + log.debug("1 retry left") + else: + log.debug("%i retries left", remaining) + + timeout = self._calculate_retry_timeout(remaining, options, response_headers) + log.info("Retrying request to %s in %f seconds", options.url, timeout) + + await anyio.sleep(timeout) + + return await self._request( + options=options, + cast_to=cast_to, + remaining_retries=remaining, + stream=stream, + stream_cls=stream_cls, + ) + + async def _process_response( + self, + *, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + response: httpx.Response, + stream: bool, + stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None, + ) -> ResponseT: + if response.request.headers.get(RAW_RESPONSE_HEADER) == "true": + return cast( + ResponseT, + LegacyAPIResponse( + raw=response, + client=self, + cast_to=cast_to, + stream=stream, + stream_cls=stream_cls, + options=options, + ), + ) + + origin = get_origin(cast_to) or cast_to + + if inspect.isclass(origin) and issubclass(origin, BaseAPIResponse): + if not issubclass(origin, AsyncAPIResponse): + raise TypeError(f"API Response types must subclass {AsyncAPIResponse}; Received {origin}") + + response_cls = cast("type[BaseAPIResponse[Any]]", cast_to) + return cast( + "ResponseT", + response_cls( + raw=response, + client=self, + cast_to=extract_response_type(response_cls), + stream=stream, + stream_cls=stream_cls, + options=options, + ), + ) + + if cast_to == httpx.Response: + return cast(ResponseT, response) + + api_response = AsyncAPIResponse( + raw=response, + client=self, + cast_to=cast("type[ResponseT]", cast_to), # pyright: ignore[reportUnnecessaryCast] + stream=stream, + stream_cls=stream_cls, + options=options, + ) + if bool(response.request.headers.get(RAW_RESPONSE_HEADER)): + return cast(ResponseT, api_response) + + return await api_response.parse() + + def _request_api_list( + self, + model: Type[_T], + page: Type[AsyncPageT], + options: FinalRequestOptions, + ) -> AsyncPaginator[_T, AsyncPageT]: + return AsyncPaginator(client=self, options=options, page_cls=page, model=model) + + @overload + async def get( + self, + path: str, + *, + cast_to: Type[ResponseT], + options: RequestOptions = {}, + stream: Literal[False] = False, + ) -> ResponseT: + ... + + @overload + async def get( + self, + path: str, + *, + cast_to: Type[ResponseT], + options: RequestOptions = {}, + stream: Literal[True], + stream_cls: type[_AsyncStreamT], + ) -> _AsyncStreamT: + ... + + @overload + async def get( + self, + path: str, + *, + cast_to: Type[ResponseT], + options: RequestOptions = {}, + stream: bool, + stream_cls: type[_AsyncStreamT] | None = None, + ) -> ResponseT | _AsyncStreamT: + ... + + async def get( + self, + path: str, + *, + cast_to: Type[ResponseT], + options: RequestOptions = {}, + stream: bool = False, + stream_cls: type[_AsyncStreamT] | None = None, + ) -> ResponseT | _AsyncStreamT: + opts = FinalRequestOptions.construct(method="get", url=path, **options) + return await self.request(cast_to, opts, stream=stream, stream_cls=stream_cls) + + @overload + async def post( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + files: RequestFiles | None = None, + options: RequestOptions = {}, + stream: Literal[False] = False, + ) -> ResponseT: + ... + + @overload + async def post( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + files: RequestFiles | None = None, + options: RequestOptions = {}, + stream: Literal[True], + stream_cls: type[_AsyncStreamT], + ) -> _AsyncStreamT: + ... + + @overload + async def post( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + files: RequestFiles | None = None, + options: RequestOptions = {}, + stream: bool, + stream_cls: type[_AsyncStreamT] | None = None, + ) -> ResponseT | _AsyncStreamT: + ... + + async def post( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + files: RequestFiles | None = None, + options: RequestOptions = {}, + stream: bool = False, + stream_cls: type[_AsyncStreamT] | None = None, + ) -> ResponseT | _AsyncStreamT: + opts = FinalRequestOptions.construct( + method="post", url=path, json_data=body, files=await async_to_httpx_files(files), **options + ) + return await self.request(cast_to, opts, stream=stream, stream_cls=stream_cls) + + async def patch( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + options: RequestOptions = {}, + ) -> ResponseT: + opts = FinalRequestOptions.construct(method="patch", url=path, json_data=body, **options) + return await self.request(cast_to, opts) + + async def put( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + files: RequestFiles | None = None, + options: RequestOptions = {}, + ) -> ResponseT: + opts = FinalRequestOptions.construct( + method="put", url=path, json_data=body, files=await async_to_httpx_files(files), **options + ) + return await self.request(cast_to, opts) + + async def delete( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + options: RequestOptions = {}, + ) -> ResponseT: + opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, **options) + return await self.request(cast_to, opts) + + def get_api_list( + self, + path: str, + *, + model: Type[_T], + page: Type[AsyncPageT], + body: Body | None = None, + options: RequestOptions = {}, + method: str = "get", + ) -> AsyncPaginator[_T, AsyncPageT]: + opts = FinalRequestOptions.construct(method=method, url=path, json_data=body, **options) + return self._request_api_list(model, page, opts) + + +def make_request_options( + *, + query: Query | None = None, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + idempotency_key: str | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + post_parser: PostParser | NotGiven = NOT_GIVEN, +) -> RequestOptions: + """Create a dict of type RequestOptions without keys of NotGiven values.""" + options: RequestOptions = {} + if extra_headers is not None: + options["headers"] = extra_headers + + if extra_body is not None: + options["extra_json"] = cast(AnyMapping, extra_body) + + if query is not None: + options["params"] = query + + if extra_query is not None: + options["params"] = {**options.get("params", {}), **extra_query} + + if not isinstance(timeout, NotGiven): + options["timeout"] = timeout + + if idempotency_key is not None: + options["idempotency_key"] = idempotency_key + + if is_given(post_parser): + # internal + options["post_parser"] = post_parser # type: ignore + + return options + + +class OtherPlatform: + def __init__(self, name: str) -> None: + self.name = name + + @override + def __str__(self) -> str: + return f"Other:{self.name}" + + +Platform = Union[ + OtherPlatform, + Literal[ + "MacOS", + "Linux", + "Windows", + "FreeBSD", + "OpenBSD", + "iOS", + "Android", + "Unknown", + ], +] + + +def get_platform() -> Platform: + try: + system = platform.system().lower() + platform_name = platform.platform().lower() + except Exception: + return "Unknown" + + if "iphone" in platform_name or "ipad" in platform_name: + # Tested using Python3IDE on an iPhone 11 and Pythonista on an iPad 7 + # system is Darwin and platform_name is a string like: + # - Darwin-21.6.0-iPhone12,1-64bit + # - Darwin-21.6.0-iPad7,11-64bit + return "iOS" + + if system == "darwin": + return "MacOS" + + if system == "windows": + return "Windows" + + if "android" in platform_name: + # Tested using Pydroid 3 + # system is Linux and platform_name is a string like 'Linux-5.10.81-android12-9-00001-geba40aecb3b7-ab8534902-aarch64-with-libc' + return "Android" + + if system == "linux": + # https://distro.readthedocs.io/en/latest/#distro.id + distro_id = distro.id() + if distro_id == "freebsd": + return "FreeBSD" + + if distro_id == "openbsd": + return "OpenBSD" + + return "Linux" + + if platform_name: + return OtherPlatform(platform_name) + + return "Unknown" + + +@lru_cache(maxsize=None) +def platform_headers(version: str) -> Dict[str, str]: + return { + "X-Stainless-Lang": "python", + "X-Stainless-Package-Version": version, + "X-Stainless-OS": str(get_platform()), + "X-Stainless-Arch": str(get_architecture()), + "X-Stainless-Runtime": get_python_runtime(), + "X-Stainless-Runtime-Version": get_python_version(), + } + + +class OtherArch: + def __init__(self, name: str) -> None: + self.name = name + + @override + def __str__(self) -> str: + return f"other:{self.name}" + + +Arch = Union[OtherArch, Literal["x32", "x64", "arm", "arm64", "unknown"]] + + +def get_python_runtime() -> str: + try: + return platform.python_implementation() + except Exception: + return "unknown" + + +def get_python_version() -> str: + try: + return platform.python_version() + except Exception: + return "unknown" + + +def get_architecture() -> Arch: + try: + python_bitness, _ = platform.architecture() + machine = platform.machine().lower() + except Exception: + return "unknown" + + if machine in ("arm64", "aarch64"): + return "arm64" + + # TODO: untested + if machine == "arm": + return "arm" + + if machine == "x86_64": + return "x64" + + # TODO: untested + if python_bitness == "32bit": + return "x32" + + if machine: + return OtherArch(machine) + + return "unknown" + + +def _merge_mappings( + obj1: Mapping[_T_co, Union[_T, Omit]], + obj2: Mapping[_T_co, Union[_T, Omit]], +) -> Dict[_T_co, _T]: + """Merge two mappings of the same type, removing any values that are instances of `Omit`. + + In cases with duplicate keys the second mapping takes precedence. + """ + merged = {**obj1, **obj2} + return {key: value for key, value in merged.items() if not isinstance(value, Omit)} diff --git a/openai/_client.py b/openai/_client.py new file mode 100644 index 00000000..7fe2c9af --- /dev/null +++ b/openai/_client.py @@ -0,0 +1,503 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, Union, Mapping +from typing_extensions import Self, override + +import httpx + +from . import resources, _exceptions +from ._qs import Querystring +from ._types import ( + NOT_GIVEN, + Omit, + Timeout, + NotGiven, + Transport, + ProxiesTypes, + RequestOptions, +) +from ._utils import ( + is_given, + is_mapping, + get_async_library, +) +from ._version import __version__ +from ._streaming import Stream as Stream, AsyncStream as AsyncStream +from ._exceptions import OpenAIError, APIStatusError +from ._base_client import ( + DEFAULT_MAX_RETRIES, + SyncAPIClient, + AsyncAPIClient, +) + +__all__ = [ + "Timeout", + "Transport", + "ProxiesTypes", + "RequestOptions", + "resources", + "OpenAI", + "AsyncOpenAI", + "Client", + "AsyncClient", +] + + +class OpenAI(SyncAPIClient): + completions: resources.Completions + chat: resources.Chat + embeddings: resources.Embeddings + files: resources.Files + images: resources.Images + audio: resources.Audio + moderations: resources.Moderations + models: resources.Models + fine_tuning: resources.FineTuning + beta: resources.Beta + with_raw_response: OpenAIWithRawResponse + with_streaming_response: OpenAIWithStreamedResponse + + # client options + api_key: str + organization: str | None + + def __init__( + self, + *, + api_key: str | None = None, + organization: str | None = None, + base_url: str | httpx.URL | None = None, + timeout: Union[float, Timeout, None, NotGiven] = NOT_GIVEN, + max_retries: int = DEFAULT_MAX_RETRIES, + default_headers: Mapping[str, str] | None = None, + default_query: Mapping[str, object] | None = None, + # Configure a custom httpx client. See the [httpx documentation](https://www.python-httpx.org/api/#client) for more details. + http_client: httpx.Client | None = None, + # Enable or disable schema validation for data returned by the API. + # When enabled an error APIResponseValidationError is raised + # if the API responds with invalid data for the expected schema. + # + # This parameter may be removed or changed in the future. + # If you rely on this feature, please open a GitHub issue + # outlining your use-case to help us decide if it should be + # part of our public interface in the future. + _strict_response_validation: bool = False, + ) -> None: + """Construct a new synchronous openai client instance. + + This automatically infers the following arguments from their corresponding environment variables if they are not provided: + - `api_key` from `OPENAI_API_KEY` + - `organization` from `OPENAI_ORG_ID` + """ + if api_key is None: + api_key = os.environ.get("OPENAI_API_KEY") + if api_key is None: + raise OpenAIError( + "The api_key client option must be set either by passing api_key to the client or by setting the OPENAI_API_KEY environment variable" + ) + self.api_key = api_key + + if organization is None: + organization = os.environ.get("OPENAI_ORG_ID") + self.organization = organization + + if base_url is None: + base_url = os.environ.get("OPENAI_BASE_URL") + if base_url is None: + base_url = f"https://api.openai.com/v1" + + super().__init__( + version=__version__, + base_url=base_url, + max_retries=max_retries, + timeout=timeout, + http_client=http_client, + custom_headers=default_headers, + custom_query=default_query, + _strict_response_validation=_strict_response_validation, + ) + + self._default_stream_cls = Stream + + self.completions = resources.Completions(self) + self.chat = resources.Chat(self) + self.embeddings = resources.Embeddings(self) + self.files = resources.Files(self) + self.images = resources.Images(self) + self.audio = resources.Audio(self) + self.moderations = resources.Moderations(self) + self.models = resources.Models(self) + self.fine_tuning = resources.FineTuning(self) + self.beta = resources.Beta(self) + self.with_raw_response = OpenAIWithRawResponse(self) + self.with_streaming_response = OpenAIWithStreamedResponse(self) + + @property + @override + def qs(self) -> Querystring: + return Querystring(array_format="comma") + + @property + @override + def auth_headers(self) -> dict[str, str]: + api_key = self.api_key + return {"Authorization": f"Bearer {api_key}"} + + @property + @override + def default_headers(self) -> dict[str, str | Omit]: + return { + **super().default_headers, + "X-Stainless-Async": "false", + "OpenAI-Organization": self.organization if self.organization is not None else Omit(), + **self._custom_headers, + } + + def copy( + self, + *, + api_key: str | None = None, + organization: str | None = None, + base_url: str | httpx.URL | None = None, + timeout: float | Timeout | None | NotGiven = NOT_GIVEN, + http_client: httpx.Client | None = None, + max_retries: int | NotGiven = NOT_GIVEN, + default_headers: Mapping[str, str] | None = None, + set_default_headers: Mapping[str, str] | None = None, + default_query: Mapping[str, object] | None = None, + set_default_query: Mapping[str, object] | None = None, + _extra_kwargs: Mapping[str, Any] = {}, + ) -> Self: + """ + Create a new client instance re-using the same options given to the current client with optional overriding. + """ + if default_headers is not None and set_default_headers is not None: + raise ValueError("The `default_headers` and `set_default_headers` arguments are mutually exclusive") + + if default_query is not None and set_default_query is not None: + raise ValueError("The `default_query` and `set_default_query` arguments are mutually exclusive") + + headers = self._custom_headers + if default_headers is not None: + headers = {**headers, **default_headers} + elif set_default_headers is not None: + headers = set_default_headers + + params = self._custom_query + if default_query is not None: + params = {**params, **default_query} + elif set_default_query is not None: + params = set_default_query + + http_client = http_client or self._client + return self.__class__( + api_key=api_key or self.api_key, + organization=organization or self.organization, + base_url=base_url or self.base_url, + timeout=self.timeout if isinstance(timeout, NotGiven) else timeout, + http_client=http_client, + max_retries=max_retries if is_given(max_retries) else self.max_retries, + default_headers=headers, + default_query=params, + **_extra_kwargs, + ) + + # Alias for `copy` for nicer inline usage, e.g. + # client.with_options(timeout=10).foo.create(...) + with_options = copy + + @override + def _make_status_error( + self, + err_msg: str, + *, + body: object, + response: httpx.Response, + ) -> APIStatusError: + data = body.get("error", body) if is_mapping(body) else body + if response.status_code == 400: + return _exceptions.BadRequestError(err_msg, response=response, body=data) + + if response.status_code == 401: + return _exceptions.AuthenticationError(err_msg, response=response, body=data) + + if response.status_code == 403: + return _exceptions.PermissionDeniedError(err_msg, response=response, body=data) + + if response.status_code == 404: + return _exceptions.NotFoundError(err_msg, response=response, body=data) + + if response.status_code == 409: + return _exceptions.ConflictError(err_msg, response=response, body=data) + + if response.status_code == 422: + return _exceptions.UnprocessableEntityError(err_msg, response=response, body=data) + + if response.status_code == 429: + return _exceptions.RateLimitError(err_msg, response=response, body=data) + + if response.status_code >= 500: + return _exceptions.InternalServerError(err_msg, response=response, body=data) + return APIStatusError(err_msg, response=response, body=data) + + +class AsyncOpenAI(AsyncAPIClient): + completions: resources.AsyncCompletions + chat: resources.AsyncChat + embeddings: resources.AsyncEmbeddings + files: resources.AsyncFiles + images: resources.AsyncImages + audio: resources.AsyncAudio + moderations: resources.AsyncModerations + models: resources.AsyncModels + fine_tuning: resources.AsyncFineTuning + beta: resources.AsyncBeta + with_raw_response: AsyncOpenAIWithRawResponse + with_streaming_response: AsyncOpenAIWithStreamedResponse + + # client options + api_key: str + organization: str | None + + def __init__( + self, + *, + api_key: str | None = None, + organization: str | None = None, + base_url: str | httpx.URL | None = None, + timeout: Union[float, Timeout, None, NotGiven] = NOT_GIVEN, + max_retries: int = DEFAULT_MAX_RETRIES, + default_headers: Mapping[str, str] | None = None, + default_query: Mapping[str, object] | None = None, + # Configure a custom httpx client. See the [httpx documentation](https://www.python-httpx.org/api/#asyncclient) for more details. + http_client: httpx.AsyncClient | None = None, + # Enable or disable schema validation for data returned by the API. + # When enabled an error APIResponseValidationError is raised + # if the API responds with invalid data for the expected schema. + # + # This parameter may be removed or changed in the future. + # If you rely on this feature, please open a GitHub issue + # outlining your use-case to help us decide if it should be + # part of our public interface in the future. + _strict_response_validation: bool = False, + ) -> None: + """Construct a new async openai client instance. + + This automatically infers the following arguments from their corresponding environment variables if they are not provided: + - `api_key` from `OPENAI_API_KEY` + - `organization` from `OPENAI_ORG_ID` + """ + if api_key is None: + api_key = os.environ.get("OPENAI_API_KEY") + if api_key is None: + raise OpenAIError( + "The api_key client option must be set either by passing api_key to the client or by setting the OPENAI_API_KEY environment variable" + ) + self.api_key = api_key + + if organization is None: + organization = os.environ.get("OPENAI_ORG_ID") + self.organization = organization + + if base_url is None: + base_url = os.environ.get("OPENAI_BASE_URL") + if base_url is None: + base_url = f"https://api.openai.com/v1" + + super().__init__( + version=__version__, + base_url=base_url, + max_retries=max_retries, + timeout=timeout, + http_client=http_client, + custom_headers=default_headers, + custom_query=default_query, + _strict_response_validation=_strict_response_validation, + ) + + self._default_stream_cls = AsyncStream + + self.completions = resources.AsyncCompletions(self) + self.chat = resources.AsyncChat(self) + self.embeddings = resources.AsyncEmbeddings(self) + self.files = resources.AsyncFiles(self) + self.images = resources.AsyncImages(self) + self.audio = resources.AsyncAudio(self) + self.moderations = resources.AsyncModerations(self) + self.models = resources.AsyncModels(self) + self.fine_tuning = resources.AsyncFineTuning(self) + self.beta = resources.AsyncBeta(self) + self.with_raw_response = AsyncOpenAIWithRawResponse(self) + self.with_streaming_response = AsyncOpenAIWithStreamedResponse(self) + + @property + @override + def qs(self) -> Querystring: + return Querystring(array_format="comma") + + @property + @override + def auth_headers(self) -> dict[str, str]: + api_key = self.api_key + return {"Authorization": f"Bearer {api_key}"} + + @property + @override + def default_headers(self) -> dict[str, str | Omit]: + return { + **super().default_headers, + "X-Stainless-Async": f"async:{get_async_library()}", + "OpenAI-Organization": self.organization if self.organization is not None else Omit(), + **self._custom_headers, + } + + def copy( + self, + *, + api_key: str | None = None, + organization: str | None = None, + base_url: str | httpx.URL | None = None, + timeout: float | Timeout | None | NotGiven = NOT_GIVEN, + http_client: httpx.AsyncClient | None = None, + max_retries: int | NotGiven = NOT_GIVEN, + default_headers: Mapping[str, str] | None = None, + set_default_headers: Mapping[str, str] | None = None, + default_query: Mapping[str, object] | None = None, + set_default_query: Mapping[str, object] | None = None, + _extra_kwargs: Mapping[str, Any] = {}, + ) -> Self: + """ + Create a new client instance re-using the same options given to the current client with optional overriding. + """ + if default_headers is not None and set_default_headers is not None: + raise ValueError("The `default_headers` and `set_default_headers` arguments are mutually exclusive") + + if default_query is not None and set_default_query is not None: + raise ValueError("The `default_query` and `set_default_query` arguments are mutually exclusive") + + headers = self._custom_headers + if default_headers is not None: + headers = {**headers, **default_headers} + elif set_default_headers is not None: + headers = set_default_headers + + params = self._custom_query + if default_query is not None: + params = {**params, **default_query} + elif set_default_query is not None: + params = set_default_query + + http_client = http_client or self._client + return self.__class__( + api_key=api_key or self.api_key, + organization=organization or self.organization, + base_url=base_url or self.base_url, + timeout=self.timeout if isinstance(timeout, NotGiven) else timeout, + http_client=http_client, + max_retries=max_retries if is_given(max_retries) else self.max_retries, + default_headers=headers, + default_query=params, + **_extra_kwargs, + ) + + # Alias for `copy` for nicer inline usage, e.g. + # client.with_options(timeout=10).foo.create(...) + with_options = copy + + @override + def _make_status_error( + self, + err_msg: str, + *, + body: object, + response: httpx.Response, + ) -> APIStatusError: + data = body.get("error", body) if is_mapping(body) else body + if response.status_code == 400: + return _exceptions.BadRequestError(err_msg, response=response, body=data) + + if response.status_code == 401: + return _exceptions.AuthenticationError(err_msg, response=response, body=data) + + if response.status_code == 403: + return _exceptions.PermissionDeniedError(err_msg, response=response, body=data) + + if response.status_code == 404: + return _exceptions.NotFoundError(err_msg, response=response, body=data) + + if response.status_code == 409: + return _exceptions.ConflictError(err_msg, response=response, body=data) + + if response.status_code == 422: + return _exceptions.UnprocessableEntityError(err_msg, response=response, body=data) + + if response.status_code == 429: + return _exceptions.RateLimitError(err_msg, response=response, body=data) + + if response.status_code >= 500: + return _exceptions.InternalServerError(err_msg, response=response, body=data) + return APIStatusError(err_msg, response=response, body=data) + + +class OpenAIWithRawResponse: + def __init__(self, client: OpenAI) -> None: + self.completions = resources.CompletionsWithRawResponse(client.completions) + self.chat = resources.ChatWithRawResponse(client.chat) + self.embeddings = resources.EmbeddingsWithRawResponse(client.embeddings) + self.files = resources.FilesWithRawResponse(client.files) + self.images = resources.ImagesWithRawResponse(client.images) + self.audio = resources.AudioWithRawResponse(client.audio) + self.moderations = resources.ModerationsWithRawResponse(client.moderations) + self.models = resources.ModelsWithRawResponse(client.models) + self.fine_tuning = resources.FineTuningWithRawResponse(client.fine_tuning) + self.beta = resources.BetaWithRawResponse(client.beta) + + +class AsyncOpenAIWithRawResponse: + def __init__(self, client: AsyncOpenAI) -> None: + self.completions = resources.AsyncCompletionsWithRawResponse(client.completions) + self.chat = resources.AsyncChatWithRawResponse(client.chat) + self.embeddings = resources.AsyncEmbeddingsWithRawResponse(client.embeddings) + self.files = resources.AsyncFilesWithRawResponse(client.files) + self.images = resources.AsyncImagesWithRawResponse(client.images) + self.audio = resources.AsyncAudioWithRawResponse(client.audio) + self.moderations = resources.AsyncModerationsWithRawResponse(client.moderations) + self.models = resources.AsyncModelsWithRawResponse(client.models) + self.fine_tuning = resources.AsyncFineTuningWithRawResponse(client.fine_tuning) + self.beta = resources.AsyncBetaWithRawResponse(client.beta) + + +class OpenAIWithStreamedResponse: + def __init__(self, client: OpenAI) -> None: + self.completions = resources.CompletionsWithStreamingResponse(client.completions) + self.chat = resources.ChatWithStreamingResponse(client.chat) + self.embeddings = resources.EmbeddingsWithStreamingResponse(client.embeddings) + self.files = resources.FilesWithStreamingResponse(client.files) + self.images = resources.ImagesWithStreamingResponse(client.images) + self.audio = resources.AudioWithStreamingResponse(client.audio) + self.moderations = resources.ModerationsWithStreamingResponse(client.moderations) + self.models = resources.ModelsWithStreamingResponse(client.models) + self.fine_tuning = resources.FineTuningWithStreamingResponse(client.fine_tuning) + self.beta = resources.BetaWithStreamingResponse(client.beta) + + +class AsyncOpenAIWithStreamedResponse: + def __init__(self, client: AsyncOpenAI) -> None: + self.completions = resources.AsyncCompletionsWithStreamingResponse(client.completions) + self.chat = resources.AsyncChatWithStreamingResponse(client.chat) + self.embeddings = resources.AsyncEmbeddingsWithStreamingResponse(client.embeddings) + self.files = resources.AsyncFilesWithStreamingResponse(client.files) + self.images = resources.AsyncImagesWithStreamingResponse(client.images) + self.audio = resources.AsyncAudioWithStreamingResponse(client.audio) + self.moderations = resources.AsyncModerationsWithStreamingResponse(client.moderations) + self.models = resources.AsyncModelsWithStreamingResponse(client.models) + self.fine_tuning = resources.AsyncFineTuningWithStreamingResponse(client.fine_tuning) + self.beta = resources.AsyncBetaWithStreamingResponse(client.beta) + + +Client = OpenAI + +AsyncClient = AsyncOpenAI diff --git a/openai/_compat.py b/openai/_compat.py new file mode 100644 index 00000000..74c7639b --- /dev/null +++ b/openai/_compat.py @@ -0,0 +1,222 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Union, Generic, TypeVar, Callable, cast, overload +from datetime import date, datetime +from typing_extensions import Self + +import pydantic +from pydantic.fields import FieldInfo + +from ._types import StrBytesIntFloat + +_T = TypeVar("_T") +_ModelT = TypeVar("_ModelT", bound=pydantic.BaseModel) + +# --------------- Pydantic v2 compatibility --------------- + +# Pyright incorrectly reports some of our functions as overriding a method when they don't +# pyright: reportIncompatibleMethodOverride=false + +PYDANTIC_V2 = pydantic.VERSION.startswith("2.") + +# v1 re-exports +if TYPE_CHECKING: + + def parse_date(value: date | StrBytesIntFloat) -> date: # noqa: ARG001 + ... + + def parse_datetime(value: Union[datetime, StrBytesIntFloat]) -> datetime: # noqa: ARG001 + ... + + def get_args(t: type[Any]) -> tuple[Any, ...]: # noqa: ARG001 + ... + + def is_union(tp: type[Any] | None) -> bool: # noqa: ARG001 + ... + + def get_origin(t: type[Any]) -> type[Any] | None: # noqa: ARG001 + ... + + def is_literal_type(type_: type[Any]) -> bool: # noqa: ARG001 + ... + + def is_typeddict(type_: type[Any]) -> bool: # noqa: ARG001 + ... + +else: + if PYDANTIC_V2: + from pydantic.v1.typing import ( + get_args as get_args, + is_union as is_union, + get_origin as get_origin, + is_typeddict as is_typeddict, + is_literal_type as is_literal_type, + ) + from pydantic.v1.datetime_parse import parse_date as parse_date, parse_datetime as parse_datetime + else: + from pydantic.typing import ( + get_args as get_args, + is_union as is_union, + get_origin as get_origin, + is_typeddict as is_typeddict, + is_literal_type as is_literal_type, + ) + from pydantic.datetime_parse import parse_date as parse_date, parse_datetime as parse_datetime + + +# refactored config +if TYPE_CHECKING: + from pydantic import ConfigDict as ConfigDict +else: + if PYDANTIC_V2: + from pydantic import ConfigDict + else: + # TODO: provide an error message here? + ConfigDict = None + + +# renamed methods / properties +def parse_obj(model: type[_ModelT], value: object) -> _ModelT: + if PYDANTIC_V2: + return model.model_validate(value) + else: + return cast(_ModelT, model.parse_obj(value)) # pyright: ignore[reportDeprecated, reportUnnecessaryCast] + + +def field_is_required(field: FieldInfo) -> bool: + if PYDANTIC_V2: + return field.is_required() + return field.required # type: ignore + + +def field_get_default(field: FieldInfo) -> Any: + value = field.get_default() + if PYDANTIC_V2: + from pydantic_core import PydanticUndefined + + if value == PydanticUndefined: + return None + return value + return value + + +def field_outer_type(field: FieldInfo) -> Any: + if PYDANTIC_V2: + return field.annotation + return field.outer_type_ # type: ignore + + +def get_model_config(model: type[pydantic.BaseModel]) -> Any: + if PYDANTIC_V2: + return model.model_config + return model.__config__ # type: ignore + + +def get_model_fields(model: type[pydantic.BaseModel]) -> dict[str, FieldInfo]: + if PYDANTIC_V2: + return model.model_fields + return model.__fields__ # type: ignore + + +def model_copy(model: _ModelT) -> _ModelT: + if PYDANTIC_V2: + return model.model_copy() + return model.copy() # type: ignore + + +def model_json(model: pydantic.BaseModel, *, indent: int | None = None) -> str: + if PYDANTIC_V2: + return model.model_dump_json(indent=indent) + return model.json(indent=indent) # type: ignore + + +def model_dump( + model: pydantic.BaseModel, + *, + exclude_unset: bool = False, + exclude_defaults: bool = False, +) -> dict[str, Any]: + if PYDANTIC_V2: + return model.model_dump( + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + ) + return cast( + "dict[str, Any]", + model.dict( # pyright: ignore[reportDeprecated, reportUnnecessaryCast] + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + ), + ) + + +def model_parse(model: type[_ModelT], data: Any) -> _ModelT: + if PYDANTIC_V2: + return model.model_validate(data) + return model.parse_obj(data) # pyright: ignore[reportDeprecated] + + +# generic models +if TYPE_CHECKING: + + class GenericModel(pydantic.BaseModel): + ... + +else: + if PYDANTIC_V2: + # there no longer needs to be a distinction in v2 but + # we still have to create our own subclass to avoid + # inconsistent MRO ordering errors + class GenericModel(pydantic.BaseModel): + ... + + else: + import pydantic.generics + + class GenericModel(pydantic.generics.GenericModel, pydantic.BaseModel): + ... + + +# cached properties +if TYPE_CHECKING: + cached_property = property + + # we define a separate type (copied from typeshed) + # that represents that `cached_property` is `set`able + # at runtime, which differs from `@property`. + # + # this is a separate type as editors likely special case + # `@property` and we don't want to cause issues just to have + # more helpful internal types. + + class typed_cached_property(Generic[_T]): + func: Callable[[Any], _T] + attrname: str | None + + def __init__(self, func: Callable[[Any], _T]) -> None: + ... + + @overload + def __get__(self, instance: None, owner: type[Any] | None = None) -> Self: + ... + + @overload + def __get__(self, instance: object, owner: type[Any] | None = None) -> _T: + ... + + def __get__(self, instance: object, owner: type[Any] | None = None) -> _T | Self: + raise NotImplementedError() + + def __set_name__(self, owner: type[Any], name: str) -> None: + ... + + # __set__ is not defined at runtime, but @cached_property is designed to be settable + def __set__(self, instance: object, value: _T) -> None: + ... +else: + try: + from functools import cached_property as cached_property + except ImportError: + from cached_property import cached_property as cached_property + + typed_cached_property = cached_property diff --git a/openai/_constants.py b/openai/_constants.py new file mode 100644 index 00000000..3f82bed0 --- /dev/null +++ b/openai/_constants.py @@ -0,0 +1,14 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import httpx + +RAW_RESPONSE_HEADER = "X-Stainless-Raw-Response" +OVERRIDE_CAST_TO_HEADER = "____stainless_override_cast_to" + +# default timeout is 10 minutes +DEFAULT_TIMEOUT = httpx.Timeout(timeout=600.0, connect=5.0) +DEFAULT_MAX_RETRIES = 2 +DEFAULT_CONNECTION_LIMITS = httpx.Limits(max_connections=1000, max_keepalive_connections=100) + +INITIAL_RETRY_DELAY = 0.5 +MAX_RETRY_DELAY = 8.0 diff --git a/openai/_exceptions.py b/openai/_exceptions.py new file mode 100644 index 00000000..074752c8 --- /dev/null +++ b/openai/_exceptions.py @@ -0,0 +1,125 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Any, Optional, cast +from typing_extensions import Literal + +import httpx + +from ._utils import is_dict +from ._models import construct_type + +__all__ = [ + "BadRequestError", + "AuthenticationError", + "PermissionDeniedError", + "NotFoundError", + "ConflictError", + "UnprocessableEntityError", + "RateLimitError", + "InternalServerError", +] + + +class OpenAIError(Exception): + pass + + +class APIError(OpenAIError): + message: str + request: httpx.Request + + body: object | None + """The API response body. + + If the API responded with a valid JSON structure then this property will be the + decoded result. + + If it isn't a valid JSON structure then this will be the raw response. + + If there was no response associated with this error then it will be `None`. + """ + + code: Optional[str] = None + param: Optional[str] = None + type: Optional[str] + + def __init__(self, message: str, request: httpx.Request, *, body: object | None) -> None: + super().__init__(message) + self.request = request + self.message = message + self.body = body + + if is_dict(body): + self.code = cast(Any, construct_type(type_=Optional[str], value=body.get("code"))) + self.param = cast(Any, construct_type(type_=Optional[str], value=body.get("param"))) + self.type = cast(Any, construct_type(type_=str, value=body.get("type"))) + else: + self.code = None + self.param = None + self.type = None + + +class APIResponseValidationError(APIError): + response: httpx.Response + status_code: int + + def __init__(self, response: httpx.Response, body: object | None, *, message: str | None = None) -> None: + super().__init__(message or "Data returned by API invalid for expected schema.", response.request, body=body) + self.response = response + self.status_code = response.status_code + + +class APIStatusError(APIError): + """Raised when an API response has a status code of 4xx or 5xx.""" + + response: httpx.Response + status_code: int + + def __init__(self, message: str, *, response: httpx.Response, body: object | None) -> None: + super().__init__(message, response.request, body=body) + self.response = response + self.status_code = response.status_code + + +class APIConnectionError(APIError): + def __init__(self, *, message: str = "Connection error.", request: httpx.Request) -> None: + super().__init__(message, request, body=None) + + +class APITimeoutError(APIConnectionError): + def __init__(self, request: httpx.Request) -> None: + super().__init__(message="Request timed out.", request=request) + + +class BadRequestError(APIStatusError): + status_code: Literal[400] = 400 # pyright: ignore[reportIncompatibleVariableOverride] + + +class AuthenticationError(APIStatusError): + status_code: Literal[401] = 401 # pyright: ignore[reportIncompatibleVariableOverride] + + +class PermissionDeniedError(APIStatusError): + status_code: Literal[403] = 403 # pyright: ignore[reportIncompatibleVariableOverride] + + +class NotFoundError(APIStatusError): + status_code: Literal[404] = 404 # pyright: ignore[reportIncompatibleVariableOverride] + + +class ConflictError(APIStatusError): + status_code: Literal[409] = 409 # pyright: ignore[reportIncompatibleVariableOverride] + + +class UnprocessableEntityError(APIStatusError): + status_code: Literal[422] = 422 # pyright: ignore[reportIncompatibleVariableOverride] + + +class RateLimitError(APIStatusError): + status_code: Literal[429] = 429 # pyright: ignore[reportIncompatibleVariableOverride] + + +class InternalServerError(APIStatusError): + pass diff --git a/openai/_extras/__init__.py b/openai/_extras/__init__.py new file mode 100644 index 00000000..864dac41 --- /dev/null +++ b/openai/_extras/__init__.py @@ -0,0 +1,2 @@ +from .numpy_proxy import numpy as numpy, has_numpy as has_numpy +from .pandas_proxy import pandas as pandas diff --git a/openai/_extras/_common.py b/openai/_extras/_common.py new file mode 100644 index 00000000..6e71720e --- /dev/null +++ b/openai/_extras/_common.py @@ -0,0 +1,21 @@ +from .._exceptions import OpenAIError + +INSTRUCTIONS = """ + +OpenAI error: + + missing `{library}` + +This feature requires additional dependencies: + + $ pip install openai[{extra}] + +""" + + +def format_instructions(*, library: str, extra: str) -> str: + return INSTRUCTIONS.format(library=library, extra=extra) + + +class MissingDependencyError(OpenAIError): + pass diff --git a/openai/_extras/numpy_proxy.py b/openai/_extras/numpy_proxy.py new file mode 100644 index 00000000..27880bf1 --- /dev/null +++ b/openai/_extras/numpy_proxy.py @@ -0,0 +1,37 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any +from typing_extensions import override + +from .._utils import LazyProxy +from ._common import MissingDependencyError, format_instructions + +if TYPE_CHECKING: + import numpy as numpy + + +NUMPY_INSTRUCTIONS = format_instructions(library="numpy", extra="datalib") + + +class NumpyProxy(LazyProxy[Any]): + @override + def __load__(self) -> Any: + try: + import numpy + except ImportError as err: + raise MissingDependencyError(NUMPY_INSTRUCTIONS) from err + + return numpy + + +if not TYPE_CHECKING: + numpy = NumpyProxy() + + +def has_numpy() -> bool: + try: + import numpy # noqa: F401 # pyright: ignore[reportUnusedImport] + except ImportError: + return False + + return True diff --git a/openai/_extras/pandas_proxy.py b/openai/_extras/pandas_proxy.py new file mode 100644 index 00000000..686377ba --- /dev/null +++ b/openai/_extras/pandas_proxy.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any +from typing_extensions import override + +from .._utils import LazyProxy +from ._common import MissingDependencyError, format_instructions + +if TYPE_CHECKING: + import pandas as pandas + + +PANDAS_INSTRUCTIONS = format_instructions(library="pandas", extra="datalib") + + +class PandasProxy(LazyProxy[Any]): + @override + def __load__(self) -> Any: + try: + import pandas + except ImportError as err: + raise MissingDependencyError(PANDAS_INSTRUCTIONS) from err + + return pandas + + +if not TYPE_CHECKING: + pandas = PandasProxy() diff --git a/openai/_files.py b/openai/_files.py new file mode 100644 index 00000000..ad7b668b --- /dev/null +++ b/openai/_files.py @@ -0,0 +1,127 @@ +from __future__ import annotations + +import io +import os +import pathlib +from typing import overload +from typing_extensions import TypeGuard + +import anyio + +from ._types import ( + FileTypes, + FileContent, + RequestFiles, + HttpxFileTypes, + Base64FileInput, + HttpxFileContent, + HttpxRequestFiles, +) +from ._utils import is_tuple_t, is_mapping_t, is_sequence_t + + +def is_base64_file_input(obj: object) -> TypeGuard[Base64FileInput]: + return isinstance(obj, io.IOBase) or isinstance(obj, os.PathLike) + + +def is_file_content(obj: object) -> TypeGuard[FileContent]: + return ( + isinstance(obj, bytes) or isinstance(obj, tuple) or isinstance(obj, io.IOBase) or isinstance(obj, os.PathLike) + ) + + +def assert_is_file_content(obj: object, *, key: str | None = None) -> None: + if not is_file_content(obj): + prefix = f"Expected entry at `{key}`" if key is not None else f"Expected file input `{obj!r}`" + raise RuntimeError( + f"{prefix} to be bytes, an io.IOBase instance, PathLike or a tuple but received {type(obj)} instead. See https://github.com/openai/openai-python/tree/main#file-uploads" + ) from None + + +@overload +def to_httpx_files(files: None) -> None: + ... + + +@overload +def to_httpx_files(files: RequestFiles) -> HttpxRequestFiles: + ... + + +def to_httpx_files(files: RequestFiles | None) -> HttpxRequestFiles | None: + if files is None: + return None + + if is_mapping_t(files): + files = {key: _transform_file(file) for key, file in files.items()} + elif is_sequence_t(files): + files = [(key, _transform_file(file)) for key, file in files] + else: + raise TypeError(f"Unexpected file type input {type(files)}, expected mapping or sequence") + + return files + + +def _transform_file(file: FileTypes) -> HttpxFileTypes: + if is_file_content(file): + if isinstance(file, os.PathLike): + path = pathlib.Path(file) + return (path.name, path.read_bytes()) + + return file + + if is_tuple_t(file): + return (file[0], _read_file_content(file[1]), *file[2:]) + + raise TypeError(f"Expected file types input to be a FileContent type or to be a tuple") + + +def _read_file_content(file: FileContent) -> HttpxFileContent: + if isinstance(file, os.PathLike): + return pathlib.Path(file).read_bytes() + return file + + +@overload +async def async_to_httpx_files(files: None) -> None: + ... + + +@overload +async def async_to_httpx_files(files: RequestFiles) -> HttpxRequestFiles: + ... + + +async def async_to_httpx_files(files: RequestFiles | None) -> HttpxRequestFiles | None: + if files is None: + return None + + if is_mapping_t(files): + files = {key: await _async_transform_file(file) for key, file in files.items()} + elif is_sequence_t(files): + files = [(key, await _async_transform_file(file)) for key, file in files] + else: + raise TypeError("Unexpected file type input {type(files)}, expected mapping or sequence") + + return files + + +async def _async_transform_file(file: FileTypes) -> HttpxFileTypes: + if is_file_content(file): + if isinstance(file, os.PathLike): + path = anyio.Path(file) + return (path.name, await path.read_bytes()) + + return file + + if is_tuple_t(file): + return (file[0], await _async_read_file_content(file[1]), *file[2:]) + + raise TypeError(f"Expected file types input to be a FileContent type or to be a tuple") + + +async def _async_read_file_content(file: FileContent) -> HttpxFileContent: + if isinstance(file, os.PathLike): + return await anyio.Path(file).read_bytes() + + return file diff --git a/openai/_legacy_response.py b/openai/_legacy_response.py new file mode 100644 index 00000000..4585cd74 --- /dev/null +++ b/openai/_legacy_response.py @@ -0,0 +1,456 @@ +from __future__ import annotations + +import os +import inspect +import logging +import datetime +import functools +from typing import TYPE_CHECKING, Any, Union, Generic, TypeVar, Callable, Iterator, AsyncIterator, cast, overload +from typing_extensions import Awaitable, ParamSpec, override, deprecated, get_origin + +import anyio +import httpx +import pydantic + +from ._types import NoneType +from ._utils import is_given, extract_type_arg, is_annotated_type +from ._models import BaseModel, is_basemodel +from ._constants import RAW_RESPONSE_HEADER +from ._streaming import Stream, AsyncStream, is_stream_class_type, extract_stream_chunk_type +from ._exceptions import APIResponseValidationError + +if TYPE_CHECKING: + from ._models import FinalRequestOptions + from ._base_client import BaseClient + + +P = ParamSpec("P") +R = TypeVar("R") +_T = TypeVar("_T") + +log: logging.Logger = logging.getLogger(__name__) + + +class LegacyAPIResponse(Generic[R]): + """This is a legacy class as it will be replaced by `APIResponse` + and `AsyncAPIResponse` in the `_response.py` file in the next major + release. + + For the sync client this will mostly be the same with the exception + of `content` & `text` will be methods instead of properties. In the + async client, all methods will be async. + + A migration script will be provided & the migration in general should + be smooth. + """ + + _cast_to: type[R] + _client: BaseClient[Any, Any] + _parsed_by_type: dict[type[Any], Any] + _stream: bool + _stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None + _options: FinalRequestOptions + + http_response: httpx.Response + + def __init__( + self, + *, + raw: httpx.Response, + cast_to: type[R], + client: BaseClient[Any, Any], + stream: bool, + stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None, + options: FinalRequestOptions, + ) -> None: + self._cast_to = cast_to + self._client = client + self._parsed_by_type = {} + self._stream = stream + self._stream_cls = stream_cls + self._options = options + self.http_response = raw + + @overload + def parse(self, *, to: type[_T]) -> _T: + ... + + @overload + def parse(self) -> R: + ... + + def parse(self, *, to: type[_T] | None = None) -> R | _T: + """Returns the rich python representation of this response's data. + + NOTE: For the async client: this will become a coroutine in the next major version. + + For lower-level control, see `.read()`, `.json()`, `.iter_bytes()`. + + You can customise the type that the response is parsed into through + the `to` argument, e.g. + + ```py + from openai import BaseModel + + + class MyModel(BaseModel): + foo: str + + + obj = response.parse(to=MyModel) + print(obj.foo) + ``` + + We support parsing: + - `BaseModel` + - `dict` + - `list` + - `Union` + - `str` + - `int` + - `float` + - `httpx.Response` + """ + cache_key = to if to is not None else self._cast_to + cached = self._parsed_by_type.get(cache_key) + if cached is not None: + return cached # type: ignore[no-any-return] + + parsed = self._parse(to=to) + if is_given(self._options.post_parser): + parsed = self._options.post_parser(parsed) + + self._parsed_by_type[cache_key] = parsed + return parsed + + @property + def headers(self) -> httpx.Headers: + return self.http_response.headers + + @property + def http_request(self) -> httpx.Request: + return self.http_response.request + + @property + def status_code(self) -> int: + return self.http_response.status_code + + @property + def url(self) -> httpx.URL: + return self.http_response.url + + @property + def method(self) -> str: + return self.http_request.method + + @property + def content(self) -> bytes: + """Return the binary response content. + + NOTE: this will be removed in favour of `.read()` in the + next major version. + """ + return self.http_response.content + + @property + def text(self) -> str: + """Return the decoded response content. + + NOTE: this will be turned into a method in the next major version. + """ + return self.http_response.text + + @property + def http_version(self) -> str: + return self.http_response.http_version + + @property + def is_closed(self) -> bool: + return self.http_response.is_closed + + @property + def elapsed(self) -> datetime.timedelta: + """The time taken for the complete request/response cycle to complete.""" + return self.http_response.elapsed + + def _parse(self, *, to: type[_T] | None = None) -> R | _T: + # unwrap `Annotated[T, ...]` -> `T` + if to and is_annotated_type(to): + to = extract_type_arg(to, 0) + + if self._stream: + if to: + if not is_stream_class_type(to): + raise TypeError(f"Expected custom parse type to be a subclass of {Stream} or {AsyncStream}") + + return cast( + _T, + to( + cast_to=extract_stream_chunk_type( + to, + failure_message="Expected custom stream type to be passed with a type argument, e.g. Stream[ChunkType]", + ), + response=self.http_response, + client=cast(Any, self._client), + ), + ) + + if self._stream_cls: + return cast( + R, + self._stream_cls( + cast_to=extract_stream_chunk_type(self._stream_cls), + response=self.http_response, + client=cast(Any, self._client), + ), + ) + + stream_cls = cast("type[Stream[Any]] | type[AsyncStream[Any]] | None", self._client._default_stream_cls) + if stream_cls is None: + raise MissingStreamClassError() + + return cast( + R, + stream_cls( + cast_to=self._cast_to, + response=self.http_response, + client=cast(Any, self._client), + ), + ) + + cast_to = to if to is not None else self._cast_to + + # unwrap `Annotated[T, ...]` -> `T` + if is_annotated_type(cast_to): + cast_to = extract_type_arg(cast_to, 0) + + if cast_to is NoneType: + return cast(R, None) + + response = self.http_response + if cast_to == str: + return cast(R, response.text) + + if cast_to == int: + return cast(R, int(response.text)) + + if cast_to == float: + return cast(R, float(response.text)) + + origin = get_origin(cast_to) or cast_to + + if inspect.isclass(origin) and issubclass(origin, HttpxBinaryResponseContent): + return cast(R, cast_to(response)) # type: ignore + + if origin == LegacyAPIResponse: + raise RuntimeError("Unexpected state - cast_to is `APIResponse`") + + if inspect.isclass(origin) and issubclass(origin, httpx.Response): + # Because of the invariance of our ResponseT TypeVar, users can subclass httpx.Response + # and pass that class to our request functions. We cannot change the variance to be either + # covariant or contravariant as that makes our usage of ResponseT illegal. We could construct + # the response class ourselves but that is something that should be supported directly in httpx + # as it would be easy to incorrectly construct the Response object due to the multitude of arguments. + if cast_to != httpx.Response: + raise ValueError(f"Subclasses of httpx.Response cannot be passed to `cast_to`") + return cast(R, response) + + if inspect.isclass(origin) and not issubclass(origin, BaseModel) and issubclass(origin, pydantic.BaseModel): + raise TypeError("Pydantic models must subclass our base model type, e.g. `from openai import BaseModel`") + + if ( + cast_to is not object + and not origin is list + and not origin is dict + and not origin is Union + and not issubclass(origin, BaseModel) + ): + raise RuntimeError( + f"Unsupported type, expected {cast_to} to be a subclass of {BaseModel}, {dict}, {list}, {Union}, {NoneType}, {str} or {httpx.Response}." + ) + + # split is required to handle cases where additional information is included + # in the response, e.g. application/json; charset=utf-8 + content_type, *_ = response.headers.get("content-type", "*").split(";") + if content_type != "application/json": + if is_basemodel(cast_to): + try: + data = response.json() + except Exception as exc: + log.debug("Could not read JSON from response data due to %s - %s", type(exc), exc) + else: + return self._client._process_response_data( + data=data, + cast_to=cast_to, # type: ignore + response=response, + ) + + if self._client._strict_response_validation: + raise APIResponseValidationError( + response=response, + message=f"Expected Content-Type response header to be `application/json` but received `{content_type}` instead.", + body=response.text, + ) + + # If the API responds with content that isn't JSON then we just return + # the (decoded) text without performing any parsing so that you can still + # handle the response however you need to. + return response.text # type: ignore + + data = response.json() + + return self._client._process_response_data( + data=data, + cast_to=cast_to, # type: ignore + response=response, + ) + + @override + def __repr__(self) -> str: + return f"" + + +class MissingStreamClassError(TypeError): + def __init__(self) -> None: + super().__init__( + "The `stream` argument was set to `True` but the `stream_cls` argument was not given. See `openai._streaming` for reference", + ) + + +def to_raw_response_wrapper(func: Callable[P, R]) -> Callable[P, LegacyAPIResponse[R]]: + """Higher order function that takes one of our bound API methods and wraps it + to support returning the raw `APIResponse` object directly. + """ + + @functools.wraps(func) + def wrapped(*args: P.args, **kwargs: P.kwargs) -> LegacyAPIResponse[R]: + extra_headers: dict[str, str] = {**(cast(Any, kwargs.get("extra_headers")) or {})} + extra_headers[RAW_RESPONSE_HEADER] = "true" + + kwargs["extra_headers"] = extra_headers + + return cast(LegacyAPIResponse[R], func(*args, **kwargs)) + + return wrapped + + +def async_to_raw_response_wrapper(func: Callable[P, Awaitable[R]]) -> Callable[P, Awaitable[LegacyAPIResponse[R]]]: + """Higher order function that takes one of our bound API methods and wraps it + to support returning the raw `APIResponse` object directly. + """ + + @functools.wraps(func) + async def wrapped(*args: P.args, **kwargs: P.kwargs) -> LegacyAPIResponse[R]: + extra_headers: dict[str, str] = {**(cast(Any, kwargs.get("extra_headers")) or {})} + extra_headers[RAW_RESPONSE_HEADER] = "true" + + kwargs["extra_headers"] = extra_headers + + return cast(LegacyAPIResponse[R], await func(*args, **kwargs)) + + return wrapped + + +class HttpxBinaryResponseContent: + response: httpx.Response + + def __init__(self, response: httpx.Response) -> None: + self.response = response + + @property + def content(self) -> bytes: + return self.response.content + + @property + def text(self) -> str: + return self.response.text + + @property + def encoding(self) -> str | None: + return self.response.encoding + + @property + def charset_encoding(self) -> str | None: + return self.response.charset_encoding + + def json(self, **kwargs: Any) -> Any: + return self.response.json(**kwargs) + + def read(self) -> bytes: + return self.response.read() + + def iter_bytes(self, chunk_size: int | None = None) -> Iterator[bytes]: + return self.response.iter_bytes(chunk_size) + + def iter_text(self, chunk_size: int | None = None) -> Iterator[str]: + return self.response.iter_text(chunk_size) + + def iter_lines(self) -> Iterator[str]: + return self.response.iter_lines() + + def iter_raw(self, chunk_size: int | None = None) -> Iterator[bytes]: + return self.response.iter_raw(chunk_size) + + def write_to_file( + self, + file: str | os.PathLike[str], + ) -> None: + """Write the output to the given file. + + Accepts a filename or any path-like object, e.g. pathlib.Path + + Note: if you want to stream the data to the file instead of writing + all at once then you should use `.with_streaming_response` when making + the API request, e.g. `client.with_streaming_response.foo().stream_to_file('my_filename.txt')` + """ + with open(file, mode="wb") as f: + for data in self.response.iter_bytes(): + f.write(data) + + @deprecated( + "Due to a bug, this method doesn't actually stream the response content, `.with_streaming_response.method()` should be used instead" + ) + def stream_to_file( + self, + file: str | os.PathLike[str], + *, + chunk_size: int | None = None, + ) -> None: + with open(file, mode="wb") as f: + for data in self.response.iter_bytes(chunk_size): + f.write(data) + + def close(self) -> None: + return self.response.close() + + async def aread(self) -> bytes: + return await self.response.aread() + + async def aiter_bytes(self, chunk_size: int | None = None) -> AsyncIterator[bytes]: + return self.response.aiter_bytes(chunk_size) + + async def aiter_text(self, chunk_size: int | None = None) -> AsyncIterator[str]: + return self.response.aiter_text(chunk_size) + + async def aiter_lines(self) -> AsyncIterator[str]: + return self.response.aiter_lines() + + async def aiter_raw(self, chunk_size: int | None = None) -> AsyncIterator[bytes]: + return self.response.aiter_raw(chunk_size) + + @deprecated( + "Due to a bug, this method doesn't actually stream the response content, `.with_streaming_response.method()` should be used instead" + ) + async def astream_to_file( + self, + file: str | os.PathLike[str], + *, + chunk_size: int | None = None, + ) -> None: + path = anyio.Path(file) + async with await path.open(mode="wb") as f: + async for data in self.response.aiter_bytes(chunk_size): + await f.write(data) + + async def aclose(self) -> None: + return await self.response.aclose() diff --git a/openai/_models.py b/openai/_models.py new file mode 100644 index 00000000..0f001150 --- /dev/null +++ b/openai/_models.py @@ -0,0 +1,654 @@ +from __future__ import annotations + +import os +import inspect +from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, cast +from datetime import date, datetime +from functools import lru_cache +from typing_extensions import ( + Unpack, + Literal, + ClassVar, + Protocol, + Required, + TypedDict, + TypeGuard, + final, + override, + runtime_checkable, +) + +import pydantic +import pydantic.generics +from pydantic.fields import FieldInfo + +from ._types import ( + Body, + IncEx, + Query, + ModelT, + Headers, + Timeout, + NotGiven, + AnyMapping, + HttpxRequestFiles, +) +from ._utils import ( + PropertyInfo, + is_list, + is_given, + is_mapping, + parse_date, + coerce_boolean, + parse_datetime, + strip_not_given, + extract_type_arg, + is_annotated_type, + strip_annotated_type, +) +from ._compat import ( + PYDANTIC_V2, + ConfigDict, + GenericModel as BaseGenericModel, + get_args, + is_union, + parse_obj, + get_origin, + is_literal_type, + get_model_config, + get_model_fields, + field_get_default, +) +from ._constants import RAW_RESPONSE_HEADER + +if TYPE_CHECKING: + from pydantic_core.core_schema import ModelField, ModelFieldsSchema + +__all__ = ["BaseModel", "GenericModel"] + +_T = TypeVar("_T") + + +@runtime_checkable +class _ConfigProtocol(Protocol): + allow_population_by_field_name: bool + + +class BaseModel(pydantic.BaseModel): + if PYDANTIC_V2: + model_config: ClassVar[ConfigDict] = ConfigDict( + extra="allow", defer_build=coerce_boolean(os.environ.get("DEFER_PYDANTIC_BUILD", "true")) + ) + else: + + @property + @override + def model_fields_set(self) -> set[str]: + # a forwards-compat shim for pydantic v2 + return self.__fields_set__ # type: ignore + + class Config(pydantic.BaseConfig): # pyright: ignore[reportDeprecated] + extra: Any = pydantic.Extra.allow # type: ignore + + @override + def __str__(self) -> str: + # mypy complains about an invalid self arg + return f'{self.__repr_name__()}({self.__repr_str__(", ")})' # type: ignore[misc] + + # Override the 'construct' method in a way that supports recursive parsing without validation. + # Based on https://github.com/samuelcolvin/pydantic/issues/1168#issuecomment-817742836. + @classmethod + @override + def construct( + cls: Type[ModelT], + _fields_set: set[str] | None = None, + **values: object, + ) -> ModelT: + m = cls.__new__(cls) + fields_values: dict[str, object] = {} + + config = get_model_config(cls) + populate_by_name = ( + config.allow_population_by_field_name + if isinstance(config, _ConfigProtocol) + else config.get("populate_by_name") + ) + + if _fields_set is None: + _fields_set = set() + + model_fields = get_model_fields(cls) + for name, field in model_fields.items(): + key = field.alias + if key is None or (key not in values and populate_by_name): + key = name + + if key in values: + fields_values[name] = _construct_field(value=values[key], field=field, key=key) + _fields_set.add(name) + else: + fields_values[name] = field_get_default(field) + + _extra = {} + for key, value in values.items(): + if key not in model_fields: + if PYDANTIC_V2: + _extra[key] = value + else: + _fields_set.add(key) + fields_values[key] = value + + object.__setattr__(m, "__dict__", fields_values) + + if PYDANTIC_V2: + # these properties are copied from Pydantic's `model_construct()` method + object.__setattr__(m, "__pydantic_private__", None) + object.__setattr__(m, "__pydantic_extra__", _extra) + object.__setattr__(m, "__pydantic_fields_set__", _fields_set) + else: + # init_private_attributes() does not exist in v2 + m._init_private_attributes() # type: ignore + + # copied from Pydantic v1's `construct()` method + object.__setattr__(m, "__fields_set__", _fields_set) + + return m + + if not TYPE_CHECKING: + # type checkers incorrectly complain about this assignment + # because the type signatures are technically different + # although not in practice + model_construct = construct + + if not PYDANTIC_V2: + # we define aliases for some of the new pydantic v2 methods so + # that we can just document these methods without having to specify + # a specific pydantic version as some users may not know which + # pydantic version they are currently using + + @override + def model_dump( + self, + *, + mode: Literal["json", "python"] | str = "python", + include: IncEx = None, + exclude: IncEx = None, + by_alias: bool = False, + exclude_unset: bool = False, + exclude_defaults: bool = False, + exclude_none: bool = False, + round_trip: bool = False, + warnings: bool = True, + ) -> dict[str, Any]: + """Usage docs: https://docs.pydantic.dev/2.4/concepts/serialization/#modelmodel_dump + + Generate a dictionary representation of the model, optionally specifying which fields to include or exclude. + + Args: + mode: The mode in which `to_python` should run. + If mode is 'json', the dictionary will only contain JSON serializable types. + If mode is 'python', the dictionary may contain any Python objects. + include: A list of fields to include in the output. + exclude: A list of fields to exclude from the output. + by_alias: Whether to use the field's alias in the dictionary key if defined. + exclude_unset: Whether to exclude fields that are unset or None from the output. + exclude_defaults: Whether to exclude fields that are set to their default value from the output. + exclude_none: Whether to exclude fields that have a value of `None` from the output. + round_trip: Whether to enable serialization and deserialization round-trip support. + warnings: Whether to log warnings when invalid fields are encountered. + + Returns: + A dictionary representation of the model. + """ + if mode != "python": + raise ValueError("mode is only supported in Pydantic v2") + if round_trip != False: + raise ValueError("round_trip is only supported in Pydantic v2") + if warnings != True: + raise ValueError("warnings is only supported in Pydantic v2") + return super().dict( # pyright: ignore[reportDeprecated] + include=include, + exclude=exclude, + by_alias=by_alias, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + ) + + @override + def model_dump_json( + self, + *, + indent: int | None = None, + include: IncEx = None, + exclude: IncEx = None, + by_alias: bool = False, + exclude_unset: bool = False, + exclude_defaults: bool = False, + exclude_none: bool = False, + round_trip: bool = False, + warnings: bool = True, + ) -> str: + """Usage docs: https://docs.pydantic.dev/2.4/concepts/serialization/#modelmodel_dump_json + + Generates a JSON representation of the model using Pydantic's `to_json` method. + + Args: + indent: Indentation to use in the JSON output. If None is passed, the output will be compact. + include: Field(s) to include in the JSON output. Can take either a string or set of strings. + exclude: Field(s) to exclude from the JSON output. Can take either a string or set of strings. + by_alias: Whether to serialize using field aliases. + exclude_unset: Whether to exclude fields that have not been explicitly set. + exclude_defaults: Whether to exclude fields that have the default value. + exclude_none: Whether to exclude fields that have a value of `None`. + round_trip: Whether to use serialization/deserialization between JSON and class instance. + warnings: Whether to show any warnings that occurred during serialization. + + Returns: + A JSON string representation of the model. + """ + if round_trip != False: + raise ValueError("round_trip is only supported in Pydantic v2") + if warnings != True: + raise ValueError("warnings is only supported in Pydantic v2") + return super().json( # type: ignore[reportDeprecated] + indent=indent, + include=include, + exclude=exclude, + by_alias=by_alias, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + ) + + +def _construct_field(value: object, field: FieldInfo, key: str) -> object: + if value is None: + return field_get_default(field) + + if PYDANTIC_V2: + type_ = field.annotation + else: + type_ = cast(type, field.outer_type_) # type: ignore + + if type_ is None: + raise RuntimeError(f"Unexpected field type is None for {key}") + + return construct_type(value=value, type_=type_) + + +def is_basemodel(type_: type) -> bool: + """Returns whether or not the given type is either a `BaseModel` or a union of `BaseModel`""" + if is_union(type_): + for variant in get_args(type_): + if is_basemodel(variant): + return True + + return False + + return is_basemodel_type(type_) + + +def is_basemodel_type(type_: type) -> TypeGuard[type[BaseModel] | type[GenericModel]]: + origin = get_origin(type_) or type_ + return issubclass(origin, BaseModel) or issubclass(origin, GenericModel) + + +def construct_type(*, value: object, type_: object) -> object: + """Loose coercion to the expected type with construction of nested values. + + If the given value does not match the expected type then it is returned as-is. + """ + # we allow `object` as the input type because otherwise, passing things like + # `Literal['value']` will be reported as a type error by type checkers + type_ = cast("type[object]", type_) + + # unwrap `Annotated[T, ...]` -> `T` + if is_annotated_type(type_): + meta = get_args(type_)[1:] + type_ = extract_type_arg(type_, 0) + else: + meta = tuple() + + # we need to use the origin class for any types that are subscripted generics + # e.g. Dict[str, object] + origin = get_origin(type_) or type_ + args = get_args(type_) + + if is_union(origin): + try: + return validate_type(type_=cast("type[object]", type_), value=value) + except Exception: + pass + + # if the type is a discriminated union then we want to construct the right variant + # in the union, even if the data doesn't match exactly, otherwise we'd break code + # that relies on the constructed class types, e.g. + # + # class FooType: + # kind: Literal['foo'] + # value: str + # + # class BarType: + # kind: Literal['bar'] + # value: int + # + # without this block, if the data we get is something like `{'kind': 'bar', 'value': 'foo'}` then + # we'd end up constructing `FooType` when it should be `BarType`. + discriminator = _build_discriminated_union_meta(union=type_, meta_annotations=meta) + if discriminator and is_mapping(value): + variant_value = value.get(discriminator.field_alias_from or discriminator.field_name) + if variant_value and isinstance(variant_value, str): + variant_type = discriminator.mapping.get(variant_value) + if variant_type: + return construct_type(type_=variant_type, value=value) + + # if the data is not valid, use the first variant that doesn't fail while deserializing + for variant in args: + try: + return construct_type(value=value, type_=variant) + except Exception: + continue + + raise RuntimeError(f"Could not convert data into a valid instance of {type_}") + + if origin == dict: + if not is_mapping(value): + return value + + _, items_type = get_args(type_) # Dict[_, items_type] + return {key: construct_type(value=item, type_=items_type) for key, item in value.items()} + + if not is_literal_type(type_) and (issubclass(origin, BaseModel) or issubclass(origin, GenericModel)): + if is_list(value): + return [cast(Any, type_).construct(**entry) if is_mapping(entry) else entry for entry in value] + + if is_mapping(value): + if issubclass(type_, BaseModel): + return type_.construct(**value) # type: ignore[arg-type] + + return cast(Any, type_).construct(**value) + + if origin == list: + if not is_list(value): + return value + + inner_type = args[0] # List[inner_type] + return [construct_type(value=entry, type_=inner_type) for entry in value] + + if origin == float: + if isinstance(value, int): + coerced = float(value) + if coerced != value: + return value + return coerced + + return value + + if type_ == datetime: + try: + return parse_datetime(value) # type: ignore + except Exception: + return value + + if type_ == date: + try: + return parse_date(value) # type: ignore + except Exception: + return value + + return value + + +@runtime_checkable +class CachedDiscriminatorType(Protocol): + __discriminator__: DiscriminatorDetails + + +class DiscriminatorDetails: + field_name: str + """The name of the discriminator field in the variant class, e.g. + + ```py + class Foo(BaseModel): + type: Literal['foo'] + ``` + + Will result in field_name='type' + """ + + field_alias_from: str | None + """The name of the discriminator field in the API response, e.g. + + ```py + class Foo(BaseModel): + type: Literal['foo'] = Field(alias='type_from_api') + ``` + + Will result in field_alias_from='type_from_api' + """ + + mapping: dict[str, type] + """Mapping of discriminator value to variant type, e.g. + + {'foo': FooVariant, 'bar': BarVariant} + """ + + def __init__( + self, + *, + mapping: dict[str, type], + discriminator_field: str, + discriminator_alias: str | None, + ) -> None: + self.mapping = mapping + self.field_name = discriminator_field + self.field_alias_from = discriminator_alias + + +def _build_discriminated_union_meta(*, union: type, meta_annotations: tuple[Any, ...]) -> DiscriminatorDetails | None: + if isinstance(union, CachedDiscriminatorType): + return union.__discriminator__ + + discriminator_field_name: str | None = None + + for annotation in meta_annotations: + if isinstance(annotation, PropertyInfo) and annotation.discriminator is not None: + discriminator_field_name = annotation.discriminator + break + + if not discriminator_field_name: + return None + + mapping: dict[str, type] = {} + discriminator_alias: str | None = None + + for variant in get_args(union): + variant = strip_annotated_type(variant) + if is_basemodel_type(variant): + if PYDANTIC_V2: + field = _extract_field_schema_pv2(variant, discriminator_field_name) + if not field: + continue + + # Note: if one variant defines an alias then they all should + discriminator_alias = field.get("serialization_alias") + + field_schema = field["schema"] + + if field_schema["type"] == "literal": + for entry in field_schema["expected"]: + if isinstance(entry, str): + mapping[entry] = variant + else: + field_info = cast("dict[str, FieldInfo]", variant.__fields__).get(discriminator_field_name) # pyright: ignore[reportDeprecated, reportUnnecessaryCast] + if not field_info: + continue + + # Note: if one variant defines an alias then they all should + discriminator_alias = field_info.alias + + if field_info.annotation and is_literal_type(field_info.annotation): + for entry in get_args(field_info.annotation): + if isinstance(entry, str): + mapping[entry] = variant + + if not mapping: + return None + + details = DiscriminatorDetails( + mapping=mapping, + discriminator_field=discriminator_field_name, + discriminator_alias=discriminator_alias, + ) + cast(CachedDiscriminatorType, union).__discriminator__ = details + return details + + +def _extract_field_schema_pv2(model: type[BaseModel], field_name: str) -> ModelField | None: + schema = model.__pydantic_core_schema__ + if schema["type"] != "model": + return None + + fields_schema = schema["schema"] + if fields_schema["type"] != "model-fields": + return None + + fields_schema = cast("ModelFieldsSchema", fields_schema) + + field = fields_schema["fields"].get(field_name) + if not field: + return None + + return cast("ModelField", field) # pyright: ignore[reportUnnecessaryCast] + + +def validate_type(*, type_: type[_T], value: object) -> _T: + """Strict validation that the given value matches the expected type""" + if inspect.isclass(type_) and issubclass(type_, pydantic.BaseModel): + return cast(_T, parse_obj(type_, value)) + + return cast(_T, _validate_non_model_type(type_=type_, value=value)) + + +# our use of subclasssing here causes weirdness for type checkers, +# so we just pretend that we don't subclass +if TYPE_CHECKING: + GenericModel = BaseModel +else: + + class GenericModel(BaseGenericModel, BaseModel): + pass + + +if PYDANTIC_V2: + from pydantic import TypeAdapter as _TypeAdapter + + _CachedTypeAdapter = cast("TypeAdapter[object]", lru_cache(maxsize=None)(_TypeAdapter)) + + if TYPE_CHECKING: + from pydantic import TypeAdapter + else: + TypeAdapter = _CachedTypeAdapter + + def _validate_non_model_type(*, type_: type[_T], value: object) -> _T: + return TypeAdapter(type_).validate_python(value) + +elif not TYPE_CHECKING: # TODO: condition is weird + + class RootModel(GenericModel, Generic[_T]): + """Used as a placeholder to easily convert runtime types to a Pydantic format + to provide validation. + + For example: + ```py + validated = RootModel[int](__root__="5").__root__ + # validated: 5 + ``` + """ + + __root__: _T + + def _validate_non_model_type(*, type_: type[_T], value: object) -> _T: + model = _create_pydantic_model(type_).validate(value) + return cast(_T, model.__root__) + + def _create_pydantic_model(type_: _T) -> Type[RootModel[_T]]: + return RootModel[type_] # type: ignore + + +class FinalRequestOptionsInput(TypedDict, total=False): + method: Required[str] + url: Required[str] + params: Query + headers: Headers + max_retries: int + timeout: float | Timeout | None + files: HttpxRequestFiles | None + idempotency_key: str + json_data: Body + extra_json: AnyMapping + + +@final +class FinalRequestOptions(pydantic.BaseModel): + method: str + url: str + params: Query = {} + headers: Union[Headers, NotGiven] = NotGiven() + max_retries: Union[int, NotGiven] = NotGiven() + timeout: Union[float, Timeout, None, NotGiven] = NotGiven() + files: Union[HttpxRequestFiles, None] = None + idempotency_key: Union[str, None] = None + post_parser: Union[Callable[[Any], Any], NotGiven] = NotGiven() + + # It should be noted that we cannot use `json` here as that would override + # a BaseModel method in an incompatible fashion. + json_data: Union[Body, None] = None + extra_json: Union[AnyMapping, None] = None + + if PYDANTIC_V2: + model_config: ClassVar[ConfigDict] = ConfigDict(arbitrary_types_allowed=True) + else: + + class Config(pydantic.BaseConfig): # pyright: ignore[reportDeprecated] + arbitrary_types_allowed: bool = True + + def get_max_retries(self, max_retries: int) -> int: + if isinstance(self.max_retries, NotGiven): + return max_retries + return self.max_retries + + def _strip_raw_response_header(self) -> None: + if not is_given(self.headers): + return + + if self.headers.get(RAW_RESPONSE_HEADER): + self.headers = {**self.headers} + self.headers.pop(RAW_RESPONSE_HEADER) + + # override the `construct` method so that we can run custom transformations. + # this is necessary as we don't want to do any actual runtime type checking + # (which means we can't use validators) but we do want to ensure that `NotGiven` + # values are not present + # + # type ignore required because we're adding explicit types to `**values` + @classmethod + def construct( # type: ignore + cls, + _fields_set: set[str] | None = None, + **values: Unpack[FinalRequestOptionsInput], + ) -> FinalRequestOptions: + kwargs: dict[str, Any] = { + # we unconditionally call `strip_not_given` on any value + # as it will just ignore any non-mapping types + key: strip_not_given(value) + for key, value in values.items() + } + if PYDANTIC_V2: + return super().model_construct(_fields_set, **kwargs) + return cast(FinalRequestOptions, super().construct(_fields_set, **kwargs)) # pyright: ignore[reportDeprecated] + + if not TYPE_CHECKING: + # type checkers incorrectly complain about this assignment + model_construct = construct diff --git a/openai/_module_client.py b/openai/_module_client.py new file mode 100644 index 00000000..9227f5e2 --- /dev/null +++ b/openai/_module_client.py @@ -0,0 +1,78 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing_extensions import override + +from . import resources, _load_client +from ._utils import LazyProxy + + +class ChatProxy(LazyProxy[resources.Chat]): + @override + def __load__(self) -> resources.Chat: + return _load_client().chat + + +class BetaProxy(LazyProxy[resources.Beta]): + @override + def __load__(self) -> resources.Beta: + return _load_client().beta + + +class FilesProxy(LazyProxy[resources.Files]): + @override + def __load__(self) -> resources.Files: + return _load_client().files + + +class AudioProxy(LazyProxy[resources.Audio]): + @override + def __load__(self) -> resources.Audio: + return _load_client().audio + + +class ImagesProxy(LazyProxy[resources.Images]): + @override + def __load__(self) -> resources.Images: + return _load_client().images + + +class ModelsProxy(LazyProxy[resources.Models]): + @override + def __load__(self) -> resources.Models: + return _load_client().models + + +class EmbeddingsProxy(LazyProxy[resources.Embeddings]): + @override + def __load__(self) -> resources.Embeddings: + return _load_client().embeddings + + +class CompletionsProxy(LazyProxy[resources.Completions]): + @override + def __load__(self) -> resources.Completions: + return _load_client().completions + + +class ModerationsProxy(LazyProxy[resources.Moderations]): + @override + def __load__(self) -> resources.Moderations: + return _load_client().moderations + + +class FineTuningProxy(LazyProxy[resources.FineTuning]): + @override + def __load__(self) -> resources.FineTuning: + return _load_client().fine_tuning + + +chat: resources.Chat = ChatProxy().__as_proxied__() +beta: resources.Beta = BetaProxy().__as_proxied__() +files: resources.Files = FilesProxy().__as_proxied__() +audio: resources.Audio = AudioProxy().__as_proxied__() +images: resources.Images = ImagesProxy().__as_proxied__() +models: resources.Models = ModelsProxy().__as_proxied__() +embeddings: resources.Embeddings = EmbeddingsProxy().__as_proxied__() +completions: resources.Completions = CompletionsProxy().__as_proxied__() +moderations: resources.Moderations = ModerationsProxy().__as_proxied__() +fine_tuning: resources.FineTuning = FineTuningProxy().__as_proxied__() diff --git a/openai/_qs.py b/openai/_qs.py new file mode 100644 index 00000000..274320ca --- /dev/null +++ b/openai/_qs.py @@ -0,0 +1,150 @@ +from __future__ import annotations + +from typing import Any, List, Tuple, Union, Mapping, TypeVar +from urllib.parse import parse_qs, urlencode +from typing_extensions import Literal, get_args + +from ._types import NOT_GIVEN, NotGiven, NotGivenOr +from ._utils import flatten + +_T = TypeVar("_T") + + +ArrayFormat = Literal["comma", "repeat", "indices", "brackets"] +NestedFormat = Literal["dots", "brackets"] + +PrimitiveData = Union[str, int, float, bool, None] +# this should be Data = Union[PrimitiveData, "List[Data]", "Tuple[Data]", "Mapping[str, Data]"] +# https://github.com/microsoft/pyright/issues/3555 +Data = Union[PrimitiveData, List[Any], Tuple[Any], "Mapping[str, Any]"] +Params = Mapping[str, Data] + + +class Querystring: + array_format: ArrayFormat + nested_format: NestedFormat + + def __init__( + self, + *, + array_format: ArrayFormat = "repeat", + nested_format: NestedFormat = "brackets", + ) -> None: + self.array_format = array_format + self.nested_format = nested_format + + def parse(self, query: str) -> Mapping[str, object]: + # Note: custom format syntax is not supported yet + return parse_qs(query) + + def stringify( + self, + params: Params, + *, + array_format: NotGivenOr[ArrayFormat] = NOT_GIVEN, + nested_format: NotGivenOr[NestedFormat] = NOT_GIVEN, + ) -> str: + return urlencode( + self.stringify_items( + params, + array_format=array_format, + nested_format=nested_format, + ) + ) + + def stringify_items( + self, + params: Params, + *, + array_format: NotGivenOr[ArrayFormat] = NOT_GIVEN, + nested_format: NotGivenOr[NestedFormat] = NOT_GIVEN, + ) -> list[tuple[str, str]]: + opts = Options( + qs=self, + array_format=array_format, + nested_format=nested_format, + ) + return flatten([self._stringify_item(key, value, opts) for key, value in params.items()]) + + def _stringify_item( + self, + key: str, + value: Data, + opts: Options, + ) -> list[tuple[str, str]]: + if isinstance(value, Mapping): + items: list[tuple[str, str]] = [] + nested_format = opts.nested_format + for subkey, subvalue in value.items(): + items.extend( + self._stringify_item( + # TODO: error if unknown format + f"{key}.{subkey}" if nested_format == "dots" else f"{key}[{subkey}]", + subvalue, + opts, + ) + ) + return items + + if isinstance(value, (list, tuple)): + array_format = opts.array_format + if array_format == "comma": + return [ + ( + key, + ",".join(self._primitive_value_to_str(item) for item in value if item is not None), + ), + ] + elif array_format == "repeat": + items = [] + for item in value: + items.extend(self._stringify_item(key, item, opts)) + return items + elif array_format == "indices": + raise NotImplementedError("The array indices format is not supported yet") + elif array_format == "brackets": + items = [] + key = key + "[]" + for item in value: + items.extend(self._stringify_item(key, item, opts)) + return items + else: + raise NotImplementedError( + f"Unknown array_format value: {array_format}, choose from {', '.join(get_args(ArrayFormat))}" + ) + + serialised = self._primitive_value_to_str(value) + if not serialised: + return [] + return [(key, serialised)] + + def _primitive_value_to_str(self, value: PrimitiveData) -> str: + # copied from httpx + if value is True: + return "true" + elif value is False: + return "false" + elif value is None: + return "" + return str(value) + + +_qs = Querystring() +parse = _qs.parse +stringify = _qs.stringify +stringify_items = _qs.stringify_items + + +class Options: + array_format: ArrayFormat + nested_format: NestedFormat + + def __init__( + self, + qs: Querystring = _qs, + *, + array_format: NotGivenOr[ArrayFormat] = NOT_GIVEN, + nested_format: NotGivenOr[NestedFormat] = NOT_GIVEN, + ) -> None: + self.array_format = qs.array_format if isinstance(array_format, NotGiven) else array_format + self.nested_format = qs.nested_format if isinstance(nested_format, NotGiven) else nested_format diff --git a/openai/_resource.py b/openai/_resource.py new file mode 100644 index 00000000..fff9ba19 --- /dev/null +++ b/openai/_resource.py @@ -0,0 +1,43 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import time +from typing import TYPE_CHECKING + +import anyio + +if TYPE_CHECKING: + from ._client import OpenAI, AsyncOpenAI + + +class SyncAPIResource: + _client: OpenAI + + def __init__(self, client: OpenAI) -> None: + self._client = client + self._get = client.get + self._post = client.post + self._patch = client.patch + self._put = client.put + self._delete = client.delete + self._get_api_list = client.get_api_list + + def _sleep(self, seconds: float) -> None: + time.sleep(seconds) + + +class AsyncAPIResource: + _client: AsyncOpenAI + + def __init__(self, client: AsyncOpenAI) -> None: + self._client = client + self._get = client.get + self._post = client.post + self._patch = client.patch + self._put = client.put + self._delete = client.delete + self._get_api_list = client.get_api_list + + async def _sleep(self, seconds: float) -> None: + await anyio.sleep(seconds) diff --git a/openai/_response.py b/openai/_response.py new file mode 100644 index 00000000..47f484ef --- /dev/null +++ b/openai/_response.py @@ -0,0 +1,824 @@ +from __future__ import annotations + +import os +import inspect +import logging +import datetime +import functools +from types import TracebackType +from typing import ( + TYPE_CHECKING, + Any, + Union, + Generic, + TypeVar, + Callable, + Iterator, + AsyncIterator, + cast, + overload, +) +from typing_extensions import Awaitable, ParamSpec, override, get_origin + +import anyio +import httpx +import pydantic + +from ._types import NoneType +from ._utils import is_given, extract_type_arg, is_annotated_type, extract_type_var_from_base +from ._models import BaseModel, is_basemodel +from ._constants import RAW_RESPONSE_HEADER, OVERRIDE_CAST_TO_HEADER +from ._streaming import Stream, AsyncStream, is_stream_class_type, extract_stream_chunk_type +from ._exceptions import OpenAIError, APIResponseValidationError + +if TYPE_CHECKING: + from ._models import FinalRequestOptions + from ._base_client import BaseClient + + +P = ParamSpec("P") +R = TypeVar("R") +_T = TypeVar("_T") +_APIResponseT = TypeVar("_APIResponseT", bound="APIResponse[Any]") +_AsyncAPIResponseT = TypeVar("_AsyncAPIResponseT", bound="AsyncAPIResponse[Any]") + +log: logging.Logger = logging.getLogger(__name__) + + +class BaseAPIResponse(Generic[R]): + _cast_to: type[R] + _client: BaseClient[Any, Any] + _parsed_by_type: dict[type[Any], Any] + _is_sse_stream: bool + _stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None + _options: FinalRequestOptions + + http_response: httpx.Response + + def __init__( + self, + *, + raw: httpx.Response, + cast_to: type[R], + client: BaseClient[Any, Any], + stream: bool, + stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None, + options: FinalRequestOptions, + ) -> None: + self._cast_to = cast_to + self._client = client + self._parsed_by_type = {} + self._is_sse_stream = stream + self._stream_cls = stream_cls + self._options = options + self.http_response = raw + + @property + def headers(self) -> httpx.Headers: + return self.http_response.headers + + @property + def http_request(self) -> httpx.Request: + """Returns the httpx Request instance associated with the current response.""" + return self.http_response.request + + @property + def status_code(self) -> int: + return self.http_response.status_code + + @property + def url(self) -> httpx.URL: + """Returns the URL for which the request was made.""" + return self.http_response.url + + @property + def method(self) -> str: + return self.http_request.method + + @property + def http_version(self) -> str: + return self.http_response.http_version + + @property + def elapsed(self) -> datetime.timedelta: + """The time taken for the complete request/response cycle to complete.""" + return self.http_response.elapsed + + @property + def is_closed(self) -> bool: + """Whether or not the response body has been closed. + + If this is False then there is response data that has not been read yet. + You must either fully consume the response body or call `.close()` + before discarding the response to prevent resource leaks. + """ + return self.http_response.is_closed + + @override + def __repr__(self) -> str: + return ( + f"<{self.__class__.__name__} [{self.status_code} {self.http_response.reason_phrase}] type={self._cast_to}>" + ) + + def _parse(self, *, to: type[_T] | None = None) -> R | _T: + # unwrap `Annotated[T, ...]` -> `T` + if to and is_annotated_type(to): + to = extract_type_arg(to, 0) + + if self._is_sse_stream: + if to: + if not is_stream_class_type(to): + raise TypeError(f"Expected custom parse type to be a subclass of {Stream} or {AsyncStream}") + + return cast( + _T, + to( + cast_to=extract_stream_chunk_type( + to, + failure_message="Expected custom stream type to be passed with a type argument, e.g. Stream[ChunkType]", + ), + response=self.http_response, + client=cast(Any, self._client), + ), + ) + + if self._stream_cls: + return cast( + R, + self._stream_cls( + cast_to=extract_stream_chunk_type(self._stream_cls), + response=self.http_response, + client=cast(Any, self._client), + ), + ) + + stream_cls = cast("type[Stream[Any]] | type[AsyncStream[Any]] | None", self._client._default_stream_cls) + if stream_cls is None: + raise MissingStreamClassError() + + return cast( + R, + stream_cls( + cast_to=self._cast_to, + response=self.http_response, + client=cast(Any, self._client), + ), + ) + + cast_to = to if to is not None else self._cast_to + + # unwrap `Annotated[T, ...]` -> `T` + if is_annotated_type(cast_to): + cast_to = extract_type_arg(cast_to, 0) + + if cast_to is NoneType: + return cast(R, None) + + response = self.http_response + if cast_to == str: + return cast(R, response.text) + + if cast_to == bytes: + return cast(R, response.content) + + if cast_to == int: + return cast(R, int(response.text)) + + if cast_to == float: + return cast(R, float(response.text)) + + origin = get_origin(cast_to) or cast_to + + # handle the legacy binary response case + if inspect.isclass(cast_to) and cast_to.__name__ == "HttpxBinaryResponseContent": + return cast(R, cast_to(response)) # type: ignore + + if origin == APIResponse: + raise RuntimeError("Unexpected state - cast_to is `APIResponse`") + + if inspect.isclass(origin) and issubclass(origin, httpx.Response): + # Because of the invariance of our ResponseT TypeVar, users can subclass httpx.Response + # and pass that class to our request functions. We cannot change the variance to be either + # covariant or contravariant as that makes our usage of ResponseT illegal. We could construct + # the response class ourselves but that is something that should be supported directly in httpx + # as it would be easy to incorrectly construct the Response object due to the multitude of arguments. + if cast_to != httpx.Response: + raise ValueError(f"Subclasses of httpx.Response cannot be passed to `cast_to`") + return cast(R, response) + + if inspect.isclass(origin) and not issubclass(origin, BaseModel) and issubclass(origin, pydantic.BaseModel): + raise TypeError("Pydantic models must subclass our base model type, e.g. `from openai import BaseModel`") + + if ( + cast_to is not object + and not origin is list + and not origin is dict + and not origin is Union + and not issubclass(origin, BaseModel) + ): + raise RuntimeError( + f"Unsupported type, expected {cast_to} to be a subclass of {BaseModel}, {dict}, {list}, {Union}, {NoneType}, {str} or {httpx.Response}." + ) + + # split is required to handle cases where additional information is included + # in the response, e.g. application/json; charset=utf-8 + content_type, *_ = response.headers.get("content-type", "*").split(";") + if content_type != "application/json": + if is_basemodel(cast_to): + try: + data = response.json() + except Exception as exc: + log.debug("Could not read JSON from response data due to %s - %s", type(exc), exc) + else: + return self._client._process_response_data( + data=data, + cast_to=cast_to, # type: ignore + response=response, + ) + + if self._client._strict_response_validation: + raise APIResponseValidationError( + response=response, + message=f"Expected Content-Type response header to be `application/json` but received `{content_type}` instead.", + body=response.text, + ) + + # If the API responds with content that isn't JSON then we just return + # the (decoded) text without performing any parsing so that you can still + # handle the response however you need to. + return response.text # type: ignore + + data = response.json() + + return self._client._process_response_data( + data=data, + cast_to=cast_to, # type: ignore + response=response, + ) + + +class APIResponse(BaseAPIResponse[R]): + @overload + def parse(self, *, to: type[_T]) -> _T: + ... + + @overload + def parse(self) -> R: + ... + + def parse(self, *, to: type[_T] | None = None) -> R | _T: + """Returns the rich python representation of this response's data. + + For lower-level control, see `.read()`, `.json()`, `.iter_bytes()`. + + You can customise the type that the response is parsed into through + the `to` argument, e.g. + + ```py + from openai import BaseModel + + + class MyModel(BaseModel): + foo: str + + + obj = response.parse(to=MyModel) + print(obj.foo) + ``` + + We support parsing: + - `BaseModel` + - `dict` + - `list` + - `Union` + - `str` + - `int` + - `float` + - `httpx.Response` + """ + cache_key = to if to is not None else self._cast_to + cached = self._parsed_by_type.get(cache_key) + if cached is not None: + return cached # type: ignore[no-any-return] + + if not self._is_sse_stream: + self.read() + + parsed = self._parse(to=to) + if is_given(self._options.post_parser): + parsed = self._options.post_parser(parsed) + + self._parsed_by_type[cache_key] = parsed + return parsed + + def read(self) -> bytes: + """Read and return the binary response content.""" + try: + return self.http_response.read() + except httpx.StreamConsumed as exc: + # The default error raised by httpx isn't very + # helpful in our case so we re-raise it with + # a different error message. + raise StreamAlreadyConsumed() from exc + + def text(self) -> str: + """Read and decode the response content into a string.""" + self.read() + return self.http_response.text + + def json(self) -> object: + """Read and decode the JSON response content.""" + self.read() + return self.http_response.json() + + def close(self) -> None: + """Close the response and release the connection. + + Automatically called if the response body is read to completion. + """ + self.http_response.close() + + def iter_bytes(self, chunk_size: int | None = None) -> Iterator[bytes]: + """ + A byte-iterator over the decoded response content. + + This automatically handles gzip, deflate and brotli encoded responses. + """ + for chunk in self.http_response.iter_bytes(chunk_size): + yield chunk + + def iter_text(self, chunk_size: int | None = None) -> Iterator[str]: + """A str-iterator over the decoded response content + that handles both gzip, deflate, etc but also detects the content's + string encoding. + """ + for chunk in self.http_response.iter_text(chunk_size): + yield chunk + + def iter_lines(self) -> Iterator[str]: + """Like `iter_text()` but will only yield chunks for each line""" + for chunk in self.http_response.iter_lines(): + yield chunk + + +class AsyncAPIResponse(BaseAPIResponse[R]): + @overload + async def parse(self, *, to: type[_T]) -> _T: + ... + + @overload + async def parse(self) -> R: + ... + + async def parse(self, *, to: type[_T] | None = None) -> R | _T: + """Returns the rich python representation of this response's data. + + For lower-level control, see `.read()`, `.json()`, `.iter_bytes()`. + + You can customise the type that the response is parsed into through + the `to` argument, e.g. + + ```py + from openai import BaseModel + + + class MyModel(BaseModel): + foo: str + + + obj = response.parse(to=MyModel) + print(obj.foo) + ``` + + We support parsing: + - `BaseModel` + - `dict` + - `list` + - `Union` + - `str` + - `httpx.Response` + """ + cache_key = to if to is not None else self._cast_to + cached = self._parsed_by_type.get(cache_key) + if cached is not None: + return cached # type: ignore[no-any-return] + + if not self._is_sse_stream: + await self.read() + + parsed = self._parse(to=to) + if is_given(self._options.post_parser): + parsed = self._options.post_parser(parsed) + + self._parsed_by_type[cache_key] = parsed + return parsed + + async def read(self) -> bytes: + """Read and return the binary response content.""" + try: + return await self.http_response.aread() + except httpx.StreamConsumed as exc: + # the default error raised by httpx isn't very + # helpful in our case so we re-raise it with + # a different error message + raise StreamAlreadyConsumed() from exc + + async def text(self) -> str: + """Read and decode the response content into a string.""" + await self.read() + return self.http_response.text + + async def json(self) -> object: + """Read and decode the JSON response content.""" + await self.read() + return self.http_response.json() + + async def close(self) -> None: + """Close the response and release the connection. + + Automatically called if the response body is read to completion. + """ + await self.http_response.aclose() + + async def iter_bytes(self, chunk_size: int | None = None) -> AsyncIterator[bytes]: + """ + A byte-iterator over the decoded response content. + + This automatically handles gzip, deflate and brotli encoded responses. + """ + async for chunk in self.http_response.aiter_bytes(chunk_size): + yield chunk + + async def iter_text(self, chunk_size: int | None = None) -> AsyncIterator[str]: + """A str-iterator over the decoded response content + that handles both gzip, deflate, etc but also detects the content's + string encoding. + """ + async for chunk in self.http_response.aiter_text(chunk_size): + yield chunk + + async def iter_lines(self) -> AsyncIterator[str]: + """Like `iter_text()` but will only yield chunks for each line""" + async for chunk in self.http_response.aiter_lines(): + yield chunk + + +class BinaryAPIResponse(APIResponse[bytes]): + """Subclass of APIResponse providing helpers for dealing with binary data. + + Note: If you want to stream the response data instead of eagerly reading it + all at once then you should use `.with_streaming_response` when making + the API request, e.g. `.with_streaming_response.get_binary_response()` + """ + + def write_to_file( + self, + file: str | os.PathLike[str], + ) -> None: + """Write the output to the given file. + + Accepts a filename or any path-like object, e.g. pathlib.Path + + Note: if you want to stream the data to the file instead of writing + all at once then you should use `.with_streaming_response` when making + the API request, e.g. `.with_streaming_response.get_binary_response()` + """ + with open(file, mode="wb") as f: + for data in self.iter_bytes(): + f.write(data) + + +class AsyncBinaryAPIResponse(AsyncAPIResponse[bytes]): + """Subclass of APIResponse providing helpers for dealing with binary data. + + Note: If you want to stream the response data instead of eagerly reading it + all at once then you should use `.with_streaming_response` when making + the API request, e.g. `.with_streaming_response.get_binary_response()` + """ + + async def write_to_file( + self, + file: str | os.PathLike[str], + ) -> None: + """Write the output to the given file. + + Accepts a filename or any path-like object, e.g. pathlib.Path + + Note: if you want to stream the data to the file instead of writing + all at once then you should use `.with_streaming_response` when making + the API request, e.g. `.with_streaming_response.get_binary_response()` + """ + path = anyio.Path(file) + async with await path.open(mode="wb") as f: + async for data in self.iter_bytes(): + await f.write(data) + + +class StreamedBinaryAPIResponse(APIResponse[bytes]): + def stream_to_file( + self, + file: str | os.PathLike[str], + *, + chunk_size: int | None = None, + ) -> None: + """Streams the output to the given file. + + Accepts a filename or any path-like object, e.g. pathlib.Path + """ + with open(file, mode="wb") as f: + for data in self.iter_bytes(chunk_size): + f.write(data) + + +class AsyncStreamedBinaryAPIResponse(AsyncAPIResponse[bytes]): + async def stream_to_file( + self, + file: str | os.PathLike[str], + *, + chunk_size: int | None = None, + ) -> None: + """Streams the output to the given file. + + Accepts a filename or any path-like object, e.g. pathlib.Path + """ + path = anyio.Path(file) + async with await path.open(mode="wb") as f: + async for data in self.iter_bytes(chunk_size): + await f.write(data) + + +class MissingStreamClassError(TypeError): + def __init__(self) -> None: + super().__init__( + "The `stream` argument was set to `True` but the `stream_cls` argument was not given. See `openai._streaming` for reference", + ) + + +class StreamAlreadyConsumed(OpenAIError): + """ + Attempted to read or stream content, but the content has already + been streamed. + + This can happen if you use a method like `.iter_lines()` and then attempt + to read th entire response body afterwards, e.g. + + ```py + response = await client.post(...) + async for line in response.iter_lines(): + ... # do something with `line` + + content = await response.read() + # ^ error + ``` + + If you want this behaviour you'll need to either manually accumulate the response + content or call `await response.read()` before iterating over the stream. + """ + + def __init__(self) -> None: + message = ( + "Attempted to read or stream some content, but the content has " + "already been streamed. " + "This could be due to attempting to stream the response " + "content more than once." + "\n\n" + "You can fix this by manually accumulating the response content while streaming " + "or by calling `.read()` before starting to stream." + ) + super().__init__(message) + + +class ResponseContextManager(Generic[_APIResponseT]): + """Context manager for ensuring that a request is not made + until it is entered and that the response will always be closed + when the context manager exits + """ + + def __init__(self, request_func: Callable[[], _APIResponseT]) -> None: + self._request_func = request_func + self.__response: _APIResponseT | None = None + + def __enter__(self) -> _APIResponseT: + self.__response = self._request_func() + return self.__response + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + if self.__response is not None: + self.__response.close() + + +class AsyncResponseContextManager(Generic[_AsyncAPIResponseT]): + """Context manager for ensuring that a request is not made + until it is entered and that the response will always be closed + when the context manager exits + """ + + def __init__(self, api_request: Awaitable[_AsyncAPIResponseT]) -> None: + self._api_request = api_request + self.__response: _AsyncAPIResponseT | None = None + + async def __aenter__(self) -> _AsyncAPIResponseT: + self.__response = await self._api_request + return self.__response + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + if self.__response is not None: + await self.__response.close() + + +def to_streamed_response_wrapper(func: Callable[P, R]) -> Callable[P, ResponseContextManager[APIResponse[R]]]: + """Higher order function that takes one of our bound API methods and wraps it + to support streaming and returning the raw `APIResponse` object directly. + """ + + @functools.wraps(func) + def wrapped(*args: P.args, **kwargs: P.kwargs) -> ResponseContextManager[APIResponse[R]]: + extra_headers: dict[str, str] = {**(cast(Any, kwargs.get("extra_headers")) or {})} + extra_headers[RAW_RESPONSE_HEADER] = "stream" + + kwargs["extra_headers"] = extra_headers + + make_request = functools.partial(func, *args, **kwargs) + + return ResponseContextManager(cast(Callable[[], APIResponse[R]], make_request)) + + return wrapped + + +def async_to_streamed_response_wrapper( + func: Callable[P, Awaitable[R]], +) -> Callable[P, AsyncResponseContextManager[AsyncAPIResponse[R]]]: + """Higher order function that takes one of our bound API methods and wraps it + to support streaming and returning the raw `APIResponse` object directly. + """ + + @functools.wraps(func) + def wrapped(*args: P.args, **kwargs: P.kwargs) -> AsyncResponseContextManager[AsyncAPIResponse[R]]: + extra_headers: dict[str, str] = {**(cast(Any, kwargs.get("extra_headers")) or {})} + extra_headers[RAW_RESPONSE_HEADER] = "stream" + + kwargs["extra_headers"] = extra_headers + + make_request = func(*args, **kwargs) + + return AsyncResponseContextManager(cast(Awaitable[AsyncAPIResponse[R]], make_request)) + + return wrapped + + +def to_custom_streamed_response_wrapper( + func: Callable[P, object], + response_cls: type[_APIResponseT], +) -> Callable[P, ResponseContextManager[_APIResponseT]]: + """Higher order function that takes one of our bound API methods and an `APIResponse` class + and wraps the method to support streaming and returning the given response class directly. + + Note: the given `response_cls` *must* be concrete, e.g. `class BinaryAPIResponse(APIResponse[bytes])` + """ + + @functools.wraps(func) + def wrapped(*args: P.args, **kwargs: P.kwargs) -> ResponseContextManager[_APIResponseT]: + extra_headers: dict[str, Any] = {**(cast(Any, kwargs.get("extra_headers")) or {})} + extra_headers[RAW_RESPONSE_HEADER] = "stream" + extra_headers[OVERRIDE_CAST_TO_HEADER] = response_cls + + kwargs["extra_headers"] = extra_headers + + make_request = functools.partial(func, *args, **kwargs) + + return ResponseContextManager(cast(Callable[[], _APIResponseT], make_request)) + + return wrapped + + +def async_to_custom_streamed_response_wrapper( + func: Callable[P, Awaitable[object]], + response_cls: type[_AsyncAPIResponseT], +) -> Callable[P, AsyncResponseContextManager[_AsyncAPIResponseT]]: + """Higher order function that takes one of our bound API methods and an `APIResponse` class + and wraps the method to support streaming and returning the given response class directly. + + Note: the given `response_cls` *must* be concrete, e.g. `class BinaryAPIResponse(APIResponse[bytes])` + """ + + @functools.wraps(func) + def wrapped(*args: P.args, **kwargs: P.kwargs) -> AsyncResponseContextManager[_AsyncAPIResponseT]: + extra_headers: dict[str, Any] = {**(cast(Any, kwargs.get("extra_headers")) or {})} + extra_headers[RAW_RESPONSE_HEADER] = "stream" + extra_headers[OVERRIDE_CAST_TO_HEADER] = response_cls + + kwargs["extra_headers"] = extra_headers + + make_request = func(*args, **kwargs) + + return AsyncResponseContextManager(cast(Awaitable[_AsyncAPIResponseT], make_request)) + + return wrapped + + +def to_raw_response_wrapper(func: Callable[P, R]) -> Callable[P, APIResponse[R]]: + """Higher order function that takes one of our bound API methods and wraps it + to support returning the raw `APIResponse` object directly. + """ + + @functools.wraps(func) + def wrapped(*args: P.args, **kwargs: P.kwargs) -> APIResponse[R]: + extra_headers: dict[str, str] = {**(cast(Any, kwargs.get("extra_headers")) or {})} + extra_headers[RAW_RESPONSE_HEADER] = "raw" + + kwargs["extra_headers"] = extra_headers + + return cast(APIResponse[R], func(*args, **kwargs)) + + return wrapped + + +def async_to_raw_response_wrapper(func: Callable[P, Awaitable[R]]) -> Callable[P, Awaitable[AsyncAPIResponse[R]]]: + """Higher order function that takes one of our bound API methods and wraps it + to support returning the raw `APIResponse` object directly. + """ + + @functools.wraps(func) + async def wrapped(*args: P.args, **kwargs: P.kwargs) -> AsyncAPIResponse[R]: + extra_headers: dict[str, str] = {**(cast(Any, kwargs.get("extra_headers")) or {})} + extra_headers[RAW_RESPONSE_HEADER] = "raw" + + kwargs["extra_headers"] = extra_headers + + return cast(AsyncAPIResponse[R], await func(*args, **kwargs)) + + return wrapped + + +def to_custom_raw_response_wrapper( + func: Callable[P, object], + response_cls: type[_APIResponseT], +) -> Callable[P, _APIResponseT]: + """Higher order function that takes one of our bound API methods and an `APIResponse` class + and wraps the method to support returning the given response class directly. + + Note: the given `response_cls` *must* be concrete, e.g. `class BinaryAPIResponse(APIResponse[bytes])` + """ + + @functools.wraps(func) + def wrapped(*args: P.args, **kwargs: P.kwargs) -> _APIResponseT: + extra_headers: dict[str, Any] = {**(cast(Any, kwargs.get("extra_headers")) or {})} + extra_headers[RAW_RESPONSE_HEADER] = "raw" + extra_headers[OVERRIDE_CAST_TO_HEADER] = response_cls + + kwargs["extra_headers"] = extra_headers + + return cast(_APIResponseT, func(*args, **kwargs)) + + return wrapped + + +def async_to_custom_raw_response_wrapper( + func: Callable[P, Awaitable[object]], + response_cls: type[_AsyncAPIResponseT], +) -> Callable[P, Awaitable[_AsyncAPIResponseT]]: + """Higher order function that takes one of our bound API methods and an `APIResponse` class + and wraps the method to support returning the given response class directly. + + Note: the given `response_cls` *must* be concrete, e.g. `class BinaryAPIResponse(APIResponse[bytes])` + """ + + @functools.wraps(func) + def wrapped(*args: P.args, **kwargs: P.kwargs) -> Awaitable[_AsyncAPIResponseT]: + extra_headers: dict[str, Any] = {**(cast(Any, kwargs.get("extra_headers")) or {})} + extra_headers[RAW_RESPONSE_HEADER] = "raw" + extra_headers[OVERRIDE_CAST_TO_HEADER] = response_cls + + kwargs["extra_headers"] = extra_headers + + return cast(Awaitable[_AsyncAPIResponseT], func(*args, **kwargs)) + + return wrapped + + +def extract_response_type(typ: type[BaseAPIResponse[Any]]) -> type: + """Given a type like `APIResponse[T]`, returns the generic type variable `T`. + + This also handles the case where a concrete subclass is given, e.g. + ```py + class MyResponse(APIResponse[bytes]): + ... + + extract_response_type(MyResponse) -> bytes + ``` + """ + return extract_type_var_from_base( + typ, + generic_bases=cast("tuple[type, ...]", (BaseAPIResponse, APIResponse, AsyncAPIResponse)), + index=0, + ) diff --git a/openai/_streaming.py b/openai/_streaming.py new file mode 100644 index 00000000..0fda992c --- /dev/null +++ b/openai/_streaming.py @@ -0,0 +1,410 @@ +# Note: initially copied from https://github.com/florimondmanca/httpx-sse/blob/master/src/httpx_sse/_decoders.py +from __future__ import annotations + +import json +import inspect +from types import TracebackType +from typing import TYPE_CHECKING, Any, Generic, TypeVar, Iterator, AsyncIterator, cast +from typing_extensions import Self, Protocol, TypeGuard, override, get_origin, runtime_checkable + +import httpx + +from ._utils import is_mapping, extract_type_var_from_base +from ._exceptions import APIError + +if TYPE_CHECKING: + from ._client import OpenAI, AsyncOpenAI + + +_T = TypeVar("_T") + + +class Stream(Generic[_T]): + """Provides the core interface to iterate over a synchronous stream response.""" + + response: httpx.Response + + _decoder: SSEBytesDecoder + + def __init__( + self, + *, + cast_to: type[_T], + response: httpx.Response, + client: OpenAI, + ) -> None: + self.response = response + self._cast_to = cast_to + self._client = client + self._decoder = client._make_sse_decoder() + self._iterator = self.__stream__() + + def __next__(self) -> _T: + return self._iterator.__next__() + + def __iter__(self) -> Iterator[_T]: + for item in self._iterator: + yield item + + def _iter_events(self) -> Iterator[ServerSentEvent]: + yield from self._decoder.iter_bytes(self.response.iter_bytes()) + + def __stream__(self) -> Iterator[_T]: + cast_to = cast(Any, self._cast_to) + response = self.response + process_data = self._client._process_response_data + iterator = self._iter_events() + + for sse in iterator: + if sse.data.startswith("[DONE]"): + break + + if sse.event is None: + data = sse.json() + if is_mapping(data) and data.get("error"): + message = None + error = data.get("error") + if is_mapping(error): + message = error.get("message") + if not message or not isinstance(message, str): + message = "An error occurred during streaming" + + raise APIError( + message=message, + request=self.response.request, + body=data["error"], + ) + + yield process_data(data=data, cast_to=cast_to, response=response) + + else: + data = sse.json() + + if sse.event == "error" and is_mapping(data) and data.get("error"): + message = None + error = data.get("error") + if is_mapping(error): + message = error.get("message") + if not message or not isinstance(message, str): + message = "An error occurred during streaming" + + raise APIError( + message=message, + request=self.response.request, + body=data["error"], + ) + + yield process_data(data={"data": data, "event": sse.event}, cast_to=cast_to, response=response) + + # Ensure the entire stream is consumed + for _sse in iterator: + ... + + def __enter__(self) -> Self: + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + self.close() + + def close(self) -> None: + """ + Close the response and release the connection. + + Automatically called if the response body is read to completion. + """ + self.response.close() + + +class AsyncStream(Generic[_T]): + """Provides the core interface to iterate over an asynchronous stream response.""" + + response: httpx.Response + + _decoder: SSEDecoder | SSEBytesDecoder + + def __init__( + self, + *, + cast_to: type[_T], + response: httpx.Response, + client: AsyncOpenAI, + ) -> None: + self.response = response + self._cast_to = cast_to + self._client = client + self._decoder = client._make_sse_decoder() + self._iterator = self.__stream__() + + async def __anext__(self) -> _T: + return await self._iterator.__anext__() + + async def __aiter__(self) -> AsyncIterator[_T]: + async for item in self._iterator: + yield item + + async def _iter_events(self) -> AsyncIterator[ServerSentEvent]: + async for sse in self._decoder.aiter_bytes(self.response.aiter_bytes()): + yield sse + + async def __stream__(self) -> AsyncIterator[_T]: + cast_to = cast(Any, self._cast_to) + response = self.response + process_data = self._client._process_response_data + iterator = self._iter_events() + + async for sse in iterator: + if sse.data.startswith("[DONE]"): + break + + if sse.event is None: + data = sse.json() + if is_mapping(data) and data.get("error"): + message = None + error = data.get("error") + if is_mapping(error): + message = error.get("message") + if not message or not isinstance(message, str): + message = "An error occurred during streaming" + + raise APIError( + message=message, + request=self.response.request, + body=data["error"], + ) + + yield process_data(data=data, cast_to=cast_to, response=response) + + else: + data = sse.json() + + if sse.event == "error" and is_mapping(data) and data.get("error"): + message = None + error = data.get("error") + if is_mapping(error): + message = error.get("message") + if not message or not isinstance(message, str): + message = "An error occurred during streaming" + + raise APIError( + message=message, + request=self.response.request, + body=data["error"], + ) + + yield process_data(data={"data": data, "event": sse.event}, cast_to=cast_to, response=response) + + # Ensure the entire stream is consumed + async for _sse in iterator: + ... + + async def __aenter__(self) -> Self: + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + await self.close() + + async def close(self) -> None: + """ + Close the response and release the connection. + + Automatically called if the response body is read to completion. + """ + await self.response.aclose() + + +class ServerSentEvent: + def __init__( + self, + *, + event: str | None = None, + data: str | None = None, + id: str | None = None, + retry: int | None = None, + ) -> None: + if data is None: + data = "" + + self._id = id + self._data = data + self._event = event or None + self._retry = retry + + @property + def event(self) -> str | None: + return self._event + + @property + def id(self) -> str | None: + return self._id + + @property + def retry(self) -> int | None: + return self._retry + + @property + def data(self) -> str: + return self._data + + def json(self) -> Any: + return json.loads(self.data) + + @override + def __repr__(self) -> str: + return f"ServerSentEvent(event={self.event}, data={self.data}, id={self.id}, retry={self.retry})" + + +class SSEDecoder: + _data: list[str] + _event: str | None + _retry: int | None + _last_event_id: str | None + + def __init__(self) -> None: + self._event = None + self._data = [] + self._last_event_id = None + self._retry = None + + def iter_bytes(self, iterator: Iterator[bytes]) -> Iterator[ServerSentEvent]: + """Given an iterator that yields raw binary data, iterate over it & yield every event encountered""" + for chunk in self._iter_chunks(iterator): + # Split before decoding so splitlines() only uses \r and \n + for raw_line in chunk.splitlines(): + line = raw_line.decode("utf-8") + sse = self.decode(line) + if sse: + yield sse + + def _iter_chunks(self, iterator: Iterator[bytes]) -> Iterator[bytes]: + """Given an iterator that yields raw binary data, iterate over it and yield individual SSE chunks""" + data = b"" + for chunk in iterator: + for line in chunk.splitlines(keepends=True): + data += line + if data.endswith((b"\r\r", b"\n\n", b"\r\n\r\n")): + yield data + data = b"" + if data: + yield data + + async def aiter_bytes(self, iterator: AsyncIterator[bytes]) -> AsyncIterator[ServerSentEvent]: + """Given an iterator that yields raw binary data, iterate over it & yield every event encountered""" + async for chunk in self._aiter_chunks(iterator): + # Split before decoding so splitlines() only uses \r and \n + for raw_line in chunk.splitlines(): + line = raw_line.decode("utf-8") + sse = self.decode(line) + if sse: + yield sse + + async def _aiter_chunks(self, iterator: AsyncIterator[bytes]) -> AsyncIterator[bytes]: + """Given an iterator that yields raw binary data, iterate over it and yield individual SSE chunks""" + data = b"" + async for chunk in iterator: + for line in chunk.splitlines(keepends=True): + data += line + if data.endswith((b"\r\r", b"\n\n", b"\r\n\r\n")): + yield data + data = b"" + if data: + yield data + + def decode(self, line: str) -> ServerSentEvent | None: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = None + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None + + +@runtime_checkable +class SSEBytesDecoder(Protocol): + def iter_bytes(self, iterator: Iterator[bytes]) -> Iterator[ServerSentEvent]: + """Given an iterator that yields raw binary data, iterate over it & yield every event encountered""" + ... + + def aiter_bytes(self, iterator: AsyncIterator[bytes]) -> AsyncIterator[ServerSentEvent]: + """Given an async iterator that yields raw binary data, iterate over it & yield every event encountered""" + ... + + +def is_stream_class_type(typ: type) -> TypeGuard[type[Stream[object]] | type[AsyncStream[object]]]: + """TypeGuard for determining whether or not the given type is a subclass of `Stream` / `AsyncStream`""" + origin = get_origin(typ) or typ + return inspect.isclass(origin) and issubclass(origin, (Stream, AsyncStream)) + + +def extract_stream_chunk_type( + stream_cls: type, + *, + failure_message: str | None = None, +) -> type: + """Given a type like `Stream[T]`, returns the generic type variable `T`. + + This also handles the case where a concrete subclass is given, e.g. + ```py + class MyStream(Stream[bytes]): + ... + + extract_stream_chunk_type(MyStream) -> bytes + ``` + """ + from ._base_client import Stream, AsyncStream + + return extract_type_var_from_base( + stream_cls, + index=0, + generic_bases=cast("tuple[type, ...]", (Stream, AsyncStream)), + failure_message=failure_message, + ) diff --git a/openai/_types.py b/openai/_types.py new file mode 100644 index 00000000..de9b1dd4 --- /dev/null +++ b/openai/_types.py @@ -0,0 +1,222 @@ +from __future__ import annotations + +from os import PathLike +from typing import ( + IO, + TYPE_CHECKING, + Any, + Dict, + List, + Type, + Tuple, + Union, + Mapping, + TypeVar, + Callable, + Optional, + Sequence, +) +from typing_extensions import Literal, Protocol, TypeAlias, TypedDict, override, runtime_checkable + +import httpx +import pydantic +from httpx import URL, Proxy, Timeout, Response, BaseTransport, AsyncBaseTransport + +if TYPE_CHECKING: + from ._models import BaseModel + from ._response import APIResponse, AsyncAPIResponse + from ._legacy_response import HttpxBinaryResponseContent + +Transport = BaseTransport +AsyncTransport = AsyncBaseTransport +Query = Mapping[str, object] +Body = object +AnyMapping = Mapping[str, object] +ModelT = TypeVar("ModelT", bound=pydantic.BaseModel) +_T = TypeVar("_T") + + +# Approximates httpx internal ProxiesTypes and RequestFiles types +# while adding support for `PathLike` instances +ProxiesDict = Dict["str | URL", Union[None, str, URL, Proxy]] +ProxiesTypes = Union[str, Proxy, ProxiesDict] +if TYPE_CHECKING: + Base64FileInput = Union[IO[bytes], PathLike[str]] + FileContent = Union[IO[bytes], bytes, PathLike[str]] +else: + Base64FileInput = Union[IO[bytes], PathLike] + FileContent = Union[IO[bytes], bytes, PathLike] # PathLike is not subscriptable in Python 3.8. +FileTypes = Union[ + # file (or bytes) + FileContent, + # (filename, file (or bytes)) + Tuple[Optional[str], FileContent], + # (filename, file (or bytes), content_type) + Tuple[Optional[str], FileContent, Optional[str]], + # (filename, file (or bytes), content_type, headers) + Tuple[Optional[str], FileContent, Optional[str], Mapping[str, str]], +] +RequestFiles = Union[Mapping[str, FileTypes], Sequence[Tuple[str, FileTypes]]] + +# duplicate of the above but without our custom file support +HttpxFileContent = Union[IO[bytes], bytes] +HttpxFileTypes = Union[ + # file (or bytes) + HttpxFileContent, + # (filename, file (or bytes)) + Tuple[Optional[str], HttpxFileContent], + # (filename, file (or bytes), content_type) + Tuple[Optional[str], HttpxFileContent, Optional[str]], + # (filename, file (or bytes), content_type, headers) + Tuple[Optional[str], HttpxFileContent, Optional[str], Mapping[str, str]], +] +HttpxRequestFiles = Union[Mapping[str, HttpxFileTypes], Sequence[Tuple[str, HttpxFileTypes]]] + +# Workaround to support (cast_to: Type[ResponseT]) -> ResponseT +# where ResponseT includes `None`. In order to support directly +# passing `None`, overloads would have to be defined for every +# method that uses `ResponseT` which would lead to an unacceptable +# amount of code duplication and make it unreadable. See _base_client.py +# for example usage. +# +# This unfortunately means that you will either have +# to import this type and pass it explicitly: +# +# from openai import NoneType +# client.get('/foo', cast_to=NoneType) +# +# or build it yourself: +# +# client.get('/foo', cast_to=type(None)) +if TYPE_CHECKING: + NoneType: Type[None] +else: + NoneType = type(None) + + +class RequestOptions(TypedDict, total=False): + headers: Headers + max_retries: int + timeout: float | Timeout | None + params: Query + extra_json: AnyMapping + idempotency_key: str + + +# Sentinel class used until PEP 0661 is accepted +class NotGiven: + """ + A sentinel singleton class used to distinguish omitted keyword arguments + from those passed in with the value None (which may have different behavior). + + For example: + + ```py + def get(timeout: Union[int, NotGiven, None] = NotGiven()) -> Response: + ... + + + get(timeout=1) # 1s timeout + get(timeout=None) # No timeout + get() # Default timeout behavior, which may not be statically known at the method definition. + ``` + """ + + def __bool__(self) -> Literal[False]: + return False + + @override + def __repr__(self) -> str: + return "NOT_GIVEN" + + +NotGivenOr = Union[_T, NotGiven] +NOT_GIVEN = NotGiven() + + +class Omit: + """In certain situations you need to be able to represent a case where a default value has + to be explicitly removed and `None` is not an appropriate substitute, for example: + + ```py + # as the default `Content-Type` header is `application/json` that will be sent + client.post("/upload/files", files={"file": b"my raw file content"}) + + # you can't explicitly override the header as it has to be dynamically generated + # to look something like: 'multipart/form-data; boundary=0d8382fcf5f8c3be01ca2e11002d2983' + client.post(..., headers={"Content-Type": "multipart/form-data"}) + + # instead you can remove the default `application/json` header by passing Omit + client.post(..., headers={"Content-Type": Omit()}) + ``` + """ + + def __bool__(self) -> Literal[False]: + return False + + +@runtime_checkable +class ModelBuilderProtocol(Protocol): + @classmethod + def build( + cls: type[_T], + *, + response: Response, + data: object, + ) -> _T: + ... + + +Headers = Mapping[str, Union[str, Omit]] + + +class HeadersLikeProtocol(Protocol): + def get(self, __key: str) -> str | None: + ... + + +HeadersLike = Union[Headers, HeadersLikeProtocol] + +ResponseT = TypeVar( + "ResponseT", + bound=Union[ + object, + str, + None, + "BaseModel", + List[Any], + Dict[str, Any], + Response, + ModelBuilderProtocol, + "APIResponse[Any]", + "AsyncAPIResponse[Any]", + "HttpxBinaryResponseContent", + ], +) + +StrBytesIntFloat = Union[str, bytes, int, float] + +# Note: copied from Pydantic +# https://github.com/pydantic/pydantic/blob/32ea570bf96e84234d2992e1ddf40ab8a565925a/pydantic/main.py#L49 +IncEx: TypeAlias = "set[int] | set[str] | dict[int, Any] | dict[str, Any] | None" + +PostParser = Callable[[Any], Any] + + +@runtime_checkable +class InheritsGeneric(Protocol): + """Represents a type that has inherited from `Generic` + + The `__orig_bases__` property can be used to determine the resolved + type variable for a given base class. + """ + + __orig_bases__: tuple[_GenericAlias] + + +class _GenericAlias(Protocol): + __origin__: type[object] + + +class HttpxSendArgs(TypedDict, total=False): + auth: httpx.Auth diff --git a/openai/_utils/__init__.py b/openai/_utils/__init__.py new file mode 100644 index 00000000..56978941 --- /dev/null +++ b/openai/_utils/__init__.py @@ -0,0 +1,50 @@ +from ._sync import asyncify as asyncify +from ._proxy import LazyProxy as LazyProxy +from ._utils import ( + flatten as flatten, + is_dict as is_dict, + is_list as is_list, + is_given as is_given, + is_tuple as is_tuple, + is_mapping as is_mapping, + is_tuple_t as is_tuple_t, + parse_date as parse_date, + is_iterable as is_iterable, + is_sequence as is_sequence, + coerce_float as coerce_float, + is_mapping_t as is_mapping_t, + removeprefix as removeprefix, + removesuffix as removesuffix, + extract_files as extract_files, + is_sequence_t as is_sequence_t, + required_args as required_args, + coerce_boolean as coerce_boolean, + coerce_integer as coerce_integer, + file_from_path as file_from_path, + parse_datetime as parse_datetime, + strip_not_given as strip_not_given, + deepcopy_minimal as deepcopy_minimal, + get_async_library as get_async_library, + maybe_coerce_float as maybe_coerce_float, + get_required_header as get_required_header, + maybe_coerce_boolean as maybe_coerce_boolean, + maybe_coerce_integer as maybe_coerce_integer, +) +from ._typing import ( + is_list_type as is_list_type, + is_union_type as is_union_type, + extract_type_arg as extract_type_arg, + is_iterable_type as is_iterable_type, + is_required_type as is_required_type, + is_annotated_type as is_annotated_type, + strip_annotated_type as strip_annotated_type, + extract_type_var_from_base as extract_type_var_from_base, +) +from ._streams import consume_sync_iterator as consume_sync_iterator, consume_async_iterator as consume_async_iterator +from ._transform import ( + PropertyInfo as PropertyInfo, + transform as transform, + async_transform as async_transform, + maybe_transform as maybe_transform, + async_maybe_transform as async_maybe_transform, +) diff --git a/openai/_utils/_logs.py b/openai/_utils/_logs.py new file mode 100644 index 00000000..e5113fd8 --- /dev/null +++ b/openai/_utils/_logs.py @@ -0,0 +1,25 @@ +import os +import logging + +logger: logging.Logger = logging.getLogger("openai") +httpx_logger: logging.Logger = logging.getLogger("httpx") + + +def _basic_config() -> None: + # e.g. [2023-10-05 14:12:26 - openai._base_client:818 - DEBUG] HTTP Request: POST http://127.0.0.1:4010/foo/bar "200 OK" + logging.basicConfig( + format="[%(asctime)s - %(name)s:%(lineno)d - %(levelname)s] %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + ) + + +def setup_logging() -> None: + env = os.environ.get("OPENAI_LOG") + if env == "debug": + _basic_config() + logger.setLevel(logging.DEBUG) + httpx_logger.setLevel(logging.DEBUG) + elif env == "info": + _basic_config() + logger.setLevel(logging.INFO) + httpx_logger.setLevel(logging.INFO) diff --git a/openai/_utils/_proxy.py b/openai/_utils/_proxy.py new file mode 100644 index 00000000..b9c12dc3 --- /dev/null +++ b/openai/_utils/_proxy.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import Generic, TypeVar, Iterable, cast +from typing_extensions import override + +T = TypeVar("T") + + +class LazyProxy(Generic[T], ABC): + """Implements data methods to pretend that an instance is another instance. + + This includes forwarding attribute access and othe methods. + """ + + # Note: we have to special case proxies that themselves return proxies + # to support using a proxy as a catch-all for any random access, e.g. `proxy.foo.bar.baz` + + def __getattr__(self, attr: str) -> object: + proxied = self.__get_proxied__() + if isinstance(proxied, LazyProxy): + return proxied # pyright: ignore + return getattr(proxied, attr) + + @override + def __repr__(self) -> str: + proxied = self.__get_proxied__() + if isinstance(proxied, LazyProxy): + return proxied.__class__.__name__ + return repr(self.__get_proxied__()) + + @override + def __str__(self) -> str: + proxied = self.__get_proxied__() + if isinstance(proxied, LazyProxy): + return proxied.__class__.__name__ + return str(proxied) + + @override + def __dir__(self) -> Iterable[str]: + proxied = self.__get_proxied__() + if isinstance(proxied, LazyProxy): + return [] + return proxied.__dir__() + + @property # type: ignore + @override + def __class__(self) -> type: # pyright: ignore + proxied = self.__get_proxied__() + if issubclass(type(proxied), LazyProxy): + return type(proxied) + return proxied.__class__ + + def __get_proxied__(self) -> T: + return self.__load__() + + def __as_proxied__(self) -> T: + """Helper method that returns the current proxy, typed as the loaded object""" + return cast(T, self) + + @abstractmethod + def __load__(self) -> T: + ... diff --git a/openai/_utils/_streams.py b/openai/_utils/_streams.py new file mode 100644 index 00000000..f4a0208f --- /dev/null +++ b/openai/_utils/_streams.py @@ -0,0 +1,12 @@ +from typing import Any +from typing_extensions import Iterator, AsyncIterator + + +def consume_sync_iterator(iterator: Iterator[Any]) -> None: + for _ in iterator: + ... + + +async def consume_async_iterator(iterator: AsyncIterator[Any]) -> None: + async for _ in iterator: + ... diff --git a/openai/_utils/_sync.py b/openai/_utils/_sync.py new file mode 100644 index 00000000..595924e5 --- /dev/null +++ b/openai/_utils/_sync.py @@ -0,0 +1,64 @@ +from __future__ import annotations + +import functools +from typing import TypeVar, Callable, Awaitable +from typing_extensions import ParamSpec + +import anyio +import anyio.to_thread + +T_Retval = TypeVar("T_Retval") +T_ParamSpec = ParamSpec("T_ParamSpec") + + +# copied from `asyncer`, https://github.com/tiangolo/asyncer +def asyncify( + function: Callable[T_ParamSpec, T_Retval], + *, + cancellable: bool = False, + limiter: anyio.CapacityLimiter | None = None, +) -> Callable[T_ParamSpec, Awaitable[T_Retval]]: + """ + Take a blocking function and create an async one that receives the same + positional and keyword arguments, and that when called, calls the original function + in a worker thread using `anyio.to_thread.run_sync()`. Internally, + `asyncer.asyncify()` uses the same `anyio.to_thread.run_sync()`, but it supports + keyword arguments additional to positional arguments and it adds better support for + autocompletion and inline errors for the arguments of the function called and the + return value. + + If the `cancellable` option is enabled and the task waiting for its completion is + cancelled, the thread will still run its course but its return value (or any raised + exception) will be ignored. + + Use it like this: + + ```Python + def do_work(arg1, arg2, kwarg1="", kwarg2="") -> str: + # Do work + return "Some result" + + + result = await to_thread.asyncify(do_work)("spam", "ham", kwarg1="a", kwarg2="b") + print(result) + ``` + + ## Arguments + + `function`: a blocking regular callable (e.g. a function) + `cancellable`: `True` to allow cancellation of the operation + `limiter`: capacity limiter to use to limit the total amount of threads running + (if omitted, the default limiter is used) + + ## Return + + An async function that takes the same positional and keyword arguments as the + original one, that when called runs the same original function in a thread worker + and returns the result. + """ + + async def wrapper(*args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs) -> T_Retval: + partial_f = functools.partial(function, *args, **kwargs) + return await anyio.to_thread.run_sync(partial_f, cancellable=cancellable, limiter=limiter) + + return wrapper diff --git a/openai/_utils/_transform.py b/openai/_utils/_transform.py new file mode 100644 index 00000000..47e262a5 --- /dev/null +++ b/openai/_utils/_transform.py @@ -0,0 +1,382 @@ +from __future__ import annotations + +import io +import base64 +import pathlib +from typing import Any, Mapping, TypeVar, cast +from datetime import date, datetime +from typing_extensions import Literal, get_args, override, get_type_hints + +import anyio +import pydantic + +from ._utils import ( + is_list, + is_mapping, + is_iterable, +) +from .._files import is_base64_file_input +from ._typing import ( + is_list_type, + is_union_type, + extract_type_arg, + is_iterable_type, + is_required_type, + is_annotated_type, + strip_annotated_type, +) +from .._compat import model_dump, is_typeddict + +_T = TypeVar("_T") + + +# TODO: support for drilling globals() and locals() +# TODO: ensure works correctly with forward references in all cases + + +PropertyFormat = Literal["iso8601", "base64", "custom"] + + +class PropertyInfo: + """Metadata class to be used in Annotated types to provide information about a given type. + + For example: + + class MyParams(TypedDict): + account_holder_name: Annotated[str, PropertyInfo(alias='accountHolderName')] + + This means that {'account_holder_name': 'Robert'} will be transformed to {'accountHolderName': 'Robert'} before being sent to the API. + """ + + alias: str | None + format: PropertyFormat | None + format_template: str | None + discriminator: str | None + + def __init__( + self, + *, + alias: str | None = None, + format: PropertyFormat | None = None, + format_template: str | None = None, + discriminator: str | None = None, + ) -> None: + self.alias = alias + self.format = format + self.format_template = format_template + self.discriminator = discriminator + + @override + def __repr__(self) -> str: + return f"{self.__class__.__name__}(alias='{self.alias}', format={self.format}, format_template='{self.format_template}', discriminator='{self.discriminator}')" + + +def maybe_transform( + data: object, + expected_type: object, +) -> Any | None: + """Wrapper over `transform()` that allows `None` to be passed. + + See `transform()` for more details. + """ + if data is None: + return None + return transform(data, expected_type) + + +# Wrapper over _transform_recursive providing fake types +def transform( + data: _T, + expected_type: object, +) -> _T: + """Transform dictionaries based off of type information from the given type, for example: + + ```py + class Params(TypedDict, total=False): + card_id: Required[Annotated[str, PropertyInfo(alias="cardID")]] + + + transformed = transform({"card_id": ""}, Params) + # {'cardID': ''} + ``` + + Any keys / data that does not have type information given will be included as is. + + It should be noted that the transformations that this function does are not represented in the type system. + """ + transformed = _transform_recursive(data, annotation=cast(type, expected_type)) + return cast(_T, transformed) + + +def _get_annotated_type(type_: type) -> type | None: + """If the given type is an `Annotated` type then it is returned, if not `None` is returned. + + This also unwraps the type when applicable, e.g. `Required[Annotated[T, ...]]` + """ + if is_required_type(type_): + # Unwrap `Required[Annotated[T, ...]]` to `Annotated[T, ...]` + type_ = get_args(type_)[0] + + if is_annotated_type(type_): + return type_ + + return None + + +def _maybe_transform_key(key: str, type_: type) -> str: + """Transform the given `data` based on the annotations provided in `type_`. + + Note: this function only looks at `Annotated` types that contain `PropertInfo` metadata. + """ + annotated_type = _get_annotated_type(type_) + if annotated_type is None: + # no `Annotated` definition for this type, no transformation needed + return key + + # ignore the first argument as it is the actual type + annotations = get_args(annotated_type)[1:] + for annotation in annotations: + if isinstance(annotation, PropertyInfo) and annotation.alias is not None: + return annotation.alias + + return key + + +def _transform_recursive( + data: object, + *, + annotation: type, + inner_type: type | None = None, +) -> object: + """Transform the given data against the expected type. + + Args: + annotation: The direct type annotation given to the particular piece of data. + This may or may not be wrapped in metadata types, e.g. `Required[T]`, `Annotated[T, ...]` etc + + inner_type: If applicable, this is the "inside" type. This is useful in certain cases where the outside type + is a container type such as `List[T]`. In that case `inner_type` should be set to `T` so that each entry in + the list can be transformed using the metadata from the container type. + + Defaults to the same value as the `annotation` argument. + """ + if inner_type is None: + inner_type = annotation + + stripped_type = strip_annotated_type(inner_type) + if is_typeddict(stripped_type) and is_mapping(data): + return _transform_typeddict(data, stripped_type) + + if ( + # List[T] + (is_list_type(stripped_type) and is_list(data)) + # Iterable[T] + or (is_iterable_type(stripped_type) and is_iterable(data) and not isinstance(data, str)) + ): + inner_type = extract_type_arg(stripped_type, 0) + return [_transform_recursive(d, annotation=annotation, inner_type=inner_type) for d in data] + + if is_union_type(stripped_type): + # For union types we run the transformation against all subtypes to ensure that everything is transformed. + # + # TODO: there may be edge cases where the same normalized field name will transform to two different names + # in different subtypes. + for subtype in get_args(stripped_type): + data = _transform_recursive(data, annotation=annotation, inner_type=subtype) + return data + + if isinstance(data, pydantic.BaseModel): + return model_dump(data, exclude_unset=True) + + annotated_type = _get_annotated_type(annotation) + if annotated_type is None: + return data + + # ignore the first argument as it is the actual type + annotations = get_args(annotated_type)[1:] + for annotation in annotations: + if isinstance(annotation, PropertyInfo) and annotation.format is not None: + return _format_data(data, annotation.format, annotation.format_template) + + return data + + +def _format_data(data: object, format_: PropertyFormat, format_template: str | None) -> object: + if isinstance(data, (date, datetime)): + if format_ == "iso8601": + return data.isoformat() + + if format_ == "custom" and format_template is not None: + return data.strftime(format_template) + + if format_ == "base64" and is_base64_file_input(data): + binary: str | bytes | None = None + + if isinstance(data, pathlib.Path): + binary = data.read_bytes() + elif isinstance(data, io.IOBase): + binary = data.read() + + if isinstance(binary, str): # type: ignore[unreachable] + binary = binary.encode() + + if not isinstance(binary, bytes): + raise RuntimeError(f"Could not read bytes from {data}; Received {type(binary)}") + + return base64.b64encode(binary).decode("ascii") + + return data + + +def _transform_typeddict( + data: Mapping[str, object], + expected_type: type, +) -> Mapping[str, object]: + result: dict[str, object] = {} + annotations = get_type_hints(expected_type, include_extras=True) + for key, value in data.items(): + type_ = annotations.get(key) + if type_ is None: + # we do not have a type annotation for this field, leave it as is + result[key] = value + else: + result[_maybe_transform_key(key, type_)] = _transform_recursive(value, annotation=type_) + return result + + +async def async_maybe_transform( + data: object, + expected_type: object, +) -> Any | None: + """Wrapper over `async_transform()` that allows `None` to be passed. + + See `async_transform()` for more details. + """ + if data is None: + return None + return await async_transform(data, expected_type) + + +async def async_transform( + data: _T, + expected_type: object, +) -> _T: + """Transform dictionaries based off of type information from the given type, for example: + + ```py + class Params(TypedDict, total=False): + card_id: Required[Annotated[str, PropertyInfo(alias="cardID")]] + + + transformed = transform({"card_id": ""}, Params) + # {'cardID': ''} + ``` + + Any keys / data that does not have type information given will be included as is. + + It should be noted that the transformations that this function does are not represented in the type system. + """ + transformed = await _async_transform_recursive(data, annotation=cast(type, expected_type)) + return cast(_T, transformed) + + +async def _async_transform_recursive( + data: object, + *, + annotation: type, + inner_type: type | None = None, +) -> object: + """Transform the given data against the expected type. + + Args: + annotation: The direct type annotation given to the particular piece of data. + This may or may not be wrapped in metadata types, e.g. `Required[T]`, `Annotated[T, ...]` etc + + inner_type: If applicable, this is the "inside" type. This is useful in certain cases where the outside type + is a container type such as `List[T]`. In that case `inner_type` should be set to `T` so that each entry in + the list can be transformed using the metadata from the container type. + + Defaults to the same value as the `annotation` argument. + """ + if inner_type is None: + inner_type = annotation + + stripped_type = strip_annotated_type(inner_type) + if is_typeddict(stripped_type) and is_mapping(data): + return await _async_transform_typeddict(data, stripped_type) + + if ( + # List[T] + (is_list_type(stripped_type) and is_list(data)) + # Iterable[T] + or (is_iterable_type(stripped_type) and is_iterable(data) and not isinstance(data, str)) + ): + inner_type = extract_type_arg(stripped_type, 0) + return [await _async_transform_recursive(d, annotation=annotation, inner_type=inner_type) for d in data] + + if is_union_type(stripped_type): + # For union types we run the transformation against all subtypes to ensure that everything is transformed. + # + # TODO: there may be edge cases where the same normalized field name will transform to two different names + # in different subtypes. + for subtype in get_args(stripped_type): + data = await _async_transform_recursive(data, annotation=annotation, inner_type=subtype) + return data + + if isinstance(data, pydantic.BaseModel): + return model_dump(data, exclude_unset=True) + + annotated_type = _get_annotated_type(annotation) + if annotated_type is None: + return data + + # ignore the first argument as it is the actual type + annotations = get_args(annotated_type)[1:] + for annotation in annotations: + if isinstance(annotation, PropertyInfo) and annotation.format is not None: + return await _async_format_data(data, annotation.format, annotation.format_template) + + return data + + +async def _async_format_data(data: object, format_: PropertyFormat, format_template: str | None) -> object: + if isinstance(data, (date, datetime)): + if format_ == "iso8601": + return data.isoformat() + + if format_ == "custom" and format_template is not None: + return data.strftime(format_template) + + if format_ == "base64" and is_base64_file_input(data): + binary: str | bytes | None = None + + if isinstance(data, pathlib.Path): + binary = await anyio.Path(data).read_bytes() + elif isinstance(data, io.IOBase): + binary = data.read() + + if isinstance(binary, str): # type: ignore[unreachable] + binary = binary.encode() + + if not isinstance(binary, bytes): + raise RuntimeError(f"Could not read bytes from {data}; Received {type(binary)}") + + return base64.b64encode(binary).decode("ascii") + + return data + + +async def _async_transform_typeddict( + data: Mapping[str, object], + expected_type: type, +) -> Mapping[str, object]: + result: dict[str, object] = {} + annotations = get_type_hints(expected_type, include_extras=True) + for key, value in data.items(): + type_ = annotations.get(key) + if type_ is None: + # we do not have a type annotation for this field, leave it as is + result[key] = value + else: + result[_maybe_transform_key(key, type_)] = await _async_transform_recursive(value, annotation=type_) + return result diff --git a/openai/_utils/_typing.py b/openai/_utils/_typing.py new file mode 100644 index 00000000..c036991f --- /dev/null +++ b/openai/_utils/_typing.py @@ -0,0 +1,120 @@ +from __future__ import annotations + +from typing import Any, TypeVar, Iterable, cast +from collections import abc as _c_abc +from typing_extensions import Required, Annotated, get_args, get_origin + +from .._types import InheritsGeneric +from .._compat import is_union as _is_union + + +def is_annotated_type(typ: type) -> bool: + return get_origin(typ) == Annotated + + +def is_list_type(typ: type) -> bool: + return (get_origin(typ) or typ) == list + + +def is_iterable_type(typ: type) -> bool: + """If the given type is `typing.Iterable[T]`""" + origin = get_origin(typ) or typ + return origin == Iterable or origin == _c_abc.Iterable + + +def is_union_type(typ: type) -> bool: + return _is_union(get_origin(typ)) + + +def is_required_type(typ: type) -> bool: + return get_origin(typ) == Required + + +def is_typevar(typ: type) -> bool: + # type ignore is required because type checkers + # think this expression will always return False + return type(typ) == TypeVar # type: ignore + + +# Extracts T from Annotated[T, ...] or from Required[Annotated[T, ...]] +def strip_annotated_type(typ: type) -> type: + if is_required_type(typ) or is_annotated_type(typ): + return strip_annotated_type(cast(type, get_args(typ)[0])) + + return typ + + +def extract_type_arg(typ: type, index: int) -> type: + args = get_args(typ) + try: + return cast(type, args[index]) + except IndexError as err: + raise RuntimeError(f"Expected type {typ} to have a type argument at index {index} but it did not") from err + + +def extract_type_var_from_base( + typ: type, + *, + generic_bases: tuple[type, ...], + index: int, + failure_message: str | None = None, +) -> type: + """Given a type like `Foo[T]`, returns the generic type variable `T`. + + This also handles the case where a concrete subclass is given, e.g. + ```py + class MyResponse(Foo[bytes]): + ... + + extract_type_var(MyResponse, bases=(Foo,), index=0) -> bytes + ``` + + And where a generic subclass is given: + ```py + _T = TypeVar('_T') + class MyResponse(Foo[_T]): + ... + + extract_type_var(MyResponse[bytes], bases=(Foo,), index=0) -> bytes + ``` + """ + cls = cast(object, get_origin(typ) or typ) + if cls in generic_bases: + # we're given the class directly + return extract_type_arg(typ, index) + + # if a subclass is given + # --- + # this is needed as __orig_bases__ is not present in the typeshed stubs + # because it is intended to be for internal use only, however there does + # not seem to be a way to resolve generic TypeVars for inherited subclasses + # without using it. + if isinstance(cls, InheritsGeneric): + target_base_class: Any | None = None + for base in cls.__orig_bases__: + if base.__origin__ in generic_bases: + target_base_class = base + break + + if target_base_class is None: + raise RuntimeError( + "Could not find the generic base class;\n" + "This should never happen;\n" + f"Does {cls} inherit from one of {generic_bases} ?" + ) + + extracted = extract_type_arg(target_base_class, index) + if is_typevar(extracted): + # If the extracted type argument is itself a type variable + # then that means the subclass itself is generic, so we have + # to resolve the type argument from the class itself, not + # the base class. + # + # Note: if there is more than 1 type argument, the subclass could + # change the ordering of the type arguments, this is not currently + # supported. + return extract_type_arg(typ, index) + + return extracted + + raise RuntimeError(failure_message or f"Could not resolve inner type variable at index {index} for {typ}") diff --git a/openai/_utils/_utils.py b/openai/_utils/_utils.py new file mode 100644 index 00000000..93c95517 --- /dev/null +++ b/openai/_utils/_utils.py @@ -0,0 +1,391 @@ +from __future__ import annotations + +import os +import re +import inspect +import functools +from typing import ( + Any, + Tuple, + Mapping, + TypeVar, + Callable, + Iterable, + Sequence, + cast, + overload, +) +from pathlib import Path +from typing_extensions import TypeGuard + +import sniffio + +from .._types import Headers, NotGiven, FileTypes, NotGivenOr, HeadersLike +from .._compat import parse_date as parse_date, parse_datetime as parse_datetime + +_T = TypeVar("_T") +_TupleT = TypeVar("_TupleT", bound=Tuple[object, ...]) +_MappingT = TypeVar("_MappingT", bound=Mapping[str, object]) +_SequenceT = TypeVar("_SequenceT", bound=Sequence[object]) +CallableT = TypeVar("CallableT", bound=Callable[..., Any]) + + +def flatten(t: Iterable[Iterable[_T]]) -> list[_T]: + return [item for sublist in t for item in sublist] + + +def extract_files( + # TODO: this needs to take Dict but variance issues..... + # create protocol type ? + query: Mapping[str, object], + *, + paths: Sequence[Sequence[str]], +) -> list[tuple[str, FileTypes]]: + """Recursively extract files from the given dictionary based on specified paths. + + A path may look like this ['foo', 'files', '', 'data']. + + Note: this mutates the given dictionary. + """ + files: list[tuple[str, FileTypes]] = [] + for path in paths: + files.extend(_extract_items(query, path, index=0, flattened_key=None)) + return files + + +def _extract_items( + obj: object, + path: Sequence[str], + *, + index: int, + flattened_key: str | None, +) -> list[tuple[str, FileTypes]]: + try: + key = path[index] + except IndexError: + if isinstance(obj, NotGiven): + # no value was provided - we can safely ignore + return [] + + # cyclical import + from .._files import assert_is_file_content + + # We have exhausted the path, return the entry we found. + assert_is_file_content(obj, key=flattened_key) + assert flattened_key is not None + return [(flattened_key, cast(FileTypes, obj))] + + index += 1 + if is_dict(obj): + try: + # We are at the last entry in the path so we must remove the field + if (len(path)) == index: + item = obj.pop(key) + else: + item = obj[key] + except KeyError: + # Key was not present in the dictionary, this is not indicative of an error + # as the given path may not point to a required field. We also do not want + # to enforce required fields as the API may differ from the spec in some cases. + return [] + if flattened_key is None: + flattened_key = key + else: + flattened_key += f"[{key}]" + return _extract_items( + item, + path, + index=index, + flattened_key=flattened_key, + ) + elif is_list(obj): + if key != "": + return [] + + return flatten( + [ + _extract_items( + item, + path, + index=index, + flattened_key=flattened_key + "[]" if flattened_key is not None else "[]", + ) + for item in obj + ] + ) + + # Something unexpected was passed, just ignore it. + return [] + + +def is_given(obj: NotGivenOr[_T]) -> TypeGuard[_T]: + return not isinstance(obj, NotGiven) + + +# Type safe methods for narrowing types with TypeVars. +# The default narrowing for isinstance(obj, dict) is dict[unknown, unknown], +# however this cause Pyright to rightfully report errors. As we know we don't +# care about the contained types we can safely use `object` in it's place. +# +# There are two separate functions defined, `is_*` and `is_*_t` for different use cases. +# `is_*` is for when you're dealing with an unknown input +# `is_*_t` is for when you're narrowing a known union type to a specific subset + + +def is_tuple(obj: object) -> TypeGuard[tuple[object, ...]]: + return isinstance(obj, tuple) + + +def is_tuple_t(obj: _TupleT | object) -> TypeGuard[_TupleT]: + return isinstance(obj, tuple) + + +def is_sequence(obj: object) -> TypeGuard[Sequence[object]]: + return isinstance(obj, Sequence) + + +def is_sequence_t(obj: _SequenceT | object) -> TypeGuard[_SequenceT]: + return isinstance(obj, Sequence) + + +def is_mapping(obj: object) -> TypeGuard[Mapping[str, object]]: + return isinstance(obj, Mapping) + + +def is_mapping_t(obj: _MappingT | object) -> TypeGuard[_MappingT]: + return isinstance(obj, Mapping) + + +def is_dict(obj: object) -> TypeGuard[dict[object, object]]: + return isinstance(obj, dict) + + +def is_list(obj: object) -> TypeGuard[list[object]]: + return isinstance(obj, list) + + +def is_iterable(obj: object) -> TypeGuard[Iterable[object]]: + return isinstance(obj, Iterable) + + +def deepcopy_minimal(item: _T) -> _T: + """Minimal reimplementation of copy.deepcopy() that will only copy certain object types: + + - mappings, e.g. `dict` + - list + + This is done for performance reasons. + """ + if is_mapping(item): + return cast(_T, {k: deepcopy_minimal(v) for k, v in item.items()}) + if is_list(item): + return cast(_T, [deepcopy_minimal(entry) for entry in item]) + return item + + +# copied from https://github.com/Rapptz/RoboDanny +def human_join(seq: Sequence[str], *, delim: str = ", ", final: str = "or") -> str: + size = len(seq) + if size == 0: + return "" + + if size == 1: + return seq[0] + + if size == 2: + return f"{seq[0]} {final} {seq[1]}" + + return delim.join(seq[:-1]) + f" {final} {seq[-1]}" + + +def quote(string: str) -> str: + """Add single quotation marks around the given string. Does *not* do any escaping.""" + return f"'{string}'" + + +def required_args(*variants: Sequence[str]) -> Callable[[CallableT], CallableT]: + """Decorator to enforce a given set of arguments or variants of arguments are passed to the decorated function. + + Useful for enforcing runtime validation of overloaded functions. + + Example usage: + ```py + @overload + def foo(*, a: str) -> str: + ... + + + @overload + def foo(*, b: bool) -> str: + ... + + + # This enforces the same constraints that a static type checker would + # i.e. that either a or b must be passed to the function + @required_args(["a"], ["b"]) + def foo(*, a: str | None = None, b: bool | None = None) -> str: + ... + ``` + """ + + def inner(func: CallableT) -> CallableT: + params = inspect.signature(func).parameters + positional = [ + name + for name, param in params.items() + if param.kind + in { + param.POSITIONAL_ONLY, + param.POSITIONAL_OR_KEYWORD, + } + ] + + @functools.wraps(func) + def wrapper(*args: object, **kwargs: object) -> object: + given_params: set[str] = set() + for i, _ in enumerate(args): + try: + given_params.add(positional[i]) + except IndexError: + raise TypeError( + f"{func.__name__}() takes {len(positional)} argument(s) but {len(args)} were given" + ) from None + + for key in kwargs.keys(): + given_params.add(key) + + for variant in variants: + matches = all((param in given_params for param in variant)) + if matches: + break + else: # no break + if len(variants) > 1: + variations = human_join( + ["(" + human_join([quote(arg) for arg in variant], final="and") + ")" for variant in variants] + ) + msg = f"Missing required arguments; Expected either {variations} arguments to be given" + else: + # TODO: this error message is not deterministic + missing = list(set(variants[0]) - given_params) + if len(missing) > 1: + msg = f"Missing required arguments: {human_join([quote(arg) for arg in missing])}" + else: + msg = f"Missing required argument: {quote(missing[0])}" + raise TypeError(msg) + return func(*args, **kwargs) + + return wrapper # type: ignore + + return inner + + +_K = TypeVar("_K") +_V = TypeVar("_V") + + +@overload +def strip_not_given(obj: None) -> None: + ... + + +@overload +def strip_not_given(obj: Mapping[_K, _V | NotGiven]) -> dict[_K, _V]: + ... + + +@overload +def strip_not_given(obj: object) -> object: + ... + + +def strip_not_given(obj: object | None) -> object: + """Remove all top-level keys where their values are instances of `NotGiven`""" + if obj is None: + return None + + if not is_mapping(obj): + return obj + + return {key: value for key, value in obj.items() if not isinstance(value, NotGiven)} + + +def coerce_integer(val: str) -> int: + return int(val, base=10) + + +def coerce_float(val: str) -> float: + return float(val) + + +def coerce_boolean(val: str) -> bool: + return val == "true" or val == "1" or val == "on" + + +def maybe_coerce_integer(val: str | None) -> int | None: + if val is None: + return None + return coerce_integer(val) + + +def maybe_coerce_float(val: str | None) -> float | None: + if val is None: + return None + return coerce_float(val) + + +def maybe_coerce_boolean(val: str | None) -> bool | None: + if val is None: + return None + return coerce_boolean(val) + + +def removeprefix(string: str, prefix: str) -> str: + """Remove a prefix from a string. + + Backport of `str.removeprefix` for Python < 3.9 + """ + if string.startswith(prefix): + return string[len(prefix) :] + return string + + +def removesuffix(string: str, suffix: str) -> str: + """Remove a suffix from a string. + + Backport of `str.removesuffix` for Python < 3.9 + """ + if string.endswith(suffix): + return string[: -len(suffix)] + return string + + +def file_from_path(path: str) -> FileTypes: + contents = Path(path).read_bytes() + file_name = os.path.basename(path) + return (file_name, contents) + + +def get_required_header(headers: HeadersLike, header: str) -> str: + lower_header = header.lower() + if isinstance(headers, Mapping): + headers = cast(Headers, headers) + for k, v in headers.items(): + if k.lower() == lower_header and isinstance(v, str): + return v + + """ to deal with the case where the header looks like Stainless-Event-Id """ + intercaps_header = re.sub(r"([^\w])(\w)", lambda pat: pat.group(1) + pat.group(2).upper(), header.capitalize()) + + for normalized_header in [header, lower_header, header.upper(), intercaps_header]: + value = headers.get(normalized_header) + if value: + return value + + raise ValueError(f"Could not find {header} header") + + +def get_async_library() -> str: + try: + return sniffio.current_async_library() + except Exception: + return "false" diff --git a/openai/_version.py b/openai/_version.py new file mode 100644 index 00000000..85803a60 --- /dev/null +++ b/openai/_version.py @@ -0,0 +1,4 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +__title__ = "openai" +__version__ = "1.16.2" # x-release-please-version diff --git a/openai/cli/__init__.py b/openai/cli/__init__.py new file mode 100644 index 00000000..d453d5e1 --- /dev/null +++ b/openai/cli/__init__.py @@ -0,0 +1 @@ +from ._cli import main as main diff --git a/openai/cli/_api/__init__.py b/openai/cli/_api/__init__.py new file mode 100644 index 00000000..56a0260a --- /dev/null +++ b/openai/cli/_api/__init__.py @@ -0,0 +1 @@ +from ._main import register_commands as register_commands diff --git a/openai/cli/_api/_main.py b/openai/cli/_api/_main.py new file mode 100644 index 00000000..fe5a5e6f --- /dev/null +++ b/openai/cli/_api/_main.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +from argparse import ArgumentParser + +from . import chat, audio, files, image, models, completions + + +def register_commands(parser: ArgumentParser) -> None: + subparsers = parser.add_subparsers(help="All API subcommands") + + chat.register(subparsers) + image.register(subparsers) + audio.register(subparsers) + files.register(subparsers) + models.register(subparsers) + completions.register(subparsers) diff --git a/openai/cli/_api/audio.py b/openai/cli/_api/audio.py new file mode 100644 index 00000000..90d21b99 --- /dev/null +++ b/openai/cli/_api/audio.py @@ -0,0 +1,94 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Optional, cast +from argparse import ArgumentParser + +from .._utils import get_client, print_model +from ..._types import NOT_GIVEN +from .._models import BaseModel +from .._progress import BufferReader + +if TYPE_CHECKING: + from argparse import _SubParsersAction + + +def register(subparser: _SubParsersAction[ArgumentParser]) -> None: + # transcriptions + sub = subparser.add_parser("audio.transcriptions.create") + + # Required + sub.add_argument("-m", "--model", type=str, default="whisper-1") + sub.add_argument("-f", "--file", type=str, required=True) + # Optional + sub.add_argument("--response-format", type=str) + sub.add_argument("--language", type=str) + sub.add_argument("-t", "--temperature", type=float) + sub.add_argument("--prompt", type=str) + sub.set_defaults(func=CLIAudio.transcribe, args_model=CLITranscribeArgs) + + # translations + sub = subparser.add_parser("audio.translations.create") + + # Required + sub.add_argument("-f", "--file", type=str, required=True) + # Optional + sub.add_argument("-m", "--model", type=str, default="whisper-1") + sub.add_argument("--response-format", type=str) + # TODO: doesn't seem to be supported by the API + # sub.add_argument("--language", type=str) + sub.add_argument("-t", "--temperature", type=float) + sub.add_argument("--prompt", type=str) + sub.set_defaults(func=CLIAudio.translate, args_model=CLITranslationArgs) + + +class CLITranscribeArgs(BaseModel): + model: str + file: str + response_format: Optional[str] = None + language: Optional[str] = None + temperature: Optional[float] = None + prompt: Optional[str] = None + + +class CLITranslationArgs(BaseModel): + model: str + file: str + response_format: Optional[str] = None + language: Optional[str] = None + temperature: Optional[float] = None + prompt: Optional[str] = None + + +class CLIAudio: + @staticmethod + def transcribe(args: CLITranscribeArgs) -> None: + with open(args.file, "rb") as file_reader: + buffer_reader = BufferReader(file_reader.read(), desc="Upload progress") + + model = get_client().audio.transcriptions.create( + file=(args.file, buffer_reader), + model=args.model, + language=args.language or NOT_GIVEN, + temperature=args.temperature or NOT_GIVEN, + prompt=args.prompt or NOT_GIVEN, + # casts required because the API is typed for enums + # but we don't want to validate that here for forwards-compat + response_format=cast(Any, args.response_format), + ) + print_model(model) + + @staticmethod + def translate(args: CLITranslationArgs) -> None: + with open(args.file, "rb") as file_reader: + buffer_reader = BufferReader(file_reader.read(), desc="Upload progress") + + model = get_client().audio.translations.create( + file=(args.file, buffer_reader), + model=args.model, + temperature=args.temperature or NOT_GIVEN, + prompt=args.prompt or NOT_GIVEN, + # casts required because the API is typed for enums + # but we don't want to validate that here for forwards-compat + response_format=cast(Any, args.response_format), + ) + print_model(model) diff --git a/openai/cli/_api/chat/__init__.py b/openai/cli/_api/chat/__init__.py new file mode 100644 index 00000000..87d97163 --- /dev/null +++ b/openai/cli/_api/chat/__init__.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING +from argparse import ArgumentParser + +from . import completions + +if TYPE_CHECKING: + from argparse import _SubParsersAction + + +def register(subparser: _SubParsersAction[ArgumentParser]) -> None: + completions.register(subparser) diff --git a/openai/cli/_api/chat/completions.py b/openai/cli/_api/chat/completions.py new file mode 100644 index 00000000..c299741f --- /dev/null +++ b/openai/cli/_api/chat/completions.py @@ -0,0 +1,156 @@ +from __future__ import annotations + +import sys +from typing import TYPE_CHECKING, List, Optional, cast +from argparse import ArgumentParser +from typing_extensions import Literal, NamedTuple + +from ..._utils import get_client +from ..._models import BaseModel +from ...._streaming import Stream +from ....types.chat import ( + ChatCompletionRole, + ChatCompletionChunk, + CompletionCreateParams, +) +from ....types.chat.completion_create_params import ( + CompletionCreateParamsStreaming, + CompletionCreateParamsNonStreaming, +) + +if TYPE_CHECKING: + from argparse import _SubParsersAction + + +def register(subparser: _SubParsersAction[ArgumentParser]) -> None: + sub = subparser.add_parser("chat.completions.create") + + sub._action_groups.pop() + req = sub.add_argument_group("required arguments") + opt = sub.add_argument_group("optional arguments") + + req.add_argument( + "-g", + "--message", + action="append", + nargs=2, + metavar=("ROLE", "CONTENT"), + help="A message in `{role} {content}` format. Use this argument multiple times to add multiple messages.", + required=True, + ) + req.add_argument( + "-m", + "--model", + help="The model to use.", + required=True, + ) + + opt.add_argument( + "-n", + "--n", + help="How many completions to generate for the conversation.", + type=int, + ) + opt.add_argument("-M", "--max-tokens", help="The maximum number of tokens to generate.", type=int) + opt.add_argument( + "-t", + "--temperature", + help="""What sampling temperature to use. Higher values means the model will take more risks. Try 0.9 for more creative applications, and 0 (argmax sampling) for ones with a well-defined answer. + +Mutually exclusive with `top_p`.""", + type=float, + ) + opt.add_argument( + "-P", + "--top_p", + help="""An alternative to sampling with temperature, called nucleus sampling, where the considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10%% probability mass are considered. + + Mutually exclusive with `temperature`.""", + type=float, + ) + opt.add_argument( + "--stop", + help="A stop sequence at which to stop generating tokens for the message.", + ) + opt.add_argument("--stream", help="Stream messages as they're ready.", action="store_true") + sub.set_defaults(func=CLIChatCompletion.create, args_model=CLIChatCompletionCreateArgs) + + +class CLIMessage(NamedTuple): + role: ChatCompletionRole + content: str + + +class CLIChatCompletionCreateArgs(BaseModel): + message: List[CLIMessage] + model: str + n: Optional[int] = None + max_tokens: Optional[int] = None + temperature: Optional[float] = None + top_p: Optional[float] = None + stop: Optional[str] = None + stream: bool = False + + +class CLIChatCompletion: + @staticmethod + def create(args: CLIChatCompletionCreateArgs) -> None: + params: CompletionCreateParams = { + "model": args.model, + "messages": [ + {"role": cast(Literal["user"], message.role), "content": message.content} for message in args.message + ], + "n": args.n, + "temperature": args.temperature, + "top_p": args.top_p, + "stop": args.stop, + # type checkers are not good at inferring union types so we have to set stream afterwards + "stream": False, + } + if args.stream: + params["stream"] = args.stream # type: ignore + if args.max_tokens is not None: + params["max_tokens"] = args.max_tokens + + if args.stream: + return CLIChatCompletion._stream_create(cast(CompletionCreateParamsStreaming, params)) + + return CLIChatCompletion._create(cast(CompletionCreateParamsNonStreaming, params)) + + @staticmethod + def _create(params: CompletionCreateParamsNonStreaming) -> None: + completion = get_client().chat.completions.create(**params) + should_print_header = len(completion.choices) > 1 + for choice in completion.choices: + if should_print_header: + sys.stdout.write("===== Chat Completion {} =====\n".format(choice.index)) + + content = choice.message.content if choice.message.content is not None else "None" + sys.stdout.write(content) + + if should_print_header or not content.endswith("\n"): + sys.stdout.write("\n") + + sys.stdout.flush() + + @staticmethod + def _stream_create(params: CompletionCreateParamsStreaming) -> None: + # cast is required for mypy + stream = cast( # pyright: ignore[reportUnnecessaryCast] + Stream[ChatCompletionChunk], get_client().chat.completions.create(**params) + ) + for chunk in stream: + should_print_header = len(chunk.choices) > 1 + for choice in chunk.choices: + if should_print_header: + sys.stdout.write("===== Chat Completion {} =====\n".format(choice.index)) + + content = choice.delta.content or "" + sys.stdout.write(content) + + if should_print_header: + sys.stdout.write("\n") + + sys.stdout.flush() + + sys.stdout.write("\n") diff --git a/openai/cli/_api/completions.py b/openai/cli/_api/completions.py new file mode 100644 index 00000000..cbdb35bf --- /dev/null +++ b/openai/cli/_api/completions.py @@ -0,0 +1,173 @@ +from __future__ import annotations + +import sys +from typing import TYPE_CHECKING, Optional, cast +from argparse import ArgumentParser +from functools import partial + +from openai.types.completion import Completion + +from .._utils import get_client +from ..._types import NOT_GIVEN, NotGivenOr +from ..._utils import is_given +from .._errors import CLIError +from .._models import BaseModel +from ..._streaming import Stream + +if TYPE_CHECKING: + from argparse import _SubParsersAction + + +def register(subparser: _SubParsersAction[ArgumentParser]) -> None: + sub = subparser.add_parser("completions.create") + + # Required + sub.add_argument( + "-m", + "--model", + help="The model to use", + required=True, + ) + + # Optional + sub.add_argument("-p", "--prompt", help="An optional prompt to complete from") + sub.add_argument("--stream", help="Stream tokens as they're ready.", action="store_true") + sub.add_argument("-M", "--max-tokens", help="The maximum number of tokens to generate", type=int) + sub.add_argument( + "-t", + "--temperature", + help="""What sampling temperature to use. Higher values means the model will take more risks. Try 0.9 for more creative applications, and 0 (argmax sampling) for ones with a well-defined answer. + +Mutually exclusive with `top_p`.""", + type=float, + ) + sub.add_argument( + "-P", + "--top_p", + help="""An alternative to sampling with temperature, called nucleus sampling, where the considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10%% probability mass are considered. + + Mutually exclusive with `temperature`.""", + type=float, + ) + sub.add_argument( + "-n", + "--n", + help="How many sub-completions to generate for each prompt.", + type=int, + ) + sub.add_argument( + "--logprobs", + help="Include the log probabilities on the `logprobs` most likely tokens, as well the chosen tokens. So for example, if `logprobs` is 10, the API will return a list of the 10 most likely tokens. If `logprobs` is 0, only the chosen tokens will have logprobs returned.", + type=int, + ) + sub.add_argument( + "--best_of", + help="Generates `best_of` completions server-side and returns the 'best' (the one with the highest log probability per token). Results cannot be streamed.", + type=int, + ) + sub.add_argument( + "--echo", + help="Echo back the prompt in addition to the completion", + action="store_true", + ) + sub.add_argument( + "--frequency_penalty", + help="Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim.", + type=float, + ) + sub.add_argument( + "--presence_penalty", + help="Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to talk about new topics.", + type=float, + ) + sub.add_argument("--suffix", help="The suffix that comes after a completion of inserted text.") + sub.add_argument("--stop", help="A stop sequence at which to stop generating tokens.") + sub.add_argument( + "--user", + help="A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse.", + ) + # TODO: add support for logit_bias + sub.set_defaults(func=CLICompletions.create, args_model=CLICompletionCreateArgs) + + +class CLICompletionCreateArgs(BaseModel): + model: str + stream: bool = False + + prompt: Optional[str] = None + n: NotGivenOr[int] = NOT_GIVEN + stop: NotGivenOr[str] = NOT_GIVEN + user: NotGivenOr[str] = NOT_GIVEN + echo: NotGivenOr[bool] = NOT_GIVEN + suffix: NotGivenOr[str] = NOT_GIVEN + best_of: NotGivenOr[int] = NOT_GIVEN + top_p: NotGivenOr[float] = NOT_GIVEN + logprobs: NotGivenOr[int] = NOT_GIVEN + max_tokens: NotGivenOr[int] = NOT_GIVEN + temperature: NotGivenOr[float] = NOT_GIVEN + presence_penalty: NotGivenOr[float] = NOT_GIVEN + frequency_penalty: NotGivenOr[float] = NOT_GIVEN + + +class CLICompletions: + @staticmethod + def create(args: CLICompletionCreateArgs) -> None: + if is_given(args.n) and args.n > 1 and args.stream: + raise CLIError("Can't stream completions with n>1 with the current CLI") + + make_request = partial( + get_client().completions.create, + n=args.n, + echo=args.echo, + stop=args.stop, + user=args.user, + model=args.model, + top_p=args.top_p, + prompt=args.prompt, + suffix=args.suffix, + best_of=args.best_of, + logprobs=args.logprobs, + max_tokens=args.max_tokens, + temperature=args.temperature, + presence_penalty=args.presence_penalty, + frequency_penalty=args.frequency_penalty, + ) + + if args.stream: + return CLICompletions._stream_create( + # mypy doesn't understand the `partial` function but pyright does + cast(Stream[Completion], make_request(stream=True)) # pyright: ignore[reportUnnecessaryCast] + ) + + return CLICompletions._create(make_request()) + + @staticmethod + def _create(completion: Completion) -> None: + should_print_header = len(completion.choices) > 1 + for choice in completion.choices: + if should_print_header: + sys.stdout.write("===== Completion {} =====\n".format(choice.index)) + + sys.stdout.write(choice.text) + + if should_print_header or not choice.text.endswith("\n"): + sys.stdout.write("\n") + + sys.stdout.flush() + + @staticmethod + def _stream_create(stream: Stream[Completion]) -> None: + for completion in stream: + should_print_header = len(completion.choices) > 1 + for choice in sorted(completion.choices, key=lambda c: c.index): + if should_print_header: + sys.stdout.write("===== Chat Completion {} =====\n".format(choice.index)) + + sys.stdout.write(choice.text) + + if should_print_header: + sys.stdout.write("\n") + + sys.stdout.flush() + + sys.stdout.write("\n") diff --git a/openai/cli/_api/files.py b/openai/cli/_api/files.py new file mode 100644 index 00000000..5f3631b2 --- /dev/null +++ b/openai/cli/_api/files.py @@ -0,0 +1,80 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, cast +from argparse import ArgumentParser + +from .._utils import get_client, print_model +from .._models import BaseModel +from .._progress import BufferReader + +if TYPE_CHECKING: + from argparse import _SubParsersAction + + +def register(subparser: _SubParsersAction[ArgumentParser]) -> None: + sub = subparser.add_parser("files.create") + + sub.add_argument( + "-f", + "--file", + required=True, + help="File to upload", + ) + sub.add_argument( + "-p", + "--purpose", + help="Why are you uploading this file? (see https://platform.openai.com/docs/api-reference/ for purposes)", + required=True, + ) + sub.set_defaults(func=CLIFile.create, args_model=CLIFileCreateArgs) + + sub = subparser.add_parser("files.retrieve") + sub.add_argument("-i", "--id", required=True, help="The files ID") + sub.set_defaults(func=CLIFile.get, args_model=CLIFileCreateArgs) + + sub = subparser.add_parser("files.delete") + sub.add_argument("-i", "--id", required=True, help="The files ID") + sub.set_defaults(func=CLIFile.delete, args_model=CLIFileCreateArgs) + + sub = subparser.add_parser("files.list") + sub.set_defaults(func=CLIFile.list) + + +class CLIFileIDArgs(BaseModel): + id: str + + +class CLIFileCreateArgs(BaseModel): + file: str + purpose: str + + +class CLIFile: + @staticmethod + def create(args: CLIFileCreateArgs) -> None: + with open(args.file, "rb") as file_reader: + buffer_reader = BufferReader(file_reader.read(), desc="Upload progress") + + file = get_client().files.create( + file=(args.file, buffer_reader), + # casts required because the API is typed for enums + # but we don't want to validate that here for forwards-compat + purpose=cast(Any, args.purpose), + ) + print_model(file) + + @staticmethod + def get(args: CLIFileIDArgs) -> None: + file = get_client().files.retrieve(file_id=args.id) + print_model(file) + + @staticmethod + def delete(args: CLIFileIDArgs) -> None: + file = get_client().files.delete(file_id=args.id) + print_model(file) + + @staticmethod + def list() -> None: + files = get_client().files.list() + for file in files: + print_model(file) diff --git a/openai/cli/_api/image.py b/openai/cli/_api/image.py new file mode 100644 index 00000000..3e2a0a90 --- /dev/null +++ b/openai/cli/_api/image.py @@ -0,0 +1,139 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, cast +from argparse import ArgumentParser + +from .._utils import get_client, print_model +from ..._types import NOT_GIVEN, NotGiven, NotGivenOr +from .._models import BaseModel +from .._progress import BufferReader + +if TYPE_CHECKING: + from argparse import _SubParsersAction + + +def register(subparser: _SubParsersAction[ArgumentParser]) -> None: + sub = subparser.add_parser("images.generate") + sub.add_argument("-m", "--model", type=str) + sub.add_argument("-p", "--prompt", type=str, required=True) + sub.add_argument("-n", "--num-images", type=int, default=1) + sub.add_argument("-s", "--size", type=str, default="1024x1024", help="Size of the output image") + sub.add_argument("--response-format", type=str, default="url") + sub.set_defaults(func=CLIImage.create, args_model=CLIImageCreateArgs) + + sub = subparser.add_parser("images.edit") + sub.add_argument("-m", "--model", type=str) + sub.add_argument("-p", "--prompt", type=str, required=True) + sub.add_argument("-n", "--num-images", type=int, default=1) + sub.add_argument( + "-I", + "--image", + type=str, + required=True, + help="Image to modify. Should be a local path and a PNG encoded image.", + ) + sub.add_argument("-s", "--size", type=str, default="1024x1024", help="Size of the output image") + sub.add_argument("--response-format", type=str, default="url") + sub.add_argument( + "-M", + "--mask", + type=str, + required=False, + help="Path to a mask image. It should be the same size as the image you're editing and a RGBA PNG image. The Alpha channel acts as the mask.", + ) + sub.set_defaults(func=CLIImage.edit, args_model=CLIImageEditArgs) + + sub = subparser.add_parser("images.create_variation") + sub.add_argument("-m", "--model", type=str) + sub.add_argument("-n", "--num-images", type=int, default=1) + sub.add_argument( + "-I", + "--image", + type=str, + required=True, + help="Image to modify. Should be a local path and a PNG encoded image.", + ) + sub.add_argument("-s", "--size", type=str, default="1024x1024", help="Size of the output image") + sub.add_argument("--response-format", type=str, default="url") + sub.set_defaults(func=CLIImage.create_variation, args_model=CLIImageCreateVariationArgs) + + +class CLIImageCreateArgs(BaseModel): + prompt: str + num_images: int + size: str + response_format: str + model: NotGivenOr[str] = NOT_GIVEN + + +class CLIImageCreateVariationArgs(BaseModel): + image: str + num_images: int + size: str + response_format: str + model: NotGivenOr[str] = NOT_GIVEN + + +class CLIImageEditArgs(BaseModel): + image: str + num_images: int + size: str + response_format: str + prompt: str + mask: NotGivenOr[str] = NOT_GIVEN + model: NotGivenOr[str] = NOT_GIVEN + + +class CLIImage: + @staticmethod + def create(args: CLIImageCreateArgs) -> None: + image = get_client().images.generate( + model=args.model, + prompt=args.prompt, + n=args.num_images, + # casts required because the API is typed for enums + # but we don't want to validate that here for forwards-compat + size=cast(Any, args.size), + response_format=cast(Any, args.response_format), + ) + print_model(image) + + @staticmethod + def create_variation(args: CLIImageCreateVariationArgs) -> None: + with open(args.image, "rb") as file_reader: + buffer_reader = BufferReader(file_reader.read(), desc="Upload progress") + + image = get_client().images.create_variation( + model=args.model, + image=("image", buffer_reader), + n=args.num_images, + # casts required because the API is typed for enums + # but we don't want to validate that here for forwards-compat + size=cast(Any, args.size), + response_format=cast(Any, args.response_format), + ) + print_model(image) + + @staticmethod + def edit(args: CLIImageEditArgs) -> None: + with open(args.image, "rb") as file_reader: + buffer_reader = BufferReader(file_reader.read(), desc="Image upload progress") + + if isinstance(args.mask, NotGiven): + mask: NotGivenOr[BufferReader] = NOT_GIVEN + else: + with open(args.mask, "rb") as file_reader: + mask = BufferReader(file_reader.read(), desc="Mask progress") + + image = get_client().images.edit( + model=args.model, + prompt=args.prompt, + image=("image", buffer_reader), + n=args.num_images, + mask=("mask", mask) if not isinstance(mask, NotGiven) else mask, + # casts required because the API is typed for enums + # but we don't want to validate that here for forwards-compat + size=cast(Any, args.size), + response_format=cast(Any, args.response_format), + ) + print_model(image) diff --git a/openai/cli/_api/models.py b/openai/cli/_api/models.py new file mode 100644 index 00000000..017218fa --- /dev/null +++ b/openai/cli/_api/models.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING +from argparse import ArgumentParser + +from .._utils import get_client, print_model +from .._models import BaseModel + +if TYPE_CHECKING: + from argparse import _SubParsersAction + + +def register(subparser: _SubParsersAction[ArgumentParser]) -> None: + sub = subparser.add_parser("models.list") + sub.set_defaults(func=CLIModels.list) + + sub = subparser.add_parser("models.retrieve") + sub.add_argument("-i", "--id", required=True, help="The model ID") + sub.set_defaults(func=CLIModels.get, args_model=CLIModelIDArgs) + + sub = subparser.add_parser("models.delete") + sub.add_argument("-i", "--id", required=True, help="The model ID") + sub.set_defaults(func=CLIModels.delete, args_model=CLIModelIDArgs) + + +class CLIModelIDArgs(BaseModel): + id: str + + +class CLIModels: + @staticmethod + def get(args: CLIModelIDArgs) -> None: + model = get_client().models.retrieve(model=args.id) + print_model(model) + + @staticmethod + def delete(args: CLIModelIDArgs) -> None: + model = get_client().models.delete(model=args.id) + print_model(model) + + @staticmethod + def list() -> None: + models = get_client().models.list() + for model in models: + print_model(model) diff --git a/openai/cli/_cli.py b/openai/cli/_cli.py new file mode 100644 index 00000000..72e5c923 --- /dev/null +++ b/openai/cli/_cli.py @@ -0,0 +1,234 @@ +from __future__ import annotations + +import sys +import logging +import argparse +from typing import Any, List, Type, Optional +from typing_extensions import ClassVar + +import httpx +import pydantic + +import openai + +from . import _tools +from .. import _ApiType, __version__ +from ._api import register_commands +from ._utils import can_use_http2 +from .._types import ProxiesDict +from ._errors import CLIError, display_error +from .._compat import PYDANTIC_V2, ConfigDict, model_parse +from .._models import BaseModel +from .._exceptions import APIError + +logger = logging.getLogger() +formatter = logging.Formatter("[%(asctime)s] %(message)s") +handler = logging.StreamHandler(sys.stderr) +handler.setFormatter(formatter) +logger.addHandler(handler) + + +class Arguments(BaseModel): + if PYDANTIC_V2: + model_config: ClassVar[ConfigDict] = ConfigDict( + extra="ignore", + ) + else: + + class Config(pydantic.BaseConfig): # type: ignore + extra: Any = pydantic.Extra.ignore # type: ignore + + verbosity: int + version: Optional[str] = None + + api_key: Optional[str] + api_base: Optional[str] + organization: Optional[str] + proxy: Optional[List[str]] + api_type: Optional[_ApiType] = None + api_version: Optional[str] = None + + # azure + azure_endpoint: Optional[str] = None + azure_ad_token: Optional[str] = None + + # internal, set by subparsers to parse their specific args + args_model: Optional[Type[BaseModel]] = None + + # internal, used so that subparsers can forward unknown arguments + unknown_args: List[str] = [] + allow_unknown_args: bool = False + + +def _build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description=None, prog="openai") + parser.add_argument( + "-v", + "--verbose", + action="count", + dest="verbosity", + default=0, + help="Set verbosity.", + ) + parser.add_argument("-b", "--api-base", help="What API base url to use.") + parser.add_argument("-k", "--api-key", help="What API key to use.") + parser.add_argument("-p", "--proxy", nargs="+", help="What proxy to use.") + parser.add_argument( + "-o", + "--organization", + help="Which organization to run as (will use your default organization if not specified)", + ) + parser.add_argument( + "-t", + "--api-type", + type=str, + choices=("openai", "azure"), + help="The backend API to call, must be `openai` or `azure`", + ) + parser.add_argument( + "--api-version", + help="The Azure API version, e.g. 'https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#rest-api-versioning'", + ) + + # azure + parser.add_argument( + "--azure-endpoint", + help="The Azure endpoint, e.g. 'https://endpoint.openai.azure.com'", + ) + parser.add_argument( + "--azure-ad-token", + help="A token from Azure Active Directory, https://www.microsoft.com/en-us/security/business/identity-access/microsoft-entra-id", + ) + + # prints the package version + parser.add_argument( + "-V", + "--version", + action="version", + version="%(prog)s " + __version__, + ) + + def help() -> None: + parser.print_help() + + parser.set_defaults(func=help) + + subparsers = parser.add_subparsers() + sub_api = subparsers.add_parser("api", help="Direct API calls") + + register_commands(sub_api) + + sub_tools = subparsers.add_parser("tools", help="Client side tools for convenience") + _tools.register_commands(sub_tools, subparsers) + + return parser + + +def main() -> int: + try: + _main() + except (APIError, CLIError, pydantic.ValidationError) as err: + display_error(err) + return 1 + except KeyboardInterrupt: + sys.stderr.write("\n") + return 1 + return 0 + + +def _parse_args(parser: argparse.ArgumentParser) -> tuple[argparse.Namespace, Arguments, list[str]]: + # argparse by default will strip out the `--` but we want to keep it for unknown arguments + if "--" in sys.argv: + idx = sys.argv.index("--") + known_args = sys.argv[1:idx] + unknown_args = sys.argv[idx:] + else: + known_args = sys.argv[1:] + unknown_args = [] + + parsed, remaining_unknown = parser.parse_known_args(known_args) + + # append any remaining unknown arguments from the initial parsing + remaining_unknown.extend(unknown_args) + + args = model_parse(Arguments, vars(parsed)) + if not args.allow_unknown_args: + # we have to parse twice to ensure any unknown arguments + # result in an error if that behaviour is desired + parser.parse_args() + + return parsed, args, remaining_unknown + + +def _main() -> None: + parser = _build_parser() + parsed, args, unknown = _parse_args(parser) + + if args.verbosity != 0: + sys.stderr.write("Warning: --verbosity isn't supported yet\n") + + proxies: ProxiesDict = {} + if args.proxy is not None: + for proxy in args.proxy: + key = "https://" if proxy.startswith("https") else "http://" + if key in proxies: + raise CLIError(f"Multiple {key} proxies given - only the last one would be used") + + proxies[key] = proxy + + http_client = httpx.Client( + proxies=proxies or None, + http2=can_use_http2(), + ) + openai.http_client = http_client + + if args.organization: + openai.organization = args.organization + + if args.api_key: + openai.api_key = args.api_key + + if args.api_base: + openai.base_url = args.api_base + + # azure + if args.api_type is not None: + openai.api_type = args.api_type + + if args.azure_endpoint is not None: + openai.azure_endpoint = args.azure_endpoint + + if args.api_version is not None: + openai.api_version = args.api_version + + if args.azure_ad_token is not None: + openai.azure_ad_token = args.azure_ad_token + + try: + if args.args_model: + parsed.func( + model_parse( + args.args_model, + { + **{ + # we omit None values so that they can be defaulted to `NotGiven` + # and we'll strip it from the API request + key: value + for key, value in vars(parsed).items() + if value is not None + }, + "unknown_args": unknown, + }, + ) + ) + else: + parsed.func() + finally: + try: + http_client.close() + except Exception: + pass + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/openai/cli/_errors.py b/openai/cli/_errors.py new file mode 100644 index 00000000..2bf06070 --- /dev/null +++ b/openai/cli/_errors.py @@ -0,0 +1,23 @@ +from __future__ import annotations + +import sys + +import pydantic + +from ._utils import Colors, organization_info +from .._exceptions import APIError, OpenAIError + + +class CLIError(OpenAIError): + ... + + +class SilentCLIError(CLIError): + ... + + +def display_error(err: CLIError | APIError | pydantic.ValidationError) -> None: + if isinstance(err, SilentCLIError): + return + + sys.stderr.write("{}{}Error:{} {}\n".format(organization_info(), Colors.FAIL, Colors.ENDC, err)) diff --git a/openai/cli/_models.py b/openai/cli/_models.py new file mode 100644 index 00000000..5583db26 --- /dev/null +++ b/openai/cli/_models.py @@ -0,0 +1,17 @@ +from typing import Any +from typing_extensions import ClassVar + +import pydantic + +from .. import _models +from .._compat import PYDANTIC_V2, ConfigDict + + +class BaseModel(_models.BaseModel): + if PYDANTIC_V2: + model_config: ClassVar[ConfigDict] = ConfigDict(extra="ignore", arbitrary_types_allowed=True) + else: + + class Config(pydantic.BaseConfig): # type: ignore + extra: Any = pydantic.Extra.ignore # type: ignore + arbitrary_types_allowed: bool = True diff --git a/openai/cli/_progress.py b/openai/cli/_progress.py new file mode 100644 index 00000000..8a7f2525 --- /dev/null +++ b/openai/cli/_progress.py @@ -0,0 +1,59 @@ +from __future__ import annotations + +import io +from typing import Callable +from typing_extensions import override + + +class CancelledError(Exception): + def __init__(self, msg: str) -> None: + self.msg = msg + super().__init__(msg) + + @override + def __str__(self) -> str: + return self.msg + + __repr__ = __str__ + + +class BufferReader(io.BytesIO): + def __init__(self, buf: bytes = b"", desc: str | None = None) -> None: + super().__init__(buf) + self._len = len(buf) + self._progress = 0 + self._callback = progress(len(buf), desc=desc) + + def __len__(self) -> int: + return self._len + + @override + def read(self, n: int | None = -1) -> bytes: + chunk = io.BytesIO.read(self, n) + self._progress += len(chunk) + + try: + self._callback(self._progress) + except Exception as e: # catches exception from the callback + raise CancelledError("The upload was cancelled: {}".format(e)) from e + + return chunk + + +def progress(total: float, desc: str | None) -> Callable[[float], None]: + import tqdm + + meter = tqdm.tqdm(total=total, unit_scale=True, desc=desc) + + def incr(progress: float) -> None: + meter.n = progress + if progress == total: + meter.close() + else: + meter.refresh() + + return incr + + +def MB(i: int) -> int: + return int(i // 1024**2) diff --git a/openai/cli/_tools/__init__.py b/openai/cli/_tools/__init__.py new file mode 100644 index 00000000..56a0260a --- /dev/null +++ b/openai/cli/_tools/__init__.py @@ -0,0 +1 @@ +from ._main import register_commands as register_commands diff --git a/openai/cli/_tools/_main.py b/openai/cli/_tools/_main.py new file mode 100644 index 00000000..bd6cda40 --- /dev/null +++ b/openai/cli/_tools/_main.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING +from argparse import ArgumentParser + +from . import migrate, fine_tunes + +if TYPE_CHECKING: + from argparse import _SubParsersAction + + +def register_commands(parser: ArgumentParser, subparser: _SubParsersAction[ArgumentParser]) -> None: + migrate.register(subparser) + + namespaced = parser.add_subparsers(title="Tools", help="Convenience client side tools") + + fine_tunes.register(namespaced) diff --git a/openai/cli/_tools/fine_tunes.py b/openai/cli/_tools/fine_tunes.py new file mode 100644 index 00000000..2128b889 --- /dev/null +++ b/openai/cli/_tools/fine_tunes.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +import sys +from typing import TYPE_CHECKING +from argparse import ArgumentParser + +from .._models import BaseModel +from ...lib._validators import ( + get_validators, + write_out_file, + read_any_format, + apply_validators, + apply_necessary_remediation, +) + +if TYPE_CHECKING: + from argparse import _SubParsersAction + + +def register(subparser: _SubParsersAction[ArgumentParser]) -> None: + sub = subparser.add_parser("fine_tunes.prepare_data") + sub.add_argument( + "-f", + "--file", + required=True, + help="JSONL, JSON, CSV, TSV, TXT or XLSX file containing prompt-completion examples to be analyzed." + "This should be the local file path.", + ) + sub.add_argument( + "-q", + "--quiet", + required=False, + action="store_true", + help="Auto accepts all suggestions, without asking for user input. To be used within scripts.", + ) + sub.set_defaults(func=prepare_data, args_model=PrepareDataArgs) + + +class PrepareDataArgs(BaseModel): + file: str + + quiet: bool + + +def prepare_data(args: PrepareDataArgs) -> None: + sys.stdout.write("Analyzing...\n") + fname = args.file + auto_accept = args.quiet + df, remediation = read_any_format(fname) + apply_necessary_remediation(None, remediation) + + validators = get_validators() + + assert df is not None + + apply_validators( + df, + fname, + remediation, + validators, + auto_accept, + write_out_file_func=write_out_file, + ) diff --git a/openai/cli/_tools/migrate.py b/openai/cli/_tools/migrate.py new file mode 100644 index 00000000..53073b86 --- /dev/null +++ b/openai/cli/_tools/migrate.py @@ -0,0 +1,181 @@ +from __future__ import annotations + +import os +import sys +import json +import shutil +import tarfile +import platform +import subprocess +from typing import TYPE_CHECKING, List +from pathlib import Path +from argparse import ArgumentParser + +import httpx + +from .._errors import CLIError, SilentCLIError +from .._models import BaseModel + +if TYPE_CHECKING: + from argparse import _SubParsersAction + + +def register(subparser: _SubParsersAction[ArgumentParser]) -> None: + sub = subparser.add_parser("migrate") + sub.set_defaults(func=migrate, args_model=MigrateArgs, allow_unknown_args=True) + + sub = subparser.add_parser("grit") + sub.set_defaults(func=grit, args_model=GritArgs, allow_unknown_args=True) + + +class GritArgs(BaseModel): + # internal + unknown_args: List[str] = [] + + +def grit(args: GritArgs) -> None: + grit_path = install() + + try: + subprocess.check_call([grit_path, *args.unknown_args]) + except subprocess.CalledProcessError: + # stdout and stderr are forwarded by subprocess so an error will already + # have been displayed + raise SilentCLIError() from None + + +class MigrateArgs(BaseModel): + # internal + unknown_args: List[str] = [] + + +def migrate(args: MigrateArgs) -> None: + grit_path = install() + + try: + subprocess.check_call([grit_path, "apply", "openai", *args.unknown_args]) + except subprocess.CalledProcessError: + # stdout and stderr are forwarded by subprocess so an error will already + # have been displayed + raise SilentCLIError() from None + + +# handles downloading the Grit CLI until they provide their own PyPi package + +KEYGEN_ACCOUNT = "custodian-dev" + + +def _cache_dir() -> Path: + xdg = os.environ.get("XDG_CACHE_HOME") + if xdg is not None: + return Path(xdg) + + return Path.home() / ".cache" + + +def _debug(message: str) -> None: + if not os.environ.get("DEBUG"): + return + + sys.stdout.write(f"[DEBUG]: {message}\n") + + +def install() -> Path: + """Installs the Grit CLI and returns the location of the binary""" + if sys.platform == "win32": + raise CLIError("Windows is not supported yet in the migration CLI") + + platform = "macos" if sys.platform == "darwin" else "linux" + + dir_name = _cache_dir() / "openai-python" + install_dir = dir_name / ".install" + target_dir = install_dir / "bin" + + target_path = target_dir / "marzano" + temp_file = target_dir / "marzano.tmp" + + if target_path.exists(): + _debug(f"{target_path} already exists") + sys.stdout.flush() + return target_path + + _debug(f"Using Grit CLI path: {target_path}") + + target_dir.mkdir(parents=True, exist_ok=True) + + if temp_file.exists(): + temp_file.unlink() + + arch = _get_arch() + _debug(f"Using architecture {arch}") + + file_name = f"marzano-{platform}-{arch}" + meta_url = f"https://api.keygen.sh/v1/accounts/{KEYGEN_ACCOUNT}/artifacts/{file_name}" + + sys.stdout.write(f"Retrieving Grit CLI metadata from {meta_url}\n") + with httpx.Client() as client: + response = client.get(meta_url) # pyright: ignore[reportUnknownMemberType] + + data = response.json() + errors = data.get("errors") + if errors: + for error in errors: + sys.stdout.write(f"{error}\n") + + raise CLIError("Could not locate Grit CLI binary - see above errors") + + write_manifest(install_dir, data["data"]["relationships"]["release"]["data"]["id"]) + + link = data["data"]["links"]["redirect"] + _debug(f"Redirect URL {link}") + + download_response = client.get(link) # pyright: ignore[reportUnknownMemberType] + with open(temp_file, "wb") as file: + for chunk in download_response.iter_bytes(): + file.write(chunk) + + unpacked_dir = target_dir / "cli-bin" + unpacked_dir.mkdir(parents=True, exist_ok=True) + + with tarfile.open(temp_file, "r:gz") as archive: + archive.extractall(unpacked_dir, filter="data") + + for item in unpacked_dir.iterdir(): + item.rename(target_dir / item.name) + + shutil.rmtree(unpacked_dir) + os.remove(temp_file) + os.chmod(target_path, 0o755) + + sys.stdout.flush() + + return target_path + + +def _get_arch() -> str: + architecture = platform.machine().lower() + + # Map the architecture names to Node.js equivalents + arch_map = { + "x86_64": "x64", + "amd64": "x64", + "armv7l": "arm", + "aarch64": "arm64", + } + + return arch_map.get(architecture, architecture) + + +def write_manifest(install_path: Path, release: str) -> None: + manifest = { + "installPath": str(install_path), + "binaries": { + "marzano": { + "name": "marzano", + "release": release, + }, + }, + } + manifest_path = Path(install_path) / "manifests.json" + with open(manifest_path, "w") as f: + json.dump(manifest, f, indent=2) diff --git a/openai/cli/_utils.py b/openai/cli/_utils.py new file mode 100644 index 00000000..673eed61 --- /dev/null +++ b/openai/cli/_utils.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +import sys + +import openai + +from .. import OpenAI, _load_client +from .._compat import model_json +from .._models import BaseModel + + +class Colors: + HEADER = "\033[95m" + OKBLUE = "\033[94m" + OKGREEN = "\033[92m" + WARNING = "\033[93m" + FAIL = "\033[91m" + ENDC = "\033[0m" + BOLD = "\033[1m" + UNDERLINE = "\033[4m" + + +def get_client() -> OpenAI: + return _load_client() + + +def organization_info() -> str: + organization = openai.organization + if organization is not None: + return "[organization={}] ".format(organization) + + return "" + + +def print_model(model: BaseModel) -> None: + sys.stdout.write(model_json(model, indent=2) + "\n") + + +def can_use_http2() -> bool: + try: + import h2 # type: ignore # noqa + except ImportError: + return False + + return True diff --git a/openai/lib/.keep b/openai/lib/.keep new file mode 100644 index 00000000..5e2c99fd --- /dev/null +++ b/openai/lib/.keep @@ -0,0 +1,4 @@ +File generated from our OpenAPI spec by Stainless. + +This directory can be used to store custom files to expand the SDK. +It is ignored by Stainless code generation and its content (other than this keep file) won't be touched. \ No newline at end of file diff --git a/openai/lib/_old_api.py b/openai/lib/_old_api.py new file mode 100644 index 00000000..929c87e8 --- /dev/null +++ b/openai/lib/_old_api.py @@ -0,0 +1,72 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any +from typing_extensions import override + +from .._utils import LazyProxy +from .._exceptions import OpenAIError + +INSTRUCTIONS = """ + +You tried to access openai.{symbol}, but this is no longer supported in openai>=1.0.0 - see the README at https://github.com/openai/openai-python for the API. + +You can run `openai migrate` to automatically upgrade your codebase to use the 1.0.0 interface. + +Alternatively, you can pin your installation to the old version, e.g. `pip install openai==0.28` + +A detailed migration guide is available here: https://github.com/openai/openai-python/discussions/742 +""" + + +class APIRemovedInV1(OpenAIError): + def __init__(self, *, symbol: str) -> None: + super().__init__(INSTRUCTIONS.format(symbol=symbol)) + + +class APIRemovedInV1Proxy(LazyProxy[Any]): + def __init__(self, *, symbol: str) -> None: + super().__init__() + self._symbol = symbol + + @override + def __load__(self) -> Any: + # return the proxy until it is eventually called so that + # we don't break people that are just checking the attributes + # of a module + return self + + def __call__(self, *_args: Any, **_kwargs: Any) -> Any: + raise APIRemovedInV1(symbol=self._symbol) + + +SYMBOLS = [ + "Edit", + "File", + "Audio", + "Image", + "Model", + "Engine", + "Customer", + "FineTune", + "Embedding", + "Completion", + "Deployment", + "Moderation", + "ErrorObject", + "FineTuningJob", + "ChatCompletion", +] + +# we explicitly tell type checkers that nothing is exported +# from this file so that when we re-export the old symbols +# in `openai/__init__.py` they aren't added to the auto-complete +# suggestions given by editors +if TYPE_CHECKING: + __all__: list[str] = [] +else: + __all__ = SYMBOLS + + +__locals = locals() +for symbol in SYMBOLS: + __locals[symbol] = APIRemovedInV1Proxy(symbol=symbol) diff --git a/openai/lib/_validators.py b/openai/lib/_validators.py new file mode 100644 index 00000000..e36f0e95 --- /dev/null +++ b/openai/lib/_validators.py @@ -0,0 +1,805 @@ +# pyright: basic +from __future__ import annotations + +import os +import sys +from typing import Any, TypeVar, Callable, Optional, NamedTuple +from typing_extensions import TypeAlias + +from .._extras import pandas as pd + + +class Remediation(NamedTuple): + name: str + immediate_msg: Optional[str] = None + necessary_msg: Optional[str] = None + necessary_fn: Optional[Callable[[Any], Any]] = None + optional_msg: Optional[str] = None + optional_fn: Optional[Callable[[Any], Any]] = None + error_msg: Optional[str] = None + + +OptionalDataFrameT = TypeVar("OptionalDataFrameT", bound="Optional[pd.DataFrame]") + + +def num_examples_validator(df: pd.DataFrame) -> Remediation: + """ + This validator will only print out the number of examples and recommend to the user to increase the number of examples if less than 100. + """ + MIN_EXAMPLES = 100 + optional_suggestion = ( + "" + if len(df) >= MIN_EXAMPLES + else ". In general, we recommend having at least a few hundred examples. We've found that performance tends to linearly increase for every doubling of the number of examples" + ) + immediate_msg = f"\n- Your file contains {len(df)} prompt-completion pairs{optional_suggestion}" + return Remediation(name="num_examples", immediate_msg=immediate_msg) + + +def necessary_column_validator(df: pd.DataFrame, necessary_column: str) -> Remediation: + """ + This validator will ensure that the necessary column is present in the dataframe. + """ + + def lower_case_column(df: pd.DataFrame, column: Any) -> pd.DataFrame: + cols = [c for c in df.columns if str(c).lower() == column] + df.rename(columns={cols[0]: column.lower()}, inplace=True) + return df + + immediate_msg = None + necessary_fn = None + necessary_msg = None + error_msg = None + + if necessary_column not in df.columns: + if necessary_column in [str(c).lower() for c in df.columns]: + + def lower_case_column_creator(df: pd.DataFrame) -> pd.DataFrame: + return lower_case_column(df, necessary_column) + + necessary_fn = lower_case_column_creator + immediate_msg = f"\n- The `{necessary_column}` column/key should be lowercase" + necessary_msg = f"Lower case column name to `{necessary_column}`" + else: + error_msg = f"`{necessary_column}` column/key is missing. Please make sure you name your columns/keys appropriately, then retry" + + return Remediation( + name="necessary_column", + immediate_msg=immediate_msg, + necessary_msg=necessary_msg, + necessary_fn=necessary_fn, + error_msg=error_msg, + ) + + +def additional_column_validator(df: pd.DataFrame, fields: list[str] = ["prompt", "completion"]) -> Remediation: + """ + This validator will remove additional columns from the dataframe. + """ + additional_columns = [] + necessary_msg = None + immediate_msg = None + necessary_fn = None # type: ignore + + if len(df.columns) > 2: + additional_columns = [c for c in df.columns if c not in fields] + warn_message = "" + for ac in additional_columns: + dups = [c for c in additional_columns if ac in c] + if len(dups) > 0: + warn_message += f"\n WARNING: Some of the additional columns/keys contain `{ac}` in their name. These will be ignored, and the column/key `{ac}` will be used instead. This could also result from a duplicate column/key in the provided file." + immediate_msg = f"\n- The input file should contain exactly two columns/keys per row. Additional columns/keys present are: {additional_columns}{warn_message}" + necessary_msg = f"Remove additional columns/keys: {additional_columns}" + + def necessary_fn(x: Any) -> Any: + return x[fields] + + return Remediation( + name="additional_column", + immediate_msg=immediate_msg, + necessary_msg=necessary_msg, + necessary_fn=necessary_fn, + ) + + +def non_empty_field_validator(df: pd.DataFrame, field: str = "completion") -> Remediation: + """ + This validator will ensure that no completion is empty. + """ + necessary_msg = None + necessary_fn = None # type: ignore + immediate_msg = None + + if df[field].apply(lambda x: x == "").any() or df[field].isnull().any(): + empty_rows = (df[field] == "") | (df[field].isnull()) + empty_indexes = df.reset_index().index[empty_rows].tolist() + immediate_msg = f"\n- `{field}` column/key should not contain empty strings. These are rows: {empty_indexes}" + + def necessary_fn(x: Any) -> Any: + return x[x[field] != ""].dropna(subset=[field]) + + necessary_msg = f"Remove {len(empty_indexes)} rows with empty {field}s" + + return Remediation( + name=f"empty_{field}", + immediate_msg=immediate_msg, + necessary_msg=necessary_msg, + necessary_fn=necessary_fn, + ) + + +def duplicated_rows_validator(df: pd.DataFrame, fields: list[str] = ["prompt", "completion"]) -> Remediation: + """ + This validator will suggest to the user to remove duplicate rows if they exist. + """ + duplicated_rows = df.duplicated(subset=fields) + duplicated_indexes = df.reset_index().index[duplicated_rows].tolist() + immediate_msg = None + optional_msg = None + optional_fn = None # type: ignore + + if len(duplicated_indexes) > 0: + immediate_msg = f"\n- There are {len(duplicated_indexes)} duplicated {'-'.join(fields)} sets. These are rows: {duplicated_indexes}" + optional_msg = f"Remove {len(duplicated_indexes)} duplicate rows" + + def optional_fn(x: Any) -> Any: + return x.drop_duplicates(subset=fields) + + return Remediation( + name="duplicated_rows", + immediate_msg=immediate_msg, + optional_msg=optional_msg, + optional_fn=optional_fn, + ) + + +def long_examples_validator(df: pd.DataFrame) -> Remediation: + """ + This validator will suggest to the user to remove examples that are too long. + """ + immediate_msg = None + optional_msg = None + optional_fn = None # type: ignore + + ft_type = infer_task_type(df) + if ft_type != "open-ended generation": + + def get_long_indexes(d: pd.DataFrame) -> Any: + long_examples = d.apply(lambda x: len(x.prompt) + len(x.completion) > 10000, axis=1) + return d.reset_index().index[long_examples].tolist() + + long_indexes = get_long_indexes(df) + + if len(long_indexes) > 0: + immediate_msg = f"\n- There are {len(long_indexes)} examples that are very long. These are rows: {long_indexes}\nFor conditional generation, and for classification the examples shouldn't be longer than 2048 tokens." + optional_msg = f"Remove {len(long_indexes)} long examples" + + def optional_fn(x: Any) -> Any: + long_indexes_to_drop = get_long_indexes(x) + if long_indexes != long_indexes_to_drop: + sys.stdout.write( + f"The indices of the long examples has changed as a result of a previously applied recommendation.\nThe {len(long_indexes_to_drop)} long examples to be dropped are now at the following indices: {long_indexes_to_drop}\n" + ) + return x.drop(long_indexes_to_drop) + + return Remediation( + name="long_examples", + immediate_msg=immediate_msg, + optional_msg=optional_msg, + optional_fn=optional_fn, + ) + + +def common_prompt_suffix_validator(df: pd.DataFrame) -> Remediation: + """ + This validator will suggest to add a common suffix to the prompt if one doesn't already exist in case of classification or conditional generation. + """ + error_msg = None + immediate_msg = None + optional_msg = None + optional_fn = None # type: ignore + + # Find a suffix which is not contained within the prompt otherwise + suggested_suffix = "\n\n### =>\n\n" + suffix_options = [ + " ->", + "\n\n###\n\n", + "\n\n===\n\n", + "\n\n---\n\n", + "\n\n===>\n\n", + "\n\n--->\n\n", + ] + for suffix_option in suffix_options: + if suffix_option == " ->": + if df.prompt.str.contains("\n").any(): + continue + if df.prompt.str.contains(suffix_option, regex=False).any(): + continue + suggested_suffix = suffix_option + break + display_suggested_suffix = suggested_suffix.replace("\n", "\\n") + + ft_type = infer_task_type(df) + if ft_type == "open-ended generation": + return Remediation(name="common_suffix") + + def add_suffix(x: Any, suffix: Any) -> Any: + x["prompt"] += suffix + return x + + common_suffix = get_common_xfix(df.prompt, xfix="suffix") + if (df.prompt == common_suffix).all(): + error_msg = f"All prompts are identical: `{common_suffix}`\nConsider leaving the prompts blank if you want to do open-ended generation, otherwise ensure prompts are different" + return Remediation(name="common_suffix", error_msg=error_msg) + + if common_suffix != "": + common_suffix_new_line_handled = common_suffix.replace("\n", "\\n") + immediate_msg = f"\n- All prompts end with suffix `{common_suffix_new_line_handled}`" + if len(common_suffix) > 10: + immediate_msg += f". This suffix seems very long. Consider replacing with a shorter suffix, such as `{display_suggested_suffix}`" + if df.prompt.str[: -len(common_suffix)].str.contains(common_suffix, regex=False).any(): + immediate_msg += f"\n WARNING: Some of your prompts contain the suffix `{common_suffix}` more than once. We strongly suggest that you review your prompts and add a unique suffix" + + else: + immediate_msg = "\n- Your data does not contain a common separator at the end of your prompts. Having a separator string appended to the end of the prompt makes it clearer to the fine-tuned model where the completion should begin. See https://platform.openai.com/docs/guides/fine-tuning/preparing-your-dataset for more detail and examples. If you intend to do open-ended generation, then you should leave the prompts empty" + + if common_suffix == "": + optional_msg = f"Add a suffix separator `{display_suggested_suffix}` to all prompts" + + def optional_fn(x: Any) -> Any: + return add_suffix(x, suggested_suffix) + + return Remediation( + name="common_completion_suffix", + immediate_msg=immediate_msg, + optional_msg=optional_msg, + optional_fn=optional_fn, + error_msg=error_msg, + ) + + +def common_prompt_prefix_validator(df: pd.DataFrame) -> Remediation: + """ + This validator will suggest to remove a common prefix from the prompt if a long one exist. + """ + MAX_PREFIX_LEN = 12 + + immediate_msg = None + optional_msg = None + optional_fn = None # type: ignore + + common_prefix = get_common_xfix(df.prompt, xfix="prefix") + if common_prefix == "": + return Remediation(name="common_prefix") + + def remove_common_prefix(x: Any, prefix: Any) -> Any: + x["prompt"] = x["prompt"].str[len(prefix) :] + return x + + if (df.prompt == common_prefix).all(): + # already handled by common_suffix_validator + return Remediation(name="common_prefix") + + if common_prefix != "": + immediate_msg = f"\n- All prompts start with prefix `{common_prefix}`" + if MAX_PREFIX_LEN < len(common_prefix): + immediate_msg += ". Fine-tuning doesn't require the instruction specifying the task, or a few-shot example scenario. Most of the time you should only add the input data into the prompt, and the desired output into the completion" + optional_msg = f"Remove prefix `{common_prefix}` from all prompts" + + def optional_fn(x: Any) -> Any: + return remove_common_prefix(x, common_prefix) + + return Remediation( + name="common_prompt_prefix", + immediate_msg=immediate_msg, + optional_msg=optional_msg, + optional_fn=optional_fn, + ) + + +def common_completion_prefix_validator(df: pd.DataFrame) -> Remediation: + """ + This validator will suggest to remove a common prefix from the completion if a long one exist. + """ + MAX_PREFIX_LEN = 5 + + common_prefix = get_common_xfix(df.completion, xfix="prefix") + ws_prefix = len(common_prefix) > 0 and common_prefix[0] == " " + if len(common_prefix) < MAX_PREFIX_LEN: + return Remediation(name="common_prefix") + + def remove_common_prefix(x: Any, prefix: Any, ws_prefix: Any) -> Any: + x["completion"] = x["completion"].str[len(prefix) :] + if ws_prefix: + # keep the single whitespace as prefix + x["completion"] = f" {x['completion']}" + return x + + if (df.completion == common_prefix).all(): + # already handled by common_suffix_validator + return Remediation(name="common_prefix") + + immediate_msg = f"\n- All completions start with prefix `{common_prefix}`. Most of the time you should only add the output data into the completion, without any prefix" + optional_msg = f"Remove prefix `{common_prefix}` from all completions" + + def optional_fn(x: Any) -> Any: + return remove_common_prefix(x, common_prefix, ws_prefix) + + return Remediation( + name="common_completion_prefix", + immediate_msg=immediate_msg, + optional_msg=optional_msg, + optional_fn=optional_fn, + ) + + +def common_completion_suffix_validator(df: pd.DataFrame) -> Remediation: + """ + This validator will suggest to add a common suffix to the completion if one doesn't already exist in case of classification or conditional generation. + """ + error_msg = None + immediate_msg = None + optional_msg = None + optional_fn = None # type: ignore + + ft_type = infer_task_type(df) + if ft_type == "open-ended generation" or ft_type == "classification": + return Remediation(name="common_suffix") + + common_suffix = get_common_xfix(df.completion, xfix="suffix") + if (df.completion == common_suffix).all(): + error_msg = f"All completions are identical: `{common_suffix}`\nEnsure completions are different, otherwise the model will just repeat `{common_suffix}`" + return Remediation(name="common_suffix", error_msg=error_msg) + + # Find a suffix which is not contained within the completion otherwise + suggested_suffix = " [END]" + suffix_options = [ + "\n", + ".", + " END", + "***", + "+++", + "&&&", + "$$$", + "@@@", + "%%%", + ] + for suffix_option in suffix_options: + if df.completion.str.contains(suffix_option, regex=False).any(): + continue + suggested_suffix = suffix_option + break + display_suggested_suffix = suggested_suffix.replace("\n", "\\n") + + def add_suffix(x: Any, suffix: Any) -> Any: + x["completion"] += suffix + return x + + if common_suffix != "": + common_suffix_new_line_handled = common_suffix.replace("\n", "\\n") + immediate_msg = f"\n- All completions end with suffix `{common_suffix_new_line_handled}`" + if len(common_suffix) > 10: + immediate_msg += f". This suffix seems very long. Consider replacing with a shorter suffix, such as `{display_suggested_suffix}`" + if df.completion.str[: -len(common_suffix)].str.contains(common_suffix, regex=False).any(): + immediate_msg += f"\n WARNING: Some of your completions contain the suffix `{common_suffix}` more than once. We suggest that you review your completions and add a unique ending" + + else: + immediate_msg = "\n- Your data does not contain a common ending at the end of your completions. Having a common ending string appended to the end of the completion makes it clearer to the fine-tuned model where the completion should end. See https://platform.openai.com/docs/guides/fine-tuning/preparing-your-dataset for more detail and examples." + + if common_suffix == "": + optional_msg = f"Add a suffix ending `{display_suggested_suffix}` to all completions" + + def optional_fn(x: Any) -> Any: + return add_suffix(x, suggested_suffix) + + return Remediation( + name="common_completion_suffix", + immediate_msg=immediate_msg, + optional_msg=optional_msg, + optional_fn=optional_fn, + error_msg=error_msg, + ) + + +def completions_space_start_validator(df: pd.DataFrame) -> Remediation: + """ + This validator will suggest to add a space at the start of the completion if it doesn't already exist. This helps with tokenization. + """ + + def add_space_start(x: Any) -> Any: + x["completion"] = x["completion"].apply(lambda s: ("" if s.startswith(" ") else " ") + s) + return x + + optional_msg = None + optional_fn = None + immediate_msg = None + + if df.completion.str[:1].nunique() != 1 or df.completion.values[0][0] != " ": + immediate_msg = "\n- The completion should start with a whitespace character (` `). This tends to produce better results due to the tokenization we use. See https://platform.openai.com/docs/guides/fine-tuning/preparing-your-dataset for more details" + optional_msg = "Add a whitespace character to the beginning of the completion" + optional_fn = add_space_start + return Remediation( + name="completion_space_start", + immediate_msg=immediate_msg, + optional_msg=optional_msg, + optional_fn=optional_fn, + ) + + +def lower_case_validator(df: pd.DataFrame, column: Any) -> Remediation | None: + """ + This validator will suggest to lowercase the column values, if more than a third of letters are uppercase. + """ + + def lower_case(x: Any) -> Any: + x[column] = x[column].str.lower() + return x + + count_upper = df[column].apply(lambda x: sum(1 for c in x if c.isalpha() and c.isupper())).sum() + count_lower = df[column].apply(lambda x: sum(1 for c in x if c.isalpha() and c.islower())).sum() + + if count_upper * 2 > count_lower: + return Remediation( + name="lower_case", + immediate_msg=f"\n- More than a third of your `{column}` column/key is uppercase. Uppercase {column}s tends to perform worse than a mixture of case encountered in normal language. We recommend to lower case the data if that makes sense in your domain. See https://platform.openai.com/docs/guides/fine-tuning/preparing-your-dataset for more details", + optional_msg=f"Lowercase all your data in column/key `{column}`", + optional_fn=lower_case, + ) + return None + + +def read_any_format( + fname: str, fields: list[str] = ["prompt", "completion"] +) -> tuple[pd.DataFrame | None, Remediation]: + """ + This function will read a file saved in .csv, .json, .txt, .xlsx or .tsv format using pandas. + - for .xlsx it will read the first sheet + - for .txt it will assume completions and split on newline + """ + remediation = None + necessary_msg = None + immediate_msg = None + error_msg = None + df = None + + if os.path.isfile(fname): + try: + if fname.lower().endswith(".csv") or fname.lower().endswith(".tsv"): + file_extension_str, separator = ("CSV", ",") if fname.lower().endswith(".csv") else ("TSV", "\t") + immediate_msg = ( + f"\n- Based on your file extension, your file is formatted as a {file_extension_str} file" + ) + necessary_msg = f"Your format `{file_extension_str}` will be converted to `JSONL`" + df = pd.read_csv(fname, sep=separator, dtype=str).fillna("") + elif fname.lower().endswith(".xlsx"): + immediate_msg = "\n- Based on your file extension, your file is formatted as an Excel file" + necessary_msg = "Your format `XLSX` will be converted to `JSONL`" + xls = pd.ExcelFile(fname) + sheets = xls.sheet_names + if len(sheets) > 1: + immediate_msg += "\n- Your Excel file contains more than one sheet. Please either save as csv or ensure all data is present in the first sheet. WARNING: Reading only the first sheet..." + df = pd.read_excel(fname, dtype=str).fillna("") + elif fname.lower().endswith(".txt"): + immediate_msg = "\n- Based on your file extension, you provided a text file" + necessary_msg = "Your format `TXT` will be converted to `JSONL`" + with open(fname, "r") as f: + content = f.read() + df = pd.DataFrame( + [["", line] for line in content.split("\n")], + columns=fields, + dtype=str, + ).fillna("") + elif fname.lower().endswith(".jsonl"): + df = pd.read_json(fname, lines=True, dtype=str).fillna("") # type: ignore + if len(df) == 1: # type: ignore + # this is NOT what we expect for a .jsonl file + immediate_msg = "\n- Your JSONL file appears to be in a JSON format. Your file will be converted to JSONL format" + necessary_msg = "Your format `JSON` will be converted to `JSONL`" + df = pd.read_json(fname, dtype=str).fillna("") # type: ignore + else: + pass # this is what we expect for a .jsonl file + elif fname.lower().endswith(".json"): + try: + # to handle case where .json file is actually a .jsonl file + df = pd.read_json(fname, lines=True, dtype=str).fillna("") # type: ignore + if len(df) == 1: # type: ignore + # this code path corresponds to a .json file that has one line + df = pd.read_json(fname, dtype=str).fillna("") # type: ignore + else: + # this is NOT what we expect for a .json file + immediate_msg = "\n- Your JSON file appears to be in a JSONL format. Your file will be converted to JSONL format" + necessary_msg = "Your format `JSON` will be converted to `JSONL`" + except ValueError: + # this code path corresponds to a .json file that has multiple lines (i.e. it is indented) + df = pd.read_json(fname, dtype=str).fillna("") # type: ignore + else: + error_msg = ( + "Your file must have one of the following extensions: .CSV, .TSV, .XLSX, .TXT, .JSON or .JSONL" + ) + if "." in fname: + error_msg += f" Your file `{fname}` ends with the extension `.{fname.split('.')[-1]}` which is not supported." + else: + error_msg += f" Your file `{fname}` is missing a file extension." + + except (ValueError, TypeError): + file_extension_str = fname.split(".")[-1].upper() + error_msg = f"Your file `{fname}` does not appear to be in valid {file_extension_str} format. Please ensure your file is formatted as a valid {file_extension_str} file." + + else: + error_msg = f"File {fname} does not exist." + + remediation = Remediation( + name="read_any_format", + necessary_msg=necessary_msg, + immediate_msg=immediate_msg, + error_msg=error_msg, + ) + return df, remediation + + +def format_inferrer_validator(df: pd.DataFrame) -> Remediation: + """ + This validator will infer the likely fine-tuning format of the data, and display it to the user if it is classification. + It will also suggest to use ada and explain train/validation split benefits. + """ + ft_type = infer_task_type(df) + immediate_msg = None + if ft_type == "classification": + immediate_msg = f"\n- Based on your data it seems like you're trying to fine-tune a model for {ft_type}\n- For classification, we recommend you try one of the faster and cheaper models, such as `ada`\n- For classification, you can estimate the expected model performance by keeping a held out dataset, which is not used for training" + return Remediation(name="num_examples", immediate_msg=immediate_msg) + + +def apply_necessary_remediation(df: OptionalDataFrameT, remediation: Remediation) -> OptionalDataFrameT: + """ + This function will apply a necessary remediation to a dataframe, or print an error message if one exists. + """ + if remediation.error_msg is not None: + sys.stderr.write(f"\n\nERROR in {remediation.name} validator: {remediation.error_msg}\n\nAborting...") + sys.exit(1) + if remediation.immediate_msg is not None: + sys.stdout.write(remediation.immediate_msg) + if remediation.necessary_fn is not None: + df = remediation.necessary_fn(df) + return df + + +def accept_suggestion(input_text: str, auto_accept: bool) -> bool: + sys.stdout.write(input_text) + if auto_accept: + sys.stdout.write("Y\n") + return True + return input().lower() != "n" + + +def apply_optional_remediation( + df: pd.DataFrame, remediation: Remediation, auto_accept: bool +) -> tuple[pd.DataFrame, bool]: + """ + This function will apply an optional remediation to a dataframe, based on the user input. + """ + optional_applied = False + input_text = f"- [Recommended] {remediation.optional_msg} [Y/n]: " + if remediation.optional_msg is not None: + if accept_suggestion(input_text, auto_accept): + assert remediation.optional_fn is not None + df = remediation.optional_fn(df) + optional_applied = True + if remediation.necessary_msg is not None: + sys.stdout.write(f"- [Necessary] {remediation.necessary_msg}\n") + return df, optional_applied + + +def estimate_fine_tuning_time(df: pd.DataFrame) -> None: + """ + Estimate the time it'll take to fine-tune the dataset + """ + ft_format = infer_task_type(df) + expected_time = 1.0 + if ft_format == "classification": + num_examples = len(df) + expected_time = num_examples * 1.44 + else: + size = df.memory_usage(index=True).sum() + expected_time = size * 0.0515 + + def format_time(time: float) -> str: + if time < 60: + return f"{round(time, 2)} seconds" + elif time < 3600: + return f"{round(time / 60, 2)} minutes" + elif time < 86400: + return f"{round(time / 3600, 2)} hours" + else: + return f"{round(time / 86400, 2)} days" + + time_string = format_time(expected_time + 140) + sys.stdout.write( + f"Once your model starts training, it'll approximately take {time_string} to train a `curie` model, and less for `ada` and `babbage`. Queue will approximately take half an hour per job ahead of you.\n" + ) + + +def get_outfnames(fname: str, split: bool) -> list[str]: + suffixes = ["_train", "_valid"] if split else [""] + i = 0 + while True: + index_suffix = f" ({i})" if i > 0 else "" + candidate_fnames = [f"{os.path.splitext(fname)[0]}_prepared{suffix}{index_suffix}.jsonl" for suffix in suffixes] + if not any(os.path.isfile(f) for f in candidate_fnames): + return candidate_fnames + i += 1 + + +def get_classification_hyperparams(df: pd.DataFrame) -> tuple[int, object]: + n_classes = df.completion.nunique() + pos_class = None + if n_classes == 2: + pos_class = df.completion.value_counts().index[0] + return n_classes, pos_class + + +def write_out_file(df: pd.DataFrame, fname: str, any_remediations: bool, auto_accept: bool) -> None: + """ + This function will write out a dataframe to a file, if the user would like to proceed, and also offer a fine-tuning command with the newly created file. + For classification it will optionally ask the user if they would like to split the data into train/valid files, and modify the suggested command to include the valid set. + """ + ft_format = infer_task_type(df) + common_prompt_suffix = get_common_xfix(df.prompt, xfix="suffix") + common_completion_suffix = get_common_xfix(df.completion, xfix="suffix") + + split = False + input_text = "- [Recommended] Would you like to split into training and validation set? [Y/n]: " + if ft_format == "classification": + if accept_suggestion(input_text, auto_accept): + split = True + + additional_params = "" + common_prompt_suffix_new_line_handled = common_prompt_suffix.replace("\n", "\\n") + common_completion_suffix_new_line_handled = common_completion_suffix.replace("\n", "\\n") + optional_ending_string = ( + f' Make sure to include `stop=["{common_completion_suffix_new_line_handled}"]` so that the generated texts ends at the expected place.' + if len(common_completion_suffix_new_line_handled) > 0 + else "" + ) + + input_text = "\n\nYour data will be written to a new JSONL file. Proceed [Y/n]: " + + if not any_remediations and not split: + sys.stdout.write( + f'\nYou can use your file for fine-tuning:\n> openai api fine_tunes.create -t "{fname}"{additional_params}\n\nAfter you’ve fine-tuned a model, remember that your prompt has to end with the indicator string `{common_prompt_suffix_new_line_handled}` for the model to start generating completions, rather than continuing with the prompt.{optional_ending_string}\n' + ) + estimate_fine_tuning_time(df) + + elif accept_suggestion(input_text, auto_accept): + fnames = get_outfnames(fname, split) + if split: + assert len(fnames) == 2 and "train" in fnames[0] and "valid" in fnames[1] + MAX_VALID_EXAMPLES = 1000 + n_train = max(len(df) - MAX_VALID_EXAMPLES, int(len(df) * 0.8)) + df_train = df.sample(n=n_train, random_state=42) + df_valid = df.drop(df_train.index) + df_train[["prompt", "completion"]].to_json( # type: ignore + fnames[0], lines=True, orient="records", force_ascii=False + ) + df_valid[["prompt", "completion"]].to_json(fnames[1], lines=True, orient="records", force_ascii=False) + + n_classes, pos_class = get_classification_hyperparams(df) + additional_params += " --compute_classification_metrics" + if n_classes == 2: + additional_params += f' --classification_positive_class "{pos_class}"' + else: + additional_params += f" --classification_n_classes {n_classes}" + else: + assert len(fnames) == 1 + df[["prompt", "completion"]].to_json(fnames[0], lines=True, orient="records", force_ascii=False) + + # Add -v VALID_FILE if we split the file into train / valid + files_string = ("s" if split else "") + " to `" + ("` and `".join(fnames)) + valid_string = f' -v "{fnames[1]}"' if split else "" + separator_reminder = ( + "" + if len(common_prompt_suffix_new_line_handled) == 0 + else f"After you’ve fine-tuned a model, remember that your prompt has to end with the indicator string `{common_prompt_suffix_new_line_handled}` for the model to start generating completions, rather than continuing with the prompt." + ) + sys.stdout.write( + f'\nWrote modified file{files_string}`\nFeel free to take a look!\n\nNow use that file when fine-tuning:\n> openai api fine_tunes.create -t "{fnames[0]}"{valid_string}{additional_params}\n\n{separator_reminder}{optional_ending_string}\n' + ) + estimate_fine_tuning_time(df) + else: + sys.stdout.write("Aborting... did not write the file\n") + + +def infer_task_type(df: pd.DataFrame) -> str: + """ + Infer the likely fine-tuning task type from the data + """ + CLASSIFICATION_THRESHOLD = 3 # min_average instances of each class + if sum(df.prompt.str.len()) == 0: + return "open-ended generation" + + if len(df.completion.unique()) < len(df) / CLASSIFICATION_THRESHOLD: + return "classification" + + return "conditional generation" + + +def get_common_xfix(series: Any, xfix: str = "suffix") -> str: + """ + Finds the longest common suffix or prefix of all the values in a series + """ + common_xfix = "" + while True: + common_xfixes = ( + series.str[-(len(common_xfix) + 1) :] if xfix == "suffix" else series.str[: len(common_xfix) + 1] + ) # first few or last few characters + if common_xfixes.nunique() != 1: # we found the character at which we don't have a unique xfix anymore + break + elif common_xfix == common_xfixes.values[0]: # the entire first row is a prefix of every other row + break + else: # the first or last few characters are still common across all rows - let's try to add one more + common_xfix = common_xfixes.values[0] + return common_xfix + + +Validator: TypeAlias = "Callable[[pd.DataFrame], Remediation | None]" + + +def get_validators() -> list[Validator]: + return [ + num_examples_validator, + lambda x: necessary_column_validator(x, "prompt"), + lambda x: necessary_column_validator(x, "completion"), + additional_column_validator, + non_empty_field_validator, + format_inferrer_validator, + duplicated_rows_validator, + long_examples_validator, + lambda x: lower_case_validator(x, "prompt"), + lambda x: lower_case_validator(x, "completion"), + common_prompt_suffix_validator, + common_prompt_prefix_validator, + common_completion_prefix_validator, + common_completion_suffix_validator, + completions_space_start_validator, + ] + + +def apply_validators( + df: pd.DataFrame, + fname: str, + remediation: Remediation | None, + validators: list[Validator], + auto_accept: bool, + write_out_file_func: Callable[..., Any], +) -> None: + optional_remediations: list[Remediation] = [] + if remediation is not None: + optional_remediations.append(remediation) + for validator in validators: + remediation = validator(df) + if remediation is not None: + optional_remediations.append(remediation) + df = apply_necessary_remediation(df, remediation) + + any_optional_or_necessary_remediations = any( + [ + remediation + for remediation in optional_remediations + if remediation.optional_msg is not None or remediation.necessary_msg is not None + ] + ) + any_necessary_applied = any( + [remediation for remediation in optional_remediations if remediation.necessary_msg is not None] + ) + any_optional_applied = False + + if any_optional_or_necessary_remediations: + sys.stdout.write("\n\nBased on the analysis we will perform the following actions:\n") + for remediation in optional_remediations: + df, optional_applied = apply_optional_remediation(df, remediation, auto_accept) + any_optional_applied = any_optional_applied or optional_applied + else: + sys.stdout.write("\n\nNo remediations found.\n") + + any_optional_or_necessary_applied = any_optional_applied or any_necessary_applied + + write_out_file_func(df, fname, any_optional_or_necessary_applied, auto_accept) diff --git a/openai/lib/azure.py b/openai/lib/azure.py new file mode 100644 index 00000000..b3b94de8 --- /dev/null +++ b/openai/lib/azure.py @@ -0,0 +1,529 @@ +from __future__ import annotations + +import os +import inspect +from typing import Any, Union, Mapping, TypeVar, Callable, Awaitable, overload +from typing_extensions import Self, override + +import httpx + +from .._types import NOT_GIVEN, Omit, Timeout, NotGiven +from .._utils import is_given, is_mapping +from .._client import OpenAI, AsyncOpenAI +from .._models import FinalRequestOptions +from .._streaming import Stream, AsyncStream +from .._exceptions import OpenAIError +from .._base_client import DEFAULT_MAX_RETRIES, BaseClient + +_deployments_endpoints = set( + [ + "/completions", + "/chat/completions", + "/embeddings", + "/audio/transcriptions", + "/audio/translations", + "/audio/speech", + "/images/generations", + ] +) + + +AzureADTokenProvider = Callable[[], str] +AsyncAzureADTokenProvider = Callable[[], "str | Awaitable[str]"] +_HttpxClientT = TypeVar("_HttpxClientT", bound=Union[httpx.Client, httpx.AsyncClient]) +_DefaultStreamT = TypeVar("_DefaultStreamT", bound=Union[Stream[Any], AsyncStream[Any]]) + + +# we need to use a sentinel API key value for Azure AD +# as we don't want to make the `api_key` in the main client Optional +# and Azure AD tokens may be retrieved on a per-request basis +API_KEY_SENTINEL = "".join(["<", "missing API key", ">"]) + + +class MutuallyExclusiveAuthError(OpenAIError): + def __init__(self) -> None: + super().__init__( + "The `api_key`, `azure_ad_token` and `azure_ad_token_provider` arguments are mutually exclusive; Only one can be passed at a time" + ) + + +class BaseAzureClient(BaseClient[_HttpxClientT, _DefaultStreamT]): + @override + def _build_request( + self, + options: FinalRequestOptions, + ) -> httpx.Request: + if options.url in _deployments_endpoints and is_mapping(options.json_data): + model = options.json_data.get("model") + if model is not None and not "/deployments" in str(self.base_url): + options.url = f"/deployments/{model}{options.url}" + + return super()._build_request(options) + + +class AzureOpenAI(BaseAzureClient[httpx.Client, Stream[Any]], OpenAI): + @overload + def __init__( + self, + *, + azure_endpoint: str, + azure_deployment: str | None = None, + api_version: str | None = None, + api_key: str | None = None, + azure_ad_token: str | None = None, + azure_ad_token_provider: AzureADTokenProvider | None = None, + organization: str | None = None, + timeout: float | Timeout | None | NotGiven = NOT_GIVEN, + max_retries: int = DEFAULT_MAX_RETRIES, + default_headers: Mapping[str, str] | None = None, + default_query: Mapping[str, object] | None = None, + http_client: httpx.Client | None = None, + _strict_response_validation: bool = False, + ) -> None: + ... + + @overload + def __init__( + self, + *, + azure_deployment: str | None = None, + api_version: str | None = None, + api_key: str | None = None, + azure_ad_token: str | None = None, + azure_ad_token_provider: AzureADTokenProvider | None = None, + organization: str | None = None, + timeout: float | Timeout | None | NotGiven = NOT_GIVEN, + max_retries: int = DEFAULT_MAX_RETRIES, + default_headers: Mapping[str, str] | None = None, + default_query: Mapping[str, object] | None = None, + http_client: httpx.Client | None = None, + _strict_response_validation: bool = False, + ) -> None: + ... + + @overload + def __init__( + self, + *, + base_url: str, + api_version: str | None = None, + api_key: str | None = None, + azure_ad_token: str | None = None, + azure_ad_token_provider: AzureADTokenProvider | None = None, + organization: str | None = None, + timeout: float | Timeout | None | NotGiven = NOT_GIVEN, + max_retries: int = DEFAULT_MAX_RETRIES, + default_headers: Mapping[str, str] | None = None, + default_query: Mapping[str, object] | None = None, + http_client: httpx.Client | None = None, + _strict_response_validation: bool = False, + ) -> None: + ... + + def __init__( + self, + *, + api_version: str | None = None, + azure_endpoint: str | None = None, + azure_deployment: str | None = None, + api_key: str | None = None, + azure_ad_token: str | None = None, + azure_ad_token_provider: AzureADTokenProvider | None = None, + organization: str | None = None, + base_url: str | None = None, + timeout: float | Timeout | None | NotGiven = NOT_GIVEN, + max_retries: int = DEFAULT_MAX_RETRIES, + default_headers: Mapping[str, str] | None = None, + default_query: Mapping[str, object] | None = None, + http_client: httpx.Client | None = None, + _strict_response_validation: bool = False, + ) -> None: + """Construct a new synchronous azure openai client instance. + + This automatically infers the following arguments from their corresponding environment variables if they are not provided: + - `api_key` from `AZURE_OPENAI_API_KEY` + - `organization` from `OPENAI_ORG_ID` + - `azure_ad_token` from `AZURE_OPENAI_AD_TOKEN` + - `api_version` from `OPENAI_API_VERSION` + - `azure_endpoint` from `AZURE_OPENAI_ENDPOINT` + + Args: + azure_endpoint: Your Azure endpoint, including the resource, e.g. `https://example-resource.azure.openai.com/` + + azure_ad_token: Your Azure Active Directory token, https://www.microsoft.com/en-us/security/business/identity-access/microsoft-entra-id + + azure_ad_token_provider: A function that returns an Azure Active Directory token, will be invoked on every request. + + azure_deployment: A model deployment, if given sets the base client URL to include `/deployments/{azure_deployment}`. + Note: this means you won't be able to use non-deployment endpoints. Not supported with Assistants APIs. + """ + if api_key is None: + api_key = os.environ.get("AZURE_OPENAI_API_KEY") + + if azure_ad_token is None: + azure_ad_token = os.environ.get("AZURE_OPENAI_AD_TOKEN") + + if api_key is None and azure_ad_token is None and azure_ad_token_provider is None: + raise OpenAIError( + "Missing credentials. Please pass one of `api_key`, `azure_ad_token`, `azure_ad_token_provider`, or the `AZURE_OPENAI_API_KEY` or `AZURE_OPENAI_AD_TOKEN` environment variables." + ) + + if api_version is None: + api_version = os.environ.get("OPENAI_API_VERSION") + + if api_version is None: + raise ValueError( + "Must provide either the `api_version` argument or the `OPENAI_API_VERSION` environment variable" + ) + + if default_query is None: + default_query = {"api-version": api_version} + else: + default_query = {**default_query, "api-version": api_version} + + if base_url is None: + if azure_endpoint is None: + azure_endpoint = os.environ.get("AZURE_OPENAI_ENDPOINT") + + if azure_endpoint is None: + raise ValueError( + "Must provide one of the `base_url` or `azure_endpoint` arguments, or the `AZURE_OPENAI_ENDPOINT` environment variable" + ) + + if azure_deployment is not None: + base_url = f"{azure_endpoint}/openai/deployments/{azure_deployment}" + else: + base_url = f"{azure_endpoint}/openai" + else: + if azure_endpoint is not None: + raise ValueError("base_url and azure_endpoint are mutually exclusive") + + if api_key is None: + # define a sentinel value to avoid any typing issues + api_key = API_KEY_SENTINEL + + super().__init__( + api_key=api_key, + organization=organization, + base_url=base_url, + timeout=timeout, + max_retries=max_retries, + default_headers=default_headers, + default_query=default_query, + http_client=http_client, + _strict_response_validation=_strict_response_validation, + ) + self._api_version = api_version + self._azure_ad_token = azure_ad_token + self._azure_ad_token_provider = azure_ad_token_provider + + @override + def copy( + self, + *, + api_key: str | None = None, + organization: str | None = None, + api_version: str | None = None, + azure_ad_token: str | None = None, + azure_ad_token_provider: AzureADTokenProvider | None = None, + base_url: str | httpx.URL | None = None, + timeout: float | Timeout | None | NotGiven = NOT_GIVEN, + http_client: httpx.Client | None = None, + max_retries: int | NotGiven = NOT_GIVEN, + default_headers: Mapping[str, str] | None = None, + set_default_headers: Mapping[str, str] | None = None, + default_query: Mapping[str, object] | None = None, + set_default_query: Mapping[str, object] | None = None, + _extra_kwargs: Mapping[str, Any] = {}, + ) -> Self: + """ + Create a new client instance re-using the same options given to the current client with optional overriding. + """ + return super().copy( + api_key=api_key, + organization=organization, + base_url=base_url, + timeout=timeout, + http_client=http_client, + max_retries=max_retries, + default_headers=default_headers, + set_default_headers=set_default_headers, + default_query=default_query, + set_default_query=set_default_query, + _extra_kwargs={ + "api_version": api_version or self._api_version, + "azure_ad_token": azure_ad_token or self._azure_ad_token, + "azure_ad_token_provider": azure_ad_token_provider or self._azure_ad_token_provider, + **_extra_kwargs, + }, + ) + + with_options = copy + + def _get_azure_ad_token(self) -> str | None: + if self._azure_ad_token is not None: + return self._azure_ad_token + + provider = self._azure_ad_token_provider + if provider is not None: + token = provider() + if not token or not isinstance(token, str): # pyright: ignore[reportUnnecessaryIsInstance] + raise ValueError( + f"Expected `azure_ad_token_provider` argument to return a string but it returned {token}", + ) + return token + + return None + + @override + def _prepare_options(self, options: FinalRequestOptions) -> None: + headers: dict[str, str | Omit] = {**options.headers} if is_given(options.headers) else {} + options.headers = headers + + azure_ad_token = self._get_azure_ad_token() + if azure_ad_token is not None: + if headers.get("Authorization") is None: + headers["Authorization"] = f"Bearer {azure_ad_token}" + elif self.api_key is not API_KEY_SENTINEL: + if headers.get("api-key") is None: + headers["api-key"] = self.api_key + else: + # should never be hit + raise ValueError("Unable to handle auth") + + return super()._prepare_options(options) + + +class AsyncAzureOpenAI(BaseAzureClient[httpx.AsyncClient, AsyncStream[Any]], AsyncOpenAI): + @overload + def __init__( + self, + *, + azure_endpoint: str, + azure_deployment: str | None = None, + api_version: str | None = None, + api_key: str | None = None, + azure_ad_token: str | None = None, + azure_ad_token_provider: AsyncAzureADTokenProvider | None = None, + organization: str | None = None, + timeout: float | Timeout | None | NotGiven = NOT_GIVEN, + max_retries: int = DEFAULT_MAX_RETRIES, + default_headers: Mapping[str, str] | None = None, + default_query: Mapping[str, object] | None = None, + http_client: httpx.AsyncClient | None = None, + _strict_response_validation: bool = False, + ) -> None: + ... + + @overload + def __init__( + self, + *, + azure_deployment: str | None = None, + api_version: str | None = None, + api_key: str | None = None, + azure_ad_token: str | None = None, + azure_ad_token_provider: AsyncAzureADTokenProvider | None = None, + organization: str | None = None, + timeout: float | Timeout | None | NotGiven = NOT_GIVEN, + max_retries: int = DEFAULT_MAX_RETRIES, + default_headers: Mapping[str, str] | None = None, + default_query: Mapping[str, object] | None = None, + http_client: httpx.AsyncClient | None = None, + _strict_response_validation: bool = False, + ) -> None: + ... + + @overload + def __init__( + self, + *, + base_url: str, + api_version: str | None = None, + api_key: str | None = None, + azure_ad_token: str | None = None, + azure_ad_token_provider: AsyncAzureADTokenProvider | None = None, + organization: str | None = None, + timeout: float | Timeout | None | NotGiven = NOT_GIVEN, + max_retries: int = DEFAULT_MAX_RETRIES, + default_headers: Mapping[str, str] | None = None, + default_query: Mapping[str, object] | None = None, + http_client: httpx.AsyncClient | None = None, + _strict_response_validation: bool = False, + ) -> None: + ... + + def __init__( + self, + *, + azure_endpoint: str | None = None, + azure_deployment: str | None = None, + api_version: str | None = None, + api_key: str | None = None, + azure_ad_token: str | None = None, + azure_ad_token_provider: AsyncAzureADTokenProvider | None = None, + organization: str | None = None, + base_url: str | None = None, + timeout: float | Timeout | None | NotGiven = NOT_GIVEN, + max_retries: int = DEFAULT_MAX_RETRIES, + default_headers: Mapping[str, str] | None = None, + default_query: Mapping[str, object] | None = None, + http_client: httpx.AsyncClient | None = None, + _strict_response_validation: bool = False, + ) -> None: + """Construct a new asynchronous azure openai client instance. + + This automatically infers the following arguments from their corresponding environment variables if they are not provided: + - `api_key` from `AZURE_OPENAI_API_KEY` + - `organization` from `OPENAI_ORG_ID` + - `azure_ad_token` from `AZURE_OPENAI_AD_TOKEN` + - `api_version` from `OPENAI_API_VERSION` + - `azure_endpoint` from `AZURE_OPENAI_ENDPOINT` + + Args: + azure_endpoint: Your Azure endpoint, including the resource, e.g. `https://example-resource.azure.openai.com/` + + azure_ad_token: Your Azure Active Directory token, https://www.microsoft.com/en-us/security/business/identity-access/microsoft-entra-id + + azure_ad_token_provider: A function that returns an Azure Active Directory token, will be invoked on every request. + + azure_deployment: A model deployment, if given sets the base client URL to include `/deployments/{azure_deployment}`. + Note: this means you won't be able to use non-deployment endpoints. Not supported with Assistants APIs. + """ + if api_key is None: + api_key = os.environ.get("AZURE_OPENAI_API_KEY") + + if azure_ad_token is None: + azure_ad_token = os.environ.get("AZURE_OPENAI_AD_TOKEN") + + if api_key is None and azure_ad_token is None and azure_ad_token_provider is None: + raise OpenAIError( + "Missing credentials. Please pass one of `api_key`, `azure_ad_token`, `azure_ad_token_provider`, or the `AZURE_OPENAI_API_KEY` or `AZURE_OPENAI_AD_TOKEN` environment variables." + ) + + if api_version is None: + api_version = os.environ.get("OPENAI_API_VERSION") + + if api_version is None: + raise ValueError( + "Must provide either the `api_version` argument or the `OPENAI_API_VERSION` environment variable" + ) + + if default_query is None: + default_query = {"api-version": api_version} + else: + default_query = {**default_query, "api-version": api_version} + + if base_url is None: + if azure_endpoint is None: + azure_endpoint = os.environ.get("AZURE_OPENAI_ENDPOINT") + + if azure_endpoint is None: + raise ValueError( + "Must provide one of the `base_url` or `azure_endpoint` arguments, or the `AZURE_OPENAI_ENDPOINT` environment variable" + ) + + if azure_deployment is not None: + base_url = f"{azure_endpoint}/openai/deployments/{azure_deployment}" + else: + base_url = f"{azure_endpoint}/openai" + else: + if azure_endpoint is not None: + raise ValueError("base_url and azure_endpoint are mutually exclusive") + + if api_key is None: + # define a sentinel value to avoid any typing issues + api_key = API_KEY_SENTINEL + + super().__init__( + api_key=api_key, + organization=organization, + base_url=base_url, + timeout=timeout, + max_retries=max_retries, + default_headers=default_headers, + default_query=default_query, + http_client=http_client, + _strict_response_validation=_strict_response_validation, + ) + self._api_version = api_version + self._azure_ad_token = azure_ad_token + self._azure_ad_token_provider = azure_ad_token_provider + + @override + def copy( + self, + *, + api_key: str | None = None, + organization: str | None = None, + api_version: str | None = None, + azure_ad_token: str | None = None, + azure_ad_token_provider: AsyncAzureADTokenProvider | None = None, + base_url: str | httpx.URL | None = None, + timeout: float | Timeout | None | NotGiven = NOT_GIVEN, + http_client: httpx.AsyncClient | None = None, + max_retries: int | NotGiven = NOT_GIVEN, + default_headers: Mapping[str, str] | None = None, + set_default_headers: Mapping[str, str] | None = None, + default_query: Mapping[str, object] | None = None, + set_default_query: Mapping[str, object] | None = None, + _extra_kwargs: Mapping[str, Any] = {}, + ) -> Self: + """ + Create a new client instance re-using the same options given to the current client with optional overriding. + """ + return super().copy( + api_key=api_key, + organization=organization, + base_url=base_url, + timeout=timeout, + http_client=http_client, + max_retries=max_retries, + default_headers=default_headers, + set_default_headers=set_default_headers, + default_query=default_query, + set_default_query=set_default_query, + _extra_kwargs={ + "api_version": api_version or self._api_version, + "azure_ad_token": azure_ad_token or self._azure_ad_token, + "azure_ad_token_provider": azure_ad_token_provider or self._azure_ad_token_provider, + **_extra_kwargs, + }, + ) + + with_options = copy + + async def _get_azure_ad_token(self) -> str | None: + if self._azure_ad_token is not None: + return self._azure_ad_token + + provider = self._azure_ad_token_provider + if provider is not None: + token = provider() + if inspect.isawaitable(token): + token = await token + if not token or not isinstance(token, str): + raise ValueError( + f"Expected `azure_ad_token_provider` argument to return a string but it returned {token}", + ) + return token + + return None + + @override + async def _prepare_options(self, options: FinalRequestOptions) -> None: + headers: dict[str, str | Omit] = {**options.headers} if is_given(options.headers) else {} + options.headers = headers + + azure_ad_token = await self._get_azure_ad_token() + if azure_ad_token is not None: + if headers.get("Authorization") is None: + headers["Authorization"] = f"Bearer {azure_ad_token}" + elif self.api_key is not API_KEY_SENTINEL: + if headers.get("api-key") is None: + headers["api-key"] = self.api_key + else: + # should never be hit + raise ValueError("Unable to handle auth") + + return await super()._prepare_options(options) diff --git a/openai/lib/streaming/__init__.py b/openai/lib/streaming/__init__.py new file mode 100644 index 00000000..eb378d25 --- /dev/null +++ b/openai/lib/streaming/__init__.py @@ -0,0 +1,8 @@ +from ._assistants import ( + AssistantEventHandler as AssistantEventHandler, + AssistantEventHandlerT as AssistantEventHandlerT, + AssistantStreamManager as AssistantStreamManager, + AsyncAssistantEventHandler as AsyncAssistantEventHandler, + AsyncAssistantEventHandlerT as AsyncAssistantEventHandlerT, + AsyncAssistantStreamManager as AsyncAssistantStreamManager, +) diff --git a/openai/lib/streaming/_assistants.py b/openai/lib/streaming/_assistants.py new file mode 100644 index 00000000..03d97ec2 --- /dev/null +++ b/openai/lib/streaming/_assistants.py @@ -0,0 +1,1035 @@ +from __future__ import annotations + +import asyncio +from types import TracebackType +from typing import TYPE_CHECKING, Any, Generic, TypeVar, Callable, Iterable, Iterator, cast +from typing_extensions import Awaitable, AsyncIterable, AsyncIterator, assert_never + +import httpx + +from ..._utils import is_dict, is_list, consume_sync_iterator, consume_async_iterator +from ..._models import construct_type +from ..._streaming import Stream, AsyncStream +from ...types.beta import AssistantStreamEvent +from ...types.beta.threads import ( + Run, + Text, + Message, + ImageFile, + TextDelta, + MessageDelta, + MessageContent, + MessageContentDelta, +) +from ...types.beta.threads.runs import RunStep, ToolCall, RunStepDelta, ToolCallDelta + + +class AssistantEventHandler: + text_deltas: Iterable[str] + """Iterator over just the text deltas in the stream. + + This corresponds to the `thread.message.delta` event + in the API. + + ```py + for text in stream.text_deltas: + print(text, end="", flush=True) + print() + ``` + """ + + def __init__(self) -> None: + self._current_event: AssistantStreamEvent | None = None + self._current_message_content_index: int | None = None + self._current_message_content: MessageContent | None = None + self._current_tool_call_index: int | None = None + self._current_tool_call: ToolCall | None = None + self.__current_run_step_id: str | None = None + self.__current_run: Run | None = None + self.__run_step_snapshots: dict[str, RunStep] = {} + self.__message_snapshots: dict[str, Message] = {} + self.__current_message_snapshot: Message | None = None + + self.text_deltas = self.__text_deltas__() + self._iterator = self.__stream__() + self.__stream: Stream[AssistantStreamEvent] | None = None + + def _init(self, stream: Stream[AssistantStreamEvent]) -> None: + if self.__stream: + raise RuntimeError( + "A single event handler cannot be shared between multiple streams; You will need to construct a new event handler instance" + ) + + self.__stream = stream + + def __next__(self) -> AssistantStreamEvent: + return self._iterator.__next__() + + def __iter__(self) -> Iterator[AssistantStreamEvent]: + for item in self._iterator: + yield item + + @property + def current_event(self) -> AssistantStreamEvent | None: + return self._current_event + + @property + def current_run(self) -> Run | None: + return self.__current_run + + @property + def current_run_step_snapshot(self) -> RunStep | None: + if not self.__current_run_step_id: + return None + + return self.__run_step_snapshots[self.__current_run_step_id] + + @property + def current_message_snapshot(self) -> Message | None: + return self.__current_message_snapshot + + def close(self) -> None: + """ + Close the response and release the connection. + + Automatically called when the context manager exits. + """ + if self.__stream: + self.__stream.close() + + def until_done(self) -> None: + """Waits until the stream has been consumed""" + consume_sync_iterator(self) + + def get_final_run(self) -> Run: + """Wait for the stream to finish and returns the completed Run object""" + self.until_done() + + if not self.__current_run: + raise RuntimeError("No final run object found") + + return self.__current_run + + def get_final_run_steps(self) -> list[RunStep]: + """Wait for the stream to finish and returns the steps taken in this run""" + self.until_done() + + if not self.__run_step_snapshots: + raise RuntimeError("No run steps found") + + return [step for step in self.__run_step_snapshots.values()] + + def get_final_messages(self) -> list[Message]: + """Wait for the stream to finish and returns the messages emitted in this run""" + self.until_done() + + if not self.__message_snapshots: + raise RuntimeError("No messages found") + + return [message for message in self.__message_snapshots.values()] + + def __text_deltas__(self) -> Iterator[str]: + for event in self: + if event.event != "thread.message.delta": + continue + + for content_delta in event.data.delta.content or []: + if content_delta.type == "text" and content_delta.text and content_delta.text.value: + yield content_delta.text.value + + # event handlers + + def on_end(self) -> None: + """Fires when the stream has finished. + + This happens if the stream is read to completion + or if an exception occurs during iteration. + """ + + def on_event(self, event: AssistantStreamEvent) -> None: + """Callback that is fired for every Server-Sent-Event""" + + def on_run_step_created(self, run_step: RunStep) -> None: + """Callback that is fired when a run step is created""" + + def on_run_step_delta(self, delta: RunStepDelta, snapshot: RunStep) -> None: + """Callback that is fired whenever a run step delta is returned from the API + + The first argument is just the delta as sent by the API and the second argument + is the accumulated snapshot of the run step. For example, a tool calls event may + look like this: + + # delta + tool_calls=[ + RunStepDeltaToolCallsCodeInterpreter( + index=0, + type='code_interpreter', + id=None, + code_interpreter=CodeInterpreter(input=' sympy', outputs=None) + ) + ] + # snapshot + tool_calls=[ + CodeToolCall( + id='call_wKayJlcYV12NiadiZuJXxcfx', + code_interpreter=CodeInterpreter(input='from sympy', outputs=[]), + type='code_interpreter', + index=0 + ) + ], + """ + + def on_run_step_done(self, run_step: RunStep) -> None: + """Callback that is fired when a run step is completed""" + + def on_tool_call_created(self, tool_call: ToolCall) -> None: + """Callback that is fired when a tool call is created""" + + def on_tool_call_delta(self, delta: ToolCallDelta, snapshot: ToolCall) -> None: + """Callback that is fired when a tool call delta is encountered""" + + def on_tool_call_done(self, tool_call: ToolCall) -> None: + """Callback that is fired when a tool call delta is encountered""" + + def on_exception(self, exception: Exception) -> None: + """Fired whenever an exception happens during streaming""" + + def on_timeout(self) -> None: + """Fires if the request times out""" + + def on_message_created(self, message: Message) -> None: + """Callback that is fired when a message is created""" + + def on_message_delta(self, delta: MessageDelta, snapshot: Message) -> None: + """Callback that is fired whenever a message delta is returned from the API + + The first argument is just the delta as sent by the API and the second argument + is the accumulated snapshot of the message. For example, a text content event may + look like this: + + # delta + MessageDeltaText( + index=0, + type='text', + text=Text( + value=' Jane' + ), + ) + # snapshot + MessageContentText( + index=0, + type='text', + text=Text( + value='Certainly, Jane' + ), + ) + """ + + def on_message_done(self, message: Message) -> None: + """Callback that is fired when a message is completed""" + + def on_text_created(self, text: Text) -> None: + """Callback that is fired when a text content block is created""" + + def on_text_delta(self, delta: TextDelta, snapshot: Text) -> None: + """Callback that is fired whenever a text content delta is returned + by the API. + + The first argument is just the delta as sent by the API and the second argument + is the accumulated snapshot of the text. For example: + + on_text_delta(TextDelta(value="The"), Text(value="The")), + on_text_delta(TextDelta(value=" solution"), Text(value="The solution")), + on_text_delta(TextDelta(value=" to"), Text(value="The solution to")), + on_text_delta(TextDelta(value=" the"), Text(value="The solution to the")), + on_text_delta(TextDelta(value=" equation"), Text(value="The solution to the equivalent")), + """ + + def on_text_done(self, text: Text) -> None: + """Callback that is fired when a text content block is finished""" + + def on_image_file_done(self, image_file: ImageFile) -> None: + """Callback that is fired when an image file block is finished""" + + def _emit_sse_event(self, event: AssistantStreamEvent) -> None: + self._current_event = event + self.on_event(event) + + self.__current_message_snapshot, new_content = accumulate_event( + event=event, + current_message_snapshot=self.__current_message_snapshot, + ) + if self.__current_message_snapshot is not None: + self.__message_snapshots[self.__current_message_snapshot.id] = self.__current_message_snapshot + + accumulate_run_step( + event=event, + run_step_snapshots=self.__run_step_snapshots, + ) + + for content_delta in new_content: + assert self.__current_message_snapshot is not None + + block = self.__current_message_snapshot.content[content_delta.index] + if block.type == "text": + self.on_text_created(block.text) + + if ( + event.event == "thread.run.completed" + or event.event == "thread.run.cancelled" + or event.event == "thread.run.expired" + or event.event == "thread.run.failed" + or event.event == "thread.run.requires_action" + ): + self.__current_run = event.data + if self._current_tool_call: + self.on_tool_call_done(self._current_tool_call) + elif ( + event.event == "thread.run.created" + or event.event == "thread.run.in_progress" + or event.event == "thread.run.cancelling" + or event.event == "thread.run.queued" + ): + self.__current_run = event.data + elif event.event == "thread.message.created": + self.on_message_created(event.data) + elif event.event == "thread.message.delta": + snapshot = self.__current_message_snapshot + assert snapshot is not None + + message_delta = event.data.delta + if message_delta.content is not None: + for content_delta in message_delta.content: + if content_delta.type == "text" and content_delta.text: + snapshot_content = snapshot.content[content_delta.index] + assert snapshot_content.type == "text" + self.on_text_delta(content_delta.text, snapshot_content.text) + + # If the delta is for a new message content: + # - emit on_text_done/on_image_file_done for the previous message content + # - emit on_text_created/on_image_created for the new message content + if content_delta.index != self._current_message_content_index: + if self._current_message_content is not None: + if self._current_message_content.type == "text": + self.on_text_done(self._current_message_content.text) + elif self._current_message_content.type == "image_file": + self.on_image_file_done(self._current_message_content.image_file) + + self._current_message_content_index = content_delta.index + self._current_message_content = snapshot.content[content_delta.index] + + # Update the current_message_content (delta event is correctly emitted already) + self._current_message_content = snapshot.content[content_delta.index] + + self.on_message_delta(event.data.delta, snapshot) + elif event.event == "thread.message.completed" or event.event == "thread.message.incomplete": + self.__current_message_snapshot = event.data + self.__message_snapshots[event.data.id] = event.data + + if self._current_message_content_index is not None: + content = event.data.content[self._current_message_content_index] + if content.type == "text": + self.on_text_done(content.text) + elif content.type == "image_file": + self.on_image_file_done(content.image_file) + + self.on_message_done(event.data) + elif event.event == "thread.run.step.created": + self.__current_run_step_id = event.data.id + self.on_run_step_created(event.data) + elif event.event == "thread.run.step.in_progress": + self.__current_run_step_id = event.data.id + elif event.event == "thread.run.step.delta": + step_snapshot = self.__run_step_snapshots[event.data.id] + + run_step_delta = event.data.delta + if ( + run_step_delta.step_details + and run_step_delta.step_details.type == "tool_calls" + and run_step_delta.step_details.tool_calls is not None + ): + assert step_snapshot.step_details.type == "tool_calls" + for tool_call_delta in run_step_delta.step_details.tool_calls: + if tool_call_delta.index == self._current_tool_call_index: + self.on_tool_call_delta( + tool_call_delta, + step_snapshot.step_details.tool_calls[tool_call_delta.index], + ) + + # If the delta is for a new tool call: + # - emit on_tool_call_done for the previous tool_call + # - emit on_tool_call_created for the new tool_call + if tool_call_delta.index != self._current_tool_call_index: + if self._current_tool_call is not None: + self.on_tool_call_done(self._current_tool_call) + + self._current_tool_call_index = tool_call_delta.index + self._current_tool_call = step_snapshot.step_details.tool_calls[tool_call_delta.index] + self.on_tool_call_created(self._current_tool_call) + + # Update the current_tool_call (delta event is correctly emitted already) + self._current_tool_call = step_snapshot.step_details.tool_calls[tool_call_delta.index] + + self.on_run_step_delta( + event.data.delta, + step_snapshot, + ) + elif ( + event.event == "thread.run.step.completed" + or event.event == "thread.run.step.cancelled" + or event.event == "thread.run.step.expired" + or event.event == "thread.run.step.failed" + ): + if self._current_tool_call: + self.on_tool_call_done(self._current_tool_call) + + self.on_run_step_done(event.data) + self.__current_run_step_id = None + elif event.event == "thread.created" or event.event == "thread.message.in_progress" or event.event == "error": + # currently no special handling + ... + else: + # we only want to error at build-time + if TYPE_CHECKING: # type: ignore[unreachable] + assert_never(event) + + self._current_event = None + + def __stream__(self) -> Iterator[AssistantStreamEvent]: + stream = self.__stream + if not stream: + raise RuntimeError("Stream has not been started yet") + + try: + for event in stream: + self._emit_sse_event(event) + + yield event + except (httpx.TimeoutException, asyncio.TimeoutError) as exc: + self.on_timeout() + self.on_exception(exc) + raise + except Exception as exc: + self.on_exception(exc) + raise + finally: + self.on_end() + + +AssistantEventHandlerT = TypeVar("AssistantEventHandlerT", bound=AssistantEventHandler) + + +class AssistantStreamManager(Generic[AssistantEventHandlerT]): + """Wrapper over AssistantStreamEventHandler that is returned by `.stream()` + so that a context manager can be used. + + ```py + with client.threads.create_and_run_stream(...) as stream: + for event in stream: + ... + ``` + """ + + def __init__( + self, + api_request: Callable[[], Stream[AssistantStreamEvent]], + *, + event_handler: AssistantEventHandlerT, + ) -> None: + self.__stream: Stream[AssistantStreamEvent] | None = None + self.__event_handler = event_handler + self.__api_request = api_request + + def __enter__(self) -> AssistantEventHandlerT: + self.__stream = self.__api_request() + self.__event_handler._init(self.__stream) + return self.__event_handler + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + if self.__stream is not None: + self.__stream.close() + + +class AsyncAssistantEventHandler: + text_deltas: AsyncIterable[str] + """Iterator over just the text deltas in the stream. + + This corresponds to the `thread.message.delta` event + in the API. + + ```py + async for text in stream.text_deltas: + print(text, end="", flush=True) + print() + ``` + """ + + def __init__(self) -> None: + self._current_event: AssistantStreamEvent | None = None + self._current_message_content_index: int | None = None + self._current_message_content: MessageContent | None = None + self._current_tool_call_index: int | None = None + self._current_tool_call: ToolCall | None = None + self.__current_run_step_id: str | None = None + self.__current_run: Run | None = None + self.__run_step_snapshots: dict[str, RunStep] = {} + self.__message_snapshots: dict[str, Message] = {} + self.__current_message_snapshot: Message | None = None + + self.text_deltas = self.__text_deltas__() + self._iterator = self.__stream__() + self.__stream: AsyncStream[AssistantStreamEvent] | None = None + + def _init(self, stream: AsyncStream[AssistantStreamEvent]) -> None: + if self.__stream: + raise RuntimeError( + "A single event handler cannot be shared between multiple streams; You will need to construct a new event handler instance" + ) + + self.__stream = stream + + async def __anext__(self) -> AssistantStreamEvent: + return await self._iterator.__anext__() + + async def __aiter__(self) -> AsyncIterator[AssistantStreamEvent]: + async for item in self._iterator: + yield item + + async def close(self) -> None: + """ + Close the response and release the connection. + + Automatically called when the context manager exits. + """ + if self.__stream: + await self.__stream.close() + + @property + def current_event(self) -> AssistantStreamEvent | None: + return self._current_event + + @property + def current_run(self) -> Run | None: + return self.__current_run + + @property + def current_run_step_snapshot(self) -> RunStep | None: + if not self.__current_run_step_id: + return None + + return self.__run_step_snapshots[self.__current_run_step_id] + + @property + def current_message_snapshot(self) -> Message | None: + return self.__current_message_snapshot + + async def until_done(self) -> None: + """Waits until the stream has been consumed""" + await consume_async_iterator(self) + + async def get_final_run(self) -> Run: + """Wait for the stream to finish and returns the completed Run object""" + await self.until_done() + + if not self.__current_run: + raise RuntimeError("No final run object found") + + return self.__current_run + + async def get_final_run_steps(self) -> list[RunStep]: + """Wait for the stream to finish and returns the steps taken in this run""" + await self.until_done() + + if not self.__run_step_snapshots: + raise RuntimeError("No run steps found") + + return [step for step in self.__run_step_snapshots.values()] + + async def get_final_messages(self) -> list[Message]: + """Wait for the stream to finish and returns the messages emitted in this run""" + await self.until_done() + + if not self.__message_snapshots: + raise RuntimeError("No messages found") + + return [message for message in self.__message_snapshots.values()] + + async def __text_deltas__(self) -> AsyncIterator[str]: + async for event in self: + if event.event != "thread.message.delta": + continue + + for content_delta in event.data.delta.content or []: + if content_delta.type == "text" and content_delta.text and content_delta.text.value: + yield content_delta.text.value + + # event handlers + + async def on_end(self) -> None: + """Fires when the stream has finished. + + This happens if the stream is read to completion + or if an exception occurs during iteration. + """ + + async def on_event(self, event: AssistantStreamEvent) -> None: + """Callback that is fired for every Server-Sent-Event""" + + async def on_run_step_created(self, run_step: RunStep) -> None: + """Callback that is fired when a run step is created""" + + async def on_run_step_delta(self, delta: RunStepDelta, snapshot: RunStep) -> None: + """Callback that is fired whenever a run step delta is returned from the API + + The first argument is just the delta as sent by the API and the second argument + is the accumulated snapshot of the run step. For example, a tool calls event may + look like this: + + # delta + tool_calls=[ + RunStepDeltaToolCallsCodeInterpreter( + index=0, + type='code_interpreter', + id=None, + code_interpreter=CodeInterpreter(input=' sympy', outputs=None) + ) + ] + # snapshot + tool_calls=[ + CodeToolCall( + id='call_wKayJlcYV12NiadiZuJXxcfx', + code_interpreter=CodeInterpreter(input='from sympy', outputs=[]), + type='code_interpreter', + index=0 + ) + ], + """ + + async def on_run_step_done(self, run_step: RunStep) -> None: + """Callback that is fired when a run step is completed""" + + async def on_tool_call_created(self, tool_call: ToolCall) -> None: + """Callback that is fired when a tool call is created""" + + async def on_tool_call_delta(self, delta: ToolCallDelta, snapshot: ToolCall) -> None: + """Callback that is fired when a tool call delta is encountered""" + + async def on_tool_call_done(self, tool_call: ToolCall) -> None: + """Callback that is fired when a tool call delta is encountered""" + + async def on_exception(self, exception: Exception) -> None: + """Fired whenever an exception happens during streaming""" + + async def on_timeout(self) -> None: + """Fires if the request times out""" + + async def on_message_created(self, message: Message) -> None: + """Callback that is fired when a message is created""" + + async def on_message_delta(self, delta: MessageDelta, snapshot: Message) -> None: + """Callback that is fired whenever a message delta is returned from the API + + The first argument is just the delta as sent by the API and the second argument + is the accumulated snapshot of the message. For example, a text content event may + look like this: + + # delta + MessageDeltaText( + index=0, + type='text', + text=Text( + value=' Jane' + ), + ) + # snapshot + MessageContentText( + index=0, + type='text', + text=Text( + value='Certainly, Jane' + ), + ) + """ + + async def on_message_done(self, message: Message) -> None: + """Callback that is fired when a message is completed""" + + async def on_text_created(self, text: Text) -> None: + """Callback that is fired when a text content block is created""" + + async def on_text_delta(self, delta: TextDelta, snapshot: Text) -> None: + """Callback that is fired whenever a text content delta is returned + by the API. + + The first argument is just the delta as sent by the API and the second argument + is the accumulated snapshot of the text. For example: + + on_text_delta(TextDelta(value="The"), Text(value="The")), + on_text_delta(TextDelta(value=" solution"), Text(value="The solution")), + on_text_delta(TextDelta(value=" to"), Text(value="The solution to")), + on_text_delta(TextDelta(value=" the"), Text(value="The solution to the")), + on_text_delta(TextDelta(value=" equation"), Text(value="The solution to the equivalent")), + """ + + async def on_text_done(self, text: Text) -> None: + """Callback that is fired when a text content block is finished""" + + async def on_image_file_done(self, image_file: ImageFile) -> None: + """Callback that is fired when an image file block is finished""" + + async def _emit_sse_event(self, event: AssistantStreamEvent) -> None: + self._current_event = event + await self.on_event(event) + + self.__current_message_snapshot, new_content = accumulate_event( + event=event, + current_message_snapshot=self.__current_message_snapshot, + ) + if self.__current_message_snapshot is not None: + self.__message_snapshots[self.__current_message_snapshot.id] = self.__current_message_snapshot + + accumulate_run_step( + event=event, + run_step_snapshots=self.__run_step_snapshots, + ) + + for content_delta in new_content: + assert self.__current_message_snapshot is not None + + block = self.__current_message_snapshot.content[content_delta.index] + if block.type == "text": + await self.on_text_created(block.text) + + if ( + event.event == "thread.run.completed" + or event.event == "thread.run.cancelled" + or event.event == "thread.run.expired" + or event.event == "thread.run.failed" + or event.event == "thread.run.requires_action" + ): + self.__current_run = event.data + if self._current_tool_call: + await self.on_tool_call_done(self._current_tool_call) + elif ( + event.event == "thread.run.created" + or event.event == "thread.run.in_progress" + or event.event == "thread.run.cancelling" + or event.event == "thread.run.queued" + ): + self.__current_run = event.data + elif event.event == "thread.message.created": + await self.on_message_created(event.data) + elif event.event == "thread.message.delta": + snapshot = self.__current_message_snapshot + assert snapshot is not None + + message_delta = event.data.delta + if message_delta.content is not None: + for content_delta in message_delta.content: + if content_delta.type == "text" and content_delta.text: + snapshot_content = snapshot.content[content_delta.index] + assert snapshot_content.type == "text" + await self.on_text_delta(content_delta.text, snapshot_content.text) + + # If the delta is for a new message content: + # - emit on_text_done/on_image_file_done for the previous message content + # - emit on_text_created/on_image_created for the new message content + if content_delta.index != self._current_message_content_index: + if self._current_message_content is not None: + if self._current_message_content.type == "text": + await self.on_text_done(self._current_message_content.text) + elif self._current_message_content.type == "image_file": + await self.on_image_file_done(self._current_message_content.image_file) + + self._current_message_content_index = content_delta.index + self._current_message_content = snapshot.content[content_delta.index] + + # Update the current_message_content (delta event is correctly emitted already) + self._current_message_content = snapshot.content[content_delta.index] + + await self.on_message_delta(event.data.delta, snapshot) + elif event.event == "thread.message.completed" or event.event == "thread.message.incomplete": + self.__current_message_snapshot = event.data + self.__message_snapshots[event.data.id] = event.data + + if self._current_message_content_index is not None: + content = event.data.content[self._current_message_content_index] + if content.type == "text": + await self.on_text_done(content.text) + elif content.type == "image_file": + await self.on_image_file_done(content.image_file) + + await self.on_message_done(event.data) + elif event.event == "thread.run.step.created": + self.__current_run_step_id = event.data.id + await self.on_run_step_created(event.data) + elif event.event == "thread.run.step.in_progress": + self.__current_run_step_id = event.data.id + elif event.event == "thread.run.step.delta": + step_snapshot = self.__run_step_snapshots[event.data.id] + + run_step_delta = event.data.delta + if ( + run_step_delta.step_details + and run_step_delta.step_details.type == "tool_calls" + and run_step_delta.step_details.tool_calls is not None + ): + assert step_snapshot.step_details.type == "tool_calls" + for tool_call_delta in run_step_delta.step_details.tool_calls: + if tool_call_delta.index == self._current_tool_call_index: + await self.on_tool_call_delta( + tool_call_delta, + step_snapshot.step_details.tool_calls[tool_call_delta.index], + ) + + # If the delta is for a new tool call: + # - emit on_tool_call_done for the previous tool_call + # - emit on_tool_call_created for the new tool_call + if tool_call_delta.index != self._current_tool_call_index: + if self._current_tool_call is not None: + await self.on_tool_call_done(self._current_tool_call) + + self._current_tool_call_index = tool_call_delta.index + self._current_tool_call = step_snapshot.step_details.tool_calls[tool_call_delta.index] + await self.on_tool_call_created(self._current_tool_call) + + # Update the current_tool_call (delta event is correctly emitted already) + self._current_tool_call = step_snapshot.step_details.tool_calls[tool_call_delta.index] + + await self.on_run_step_delta( + event.data.delta, + step_snapshot, + ) + elif ( + event.event == "thread.run.step.completed" + or event.event == "thread.run.step.cancelled" + or event.event == "thread.run.step.expired" + or event.event == "thread.run.step.failed" + ): + if self._current_tool_call: + await self.on_tool_call_done(self._current_tool_call) + + await self.on_run_step_done(event.data) + self.__current_run_step_id = None + elif event.event == "thread.created" or event.event == "thread.message.in_progress" or event.event == "error": + # currently no special handling + ... + else: + # we only want to error at build-time + if TYPE_CHECKING: # type: ignore[unreachable] + assert_never(event) + + self._current_event = None + + async def __stream__(self) -> AsyncIterator[AssistantStreamEvent]: + stream = self.__stream + if not stream: + raise RuntimeError("Stream has not been started yet") + + try: + async for event in stream: + await self._emit_sse_event(event) + + yield event + except (httpx.TimeoutException, asyncio.TimeoutError) as exc: + await self.on_timeout() + await self.on_exception(exc) + raise + except Exception as exc: + await self.on_exception(exc) + raise + finally: + await self.on_end() + + +AsyncAssistantEventHandlerT = TypeVar("AsyncAssistantEventHandlerT", bound=AsyncAssistantEventHandler) + + +class AsyncAssistantStreamManager(Generic[AsyncAssistantEventHandlerT]): + """Wrapper over AsyncAssistantStreamEventHandler that is returned by `.stream()` + so that an async context manager can be used without `await`ing the + original client call. + + ```py + async with client.threads.create_and_run_stream(...) as stream: + async for event in stream: + ... + ``` + """ + + def __init__( + self, + api_request: Awaitable[AsyncStream[AssistantStreamEvent]], + *, + event_handler: AsyncAssistantEventHandlerT, + ) -> None: + self.__stream: AsyncStream[AssistantStreamEvent] | None = None + self.__event_handler = event_handler + self.__api_request = api_request + + async def __aenter__(self) -> AsyncAssistantEventHandlerT: + self.__stream = await self.__api_request + self.__event_handler._init(self.__stream) + return self.__event_handler + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + if self.__stream is not None: + await self.__stream.close() + + +def accumulate_run_step( + *, + event: AssistantStreamEvent, + run_step_snapshots: dict[str, RunStep], +) -> None: + if event.event == "thread.run.step.created": + run_step_snapshots[event.data.id] = event.data + return + + if event.event == "thread.run.step.delta": + data = event.data + snapshot = run_step_snapshots[data.id] + + if data.delta: + merged = accumulate_delta( + cast( + "dict[object, object]", + snapshot.model_dump(exclude_unset=True), + ), + cast( + "dict[object, object]", + data.delta.model_dump(exclude_unset=True), + ), + ) + run_step_snapshots[snapshot.id] = cast(RunStep, construct_type(type_=RunStep, value=merged)) + + return None + + +def accumulate_event( + *, + event: AssistantStreamEvent, + current_message_snapshot: Message | None, +) -> tuple[Message | None, list[MessageContentDelta]]: + """Returns a tuple of message snapshot and newly created text message deltas""" + if event.event == "thread.message.created": + return event.data, [] + + new_content: list[MessageContentDelta] = [] + + if event.event != "thread.message.delta": + return current_message_snapshot, [] + + if not current_message_snapshot: + raise RuntimeError("Encountered a message delta with no previous snapshot") + + data = event.data + if data.delta.content: + for content_delta in data.delta.content: + try: + block = current_message_snapshot.content[content_delta.index] + except IndexError: + current_message_snapshot.content.insert( + content_delta.index, + cast( + MessageContent, + construct_type( + # mypy doesn't allow Content for some reason + type_=cast(Any, MessageContent), + value=content_delta.model_dump(exclude_unset=True), + ), + ), + ) + new_content.append(content_delta) + else: + merged = accumulate_delta( + cast( + "dict[object, object]", + block.model_dump(exclude_unset=True), + ), + cast( + "dict[object, object]", + content_delta.model_dump(exclude_unset=True), + ), + ) + current_message_snapshot.content[content_delta.index] = cast( + MessageContent, + construct_type( + # mypy doesn't allow Content for some reason + type_=cast(Any, MessageContent), + value=merged, + ), + ) + + return current_message_snapshot, new_content + + +def accumulate_delta(acc: dict[object, object], delta: dict[object, object]) -> dict[object, object]: + for key, delta_value in delta.items(): + if key not in acc: + acc[key] = delta_value + continue + + acc_value = acc[key] + if acc_value is None: + acc[key] = delta_value + continue + + # the `index` property is used in arrays of objects so it should + # not be accumulated like other values e.g. + # [{'foo': 'bar', 'index': 0}] + # + # the same applies to `type` properties as they're used for + # discriminated unions + if key == "index" or key == "type": + acc[key] = delta_value + continue + + if isinstance(acc_value, str) and isinstance(delta_value, str): + acc_value += delta_value + elif isinstance(acc_value, (int, float)) and isinstance(delta_value, (int, float)): + acc_value += delta_value + elif is_dict(acc_value) and is_dict(delta_value): + acc_value = accumulate_delta(acc_value, delta_value) + elif is_list(acc_value) and is_list(delta_value): + # for lists of non-dictionary items we'll only ever get new entries + # in the array, existing entries will never be changed + if all(isinstance(x, (str, int, float)) for x in acc_value): + acc_value.extend(delta_value) + continue + + for delta_entry in delta_value: + if not is_dict(delta_entry): + raise TypeError(f"Unexpected list delta entry is not a dictionary: {delta_entry}") + + try: + index = delta_entry["index"] + except KeyError as exc: + raise RuntimeError(f"Expected list delta entry to have an `index` key; {delta_entry}") from exc + + if not isinstance(index, int): + raise TypeError(f"Unexpected, list delta entry `index` value is not an integer; {index}") + + try: + acc_entry = acc_value[index] + except IndexError: + acc_value.insert(index, delta_entry) + else: + if not is_dict(acc_entry): + raise TypeError("not handled yet") + + acc_value[index] = accumulate_delta(acc_entry, delta_entry) + + acc[key] = acc_value + + return acc diff --git a/openai/pagination.py b/openai/pagination.py new file mode 100644 index 00000000..82936382 --- /dev/null +++ b/openai/pagination.py @@ -0,0 +1,107 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Any, List, Generic, TypeVar, Optional, cast +from typing_extensions import Protocol, override, runtime_checkable + +from ._base_client import BasePage, PageInfo, BaseSyncPage, BaseAsyncPage + +__all__ = ["SyncPage", "AsyncPage", "SyncCursorPage", "AsyncCursorPage"] + +_T = TypeVar("_T") + + +@runtime_checkable +class CursorPageItem(Protocol): + id: Optional[str] + + +class SyncPage(BaseSyncPage[_T], BasePage[_T], Generic[_T]): + """Note: no pagination actually occurs yet, this is for forwards-compatibility.""" + + data: List[_T] + object: str + + @override + def _get_page_items(self) -> List[_T]: + data = self.data + if not data: + return [] + return data + + @override + def next_page_info(self) -> None: + """ + This page represents a response that isn't actually paginated at the API level + so there will never be a next page. + """ + return None + + +class AsyncPage(BaseAsyncPage[_T], BasePage[_T], Generic[_T]): + """Note: no pagination actually occurs yet, this is for forwards-compatibility.""" + + data: List[_T] + object: str + + @override + def _get_page_items(self) -> List[_T]: + data = self.data + if not data: + return [] + return data + + @override + def next_page_info(self) -> None: + """ + This page represents a response that isn't actually paginated at the API level + so there will never be a next page. + """ + return None + + +class SyncCursorPage(BaseSyncPage[_T], BasePage[_T], Generic[_T]): + data: List[_T] + + @override + def _get_page_items(self) -> List[_T]: + data = self.data + if not data: + return [] + return data + + @override + def next_page_info(self) -> Optional[PageInfo]: + data = self.data + if not data: + return None + + item = cast(Any, data[-1]) + if not isinstance(item, CursorPageItem) or item.id is None: + # TODO emit warning log + return None + + return PageInfo(params={"after": item.id}) + + +class AsyncCursorPage(BaseAsyncPage[_T], BasePage[_T], Generic[_T]): + data: List[_T] + + @override + def _get_page_items(self) -> List[_T]: + data = self.data + if not data: + return [] + return data + + @override + def next_page_info(self) -> Optional[PageInfo]: + data = self.data + if not data: + return None + + item = cast(Any, data[-1]) + if not isinstance(item, CursorPageItem) or item.id is None: + # TODO emit warning log + return None + + return PageInfo(params={"after": item.id}) diff --git a/openai/py.typed b/openai/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/openai/resources/__init__.py b/openai/resources/__init__.py new file mode 100644 index 00000000..64aa12d2 --- /dev/null +++ b/openai/resources/__init__.py @@ -0,0 +1,145 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .beta import ( + Beta, + AsyncBeta, + BetaWithRawResponse, + AsyncBetaWithRawResponse, + BetaWithStreamingResponse, + AsyncBetaWithStreamingResponse, +) +from .chat import ( + Chat, + AsyncChat, + ChatWithRawResponse, + AsyncChatWithRawResponse, + ChatWithStreamingResponse, + AsyncChatWithStreamingResponse, +) +from .audio import ( + Audio, + AsyncAudio, + AudioWithRawResponse, + AsyncAudioWithRawResponse, + AudioWithStreamingResponse, + AsyncAudioWithStreamingResponse, +) +from .files import ( + Files, + AsyncFiles, + FilesWithRawResponse, + AsyncFilesWithRawResponse, + FilesWithStreamingResponse, + AsyncFilesWithStreamingResponse, +) +from .images import ( + Images, + AsyncImages, + ImagesWithRawResponse, + AsyncImagesWithRawResponse, + ImagesWithStreamingResponse, + AsyncImagesWithStreamingResponse, +) +from .models import ( + Models, + AsyncModels, + ModelsWithRawResponse, + AsyncModelsWithRawResponse, + ModelsWithStreamingResponse, + AsyncModelsWithStreamingResponse, +) +from .embeddings import ( + Embeddings, + AsyncEmbeddings, + EmbeddingsWithRawResponse, + AsyncEmbeddingsWithRawResponse, + EmbeddingsWithStreamingResponse, + AsyncEmbeddingsWithStreamingResponse, +) +from .completions import ( + Completions, + AsyncCompletions, + CompletionsWithRawResponse, + AsyncCompletionsWithRawResponse, + CompletionsWithStreamingResponse, + AsyncCompletionsWithStreamingResponse, +) +from .fine_tuning import ( + FineTuning, + AsyncFineTuning, + FineTuningWithRawResponse, + AsyncFineTuningWithRawResponse, + FineTuningWithStreamingResponse, + AsyncFineTuningWithStreamingResponse, +) +from .moderations import ( + Moderations, + AsyncModerations, + ModerationsWithRawResponse, + AsyncModerationsWithRawResponse, + ModerationsWithStreamingResponse, + AsyncModerationsWithStreamingResponse, +) + +__all__ = [ + "Completions", + "AsyncCompletions", + "CompletionsWithRawResponse", + "AsyncCompletionsWithRawResponse", + "CompletionsWithStreamingResponse", + "AsyncCompletionsWithStreamingResponse", + "Chat", + "AsyncChat", + "ChatWithRawResponse", + "AsyncChatWithRawResponse", + "ChatWithStreamingResponse", + "AsyncChatWithStreamingResponse", + "Embeddings", + "AsyncEmbeddings", + "EmbeddingsWithRawResponse", + "AsyncEmbeddingsWithRawResponse", + "EmbeddingsWithStreamingResponse", + "AsyncEmbeddingsWithStreamingResponse", + "Files", + "AsyncFiles", + "FilesWithRawResponse", + "AsyncFilesWithRawResponse", + "FilesWithStreamingResponse", + "AsyncFilesWithStreamingResponse", + "Images", + "AsyncImages", + "ImagesWithRawResponse", + "AsyncImagesWithRawResponse", + "ImagesWithStreamingResponse", + "AsyncImagesWithStreamingResponse", + "Audio", + "AsyncAudio", + "AudioWithRawResponse", + "AsyncAudioWithRawResponse", + "AudioWithStreamingResponse", + "AsyncAudioWithStreamingResponse", + "Moderations", + "AsyncModerations", + "ModerationsWithRawResponse", + "AsyncModerationsWithRawResponse", + "ModerationsWithStreamingResponse", + "AsyncModerationsWithStreamingResponse", + "Models", + "AsyncModels", + "ModelsWithRawResponse", + "AsyncModelsWithRawResponse", + "ModelsWithStreamingResponse", + "AsyncModelsWithStreamingResponse", + "FineTuning", + "AsyncFineTuning", + "FineTuningWithRawResponse", + "AsyncFineTuningWithRawResponse", + "FineTuningWithStreamingResponse", + "AsyncFineTuningWithStreamingResponse", + "Beta", + "AsyncBeta", + "BetaWithRawResponse", + "AsyncBetaWithRawResponse", + "BetaWithStreamingResponse", + "AsyncBetaWithStreamingResponse", +] diff --git a/openai/resources/audio/__init__.py b/openai/resources/audio/__init__.py new file mode 100644 index 00000000..7da1d2db --- /dev/null +++ b/openai/resources/audio/__init__.py @@ -0,0 +1,61 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .audio import ( + Audio, + AsyncAudio, + AudioWithRawResponse, + AsyncAudioWithRawResponse, + AudioWithStreamingResponse, + AsyncAudioWithStreamingResponse, +) +from .speech import ( + Speech, + AsyncSpeech, + SpeechWithRawResponse, + AsyncSpeechWithRawResponse, + SpeechWithStreamingResponse, + AsyncSpeechWithStreamingResponse, +) +from .translations import ( + Translations, + AsyncTranslations, + TranslationsWithRawResponse, + AsyncTranslationsWithRawResponse, + TranslationsWithStreamingResponse, + AsyncTranslationsWithStreamingResponse, +) +from .transcriptions import ( + Transcriptions, + AsyncTranscriptions, + TranscriptionsWithRawResponse, + AsyncTranscriptionsWithRawResponse, + TranscriptionsWithStreamingResponse, + AsyncTranscriptionsWithStreamingResponse, +) + +__all__ = [ + "Transcriptions", + "AsyncTranscriptions", + "TranscriptionsWithRawResponse", + "AsyncTranscriptionsWithRawResponse", + "TranscriptionsWithStreamingResponse", + "AsyncTranscriptionsWithStreamingResponse", + "Translations", + "AsyncTranslations", + "TranslationsWithRawResponse", + "AsyncTranslationsWithRawResponse", + "TranslationsWithStreamingResponse", + "AsyncTranslationsWithStreamingResponse", + "Speech", + "AsyncSpeech", + "SpeechWithRawResponse", + "AsyncSpeechWithRawResponse", + "SpeechWithStreamingResponse", + "AsyncSpeechWithStreamingResponse", + "Audio", + "AsyncAudio", + "AudioWithRawResponse", + "AsyncAudioWithRawResponse", + "AudioWithStreamingResponse", + "AsyncAudioWithStreamingResponse", +] diff --git a/openai/resources/audio/audio.py b/openai/resources/audio/audio.py new file mode 100644 index 00000000..537ad573 --- /dev/null +++ b/openai/resources/audio/audio.py @@ -0,0 +1,144 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from .speech import ( + Speech, + AsyncSpeech, + SpeechWithRawResponse, + AsyncSpeechWithRawResponse, + SpeechWithStreamingResponse, + AsyncSpeechWithStreamingResponse, +) +from ..._compat import cached_property +from ..._resource import SyncAPIResource, AsyncAPIResource +from .translations import ( + Translations, + AsyncTranslations, + TranslationsWithRawResponse, + AsyncTranslationsWithRawResponse, + TranslationsWithStreamingResponse, + AsyncTranslationsWithStreamingResponse, +) +from .transcriptions import ( + Transcriptions, + AsyncTranscriptions, + TranscriptionsWithRawResponse, + AsyncTranscriptionsWithRawResponse, + TranscriptionsWithStreamingResponse, + AsyncTranscriptionsWithStreamingResponse, +) + +__all__ = ["Audio", "AsyncAudio"] + + +class Audio(SyncAPIResource): + @cached_property + def transcriptions(self) -> Transcriptions: + return Transcriptions(self._client) + + @cached_property + def translations(self) -> Translations: + return Translations(self._client) + + @cached_property + def speech(self) -> Speech: + return Speech(self._client) + + @cached_property + def with_raw_response(self) -> AudioWithRawResponse: + return AudioWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AudioWithStreamingResponse: + return AudioWithStreamingResponse(self) + + +class AsyncAudio(AsyncAPIResource): + @cached_property + def transcriptions(self) -> AsyncTranscriptions: + return AsyncTranscriptions(self._client) + + @cached_property + def translations(self) -> AsyncTranslations: + return AsyncTranslations(self._client) + + @cached_property + def speech(self) -> AsyncSpeech: + return AsyncSpeech(self._client) + + @cached_property + def with_raw_response(self) -> AsyncAudioWithRawResponse: + return AsyncAudioWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncAudioWithStreamingResponse: + return AsyncAudioWithStreamingResponse(self) + + +class AudioWithRawResponse: + def __init__(self, audio: Audio) -> None: + self._audio = audio + + @cached_property + def transcriptions(self) -> TranscriptionsWithRawResponse: + return TranscriptionsWithRawResponse(self._audio.transcriptions) + + @cached_property + def translations(self) -> TranslationsWithRawResponse: + return TranslationsWithRawResponse(self._audio.translations) + + @cached_property + def speech(self) -> SpeechWithRawResponse: + return SpeechWithRawResponse(self._audio.speech) + + +class AsyncAudioWithRawResponse: + def __init__(self, audio: AsyncAudio) -> None: + self._audio = audio + + @cached_property + def transcriptions(self) -> AsyncTranscriptionsWithRawResponse: + return AsyncTranscriptionsWithRawResponse(self._audio.transcriptions) + + @cached_property + def translations(self) -> AsyncTranslationsWithRawResponse: + return AsyncTranslationsWithRawResponse(self._audio.translations) + + @cached_property + def speech(self) -> AsyncSpeechWithRawResponse: + return AsyncSpeechWithRawResponse(self._audio.speech) + + +class AudioWithStreamingResponse: + def __init__(self, audio: Audio) -> None: + self._audio = audio + + @cached_property + def transcriptions(self) -> TranscriptionsWithStreamingResponse: + return TranscriptionsWithStreamingResponse(self._audio.transcriptions) + + @cached_property + def translations(self) -> TranslationsWithStreamingResponse: + return TranslationsWithStreamingResponse(self._audio.translations) + + @cached_property + def speech(self) -> SpeechWithStreamingResponse: + return SpeechWithStreamingResponse(self._audio.speech) + + +class AsyncAudioWithStreamingResponse: + def __init__(self, audio: AsyncAudio) -> None: + self._audio = audio + + @cached_property + def transcriptions(self) -> AsyncTranscriptionsWithStreamingResponse: + return AsyncTranscriptionsWithStreamingResponse(self._audio.transcriptions) + + @cached_property + def translations(self) -> AsyncTranslationsWithStreamingResponse: + return AsyncTranslationsWithStreamingResponse(self._audio.translations) + + @cached_property + def speech(self) -> AsyncSpeechWithStreamingResponse: + return AsyncSpeechWithStreamingResponse(self._audio.speech) diff --git a/openai/resources/audio/speech.py b/openai/resources/audio/speech.py new file mode 100644 index 00000000..e26c5805 --- /dev/null +++ b/openai/resources/audio/speech.py @@ -0,0 +1,213 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Union +from typing_extensions import Literal + +import httpx + +from ... import _legacy_response +from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from ..._utils import ( + maybe_transform, + async_maybe_transform, +) +from ..._compat import cached_property +from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import ( + StreamedBinaryAPIResponse, + AsyncStreamedBinaryAPIResponse, + to_custom_streamed_response_wrapper, + async_to_custom_streamed_response_wrapper, +) +from ...types.audio import speech_create_params +from ..._base_client import ( + make_request_options, +) + +__all__ = ["Speech", "AsyncSpeech"] + + +class Speech(SyncAPIResource): + @cached_property + def with_raw_response(self) -> SpeechWithRawResponse: + return SpeechWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> SpeechWithStreamingResponse: + return SpeechWithStreamingResponse(self) + + def create( + self, + *, + input: str, + model: Union[str, Literal["tts-1", "tts-1-hd"]], + voice: Literal["alloy", "echo", "fable", "onyx", "nova", "shimmer"], + response_format: Literal["mp3", "opus", "aac", "flac", "wav", "pcm"] | NotGiven = NOT_GIVEN, + speed: float | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> _legacy_response.HttpxBinaryResponseContent: + """ + Generates audio from the input text. + + Args: + input: The text to generate audio for. The maximum length is 4096 characters. + + model: + One of the available [TTS models](https://platform.openai.com/docs/models/tts): + `tts-1` or `tts-1-hd` + + voice: The voice to use when generating the audio. Supported voices are `alloy`, + `echo`, `fable`, `onyx`, `nova`, and `shimmer`. Previews of the voices are + available in the + [Text to speech guide](https://platform.openai.com/docs/guides/text-to-speech/voice-options). + + response_format: The format to audio in. Supported formats are `mp3`, `opus`, `aac`, `flac`, + `wav`, and `pcm`. + + speed: The speed of the generated audio. Select a value from `0.25` to `4.0`. `1.0` is + the default. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})} + return self._post( + "/audio/speech", + body=maybe_transform( + { + "input": input, + "model": model, + "voice": voice, + "response_format": response_format, + "speed": speed, + }, + speech_create_params.SpeechCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=_legacy_response.HttpxBinaryResponseContent, + ) + + +class AsyncSpeech(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncSpeechWithRawResponse: + return AsyncSpeechWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncSpeechWithStreamingResponse: + return AsyncSpeechWithStreamingResponse(self) + + async def create( + self, + *, + input: str, + model: Union[str, Literal["tts-1", "tts-1-hd"]], + voice: Literal["alloy", "echo", "fable", "onyx", "nova", "shimmer"], + response_format: Literal["mp3", "opus", "aac", "flac", "wav", "pcm"] | NotGiven = NOT_GIVEN, + speed: float | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> _legacy_response.HttpxBinaryResponseContent: + """ + Generates audio from the input text. + + Args: + input: The text to generate audio for. The maximum length is 4096 characters. + + model: + One of the available [TTS models](https://platform.openai.com/docs/models/tts): + `tts-1` or `tts-1-hd` + + voice: The voice to use when generating the audio. Supported voices are `alloy`, + `echo`, `fable`, `onyx`, `nova`, and `shimmer`. Previews of the voices are + available in the + [Text to speech guide](https://platform.openai.com/docs/guides/text-to-speech/voice-options). + + response_format: The format to audio in. Supported formats are `mp3`, `opus`, `aac`, `flac`, + `wav`, and `pcm`. + + speed: The speed of the generated audio. Select a value from `0.25` to `4.0`. `1.0` is + the default. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})} + return await self._post( + "/audio/speech", + body=await async_maybe_transform( + { + "input": input, + "model": model, + "voice": voice, + "response_format": response_format, + "speed": speed, + }, + speech_create_params.SpeechCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=_legacy_response.HttpxBinaryResponseContent, + ) + + +class SpeechWithRawResponse: + def __init__(self, speech: Speech) -> None: + self._speech = speech + + self.create = _legacy_response.to_raw_response_wrapper( + speech.create, + ) + + +class AsyncSpeechWithRawResponse: + def __init__(self, speech: AsyncSpeech) -> None: + self._speech = speech + + self.create = _legacy_response.async_to_raw_response_wrapper( + speech.create, + ) + + +class SpeechWithStreamingResponse: + def __init__(self, speech: Speech) -> None: + self._speech = speech + + self.create = to_custom_streamed_response_wrapper( + speech.create, + StreamedBinaryAPIResponse, + ) + + +class AsyncSpeechWithStreamingResponse: + def __init__(self, speech: AsyncSpeech) -> None: + self._speech = speech + + self.create = async_to_custom_streamed_response_wrapper( + speech.create, + AsyncStreamedBinaryAPIResponse, + ) diff --git a/openai/resources/audio/transcriptions.py b/openai/resources/audio/transcriptions.py new file mode 100644 index 00000000..353f28ab --- /dev/null +++ b/openai/resources/audio/transcriptions.py @@ -0,0 +1,256 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List, Union, Mapping, cast +from typing_extensions import Literal + +import httpx + +from ... import _legacy_response +from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven, FileTypes +from ..._utils import ( + extract_files, + maybe_transform, + deepcopy_minimal, + async_maybe_transform, +) +from ..._compat import cached_property +from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper +from ...types.audio import Transcription, transcription_create_params +from ..._base_client import ( + make_request_options, +) + +__all__ = ["Transcriptions", "AsyncTranscriptions"] + + +class Transcriptions(SyncAPIResource): + @cached_property + def with_raw_response(self) -> TranscriptionsWithRawResponse: + return TranscriptionsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> TranscriptionsWithStreamingResponse: + return TranscriptionsWithStreamingResponse(self) + + def create( + self, + *, + file: FileTypes, + model: Union[str, Literal["whisper-1"]], + language: str | NotGiven = NOT_GIVEN, + prompt: str | NotGiven = NOT_GIVEN, + response_format: Literal["json", "text", "srt", "verbose_json", "vtt"] | NotGiven = NOT_GIVEN, + temperature: float | NotGiven = NOT_GIVEN, + timestamp_granularities: List[Literal["word", "segment"]] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Transcription: + """ + Transcribes audio into the input language. + + Args: + file: + The audio file object (not file name) to transcribe, in one of these formats: + flac, mp3, mp4, mpeg, mpga, m4a, ogg, wav, or webm. + + model: ID of the model to use. Only `whisper-1` (which is powered by our open source + Whisper V2 model) is currently available. + + language: The language of the input audio. Supplying the input language in + [ISO-639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) format will + improve accuracy and latency. + + prompt: An optional text to guide the model's style or continue a previous audio + segment. The + [prompt](https://platform.openai.com/docs/guides/speech-to-text/prompting) + should match the audio language. + + response_format: The format of the transcript output, in one of these options: `json`, `text`, + `srt`, `verbose_json`, or `vtt`. + + temperature: The sampling temperature, between 0 and 1. Higher values like 0.8 will make the + output more random, while lower values like 0.2 will make it more focused and + deterministic. If set to 0, the model will use + [log probability](https://en.wikipedia.org/wiki/Log_probability) to + automatically increase the temperature until certain thresholds are hit. + + timestamp_granularities: The timestamp granularities to populate for this transcription. + `response_format` must be set `verbose_json` to use timestamp granularities. + Either or both of these options are supported: `word`, or `segment`. Note: There + is no additional latency for segment timestamps, but generating word timestamps + incurs additional latency. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + body = deepcopy_minimal( + { + "file": file, + "model": model, + "language": language, + "prompt": prompt, + "response_format": response_format, + "temperature": temperature, + "timestamp_granularities": timestamp_granularities, + } + ) + files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) + if files: + # It should be noted that the actual Content-Type header that will be + # sent to the server will contain a `boundary` parameter, e.g. + # multipart/form-data; boundary=---abc-- + extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} + return self._post( + "/audio/transcriptions", + body=maybe_transform(body, transcription_create_params.TranscriptionCreateParams), + files=files, + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Transcription, + ) + + +class AsyncTranscriptions(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncTranscriptionsWithRawResponse: + return AsyncTranscriptionsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncTranscriptionsWithStreamingResponse: + return AsyncTranscriptionsWithStreamingResponse(self) + + async def create( + self, + *, + file: FileTypes, + model: Union[str, Literal["whisper-1"]], + language: str | NotGiven = NOT_GIVEN, + prompt: str | NotGiven = NOT_GIVEN, + response_format: Literal["json", "text", "srt", "verbose_json", "vtt"] | NotGiven = NOT_GIVEN, + temperature: float | NotGiven = NOT_GIVEN, + timestamp_granularities: List[Literal["word", "segment"]] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Transcription: + """ + Transcribes audio into the input language. + + Args: + file: + The audio file object (not file name) to transcribe, in one of these formats: + flac, mp3, mp4, mpeg, mpga, m4a, ogg, wav, or webm. + + model: ID of the model to use. Only `whisper-1` (which is powered by our open source + Whisper V2 model) is currently available. + + language: The language of the input audio. Supplying the input language in + [ISO-639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) format will + improve accuracy and latency. + + prompt: An optional text to guide the model's style or continue a previous audio + segment. The + [prompt](https://platform.openai.com/docs/guides/speech-to-text/prompting) + should match the audio language. + + response_format: The format of the transcript output, in one of these options: `json`, `text`, + `srt`, `verbose_json`, or `vtt`. + + temperature: The sampling temperature, between 0 and 1. Higher values like 0.8 will make the + output more random, while lower values like 0.2 will make it more focused and + deterministic. If set to 0, the model will use + [log probability](https://en.wikipedia.org/wiki/Log_probability) to + automatically increase the temperature until certain thresholds are hit. + + timestamp_granularities: The timestamp granularities to populate for this transcription. + `response_format` must be set `verbose_json` to use timestamp granularities. + Either or both of these options are supported: `word`, or `segment`. Note: There + is no additional latency for segment timestamps, but generating word timestamps + incurs additional latency. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + body = deepcopy_minimal( + { + "file": file, + "model": model, + "language": language, + "prompt": prompt, + "response_format": response_format, + "temperature": temperature, + "timestamp_granularities": timestamp_granularities, + } + ) + files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) + if files: + # It should be noted that the actual Content-Type header that will be + # sent to the server will contain a `boundary` parameter, e.g. + # multipart/form-data; boundary=---abc-- + extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} + return await self._post( + "/audio/transcriptions", + body=await async_maybe_transform(body, transcription_create_params.TranscriptionCreateParams), + files=files, + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Transcription, + ) + + +class TranscriptionsWithRawResponse: + def __init__(self, transcriptions: Transcriptions) -> None: + self._transcriptions = transcriptions + + self.create = _legacy_response.to_raw_response_wrapper( + transcriptions.create, + ) + + +class AsyncTranscriptionsWithRawResponse: + def __init__(self, transcriptions: AsyncTranscriptions) -> None: + self._transcriptions = transcriptions + + self.create = _legacy_response.async_to_raw_response_wrapper( + transcriptions.create, + ) + + +class TranscriptionsWithStreamingResponse: + def __init__(self, transcriptions: Transcriptions) -> None: + self._transcriptions = transcriptions + + self.create = to_streamed_response_wrapper( + transcriptions.create, + ) + + +class AsyncTranscriptionsWithStreamingResponse: + def __init__(self, transcriptions: AsyncTranscriptions) -> None: + self._transcriptions = transcriptions + + self.create = async_to_streamed_response_wrapper( + transcriptions.create, + ) diff --git a/openai/resources/audio/translations.py b/openai/resources/audio/translations.py new file mode 100644 index 00000000..79020a5e --- /dev/null +++ b/openai/resources/audio/translations.py @@ -0,0 +1,226 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Union, Mapping, cast +from typing_extensions import Literal + +import httpx + +from ... import _legacy_response +from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven, FileTypes +from ..._utils import ( + extract_files, + maybe_transform, + deepcopy_minimal, + async_maybe_transform, +) +from ..._compat import cached_property +from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper +from ...types.audio import Translation, translation_create_params +from ..._base_client import ( + make_request_options, +) + +__all__ = ["Translations", "AsyncTranslations"] + + +class Translations(SyncAPIResource): + @cached_property + def with_raw_response(self) -> TranslationsWithRawResponse: + return TranslationsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> TranslationsWithStreamingResponse: + return TranslationsWithStreamingResponse(self) + + def create( + self, + *, + file: FileTypes, + model: Union[str, Literal["whisper-1"]], + prompt: str | NotGiven = NOT_GIVEN, + response_format: str | NotGiven = NOT_GIVEN, + temperature: float | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Translation: + """ + Translates audio into English. + + Args: + file: The audio file object (not file name) translate, in one of these formats: flac, + mp3, mp4, mpeg, mpga, m4a, ogg, wav, or webm. + + model: ID of the model to use. Only `whisper-1` (which is powered by our open source + Whisper V2 model) is currently available. + + prompt: An optional text to guide the model's style or continue a previous audio + segment. The + [prompt](https://platform.openai.com/docs/guides/speech-to-text/prompting) + should be in English. + + response_format: The format of the transcript output, in one of these options: `json`, `text`, + `srt`, `verbose_json`, or `vtt`. + + temperature: The sampling temperature, between 0 and 1. Higher values like 0.8 will make the + output more random, while lower values like 0.2 will make it more focused and + deterministic. If set to 0, the model will use + [log probability](https://en.wikipedia.org/wiki/Log_probability) to + automatically increase the temperature until certain thresholds are hit. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + body = deepcopy_minimal( + { + "file": file, + "model": model, + "prompt": prompt, + "response_format": response_format, + "temperature": temperature, + } + ) + files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) + if files: + # It should be noted that the actual Content-Type header that will be + # sent to the server will contain a `boundary` parameter, e.g. + # multipart/form-data; boundary=---abc-- + extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} + return self._post( + "/audio/translations", + body=maybe_transform(body, translation_create_params.TranslationCreateParams), + files=files, + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Translation, + ) + + +class AsyncTranslations(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncTranslationsWithRawResponse: + return AsyncTranslationsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncTranslationsWithStreamingResponse: + return AsyncTranslationsWithStreamingResponse(self) + + async def create( + self, + *, + file: FileTypes, + model: Union[str, Literal["whisper-1"]], + prompt: str | NotGiven = NOT_GIVEN, + response_format: str | NotGiven = NOT_GIVEN, + temperature: float | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Translation: + """ + Translates audio into English. + + Args: + file: The audio file object (not file name) translate, in one of these formats: flac, + mp3, mp4, mpeg, mpga, m4a, ogg, wav, or webm. + + model: ID of the model to use. Only `whisper-1` (which is powered by our open source + Whisper V2 model) is currently available. + + prompt: An optional text to guide the model's style or continue a previous audio + segment. The + [prompt](https://platform.openai.com/docs/guides/speech-to-text/prompting) + should be in English. + + response_format: The format of the transcript output, in one of these options: `json`, `text`, + `srt`, `verbose_json`, or `vtt`. + + temperature: The sampling temperature, between 0 and 1. Higher values like 0.8 will make the + output more random, while lower values like 0.2 will make it more focused and + deterministic. If set to 0, the model will use + [log probability](https://en.wikipedia.org/wiki/Log_probability) to + automatically increase the temperature until certain thresholds are hit. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + body = deepcopy_minimal( + { + "file": file, + "model": model, + "prompt": prompt, + "response_format": response_format, + "temperature": temperature, + } + ) + files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) + if files: + # It should be noted that the actual Content-Type header that will be + # sent to the server will contain a `boundary` parameter, e.g. + # multipart/form-data; boundary=---abc-- + extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} + return await self._post( + "/audio/translations", + body=await async_maybe_transform(body, translation_create_params.TranslationCreateParams), + files=files, + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Translation, + ) + + +class TranslationsWithRawResponse: + def __init__(self, translations: Translations) -> None: + self._translations = translations + + self.create = _legacy_response.to_raw_response_wrapper( + translations.create, + ) + + +class AsyncTranslationsWithRawResponse: + def __init__(self, translations: AsyncTranslations) -> None: + self._translations = translations + + self.create = _legacy_response.async_to_raw_response_wrapper( + translations.create, + ) + + +class TranslationsWithStreamingResponse: + def __init__(self, translations: Translations) -> None: + self._translations = translations + + self.create = to_streamed_response_wrapper( + translations.create, + ) + + +class AsyncTranslationsWithStreamingResponse: + def __init__(self, translations: AsyncTranslations) -> None: + self._translations = translations + + self.create = async_to_streamed_response_wrapper( + translations.create, + ) diff --git a/openai/resources/beta/__init__.py b/openai/resources/beta/__init__.py new file mode 100644 index 00000000..87fea252 --- /dev/null +++ b/openai/resources/beta/__init__.py @@ -0,0 +1,47 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .beta import ( + Beta, + AsyncBeta, + BetaWithRawResponse, + AsyncBetaWithRawResponse, + BetaWithStreamingResponse, + AsyncBetaWithStreamingResponse, +) +from .threads import ( + Threads, + AsyncThreads, + ThreadsWithRawResponse, + AsyncThreadsWithRawResponse, + ThreadsWithStreamingResponse, + AsyncThreadsWithStreamingResponse, +) +from .assistants import ( + Assistants, + AsyncAssistants, + AssistantsWithRawResponse, + AsyncAssistantsWithRawResponse, + AssistantsWithStreamingResponse, + AsyncAssistantsWithStreamingResponse, +) + +__all__ = [ + "Assistants", + "AsyncAssistants", + "AssistantsWithRawResponse", + "AsyncAssistantsWithRawResponse", + "AssistantsWithStreamingResponse", + "AsyncAssistantsWithStreamingResponse", + "Threads", + "AsyncThreads", + "ThreadsWithRawResponse", + "AsyncThreadsWithRawResponse", + "ThreadsWithStreamingResponse", + "AsyncThreadsWithStreamingResponse", + "Beta", + "AsyncBeta", + "BetaWithRawResponse", + "AsyncBetaWithRawResponse", + "BetaWithStreamingResponse", + "AsyncBetaWithStreamingResponse", +] diff --git a/openai/resources/beta/assistants/__init__.py b/openai/resources/beta/assistants/__init__.py new file mode 100644 index 00000000..736def93 --- /dev/null +++ b/openai/resources/beta/assistants/__init__.py @@ -0,0 +1,33 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .files import ( + Files, + AsyncFiles, + FilesWithRawResponse, + AsyncFilesWithRawResponse, + FilesWithStreamingResponse, + AsyncFilesWithStreamingResponse, +) +from .assistants import ( + Assistants, + AsyncAssistants, + AssistantsWithRawResponse, + AsyncAssistantsWithRawResponse, + AssistantsWithStreamingResponse, + AsyncAssistantsWithStreamingResponse, +) + +__all__ = [ + "Files", + "AsyncFiles", + "FilesWithRawResponse", + "AsyncFilesWithRawResponse", + "FilesWithStreamingResponse", + "AsyncFilesWithStreamingResponse", + "Assistants", + "AsyncAssistants", + "AssistantsWithRawResponse", + "AsyncAssistantsWithRawResponse", + "AssistantsWithStreamingResponse", + "AsyncAssistantsWithStreamingResponse", +] diff --git a/openai/resources/beta/assistants/assistants.py b/openai/resources/beta/assistants/assistants.py new file mode 100644 index 00000000..232451ab --- /dev/null +++ b/openai/resources/beta/assistants/assistants.py @@ -0,0 +1,747 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List, Iterable, Optional +from typing_extensions import Literal + +import httpx + +from .... import _legacy_response +from .files import ( + Files, + AsyncFiles, + FilesWithRawResponse, + AsyncFilesWithRawResponse, + FilesWithStreamingResponse, + AsyncFilesWithStreamingResponse, +) +from ...._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from ...._utils import ( + maybe_transform, + async_maybe_transform, +) +from ...._compat import cached_property +from ...._resource import SyncAPIResource, AsyncAPIResource +from ...._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper +from ....pagination import SyncCursorPage, AsyncCursorPage +from ....types.beta import ( + Assistant, + AssistantDeleted, + AssistantToolParam, + assistant_list_params, + assistant_create_params, + assistant_update_params, +) +from ...._base_client import ( + AsyncPaginator, + make_request_options, +) + +__all__ = ["Assistants", "AsyncAssistants"] + + +class Assistants(SyncAPIResource): + @cached_property + def files(self) -> Files: + return Files(self._client) + + @cached_property + def with_raw_response(self) -> AssistantsWithRawResponse: + return AssistantsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AssistantsWithStreamingResponse: + return AssistantsWithStreamingResponse(self) + + def create( + self, + *, + model: str, + description: Optional[str] | NotGiven = NOT_GIVEN, + file_ids: List[str] | NotGiven = NOT_GIVEN, + instructions: Optional[str] | NotGiven = NOT_GIVEN, + metadata: Optional[object] | NotGiven = NOT_GIVEN, + name: Optional[str] | NotGiven = NOT_GIVEN, + tools: Iterable[AssistantToolParam] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Assistant: + """ + Create an assistant with a model and instructions. + + Args: + model: ID of the model to use. You can use the + [List models](https://platform.openai.com/docs/api-reference/models/list) API to + see all of your available models, or see our + [Model overview](https://platform.openai.com/docs/models/overview) for + descriptions of them. + + description: The description of the assistant. The maximum length is 512 characters. + + file_ids: A list of [file](https://platform.openai.com/docs/api-reference/files) IDs + attached to this assistant. There can be a maximum of 20 files attached to the + assistant. Files are ordered by their creation date in ascending order. + + instructions: The system instructions that the assistant uses. The maximum length is 32768 + characters. + + metadata: Set of 16 key-value pairs that can be attached to an object. This can be useful + for storing additional information about the object in a structured format. Keys + can be a maximum of 64 characters long and values can be a maxium of 512 + characters long. + + name: The name of the assistant. The maximum length is 256 characters. + + tools: A list of tool enabled on the assistant. There can be a maximum of 128 tools per + assistant. Tools can be of types `code_interpreter`, `retrieval`, or `function`. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})} + return self._post( + "/assistants", + body=maybe_transform( + { + "model": model, + "description": description, + "file_ids": file_ids, + "instructions": instructions, + "metadata": metadata, + "name": name, + "tools": tools, + }, + assistant_create_params.AssistantCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Assistant, + ) + + def retrieve( + self, + assistant_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Assistant: + """ + Retrieves an assistant. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not assistant_id: + raise ValueError(f"Expected a non-empty value for `assistant_id` but received {assistant_id!r}") + extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})} + return self._get( + f"/assistants/{assistant_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Assistant, + ) + + def update( + self, + assistant_id: str, + *, + description: Optional[str] | NotGiven = NOT_GIVEN, + file_ids: List[str] | NotGiven = NOT_GIVEN, + instructions: Optional[str] | NotGiven = NOT_GIVEN, + metadata: Optional[object] | NotGiven = NOT_GIVEN, + model: str | NotGiven = NOT_GIVEN, + name: Optional[str] | NotGiven = NOT_GIVEN, + tools: Iterable[AssistantToolParam] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Assistant: + """Modifies an assistant. + + Args: + description: The description of the assistant. + + The maximum length is 512 characters. + + file_ids: A list of [File](https://platform.openai.com/docs/api-reference/files) IDs + attached to this assistant. There can be a maximum of 20 files attached to the + assistant. Files are ordered by their creation date in ascending order. If a + file was previously attached to the list but does not show up in the list, it + will be deleted from the assistant. + + instructions: The system instructions that the assistant uses. The maximum length is 32768 + characters. + + metadata: Set of 16 key-value pairs that can be attached to an object. This can be useful + for storing additional information about the object in a structured format. Keys + can be a maximum of 64 characters long and values can be a maxium of 512 + characters long. + + model: ID of the model to use. You can use the + [List models](https://platform.openai.com/docs/api-reference/models/list) API to + see all of your available models, or see our + [Model overview](https://platform.openai.com/docs/models/overview) for + descriptions of them. + + name: The name of the assistant. The maximum length is 256 characters. + + tools: A list of tool enabled on the assistant. There can be a maximum of 128 tools per + assistant. Tools can be of types `code_interpreter`, `retrieval`, or `function`. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not assistant_id: + raise ValueError(f"Expected a non-empty value for `assistant_id` but received {assistant_id!r}") + extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})} + return self._post( + f"/assistants/{assistant_id}", + body=maybe_transform( + { + "description": description, + "file_ids": file_ids, + "instructions": instructions, + "metadata": metadata, + "model": model, + "name": name, + "tools": tools, + }, + assistant_update_params.AssistantUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Assistant, + ) + + def list( + self, + *, + after: str | NotGiven = NOT_GIVEN, + before: str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + order: Literal["asc", "desc"] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SyncCursorPage[Assistant]: + """Returns a list of assistants. + + Args: + after: A cursor for use in pagination. + + `after` is an object ID that defines your place + in the list. For instance, if you make a list request and receive 100 objects, + ending with obj_foo, your subsequent call can include after=obj_foo in order to + fetch the next page of the list. + + before: A cursor for use in pagination. `before` is an object ID that defines your place + in the list. For instance, if you make a list request and receive 100 objects, + ending with obj_foo, your subsequent call can include before=obj_foo in order to + fetch the previous page of the list. + + limit: A limit on the number of objects to be returned. Limit can range between 1 and + 100, and the default is 20. + + order: Sort order by the `created_at` timestamp of the objects. `asc` for ascending + order and `desc` for descending order. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})} + return self._get_api_list( + "/assistants", + page=SyncCursorPage[Assistant], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "after": after, + "before": before, + "limit": limit, + "order": order, + }, + assistant_list_params.AssistantListParams, + ), + ), + model=Assistant, + ) + + def delete( + self, + assistant_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AssistantDeleted: + """ + Delete an assistant. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not assistant_id: + raise ValueError(f"Expected a non-empty value for `assistant_id` but received {assistant_id!r}") + extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})} + return self._delete( + f"/assistants/{assistant_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AssistantDeleted, + ) + + +class AsyncAssistants(AsyncAPIResource): + @cached_property + def files(self) -> AsyncFiles: + return AsyncFiles(self._client) + + @cached_property + def with_raw_response(self) -> AsyncAssistantsWithRawResponse: + return AsyncAssistantsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncAssistantsWithStreamingResponse: + return AsyncAssistantsWithStreamingResponse(self) + + async def create( + self, + *, + model: str, + description: Optional[str] | NotGiven = NOT_GIVEN, + file_ids: List[str] | NotGiven = NOT_GIVEN, + instructions: Optional[str] | NotGiven = NOT_GIVEN, + metadata: Optional[object] | NotGiven = NOT_GIVEN, + name: Optional[str] | NotGiven = NOT_GIVEN, + tools: Iterable[AssistantToolParam] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Assistant: + """ + Create an assistant with a model and instructions. + + Args: + model: ID of the model to use. You can use the + [List models](https://platform.openai.com/docs/api-reference/models/list) API to + see all of your available models, or see our + [Model overview](https://platform.openai.com/docs/models/overview) for + descriptions of them. + + description: The description of the assistant. The maximum length is 512 characters. + + file_ids: A list of [file](https://platform.openai.com/docs/api-reference/files) IDs + attached to this assistant. There can be a maximum of 20 files attached to the + assistant. Files are ordered by their creation date in ascending order. + + instructions: The system instructions that the assistant uses. The maximum length is 32768 + characters. + + metadata: Set of 16 key-value pairs that can be attached to an object. This can be useful + for storing additional information about the object in a structured format. Keys + can be a maximum of 64 characters long and values can be a maxium of 512 + characters long. + + name: The name of the assistant. The maximum length is 256 characters. + + tools: A list of tool enabled on the assistant. There can be a maximum of 128 tools per + assistant. Tools can be of types `code_interpreter`, `retrieval`, or `function`. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})} + return await self._post( + "/assistants", + body=await async_maybe_transform( + { + "model": model, + "description": description, + "file_ids": file_ids, + "instructions": instructions, + "metadata": metadata, + "name": name, + "tools": tools, + }, + assistant_create_params.AssistantCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Assistant, + ) + + async def retrieve( + self, + assistant_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Assistant: + """ + Retrieves an assistant. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not assistant_id: + raise ValueError(f"Expected a non-empty value for `assistant_id` but received {assistant_id!r}") + extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})} + return await self._get( + f"/assistants/{assistant_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Assistant, + ) + + async def update( + self, + assistant_id: str, + *, + description: Optional[str] | NotGiven = NOT_GIVEN, + file_ids: List[str] | NotGiven = NOT_GIVEN, + instructions: Optional[str] | NotGiven = NOT_GIVEN, + metadata: Optional[object] | NotGiven = NOT_GIVEN, + model: str | NotGiven = NOT_GIVEN, + name: Optional[str] | NotGiven = NOT_GIVEN, + tools: Iterable[AssistantToolParam] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Assistant: + """Modifies an assistant. + + Args: + description: The description of the assistant. + + The maximum length is 512 characters. + + file_ids: A list of [File](https://platform.openai.com/docs/api-reference/files) IDs + attached to this assistant. There can be a maximum of 20 files attached to the + assistant. Files are ordered by their creation date in ascending order. If a + file was previously attached to the list but does not show up in the list, it + will be deleted from the assistant. + + instructions: The system instructions that the assistant uses. The maximum length is 32768 + characters. + + metadata: Set of 16 key-value pairs that can be attached to an object. This can be useful + for storing additional information about the object in a structured format. Keys + can be a maximum of 64 characters long and values can be a maxium of 512 + characters long. + + model: ID of the model to use. You can use the + [List models](https://platform.openai.com/docs/api-reference/models/list) API to + see all of your available models, or see our + [Model overview](https://platform.openai.com/docs/models/overview) for + descriptions of them. + + name: The name of the assistant. The maximum length is 256 characters. + + tools: A list of tool enabled on the assistant. There can be a maximum of 128 tools per + assistant. Tools can be of types `code_interpreter`, `retrieval`, or `function`. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not assistant_id: + raise ValueError(f"Expected a non-empty value for `assistant_id` but received {assistant_id!r}") + extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})} + return await self._post( + f"/assistants/{assistant_id}", + body=await async_maybe_transform( + { + "description": description, + "file_ids": file_ids, + "instructions": instructions, + "metadata": metadata, + "model": model, + "name": name, + "tools": tools, + }, + assistant_update_params.AssistantUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Assistant, + ) + + def list( + self, + *, + after: str | NotGiven = NOT_GIVEN, + before: str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + order: Literal["asc", "desc"] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncPaginator[Assistant, AsyncCursorPage[Assistant]]: + """Returns a list of assistants. + + Args: + after: A cursor for use in pagination. + + `after` is an object ID that defines your place + in the list. For instance, if you make a list request and receive 100 objects, + ending with obj_foo, your subsequent call can include after=obj_foo in order to + fetch the next page of the list. + + before: A cursor for use in pagination. `before` is an object ID that defines your place + in the list. For instance, if you make a list request and receive 100 objects, + ending with obj_foo, your subsequent call can include before=obj_foo in order to + fetch the previous page of the list. + + limit: A limit on the number of objects to be returned. Limit can range between 1 and + 100, and the default is 20. + + order: Sort order by the `created_at` timestamp of the objects. `asc` for ascending + order and `desc` for descending order. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})} + return self._get_api_list( + "/assistants", + page=AsyncCursorPage[Assistant], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "after": after, + "before": before, + "limit": limit, + "order": order, + }, + assistant_list_params.AssistantListParams, + ), + ), + model=Assistant, + ) + + async def delete( + self, + assistant_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AssistantDeleted: + """ + Delete an assistant. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not assistant_id: + raise ValueError(f"Expected a non-empty value for `assistant_id` but received {assistant_id!r}") + extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})} + return await self._delete( + f"/assistants/{assistant_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AssistantDeleted, + ) + + +class AssistantsWithRawResponse: + def __init__(self, assistants: Assistants) -> None: + self._assistants = assistants + + self.create = _legacy_response.to_raw_response_wrapper( + assistants.create, + ) + self.retrieve = _legacy_response.to_raw_response_wrapper( + assistants.retrieve, + ) + self.update = _legacy_response.to_raw_response_wrapper( + assistants.update, + ) + self.list = _legacy_response.to_raw_response_wrapper( + assistants.list, + ) + self.delete = _legacy_response.to_raw_response_wrapper( + assistants.delete, + ) + + @cached_property + def files(self) -> FilesWithRawResponse: + return FilesWithRawResponse(self._assistants.files) + + +class AsyncAssistantsWithRawResponse: + def __init__(self, assistants: AsyncAssistants) -> None: + self._assistants = assistants + + self.create = _legacy_response.async_to_raw_response_wrapper( + assistants.create, + ) + self.retrieve = _legacy_response.async_to_raw_response_wrapper( + assistants.retrieve, + ) + self.update = _legacy_response.async_to_raw_response_wrapper( + assistants.update, + ) + self.list = _legacy_response.async_to_raw_response_wrapper( + assistants.list, + ) + self.delete = _legacy_response.async_to_raw_response_wrapper( + assistants.delete, + ) + + @cached_property + def files(self) -> AsyncFilesWithRawResponse: + return AsyncFilesWithRawResponse(self._assistants.files) + + +class AssistantsWithStreamingResponse: + def __init__(self, assistants: Assistants) -> None: + self._assistants = assistants + + self.create = to_streamed_response_wrapper( + assistants.create, + ) + self.retrieve = to_streamed_response_wrapper( + assistants.retrieve, + ) + self.update = to_streamed_response_wrapper( + assistants.update, + ) + self.list = to_streamed_response_wrapper( + assistants.list, + ) + self.delete = to_streamed_response_wrapper( + assistants.delete, + ) + + @cached_property + def files(self) -> FilesWithStreamingResponse: + return FilesWithStreamingResponse(self._assistants.files) + + +class AsyncAssistantsWithStreamingResponse: + def __init__(self, assistants: AsyncAssistants) -> None: + self._assistants = assistants + + self.create = async_to_streamed_response_wrapper( + assistants.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + assistants.retrieve, + ) + self.update = async_to_streamed_response_wrapper( + assistants.update, + ) + self.list = async_to_streamed_response_wrapper( + assistants.list, + ) + self.delete = async_to_streamed_response_wrapper( + assistants.delete, + ) + + @cached_property + def files(self) -> AsyncFilesWithStreamingResponse: + return AsyncFilesWithStreamingResponse(self._assistants.files) diff --git a/openai/resources/beta/assistants/files.py b/openai/resources/beta/assistants/files.py new file mode 100644 index 00000000..dc57dfb9 --- /dev/null +++ b/openai/resources/beta/assistants/files.py @@ -0,0 +1,483 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal + +import httpx + +from .... import _legacy_response +from ...._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from ...._utils import ( + maybe_transform, + async_maybe_transform, +) +from ...._compat import cached_property +from ...._resource import SyncAPIResource, AsyncAPIResource +from ...._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper +from ....pagination import SyncCursorPage, AsyncCursorPage +from ...._base_client import ( + AsyncPaginator, + make_request_options, +) +from ....types.beta.assistants import AssistantFile, FileDeleteResponse, file_list_params, file_create_params + +__all__ = ["Files", "AsyncFiles"] + + +class Files(SyncAPIResource): + @cached_property + def with_raw_response(self) -> FilesWithRawResponse: + return FilesWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> FilesWithStreamingResponse: + return FilesWithStreamingResponse(self) + + def create( + self, + assistant_id: str, + *, + file_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AssistantFile: + """ + Create an assistant file by attaching a + [File](https://platform.openai.com/docs/api-reference/files) to an + [assistant](https://platform.openai.com/docs/api-reference/assistants). + + Args: + file_id: A [File](https://platform.openai.com/docs/api-reference/files) ID (with + `purpose="assistants"`) that the assistant should use. Useful for tools like + `retrieval` and `code_interpreter` that can access files. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not assistant_id: + raise ValueError(f"Expected a non-empty value for `assistant_id` but received {assistant_id!r}") + extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})} + return self._post( + f"/assistants/{assistant_id}/files", + body=maybe_transform({"file_id": file_id}, file_create_params.FileCreateParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AssistantFile, + ) + + def retrieve( + self, + file_id: str, + *, + assistant_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AssistantFile: + """ + Retrieves an AssistantFile. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not assistant_id: + raise ValueError(f"Expected a non-empty value for `assistant_id` but received {assistant_id!r}") + if not file_id: + raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") + extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})} + return self._get( + f"/assistants/{assistant_id}/files/{file_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AssistantFile, + ) + + def list( + self, + assistant_id: str, + *, + after: str | NotGiven = NOT_GIVEN, + before: str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + order: Literal["asc", "desc"] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SyncCursorPage[AssistantFile]: + """ + Returns a list of assistant files. + + Args: + after: A cursor for use in pagination. `after` is an object ID that defines your place + in the list. For instance, if you make a list request and receive 100 objects, + ending with obj_foo, your subsequent call can include after=obj_foo in order to + fetch the next page of the list. + + before: A cursor for use in pagination. `before` is an object ID that defines your place + in the list. For instance, if you make a list request and receive 100 objects, + ending with obj_foo, your subsequent call can include before=obj_foo in order to + fetch the previous page of the list. + + limit: A limit on the number of objects to be returned. Limit can range between 1 and + 100, and the default is 20. + + order: Sort order by the `created_at` timestamp of the objects. `asc` for ascending + order and `desc` for descending order. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not assistant_id: + raise ValueError(f"Expected a non-empty value for `assistant_id` but received {assistant_id!r}") + extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})} + return self._get_api_list( + f"/assistants/{assistant_id}/files", + page=SyncCursorPage[AssistantFile], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "after": after, + "before": before, + "limit": limit, + "order": order, + }, + file_list_params.FileListParams, + ), + ), + model=AssistantFile, + ) + + def delete( + self, + file_id: str, + *, + assistant_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> FileDeleteResponse: + """ + Delete an assistant file. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not assistant_id: + raise ValueError(f"Expected a non-empty value for `assistant_id` but received {assistant_id!r}") + if not file_id: + raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") + extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})} + return self._delete( + f"/assistants/{assistant_id}/files/{file_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=FileDeleteResponse, + ) + + +class AsyncFiles(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncFilesWithRawResponse: + return AsyncFilesWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncFilesWithStreamingResponse: + return AsyncFilesWithStreamingResponse(self) + + async def create( + self, + assistant_id: str, + *, + file_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AssistantFile: + """ + Create an assistant file by attaching a + [File](https://platform.openai.com/docs/api-reference/files) to an + [assistant](https://platform.openai.com/docs/api-reference/assistants). + + Args: + file_id: A [File](https://platform.openai.com/docs/api-reference/files) ID (with + `purpose="assistants"`) that the assistant should use. Useful for tools like + `retrieval` and `code_interpreter` that can access files. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not assistant_id: + raise ValueError(f"Expected a non-empty value for `assistant_id` but received {assistant_id!r}") + extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})} + return await self._post( + f"/assistants/{assistant_id}/files", + body=await async_maybe_transform({"file_id": file_id}, file_create_params.FileCreateParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AssistantFile, + ) + + async def retrieve( + self, + file_id: str, + *, + assistant_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AssistantFile: + """ + Retrieves an AssistantFile. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not assistant_id: + raise ValueError(f"Expected a non-empty value for `assistant_id` but received {assistant_id!r}") + if not file_id: + raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") + extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})} + return await self._get( + f"/assistants/{assistant_id}/files/{file_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AssistantFile, + ) + + def list( + self, + assistant_id: str, + *, + after: str | NotGiven = NOT_GIVEN, + before: str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + order: Literal["asc", "desc"] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncPaginator[AssistantFile, AsyncCursorPage[AssistantFile]]: + """ + Returns a list of assistant files. + + Args: + after: A cursor for use in pagination. `after` is an object ID that defines your place + in the list. For instance, if you make a list request and receive 100 objects, + ending with obj_foo, your subsequent call can include after=obj_foo in order to + fetch the next page of the list. + + before: A cursor for use in pagination. `before` is an object ID that defines your place + in the list. For instance, if you make a list request and receive 100 objects, + ending with obj_foo, your subsequent call can include before=obj_foo in order to + fetch the previous page of the list. + + limit: A limit on the number of objects to be returned. Limit can range between 1 and + 100, and the default is 20. + + order: Sort order by the `created_at` timestamp of the objects. `asc` for ascending + order and `desc` for descending order. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not assistant_id: + raise ValueError(f"Expected a non-empty value for `assistant_id` but received {assistant_id!r}") + extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})} + return self._get_api_list( + f"/assistants/{assistant_id}/files", + page=AsyncCursorPage[AssistantFile], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "after": after, + "before": before, + "limit": limit, + "order": order, + }, + file_list_params.FileListParams, + ), + ), + model=AssistantFile, + ) + + async def delete( + self, + file_id: str, + *, + assistant_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> FileDeleteResponse: + """ + Delete an assistant file. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not assistant_id: + raise ValueError(f"Expected a non-empty value for `assistant_id` but received {assistant_id!r}") + if not file_id: + raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") + extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})} + return await self._delete( + f"/assistants/{assistant_id}/files/{file_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=FileDeleteResponse, + ) + + +class FilesWithRawResponse: + def __init__(self, files: Files) -> None: + self._files = files + + self.create = _legacy_response.to_raw_response_wrapper( + files.create, + ) + self.retrieve = _legacy_response.to_raw_response_wrapper( + files.retrieve, + ) + self.list = _legacy_response.to_raw_response_wrapper( + files.list, + ) + self.delete = _legacy_response.to_raw_response_wrapper( + files.delete, + ) + + +class AsyncFilesWithRawResponse: + def __init__(self, files: AsyncFiles) -> None: + self._files = files + + self.create = _legacy_response.async_to_raw_response_wrapper( + files.create, + ) + self.retrieve = _legacy_response.async_to_raw_response_wrapper( + files.retrieve, + ) + self.list = _legacy_response.async_to_raw_response_wrapper( + files.list, + ) + self.delete = _legacy_response.async_to_raw_response_wrapper( + files.delete, + ) + + +class FilesWithStreamingResponse: + def __init__(self, files: Files) -> None: + self._files = files + + self.create = to_streamed_response_wrapper( + files.create, + ) + self.retrieve = to_streamed_response_wrapper( + files.retrieve, + ) + self.list = to_streamed_response_wrapper( + files.list, + ) + self.delete = to_streamed_response_wrapper( + files.delete, + ) + + +class AsyncFilesWithStreamingResponse: + def __init__(self, files: AsyncFiles) -> None: + self._files = files + + self.create = async_to_streamed_response_wrapper( + files.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + files.retrieve, + ) + self.list = async_to_streamed_response_wrapper( + files.list, + ) + self.delete = async_to_streamed_response_wrapper( + files.delete, + ) diff --git a/openai/resources/beta/beta.py b/openai/resources/beta/beta.py new file mode 100644 index 00000000..67baad27 --- /dev/null +++ b/openai/resources/beta/beta.py @@ -0,0 +1,114 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from .threads import ( + Threads, + AsyncThreads, + ThreadsWithRawResponse, + AsyncThreadsWithRawResponse, + ThreadsWithStreamingResponse, + AsyncThreadsWithStreamingResponse, +) +from ..._compat import cached_property +from .assistants import ( + Assistants, + AsyncAssistants, + AssistantsWithRawResponse, + AsyncAssistantsWithRawResponse, + AssistantsWithStreamingResponse, + AsyncAssistantsWithStreamingResponse, +) +from ..._resource import SyncAPIResource, AsyncAPIResource +from .threads.threads import Threads, AsyncThreads +from .assistants.assistants import Assistants, AsyncAssistants + +__all__ = ["Beta", "AsyncBeta"] + + +class Beta(SyncAPIResource): + @cached_property + def assistants(self) -> Assistants: + return Assistants(self._client) + + @cached_property + def threads(self) -> Threads: + return Threads(self._client) + + @cached_property + def with_raw_response(self) -> BetaWithRawResponse: + return BetaWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> BetaWithStreamingResponse: + return BetaWithStreamingResponse(self) + + +class AsyncBeta(AsyncAPIResource): + @cached_property + def assistants(self) -> AsyncAssistants: + return AsyncAssistants(self._client) + + @cached_property + def threads(self) -> AsyncThreads: + return AsyncThreads(self._client) + + @cached_property + def with_raw_response(self) -> AsyncBetaWithRawResponse: + return AsyncBetaWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncBetaWithStreamingResponse: + return AsyncBetaWithStreamingResponse(self) + + +class BetaWithRawResponse: + def __init__(self, beta: Beta) -> None: + self._beta = beta + + @cached_property + def assistants(self) -> AssistantsWithRawResponse: + return AssistantsWithRawResponse(self._beta.assistants) + + @cached_property + def threads(self) -> ThreadsWithRawResponse: + return ThreadsWithRawResponse(self._beta.threads) + + +class AsyncBetaWithRawResponse: + def __init__(self, beta: AsyncBeta) -> None: + self._beta = beta + + @cached_property + def assistants(self) -> AsyncAssistantsWithRawResponse: + return AsyncAssistantsWithRawResponse(self._beta.assistants) + + @cached_property + def threads(self) -> AsyncThreadsWithRawResponse: + return AsyncThreadsWithRawResponse(self._beta.threads) + + +class BetaWithStreamingResponse: + def __init__(self, beta: Beta) -> None: + self._beta = beta + + @cached_property + def assistants(self) -> AssistantsWithStreamingResponse: + return AssistantsWithStreamingResponse(self._beta.assistants) + + @cached_property + def threads(self) -> ThreadsWithStreamingResponse: + return ThreadsWithStreamingResponse(self._beta.threads) + + +class AsyncBetaWithStreamingResponse: + def __init__(self, beta: AsyncBeta) -> None: + self._beta = beta + + @cached_property + def assistants(self) -> AsyncAssistantsWithStreamingResponse: + return AsyncAssistantsWithStreamingResponse(self._beta.assistants) + + @cached_property + def threads(self) -> AsyncThreadsWithStreamingResponse: + return AsyncThreadsWithStreamingResponse(self._beta.threads) diff --git a/openai/resources/beta/threads/__init__.py b/openai/resources/beta/threads/__init__.py new file mode 100644 index 00000000..a66e445b --- /dev/null +++ b/openai/resources/beta/threads/__init__.py @@ -0,0 +1,47 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .runs import ( + Runs, + AsyncRuns, + RunsWithRawResponse, + AsyncRunsWithRawResponse, + RunsWithStreamingResponse, + AsyncRunsWithStreamingResponse, +) +from .threads import ( + Threads, + AsyncThreads, + ThreadsWithRawResponse, + AsyncThreadsWithRawResponse, + ThreadsWithStreamingResponse, + AsyncThreadsWithStreamingResponse, +) +from .messages import ( + Messages, + AsyncMessages, + MessagesWithRawResponse, + AsyncMessagesWithRawResponse, + MessagesWithStreamingResponse, + AsyncMessagesWithStreamingResponse, +) + +__all__ = [ + "Runs", + "AsyncRuns", + "RunsWithRawResponse", + "AsyncRunsWithRawResponse", + "RunsWithStreamingResponse", + "AsyncRunsWithStreamingResponse", + "Messages", + "AsyncMessages", + "MessagesWithRawResponse", + "AsyncMessagesWithRawResponse", + "MessagesWithStreamingResponse", + "AsyncMessagesWithStreamingResponse", + "Threads", + "AsyncThreads", + "ThreadsWithRawResponse", + "AsyncThreadsWithRawResponse", + "ThreadsWithStreamingResponse", + "AsyncThreadsWithStreamingResponse", +] diff --git a/openai/resources/beta/threads/messages/__init__.py b/openai/resources/beta/threads/messages/__init__.py new file mode 100644 index 00000000..a3286e6a --- /dev/null +++ b/openai/resources/beta/threads/messages/__init__.py @@ -0,0 +1,33 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .files import ( + Files, + AsyncFiles, + FilesWithRawResponse, + AsyncFilesWithRawResponse, + FilesWithStreamingResponse, + AsyncFilesWithStreamingResponse, +) +from .messages import ( + Messages, + AsyncMessages, + MessagesWithRawResponse, + AsyncMessagesWithRawResponse, + MessagesWithStreamingResponse, + AsyncMessagesWithStreamingResponse, +) + +__all__ = [ + "Files", + "AsyncFiles", + "FilesWithRawResponse", + "AsyncFilesWithRawResponse", + "FilesWithStreamingResponse", + "AsyncFilesWithStreamingResponse", + "Messages", + "AsyncMessages", + "MessagesWithRawResponse", + "AsyncMessagesWithRawResponse", + "MessagesWithStreamingResponse", + "AsyncMessagesWithStreamingResponse", +] diff --git a/openai/resources/beta/threads/messages/files.py b/openai/resources/beta/threads/messages/files.py new file mode 100644 index 00000000..349f9972 --- /dev/null +++ b/openai/resources/beta/threads/messages/files.py @@ -0,0 +1,312 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal + +import httpx + +from ..... import _legacy_response +from ....._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from ....._utils import maybe_transform +from ....._compat import cached_property +from ....._resource import SyncAPIResource, AsyncAPIResource +from ....._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper +from .....pagination import SyncCursorPage, AsyncCursorPage +from ....._base_client import ( + AsyncPaginator, + make_request_options, +) +from .....types.beta.threads.messages import MessageFile, file_list_params + +__all__ = ["Files", "AsyncFiles"] + + +class Files(SyncAPIResource): + @cached_property + def with_raw_response(self) -> FilesWithRawResponse: + return FilesWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> FilesWithStreamingResponse: + return FilesWithStreamingResponse(self) + + def retrieve( + self, + file_id: str, + *, + thread_id: str, + message_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> MessageFile: + """ + Retrieves a message file. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + if not message_id: + raise ValueError(f"Expected a non-empty value for `message_id` but received {message_id!r}") + if not file_id: + raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") + extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})} + return self._get( + f"/threads/{thread_id}/messages/{message_id}/files/{file_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=MessageFile, + ) + + def list( + self, + message_id: str, + *, + thread_id: str, + after: str | NotGiven = NOT_GIVEN, + before: str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + order: Literal["asc", "desc"] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SyncCursorPage[MessageFile]: + """Returns a list of message files. + + Args: + after: A cursor for use in pagination. + + `after` is an object ID that defines your place + in the list. For instance, if you make a list request and receive 100 objects, + ending with obj_foo, your subsequent call can include after=obj_foo in order to + fetch the next page of the list. + + before: A cursor for use in pagination. `before` is an object ID that defines your place + in the list. For instance, if you make a list request and receive 100 objects, + ending with obj_foo, your subsequent call can include before=obj_foo in order to + fetch the previous page of the list. + + limit: A limit on the number of objects to be returned. Limit can range between 1 and + 100, and the default is 20. + + order: Sort order by the `created_at` timestamp of the objects. `asc` for ascending + order and `desc` for descending order. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + if not message_id: + raise ValueError(f"Expected a non-empty value for `message_id` but received {message_id!r}") + extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})} + return self._get_api_list( + f"/threads/{thread_id}/messages/{message_id}/files", + page=SyncCursorPage[MessageFile], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "after": after, + "before": before, + "limit": limit, + "order": order, + }, + file_list_params.FileListParams, + ), + ), + model=MessageFile, + ) + + +class AsyncFiles(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncFilesWithRawResponse: + return AsyncFilesWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncFilesWithStreamingResponse: + return AsyncFilesWithStreamingResponse(self) + + async def retrieve( + self, + file_id: str, + *, + thread_id: str, + message_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> MessageFile: + """ + Retrieves a message file. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + if not message_id: + raise ValueError(f"Expected a non-empty value for `message_id` but received {message_id!r}") + if not file_id: + raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") + extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})} + return await self._get( + f"/threads/{thread_id}/messages/{message_id}/files/{file_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=MessageFile, + ) + + def list( + self, + message_id: str, + *, + thread_id: str, + after: str | NotGiven = NOT_GIVEN, + before: str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + order: Literal["asc", "desc"] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncPaginator[MessageFile, AsyncCursorPage[MessageFile]]: + """Returns a list of message files. + + Args: + after: A cursor for use in pagination. + + `after` is an object ID that defines your place + in the list. For instance, if you make a list request and receive 100 objects, + ending with obj_foo, your subsequent call can include after=obj_foo in order to + fetch the next page of the list. + + before: A cursor for use in pagination. `before` is an object ID that defines your place + in the list. For instance, if you make a list request and receive 100 objects, + ending with obj_foo, your subsequent call can include before=obj_foo in order to + fetch the previous page of the list. + + limit: A limit on the number of objects to be returned. Limit can range between 1 and + 100, and the default is 20. + + order: Sort order by the `created_at` timestamp of the objects. `asc` for ascending + order and `desc` for descending order. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + if not message_id: + raise ValueError(f"Expected a non-empty value for `message_id` but received {message_id!r}") + extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})} + return self._get_api_list( + f"/threads/{thread_id}/messages/{message_id}/files", + page=AsyncCursorPage[MessageFile], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "after": after, + "before": before, + "limit": limit, + "order": order, + }, + file_list_params.FileListParams, + ), + ), + model=MessageFile, + ) + + +class FilesWithRawResponse: + def __init__(self, files: Files) -> None: + self._files = files + + self.retrieve = _legacy_response.to_raw_response_wrapper( + files.retrieve, + ) + self.list = _legacy_response.to_raw_response_wrapper( + files.list, + ) + + +class AsyncFilesWithRawResponse: + def __init__(self, files: AsyncFiles) -> None: + self._files = files + + self.retrieve = _legacy_response.async_to_raw_response_wrapper( + files.retrieve, + ) + self.list = _legacy_response.async_to_raw_response_wrapper( + files.list, + ) + + +class FilesWithStreamingResponse: + def __init__(self, files: Files) -> None: + self._files = files + + self.retrieve = to_streamed_response_wrapper( + files.retrieve, + ) + self.list = to_streamed_response_wrapper( + files.list, + ) + + +class AsyncFilesWithStreamingResponse: + def __init__(self, files: AsyncFiles) -> None: + self._files = files + + self.retrieve = async_to_streamed_response_wrapper( + files.retrieve, + ) + self.list = async_to_streamed_response_wrapper( + files.list, + ) diff --git a/openai/resources/beta/threads/messages/messages.py b/openai/resources/beta/threads/messages/messages.py new file mode 100644 index 00000000..bbce3e99 --- /dev/null +++ b/openai/resources/beta/threads/messages/messages.py @@ -0,0 +1,588 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List, Optional +from typing_extensions import Literal + +import httpx + +from ..... import _legacy_response +from .files import ( + Files, + AsyncFiles, + FilesWithRawResponse, + AsyncFilesWithRawResponse, + FilesWithStreamingResponse, + AsyncFilesWithStreamingResponse, +) +from ....._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from ....._utils import ( + maybe_transform, + async_maybe_transform, +) +from ....._compat import cached_property +from ....._resource import SyncAPIResource, AsyncAPIResource +from ....._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper +from .....pagination import SyncCursorPage, AsyncCursorPage +from ....._base_client import ( + AsyncPaginator, + make_request_options, +) +from .....types.beta.threads import Message, message_list_params, message_create_params, message_update_params + +__all__ = ["Messages", "AsyncMessages"] + + +class Messages(SyncAPIResource): + @cached_property + def files(self) -> Files: + return Files(self._client) + + @cached_property + def with_raw_response(self) -> MessagesWithRawResponse: + return MessagesWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> MessagesWithStreamingResponse: + return MessagesWithStreamingResponse(self) + + def create( + self, + thread_id: str, + *, + content: str, + role: Literal["user", "assistant"], + file_ids: List[str] | NotGiven = NOT_GIVEN, + metadata: Optional[object] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Message: + """ + Create a message. + + Args: + content: The content of the message. + + role: + The role of the entity that is creating the message. Allowed values include: + + - `user`: Indicates the message is sent by an actual user and should be used in + most cases to represent user-generated messages. + - `assistant`: Indicates the message is generated by the assistant. Use this + value to insert messages from the assistant into the conversation. + + file_ids: A list of [File](https://platform.openai.com/docs/api-reference/files) IDs that + the message should use. There can be a maximum of 10 files attached to a + message. Useful for tools like `retrieval` and `code_interpreter` that can + access and use files. + + metadata: Set of 16 key-value pairs that can be attached to an object. This can be useful + for storing additional information about the object in a structured format. Keys + can be a maximum of 64 characters long and values can be a maxium of 512 + characters long. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})} + return self._post( + f"/threads/{thread_id}/messages", + body=maybe_transform( + { + "content": content, + "role": role, + "file_ids": file_ids, + "metadata": metadata, + }, + message_create_params.MessageCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Message, + ) + + def retrieve( + self, + message_id: str, + *, + thread_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Message: + """ + Retrieve a message. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + if not message_id: + raise ValueError(f"Expected a non-empty value for `message_id` but received {message_id!r}") + extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})} + return self._get( + f"/threads/{thread_id}/messages/{message_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Message, + ) + + def update( + self, + message_id: str, + *, + thread_id: str, + metadata: Optional[object] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Message: + """ + Modifies a message. + + Args: + metadata: Set of 16 key-value pairs that can be attached to an object. This can be useful + for storing additional information about the object in a structured format. Keys + can be a maximum of 64 characters long and values can be a maxium of 512 + characters long. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + if not message_id: + raise ValueError(f"Expected a non-empty value for `message_id` but received {message_id!r}") + extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})} + return self._post( + f"/threads/{thread_id}/messages/{message_id}", + body=maybe_transform({"metadata": metadata}, message_update_params.MessageUpdateParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Message, + ) + + def list( + self, + thread_id: str, + *, + after: str | NotGiven = NOT_GIVEN, + before: str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + order: Literal["asc", "desc"] | NotGiven = NOT_GIVEN, + run_id: str | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SyncCursorPage[Message]: + """ + Returns a list of messages for a given thread. + + Args: + after: A cursor for use in pagination. `after` is an object ID that defines your place + in the list. For instance, if you make a list request and receive 100 objects, + ending with obj_foo, your subsequent call can include after=obj_foo in order to + fetch the next page of the list. + + before: A cursor for use in pagination. `before` is an object ID that defines your place + in the list. For instance, if you make a list request and receive 100 objects, + ending with obj_foo, your subsequent call can include before=obj_foo in order to + fetch the previous page of the list. + + limit: A limit on the number of objects to be returned. Limit can range between 1 and + 100, and the default is 20. + + order: Sort order by the `created_at` timestamp of the objects. `asc` for ascending + order and `desc` for descending order. + + run_id: Filter messages by the run ID that generated them. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})} + return self._get_api_list( + f"/threads/{thread_id}/messages", + page=SyncCursorPage[Message], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "after": after, + "before": before, + "limit": limit, + "order": order, + "run_id": run_id, + }, + message_list_params.MessageListParams, + ), + ), + model=Message, + ) + + +class AsyncMessages(AsyncAPIResource): + @cached_property + def files(self) -> AsyncFiles: + return AsyncFiles(self._client) + + @cached_property + def with_raw_response(self) -> AsyncMessagesWithRawResponse: + return AsyncMessagesWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncMessagesWithStreamingResponse: + return AsyncMessagesWithStreamingResponse(self) + + async def create( + self, + thread_id: str, + *, + content: str, + role: Literal["user", "assistant"], + file_ids: List[str] | NotGiven = NOT_GIVEN, + metadata: Optional[object] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Message: + """ + Create a message. + + Args: + content: The content of the message. + + role: + The role of the entity that is creating the message. Allowed values include: + + - `user`: Indicates the message is sent by an actual user and should be used in + most cases to represent user-generated messages. + - `assistant`: Indicates the message is generated by the assistant. Use this + value to insert messages from the assistant into the conversation. + + file_ids: A list of [File](https://platform.openai.com/docs/api-reference/files) IDs that + the message should use. There can be a maximum of 10 files attached to a + message. Useful for tools like `retrieval` and `code_interpreter` that can + access and use files. + + metadata: Set of 16 key-value pairs that can be attached to an object. This can be useful + for storing additional information about the object in a structured format. Keys + can be a maximum of 64 characters long and values can be a maxium of 512 + characters long. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})} + return await self._post( + f"/threads/{thread_id}/messages", + body=await async_maybe_transform( + { + "content": content, + "role": role, + "file_ids": file_ids, + "metadata": metadata, + }, + message_create_params.MessageCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Message, + ) + + async def retrieve( + self, + message_id: str, + *, + thread_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Message: + """ + Retrieve a message. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + if not message_id: + raise ValueError(f"Expected a non-empty value for `message_id` but received {message_id!r}") + extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})} + return await self._get( + f"/threads/{thread_id}/messages/{message_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Message, + ) + + async def update( + self, + message_id: str, + *, + thread_id: str, + metadata: Optional[object] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Message: + """ + Modifies a message. + + Args: + metadata: Set of 16 key-value pairs that can be attached to an object. This can be useful + for storing additional information about the object in a structured format. Keys + can be a maximum of 64 characters long and values can be a maxium of 512 + characters long. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + if not message_id: + raise ValueError(f"Expected a non-empty value for `message_id` but received {message_id!r}") + extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})} + return await self._post( + f"/threads/{thread_id}/messages/{message_id}", + body=await async_maybe_transform({"metadata": metadata}, message_update_params.MessageUpdateParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Message, + ) + + def list( + self, + thread_id: str, + *, + after: str | NotGiven = NOT_GIVEN, + before: str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + order: Literal["asc", "desc"] | NotGiven = NOT_GIVEN, + run_id: str | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncPaginator[Message, AsyncCursorPage[Message]]: + """ + Returns a list of messages for a given thread. + + Args: + after: A cursor for use in pagination. `after` is an object ID that defines your place + in the list. For instance, if you make a list request and receive 100 objects, + ending with obj_foo, your subsequent call can include after=obj_foo in order to + fetch the next page of the list. + + before: A cursor for use in pagination. `before` is an object ID that defines your place + in the list. For instance, if you make a list request and receive 100 objects, + ending with obj_foo, your subsequent call can include before=obj_foo in order to + fetch the previous page of the list. + + limit: A limit on the number of objects to be returned. Limit can range between 1 and + 100, and the default is 20. + + order: Sort order by the `created_at` timestamp of the objects. `asc` for ascending + order and `desc` for descending order. + + run_id: Filter messages by the run ID that generated them. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})} + return self._get_api_list( + f"/threads/{thread_id}/messages", + page=AsyncCursorPage[Message], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "after": after, + "before": before, + "limit": limit, + "order": order, + "run_id": run_id, + }, + message_list_params.MessageListParams, + ), + ), + model=Message, + ) + + +class MessagesWithRawResponse: + def __init__(self, messages: Messages) -> None: + self._messages = messages + + self.create = _legacy_response.to_raw_response_wrapper( + messages.create, + ) + self.retrieve = _legacy_response.to_raw_response_wrapper( + messages.retrieve, + ) + self.update = _legacy_response.to_raw_response_wrapper( + messages.update, + ) + self.list = _legacy_response.to_raw_response_wrapper( + messages.list, + ) + + @cached_property + def files(self) -> FilesWithRawResponse: + return FilesWithRawResponse(self._messages.files) + + +class AsyncMessagesWithRawResponse: + def __init__(self, messages: AsyncMessages) -> None: + self._messages = messages + + self.create = _legacy_response.async_to_raw_response_wrapper( + messages.create, + ) + self.retrieve = _legacy_response.async_to_raw_response_wrapper( + messages.retrieve, + ) + self.update = _legacy_response.async_to_raw_response_wrapper( + messages.update, + ) + self.list = _legacy_response.async_to_raw_response_wrapper( + messages.list, + ) + + @cached_property + def files(self) -> AsyncFilesWithRawResponse: + return AsyncFilesWithRawResponse(self._messages.files) + + +class MessagesWithStreamingResponse: + def __init__(self, messages: Messages) -> None: + self._messages = messages + + self.create = to_streamed_response_wrapper( + messages.create, + ) + self.retrieve = to_streamed_response_wrapper( + messages.retrieve, + ) + self.update = to_streamed_response_wrapper( + messages.update, + ) + self.list = to_streamed_response_wrapper( + messages.list, + ) + + @cached_property + def files(self) -> FilesWithStreamingResponse: + return FilesWithStreamingResponse(self._messages.files) + + +class AsyncMessagesWithStreamingResponse: + def __init__(self, messages: AsyncMessages) -> None: + self._messages = messages + + self.create = async_to_streamed_response_wrapper( + messages.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + messages.retrieve, + ) + self.update = async_to_streamed_response_wrapper( + messages.update, + ) + self.list = async_to_streamed_response_wrapper( + messages.list, + ) + + @cached_property + def files(self) -> AsyncFilesWithStreamingResponse: + return AsyncFilesWithStreamingResponse(self._messages.files) diff --git a/openai/resources/beta/threads/runs/__init__.py b/openai/resources/beta/threads/runs/__init__.py new file mode 100644 index 00000000..50aa9fae --- /dev/null +++ b/openai/resources/beta/threads/runs/__init__.py @@ -0,0 +1,33 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .runs import ( + Runs, + AsyncRuns, + RunsWithRawResponse, + AsyncRunsWithRawResponse, + RunsWithStreamingResponse, + AsyncRunsWithStreamingResponse, +) +from .steps import ( + Steps, + AsyncSteps, + StepsWithRawResponse, + AsyncStepsWithRawResponse, + StepsWithStreamingResponse, + AsyncStepsWithStreamingResponse, +) + +__all__ = [ + "Steps", + "AsyncSteps", + "StepsWithRawResponse", + "AsyncStepsWithRawResponse", + "StepsWithStreamingResponse", + "AsyncStepsWithStreamingResponse", + "Runs", + "AsyncRuns", + "RunsWithRawResponse", + "AsyncRunsWithRawResponse", + "RunsWithStreamingResponse", + "AsyncRunsWithStreamingResponse", +] diff --git a/openai/resources/beta/threads/runs/runs.py b/openai/resources/beta/threads/runs/runs.py new file mode 100644 index 00000000..4529c650 --- /dev/null +++ b/openai/resources/beta/threads/runs/runs.py @@ -0,0 +1,2223 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import time +import typing_extensions +from typing import Iterable, Optional, overload +from functools import partial +from typing_extensions import Literal + +import httpx + +from ..... import _legacy_response +from .steps import ( + Steps, + AsyncSteps, + StepsWithRawResponse, + AsyncStepsWithRawResponse, + StepsWithStreamingResponse, + AsyncStepsWithStreamingResponse, +) +from ....._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from ....._utils import ( + is_given, + required_args, + maybe_transform, + async_maybe_transform, +) +from ....._compat import cached_property +from ....._resource import SyncAPIResource, AsyncAPIResource +from ....._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper +from ....._streaming import Stream, AsyncStream +from .....pagination import SyncCursorPage, AsyncCursorPage +from .....types.beta import AssistantToolParam, AssistantStreamEvent +from ....._base_client import ( + AsyncPaginator, + make_request_options, +) +from .....lib.streaming import ( + AssistantEventHandler, + AssistantEventHandlerT, + AssistantStreamManager, + AsyncAssistantEventHandler, + AsyncAssistantEventHandlerT, + AsyncAssistantStreamManager, +) +from .....types.beta.threads import ( + Run, + run_list_params, + run_create_params, + run_update_params, + run_submit_tool_outputs_params, +) + +__all__ = ["Runs", "AsyncRuns"] + + +class Runs(SyncAPIResource): + @cached_property + def steps(self) -> Steps: + return Steps(self._client) + + @cached_property + def with_raw_response(self) -> RunsWithRawResponse: + return RunsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> RunsWithStreamingResponse: + return RunsWithStreamingResponse(self) + + @overload + def create( + self, + thread_id: str, + *, + assistant_id: str, + additional_instructions: Optional[str] | NotGiven = NOT_GIVEN, + instructions: Optional[str] | NotGiven = NOT_GIVEN, + metadata: Optional[object] | NotGiven = NOT_GIVEN, + model: Optional[str] | NotGiven = NOT_GIVEN, + stream: Optional[Literal[False]] | NotGiven = NOT_GIVEN, + temperature: Optional[float] | NotGiven = NOT_GIVEN, + tools: Optional[Iterable[AssistantToolParam]] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Run: + """ + Create a run. + + Args: + assistant_id: The ID of the + [assistant](https://platform.openai.com/docs/api-reference/assistants) to use to + execute this run. + + additional_instructions: Appends additional instructions at the end of the instructions for the run. This + is useful for modifying the behavior on a per-run basis without overriding other + instructions. + + instructions: Overrides the + [instructions](https://platform.openai.com/docs/api-reference/assistants/createAssistant) + of the assistant. This is useful for modifying the behavior on a per-run basis. + + metadata: Set of 16 key-value pairs that can be attached to an object. This can be useful + for storing additional information about the object in a structured format. Keys + can be a maximum of 64 characters long and values can be a maxium of 512 + characters long. + + model: The ID of the [Model](https://platform.openai.com/docs/api-reference/models) to + be used to execute this run. If a value is provided here, it will override the + model associated with the assistant. If not, the model associated with the + assistant will be used. + + stream: If `true`, returns a stream of events that happen during the Run as server-sent + events, terminating when the Run enters a terminal state with a `data: [DONE]` + message. + + temperature: What sampling temperature to use, between 0 and 2. Higher values like 0.8 will + make the output more random, while lower values like 0.2 will make it more + focused and deterministic. + + tools: Override the tools the assistant can use for this run. This is useful for + modifying the behavior on a per-run basis. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + ... + + @overload + def create( + self, + thread_id: str, + *, + assistant_id: str, + stream: Literal[True], + additional_instructions: Optional[str] | NotGiven = NOT_GIVEN, + instructions: Optional[str] | NotGiven = NOT_GIVEN, + metadata: Optional[object] | NotGiven = NOT_GIVEN, + model: Optional[str] | NotGiven = NOT_GIVEN, + temperature: Optional[float] | NotGiven = NOT_GIVEN, + tools: Optional[Iterable[AssistantToolParam]] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Stream[AssistantStreamEvent]: + """ + Create a run. + + Args: + assistant_id: The ID of the + [assistant](https://platform.openai.com/docs/api-reference/assistants) to use to + execute this run. + + stream: If `true`, returns a stream of events that happen during the Run as server-sent + events, terminating when the Run enters a terminal state with a `data: [DONE]` + message. + + additional_instructions: Appends additional instructions at the end of the instructions for the run. This + is useful for modifying the behavior on a per-run basis without overriding other + instructions. + + instructions: Overrides the + [instructions](https://platform.openai.com/docs/api-reference/assistants/createAssistant) + of the assistant. This is useful for modifying the behavior on a per-run basis. + + metadata: Set of 16 key-value pairs that can be attached to an object. This can be useful + for storing additional information about the object in a structured format. Keys + can be a maximum of 64 characters long and values can be a maxium of 512 + characters long. + + model: The ID of the [Model](https://platform.openai.com/docs/api-reference/models) to + be used to execute this run. If a value is provided here, it will override the + model associated with the assistant. If not, the model associated with the + assistant will be used. + + temperature: What sampling temperature to use, between 0 and 2. Higher values like 0.8 will + make the output more random, while lower values like 0.2 will make it more + focused and deterministic. + + tools: Override the tools the assistant can use for this run. This is useful for + modifying the behavior on a per-run basis. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + ... + + @overload + def create( + self, + thread_id: str, + *, + assistant_id: str, + stream: bool, + additional_instructions: Optional[str] | NotGiven = NOT_GIVEN, + instructions: Optional[str] | NotGiven = NOT_GIVEN, + metadata: Optional[object] | NotGiven = NOT_GIVEN, + model: Optional[str] | NotGiven = NOT_GIVEN, + temperature: Optional[float] | NotGiven = NOT_GIVEN, + tools: Optional[Iterable[AssistantToolParam]] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Run | Stream[AssistantStreamEvent]: + """ + Create a run. + + Args: + assistant_id: The ID of the + [assistant](https://platform.openai.com/docs/api-reference/assistants) to use to + execute this run. + + stream: If `true`, returns a stream of events that happen during the Run as server-sent + events, terminating when the Run enters a terminal state with a `data: [DONE]` + message. + + additional_instructions: Appends additional instructions at the end of the instructions for the run. This + is useful for modifying the behavior on a per-run basis without overriding other + instructions. + + instructions: Overrides the + [instructions](https://platform.openai.com/docs/api-reference/assistants/createAssistant) + of the assistant. This is useful for modifying the behavior on a per-run basis. + + metadata: Set of 16 key-value pairs that can be attached to an object. This can be useful + for storing additional information about the object in a structured format. Keys + can be a maximum of 64 characters long and values can be a maxium of 512 + characters long. + + model: The ID of the [Model](https://platform.openai.com/docs/api-reference/models) to + be used to execute this run. If a value is provided here, it will override the + model associated with the assistant. If not, the model associated with the + assistant will be used. + + temperature: What sampling temperature to use, between 0 and 2. Higher values like 0.8 will + make the output more random, while lower values like 0.2 will make it more + focused and deterministic. + + tools: Override the tools the assistant can use for this run. This is useful for + modifying the behavior on a per-run basis. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + ... + + @required_args(["assistant_id"], ["assistant_id", "stream"]) + def create( + self, + thread_id: str, + *, + assistant_id: str, + additional_instructions: Optional[str] | NotGiven = NOT_GIVEN, + instructions: Optional[str] | NotGiven = NOT_GIVEN, + metadata: Optional[object] | NotGiven = NOT_GIVEN, + model: Optional[str] | NotGiven = NOT_GIVEN, + stream: Optional[Literal[False]] | Literal[True] | NotGiven = NOT_GIVEN, + temperature: Optional[float] | NotGiven = NOT_GIVEN, + tools: Optional[Iterable[AssistantToolParam]] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Run | Stream[AssistantStreamEvent]: + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})} + return self._post( + f"/threads/{thread_id}/runs", + body=maybe_transform( + { + "assistant_id": assistant_id, + "additional_instructions": additional_instructions, + "instructions": instructions, + "metadata": metadata, + "model": model, + "stream": stream, + "temperature": temperature, + "tools": tools, + }, + run_create_params.RunCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Run, + stream=stream or False, + stream_cls=Stream[AssistantStreamEvent], + ) + + def retrieve( + self, + run_id: str, + *, + thread_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Run: + """ + Retrieves a run. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + if not run_id: + raise ValueError(f"Expected a non-empty value for `run_id` but received {run_id!r}") + extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})} + return self._get( + f"/threads/{thread_id}/runs/{run_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Run, + ) + + def update( + self, + run_id: str, + *, + thread_id: str, + metadata: Optional[object] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Run: + """ + Modifies a run. + + Args: + metadata: Set of 16 key-value pairs that can be attached to an object. This can be useful + for storing additional information about the object in a structured format. Keys + can be a maximum of 64 characters long and values can be a maxium of 512 + characters long. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + if not run_id: + raise ValueError(f"Expected a non-empty value for `run_id` but received {run_id!r}") + extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})} + return self._post( + f"/threads/{thread_id}/runs/{run_id}", + body=maybe_transform({"metadata": metadata}, run_update_params.RunUpdateParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Run, + ) + + def list( + self, + thread_id: str, + *, + after: str | NotGiven = NOT_GIVEN, + before: str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + order: Literal["asc", "desc"] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SyncCursorPage[Run]: + """ + Returns a list of runs belonging to a thread. + + Args: + after: A cursor for use in pagination. `after` is an object ID that defines your place + in the list. For instance, if you make a list request and receive 100 objects, + ending with obj_foo, your subsequent call can include after=obj_foo in order to + fetch the next page of the list. + + before: A cursor for use in pagination. `before` is an object ID that defines your place + in the list. For instance, if you make a list request and receive 100 objects, + ending with obj_foo, your subsequent call can include before=obj_foo in order to + fetch the previous page of the list. + + limit: A limit on the number of objects to be returned. Limit can range between 1 and + 100, and the default is 20. + + order: Sort order by the `created_at` timestamp of the objects. `asc` for ascending + order and `desc` for descending order. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})} + return self._get_api_list( + f"/threads/{thread_id}/runs", + page=SyncCursorPage[Run], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "after": after, + "before": before, + "limit": limit, + "order": order, + }, + run_list_params.RunListParams, + ), + ), + model=Run, + ) + + def cancel( + self, + run_id: str, + *, + thread_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Run: + """ + Cancels a run that is `in_progress`. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + if not run_id: + raise ValueError(f"Expected a non-empty value for `run_id` but received {run_id!r}") + extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})} + return self._post( + f"/threads/{thread_id}/runs/{run_id}/cancel", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Run, + ) + + def create_and_poll( + self, + *, + assistant_id: str, + additional_instructions: Optional[str] | NotGiven = NOT_GIVEN, + instructions: Optional[str] | NotGiven = NOT_GIVEN, + metadata: Optional[object] | NotGiven = NOT_GIVEN, + model: Optional[str] | NotGiven = NOT_GIVEN, + temperature: Optional[float] | NotGiven = NOT_GIVEN, + tools: Optional[Iterable[AssistantToolParam]] | NotGiven = NOT_GIVEN, + poll_interval_ms: int | NotGiven = NOT_GIVEN, + thread_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Run: + """ + A helper to create a run an poll for a terminal state. More information on Run + lifecycles can be found here: + https://platform.openai.com/docs/assistants/how-it-works/runs-and-run-steps + """ + run = self.create( + thread_id=thread_id, + assistant_id=assistant_id, + additional_instructions=additional_instructions, + instructions=instructions, + metadata=metadata, + model=model, + temperature=temperature, + # We assume we are not streaming when polling + stream=False, + tools=tools, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ) + return self.poll( + run.id, + thread_id=thread_id, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + poll_interval_ms=poll_interval_ms, + timeout=timeout, + ) + + @overload + @typing_extensions.deprecated("use `stream` instead") + def create_and_stream( + self, + *, + assistant_id: str, + additional_instructions: Optional[str] | NotGiven = NOT_GIVEN, + instructions: Optional[str] | NotGiven = NOT_GIVEN, + metadata: Optional[object] | NotGiven = NOT_GIVEN, + model: Optional[str] | NotGiven = NOT_GIVEN, + temperature: Optional[float] | NotGiven = NOT_GIVEN, + tools: Optional[Iterable[AssistantToolParam]] | NotGiven = NOT_GIVEN, + thread_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AssistantStreamManager[AssistantEventHandler]: + """Create a Run stream""" + ... + + @overload + @typing_extensions.deprecated("use `stream` instead") + def create_and_stream( + self, + *, + assistant_id: str, + additional_instructions: Optional[str] | NotGiven = NOT_GIVEN, + instructions: Optional[str] | NotGiven = NOT_GIVEN, + metadata: Optional[object] | NotGiven = NOT_GIVEN, + model: Optional[str] | NotGiven = NOT_GIVEN, + temperature: Optional[float] | NotGiven = NOT_GIVEN, + tools: Optional[Iterable[AssistantToolParam]] | NotGiven = NOT_GIVEN, + thread_id: str, + event_handler: AssistantEventHandlerT, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AssistantStreamManager[AssistantEventHandlerT]: + """Create a Run stream""" + ... + + @typing_extensions.deprecated("use `stream` instead") + def create_and_stream( + self, + *, + assistant_id: str, + additional_instructions: Optional[str] | NotGiven = NOT_GIVEN, + instructions: Optional[str] | NotGiven = NOT_GIVEN, + metadata: Optional[object] | NotGiven = NOT_GIVEN, + model: Optional[str] | NotGiven = NOT_GIVEN, + temperature: Optional[float] | NotGiven = NOT_GIVEN, + tools: Optional[Iterable[AssistantToolParam]] | NotGiven = NOT_GIVEN, + thread_id: str, + event_handler: AssistantEventHandlerT | None = None, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AssistantStreamManager[AssistantEventHandler] | AssistantStreamManager[AssistantEventHandlerT]: + """Create a Run stream""" + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + + extra_headers = { + "OpenAI-Beta": "assistants=v1", + "X-Stainless-Stream-Helper": "threads.runs.create_and_stream", + "X-Stainless-Custom-Event-Handler": "true" if event_handler else "false", + **(extra_headers or {}), + } + make_request = partial( + self._post, + f"/threads/{thread_id}/runs", + body=maybe_transform( + { + "assistant_id": assistant_id, + "additional_instructions": additional_instructions, + "instructions": instructions, + "metadata": metadata, + "model": model, + "temperature": temperature, + "stream": True, + "tools": tools, + }, + run_create_params.RunCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Run, + stream=True, + stream_cls=Stream[AssistantStreamEvent], + ) + return AssistantStreamManager(make_request, event_handler=event_handler or AssistantEventHandler()) + + def poll( + self, + run_id: str, + thread_id: str, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + poll_interval_ms: int | NotGiven = NOT_GIVEN, + ) -> Run: + """ + A helper to poll a run status until it reaches a terminal state. More + information on Run lifecycles can be found here: + https://platform.openai.com/docs/assistants/how-it-works/runs-and-run-steps + """ + extra_headers = {"X-Stainless-Poll-Helper": "true", **(extra_headers or {})} + + if is_given(poll_interval_ms): + extra_headers["X-Stainless-Custom-Poll-Interval"] = str(poll_interval_ms) + + terminal_states = {"requires_action", "cancelled", "completed", "failed", "expired"} + while True: + response = self.with_raw_response.retrieve( + thread_id=thread_id, + run_id=run_id, + extra_headers=extra_headers, + extra_body=extra_body, + extra_query=extra_query, + timeout=timeout, + ) + + run = response.parse() + # Return if we reached a terminal state + if run.status in terminal_states: + return run + + if not is_given(poll_interval_ms): + from_header = response.headers.get("openai-poll-after-ms") + if from_header is not None: + poll_interval_ms = int(from_header) + else: + poll_interval_ms = 1000 + + time.sleep(poll_interval_ms / 1000) + + @overload + def stream( + self, + *, + assistant_id: str, + additional_instructions: Optional[str] | NotGiven = NOT_GIVEN, + instructions: Optional[str] | NotGiven = NOT_GIVEN, + metadata: Optional[object] | NotGiven = NOT_GIVEN, + model: Optional[str] | NotGiven = NOT_GIVEN, + temperature: Optional[float] | NotGiven = NOT_GIVEN, + tools: Optional[Iterable[AssistantToolParam]] | NotGiven = NOT_GIVEN, + thread_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AssistantStreamManager[AssistantEventHandler]: + """Create a Run stream""" + ... + + @overload + def stream( + self, + *, + assistant_id: str, + additional_instructions: Optional[str] | NotGiven = NOT_GIVEN, + instructions: Optional[str] | NotGiven = NOT_GIVEN, + metadata: Optional[object] | NotGiven = NOT_GIVEN, + model: Optional[str] | NotGiven = NOT_GIVEN, + temperature: Optional[float] | NotGiven = NOT_GIVEN, + tools: Optional[Iterable[AssistantToolParam]] | NotGiven = NOT_GIVEN, + thread_id: str, + event_handler: AssistantEventHandlerT, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AssistantStreamManager[AssistantEventHandlerT]: + """Create a Run stream""" + ... + + def stream( + self, + *, + assistant_id: str, + additional_instructions: Optional[str] | NotGiven = NOT_GIVEN, + instructions: Optional[str] | NotGiven = NOT_GIVEN, + metadata: Optional[object] | NotGiven = NOT_GIVEN, + model: Optional[str] | NotGiven = NOT_GIVEN, + temperature: Optional[float] | NotGiven = NOT_GIVEN, + tools: Optional[Iterable[AssistantToolParam]] | NotGiven = NOT_GIVEN, + thread_id: str, + event_handler: AssistantEventHandlerT | None = None, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AssistantStreamManager[AssistantEventHandler] | AssistantStreamManager[AssistantEventHandlerT]: + """Create a Run stream""" + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + + extra_headers = { + "OpenAI-Beta": "assistants=v1", + "X-Stainless-Stream-Helper": "threads.runs.create_and_stream", + "X-Stainless-Custom-Event-Handler": "true" if event_handler else "false", + **(extra_headers or {}), + } + make_request = partial( + self._post, + f"/threads/{thread_id}/runs", + body=maybe_transform( + { + "assistant_id": assistant_id, + "additional_instructions": additional_instructions, + "instructions": instructions, + "metadata": metadata, + "model": model, + "temperature": temperature, + "stream": True, + "tools": tools, + }, + run_create_params.RunCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Run, + stream=True, + stream_cls=Stream[AssistantStreamEvent], + ) + return AssistantStreamManager(make_request, event_handler=event_handler or AssistantEventHandler()) + + @overload + def submit_tool_outputs( + self, + run_id: str, + *, + thread_id: str, + tool_outputs: Iterable[run_submit_tool_outputs_params.ToolOutput], + stream: Optional[Literal[False]] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Run: + """ + When a run has the `status: "requires_action"` and `required_action.type` is + `submit_tool_outputs`, this endpoint can be used to submit the outputs from the + tool calls once they're all completed. All outputs must be submitted in a single + request. + + Args: + tool_outputs: A list of tools for which the outputs are being submitted. + + stream: If `true`, returns a stream of events that happen during the Run as server-sent + events, terminating when the Run enters a terminal state with a `data: [DONE]` + message. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + ... + + @overload + def submit_tool_outputs( + self, + run_id: str, + *, + thread_id: str, + stream: Literal[True], + tool_outputs: Iterable[run_submit_tool_outputs_params.ToolOutput], + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Stream[AssistantStreamEvent]: + """ + When a run has the `status: "requires_action"` and `required_action.type` is + `submit_tool_outputs`, this endpoint can be used to submit the outputs from the + tool calls once they're all completed. All outputs must be submitted in a single + request. + + Args: + stream: If `true`, returns a stream of events that happen during the Run as server-sent + events, terminating when the Run enters a terminal state with a `data: [DONE]` + message. + + tool_outputs: A list of tools for which the outputs are being submitted. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + ... + + @overload + def submit_tool_outputs( + self, + run_id: str, + *, + thread_id: str, + stream: bool, + tool_outputs: Iterable[run_submit_tool_outputs_params.ToolOutput], + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Run | Stream[AssistantStreamEvent]: + """ + When a run has the `status: "requires_action"` and `required_action.type` is + `submit_tool_outputs`, this endpoint can be used to submit the outputs from the + tool calls once they're all completed. All outputs must be submitted in a single + request. + + Args: + stream: If `true`, returns a stream of events that happen during the Run as server-sent + events, terminating when the Run enters a terminal state with a `data: [DONE]` + message. + + tool_outputs: A list of tools for which the outputs are being submitted. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + ... + + @required_args(["thread_id", "tool_outputs"], ["thread_id", "stream", "tool_outputs"]) + def submit_tool_outputs( + self, + run_id: str, + *, + thread_id: str, + tool_outputs: Iterable[run_submit_tool_outputs_params.ToolOutput], + stream: Optional[Literal[False]] | Literal[True] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Run | Stream[AssistantStreamEvent]: + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + if not run_id: + raise ValueError(f"Expected a non-empty value for `run_id` but received {run_id!r}") + extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})} + return self._post( + f"/threads/{thread_id}/runs/{run_id}/submit_tool_outputs", + body=maybe_transform( + { + "tool_outputs": tool_outputs, + "stream": stream, + }, + run_submit_tool_outputs_params.RunSubmitToolOutputsParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Run, + stream=stream or False, + stream_cls=Stream[AssistantStreamEvent], + ) + + def submit_tool_outputs_and_poll( + self, + *, + tool_outputs: Iterable[run_submit_tool_outputs_params.ToolOutput], + run_id: str, + thread_id: str, + poll_interval_ms: int | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Run: + """ + A helper to submit a tool output to a run and poll for a terminal run state. + More information on Run lifecycles can be found here: + https://platform.openai.com/docs/assistants/how-it-works/runs-and-run-steps + """ + run = self.submit_tool_outputs( + run_id=run_id, + thread_id=thread_id, + tool_outputs=tool_outputs, + stream=False, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ) + return self.poll( + run_id=run.id, + thread_id=thread_id, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + poll_interval_ms=poll_interval_ms, + ) + + @overload + def submit_tool_outputs_stream( + self, + *, + tool_outputs: Iterable[run_submit_tool_outputs_params.ToolOutput], + run_id: str, + thread_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AssistantStreamManager[AssistantEventHandler]: + """ + Submit the tool outputs from a previous run and stream the run to a terminal + state. More information on Run lifecycles can be found here: + https://platform.openai.com/docs/assistants/how-it-works/runs-and-run-steps + """ + ... + + @overload + def submit_tool_outputs_stream( + self, + *, + tool_outputs: Iterable[run_submit_tool_outputs_params.ToolOutput], + run_id: str, + thread_id: str, + event_handler: AssistantEventHandlerT, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AssistantStreamManager[AssistantEventHandlerT]: + """ + Submit the tool outputs from a previous run and stream the run to a terminal + state. More information on Run lifecycles can be found here: + https://platform.openai.com/docs/assistants/how-it-works/runs-and-run-steps + """ + ... + + def submit_tool_outputs_stream( + self, + *, + tool_outputs: Iterable[run_submit_tool_outputs_params.ToolOutput], + run_id: str, + thread_id: str, + event_handler: AssistantEventHandlerT | None = None, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AssistantStreamManager[AssistantEventHandler] | AssistantStreamManager[AssistantEventHandlerT]: + """ + Submit the tool outputs from a previous run and stream the run to a terminal + state. More information on Run lifecycles can be found here: + https://platform.openai.com/docs/assistants/how-it-works/runs-and-run-steps + """ + if not run_id: + raise ValueError(f"Expected a non-empty value for `run_id` but received {run_id!r}") + + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + + extra_headers = { + "OpenAI-Beta": "assistants=v1", + "X-Stainless-Stream-Helper": "threads.runs.submit_tool_outputs_stream", + "X-Stainless-Custom-Event-Handler": "true" if event_handler else "false", + **(extra_headers or {}), + } + request = partial( + self._post, + f"/threads/{thread_id}/runs/{run_id}/submit_tool_outputs", + body=maybe_transform( + { + "tool_outputs": tool_outputs, + "stream": True, + }, + run_submit_tool_outputs_params.RunSubmitToolOutputsParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Run, + stream=True, + stream_cls=Stream[AssistantStreamEvent], + ) + return AssistantStreamManager(request, event_handler=event_handler or AssistantEventHandler()) + + +class AsyncRuns(AsyncAPIResource): + @cached_property + def steps(self) -> AsyncSteps: + return AsyncSteps(self._client) + + @cached_property + def with_raw_response(self) -> AsyncRunsWithRawResponse: + return AsyncRunsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncRunsWithStreamingResponse: + return AsyncRunsWithStreamingResponse(self) + + @overload + async def create( + self, + thread_id: str, + *, + assistant_id: str, + additional_instructions: Optional[str] | NotGiven = NOT_GIVEN, + instructions: Optional[str] | NotGiven = NOT_GIVEN, + metadata: Optional[object] | NotGiven = NOT_GIVEN, + model: Optional[str] | NotGiven = NOT_GIVEN, + stream: Optional[Literal[False]] | NotGiven = NOT_GIVEN, + temperature: Optional[float] | NotGiven = NOT_GIVEN, + tools: Optional[Iterable[AssistantToolParam]] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Run: + """ + Create a run. + + Args: + assistant_id: The ID of the + [assistant](https://platform.openai.com/docs/api-reference/assistants) to use to + execute this run. + + additional_instructions: Appends additional instructions at the end of the instructions for the run. This + is useful for modifying the behavior on a per-run basis without overriding other + instructions. + + instructions: Overrides the + [instructions](https://platform.openai.com/docs/api-reference/assistants/createAssistant) + of the assistant. This is useful for modifying the behavior on a per-run basis. + + metadata: Set of 16 key-value pairs that can be attached to an object. This can be useful + for storing additional information about the object in a structured format. Keys + can be a maximum of 64 characters long and values can be a maxium of 512 + characters long. + + model: The ID of the [Model](https://platform.openai.com/docs/api-reference/models) to + be used to execute this run. If a value is provided here, it will override the + model associated with the assistant. If not, the model associated with the + assistant will be used. + + stream: If `true`, returns a stream of events that happen during the Run as server-sent + events, terminating when the Run enters a terminal state with a `data: [DONE]` + message. + + temperature: What sampling temperature to use, between 0 and 2. Higher values like 0.8 will + make the output more random, while lower values like 0.2 will make it more + focused and deterministic. + + tools: Override the tools the assistant can use for this run. This is useful for + modifying the behavior on a per-run basis. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + ... + + @overload + async def create( + self, + thread_id: str, + *, + assistant_id: str, + stream: Literal[True], + additional_instructions: Optional[str] | NotGiven = NOT_GIVEN, + instructions: Optional[str] | NotGiven = NOT_GIVEN, + metadata: Optional[object] | NotGiven = NOT_GIVEN, + model: Optional[str] | NotGiven = NOT_GIVEN, + temperature: Optional[float] | NotGiven = NOT_GIVEN, + tools: Optional[Iterable[AssistantToolParam]] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncStream[AssistantStreamEvent]: + """ + Create a run. + + Args: + assistant_id: The ID of the + [assistant](https://platform.openai.com/docs/api-reference/assistants) to use to + execute this run. + + stream: If `true`, returns a stream of events that happen during the Run as server-sent + events, terminating when the Run enters a terminal state with a `data: [DONE]` + message. + + additional_instructions: Appends additional instructions at the end of the instructions for the run. This + is useful for modifying the behavior on a per-run basis without overriding other + instructions. + + instructions: Overrides the + [instructions](https://platform.openai.com/docs/api-reference/assistants/createAssistant) + of the assistant. This is useful for modifying the behavior on a per-run basis. + + metadata: Set of 16 key-value pairs that can be attached to an object. This can be useful + for storing additional information about the object in a structured format. Keys + can be a maximum of 64 characters long and values can be a maxium of 512 + characters long. + + model: The ID of the [Model](https://platform.openai.com/docs/api-reference/models) to + be used to execute this run. If a value is provided here, it will override the + model associated with the assistant. If not, the model associated with the + assistant will be used. + + temperature: What sampling temperature to use, between 0 and 2. Higher values like 0.8 will + make the output more random, while lower values like 0.2 will make it more + focused and deterministic. + + tools: Override the tools the assistant can use for this run. This is useful for + modifying the behavior on a per-run basis. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + ... + + @overload + async def create( + self, + thread_id: str, + *, + assistant_id: str, + stream: bool, + additional_instructions: Optional[str] | NotGiven = NOT_GIVEN, + instructions: Optional[str] | NotGiven = NOT_GIVEN, + metadata: Optional[object] | NotGiven = NOT_GIVEN, + model: Optional[str] | NotGiven = NOT_GIVEN, + temperature: Optional[float] | NotGiven = NOT_GIVEN, + tools: Optional[Iterable[AssistantToolParam]] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Run | AsyncStream[AssistantStreamEvent]: + """ + Create a run. + + Args: + assistant_id: The ID of the + [assistant](https://platform.openai.com/docs/api-reference/assistants) to use to + execute this run. + + stream: If `true`, returns a stream of events that happen during the Run as server-sent + events, terminating when the Run enters a terminal state with a `data: [DONE]` + message. + + additional_instructions: Appends additional instructions at the end of the instructions for the run. This + is useful for modifying the behavior on a per-run basis without overriding other + instructions. + + instructions: Overrides the + [instructions](https://platform.openai.com/docs/api-reference/assistants/createAssistant) + of the assistant. This is useful for modifying the behavior on a per-run basis. + + metadata: Set of 16 key-value pairs that can be attached to an object. This can be useful + for storing additional information about the object in a structured format. Keys + can be a maximum of 64 characters long and values can be a maxium of 512 + characters long. + + model: The ID of the [Model](https://platform.openai.com/docs/api-reference/models) to + be used to execute this run. If a value is provided here, it will override the + model associated with the assistant. If not, the model associated with the + assistant will be used. + + temperature: What sampling temperature to use, between 0 and 2. Higher values like 0.8 will + make the output more random, while lower values like 0.2 will make it more + focused and deterministic. + + tools: Override the tools the assistant can use for this run. This is useful for + modifying the behavior on a per-run basis. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + ... + + @required_args(["assistant_id"], ["assistant_id", "stream"]) + async def create( + self, + thread_id: str, + *, + assistant_id: str, + additional_instructions: Optional[str] | NotGiven = NOT_GIVEN, + instructions: Optional[str] | NotGiven = NOT_GIVEN, + metadata: Optional[object] | NotGiven = NOT_GIVEN, + model: Optional[str] | NotGiven = NOT_GIVEN, + stream: Optional[Literal[False]] | Literal[True] | NotGiven = NOT_GIVEN, + temperature: Optional[float] | NotGiven = NOT_GIVEN, + tools: Optional[Iterable[AssistantToolParam]] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Run | AsyncStream[AssistantStreamEvent]: + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})} + return await self._post( + f"/threads/{thread_id}/runs", + body=await async_maybe_transform( + { + "assistant_id": assistant_id, + "additional_instructions": additional_instructions, + "instructions": instructions, + "metadata": metadata, + "model": model, + "stream": stream, + "temperature": temperature, + "tools": tools, + }, + run_create_params.RunCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Run, + stream=stream or False, + stream_cls=AsyncStream[AssistantStreamEvent], + ) + + async def retrieve( + self, + run_id: str, + *, + thread_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Run: + """ + Retrieves a run. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + if not run_id: + raise ValueError(f"Expected a non-empty value for `run_id` but received {run_id!r}") + extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})} + return await self._get( + f"/threads/{thread_id}/runs/{run_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Run, + ) + + async def update( + self, + run_id: str, + *, + thread_id: str, + metadata: Optional[object] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Run: + """ + Modifies a run. + + Args: + metadata: Set of 16 key-value pairs that can be attached to an object. This can be useful + for storing additional information about the object in a structured format. Keys + can be a maximum of 64 characters long and values can be a maxium of 512 + characters long. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + if not run_id: + raise ValueError(f"Expected a non-empty value for `run_id` but received {run_id!r}") + extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})} + return await self._post( + f"/threads/{thread_id}/runs/{run_id}", + body=await async_maybe_transform({"metadata": metadata}, run_update_params.RunUpdateParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Run, + ) + + def list( + self, + thread_id: str, + *, + after: str | NotGiven = NOT_GIVEN, + before: str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + order: Literal["asc", "desc"] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncPaginator[Run, AsyncCursorPage[Run]]: + """ + Returns a list of runs belonging to a thread. + + Args: + after: A cursor for use in pagination. `after` is an object ID that defines your place + in the list. For instance, if you make a list request and receive 100 objects, + ending with obj_foo, your subsequent call can include after=obj_foo in order to + fetch the next page of the list. + + before: A cursor for use in pagination. `before` is an object ID that defines your place + in the list. For instance, if you make a list request and receive 100 objects, + ending with obj_foo, your subsequent call can include before=obj_foo in order to + fetch the previous page of the list. + + limit: A limit on the number of objects to be returned. Limit can range between 1 and + 100, and the default is 20. + + order: Sort order by the `created_at` timestamp of the objects. `asc` for ascending + order and `desc` for descending order. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})} + return self._get_api_list( + f"/threads/{thread_id}/runs", + page=AsyncCursorPage[Run], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "after": after, + "before": before, + "limit": limit, + "order": order, + }, + run_list_params.RunListParams, + ), + ), + model=Run, + ) + + async def cancel( + self, + run_id: str, + *, + thread_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Run: + """ + Cancels a run that is `in_progress`. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + if not run_id: + raise ValueError(f"Expected a non-empty value for `run_id` but received {run_id!r}") + extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})} + return await self._post( + f"/threads/{thread_id}/runs/{run_id}/cancel", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Run, + ) + + async def create_and_poll( + self, + *, + assistant_id: str, + additional_instructions: Optional[str] | NotGiven = NOT_GIVEN, + instructions: Optional[str] | NotGiven = NOT_GIVEN, + metadata: Optional[object] | NotGiven = NOT_GIVEN, + model: Optional[str] | NotGiven = NOT_GIVEN, + temperature: Optional[float] | NotGiven = NOT_GIVEN, + tools: Optional[Iterable[AssistantToolParam]] | NotGiven = NOT_GIVEN, + poll_interval_ms: int | NotGiven = NOT_GIVEN, + thread_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Run: + """ + A helper to create a run an poll for a terminal state. More information on Run + lifecycles can be found here: + https://platform.openai.com/docs/assistants/how-it-works/runs-and-run-steps + """ + run = await self.create( + thread_id=thread_id, + assistant_id=assistant_id, + additional_instructions=additional_instructions, + instructions=instructions, + metadata=metadata, + model=model, + temperature=temperature, + # We assume we are not streaming when polling + stream=False, + tools=tools, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ) + return await self.poll( + run.id, + thread_id=thread_id, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + poll_interval_ms=poll_interval_ms, + timeout=timeout, + ) + + @overload + @typing_extensions.deprecated("use `stream` instead") + def create_and_stream( + self, + *, + assistant_id: str, + additional_instructions: Optional[str] | NotGiven = NOT_GIVEN, + instructions: Optional[str] | NotGiven = NOT_GIVEN, + metadata: Optional[object] | NotGiven = NOT_GIVEN, + model: Optional[str] | NotGiven = NOT_GIVEN, + temperature: Optional[float] | NotGiven = NOT_GIVEN, + tools: Optional[Iterable[AssistantToolParam]] | NotGiven = NOT_GIVEN, + thread_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncAssistantStreamManager[AsyncAssistantEventHandler]: + """Create a Run stream""" + ... + + @overload + @typing_extensions.deprecated("use `stream` instead") + def create_and_stream( + self, + *, + assistant_id: str, + additional_instructions: Optional[str] | NotGiven = NOT_GIVEN, + instructions: Optional[str] | NotGiven = NOT_GIVEN, + metadata: Optional[object] | NotGiven = NOT_GIVEN, + model: Optional[str] | NotGiven = NOT_GIVEN, + temperature: Optional[float] | NotGiven = NOT_GIVEN, + tools: Optional[Iterable[AssistantToolParam]] | NotGiven = NOT_GIVEN, + thread_id: str, + event_handler: AsyncAssistantEventHandlerT, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncAssistantStreamManager[AsyncAssistantEventHandlerT]: + """Create a Run stream""" + ... + + @typing_extensions.deprecated("use `stream` instead") + def create_and_stream( + self, + *, + assistant_id: str, + additional_instructions: Optional[str] | NotGiven = NOT_GIVEN, + instructions: Optional[str] | NotGiven = NOT_GIVEN, + metadata: Optional[object] | NotGiven = NOT_GIVEN, + model: Optional[str] | NotGiven = NOT_GIVEN, + temperature: Optional[float] | NotGiven = NOT_GIVEN, + tools: Optional[Iterable[AssistantToolParam]] | NotGiven = NOT_GIVEN, + thread_id: str, + event_handler: AsyncAssistantEventHandlerT | None = None, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ( + AsyncAssistantStreamManager[AsyncAssistantEventHandler] + | AsyncAssistantStreamManager[AsyncAssistantEventHandlerT] + ): + """Create a Run stream""" + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + + extra_headers = { + "OpenAI-Beta": "assistants=v1", + "X-Stainless-Stream-Helper": "threads.runs.create_and_stream", + "X-Stainless-Custom-Event-Handler": "true" if event_handler else "false", + **(extra_headers or {}), + } + request = self._post( + f"/threads/{thread_id}/runs", + body=maybe_transform( + { + "assistant_id": assistant_id, + "additional_instructions": additional_instructions, + "instructions": instructions, + "metadata": metadata, + "model": model, + "temperature": temperature, + "stream": True, + "tools": tools, + }, + run_create_params.RunCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Run, + stream=True, + stream_cls=AsyncStream[AssistantStreamEvent], + ) + return AsyncAssistantStreamManager(request, event_handler=event_handler or AsyncAssistantEventHandler()) + + async def poll( + self, + run_id: str, + thread_id: str, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + poll_interval_ms: int | NotGiven = NOT_GIVEN, + ) -> Run: + """ + A helper to poll a run status until it reaches a terminal state. More + information on Run lifecycles can be found here: + https://platform.openai.com/docs/assistants/how-it-works/runs-and-run-steps + """ + extra_headers = {"X-Stainless-Poll-Helper": "true", **(extra_headers or {})} + + if is_given(poll_interval_ms): + extra_headers["X-Stainless-Custom-Poll-Interval"] = str(poll_interval_ms) + + terminal_states = {"requires_action", "cancelled", "completed", "failed", "expired"} + while True: + response = await self.with_raw_response.retrieve( + thread_id=thread_id, + run_id=run_id, + extra_headers=extra_headers, + extra_body=extra_body, + extra_query=extra_query, + timeout=timeout, + ) + + run = response.parse() + # Return if we reached a terminal state + if run.status in terminal_states: + return run + + if not is_given(poll_interval_ms): + from_header = response.headers.get("openai-poll-after-ms") + if from_header is not None: + poll_interval_ms = int(from_header) + else: + poll_interval_ms = 1000 + + time.sleep(poll_interval_ms / 1000) + + @overload + def stream( + self, + *, + assistant_id: str, + additional_instructions: Optional[str] | NotGiven = NOT_GIVEN, + instructions: Optional[str] | NotGiven = NOT_GIVEN, + metadata: Optional[object] | NotGiven = NOT_GIVEN, + model: Optional[str] | NotGiven = NOT_GIVEN, + temperature: Optional[float] | NotGiven = NOT_GIVEN, + tools: Optional[Iterable[AssistantToolParam]] | NotGiven = NOT_GIVEN, + thread_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncAssistantStreamManager[AsyncAssistantEventHandler]: + """Create a Run stream""" + ... + + @overload + def stream( + self, + *, + assistant_id: str, + additional_instructions: Optional[str] | NotGiven = NOT_GIVEN, + instructions: Optional[str] | NotGiven = NOT_GIVEN, + metadata: Optional[object] | NotGiven = NOT_GIVEN, + model: Optional[str] | NotGiven = NOT_GIVEN, + temperature: Optional[float] | NotGiven = NOT_GIVEN, + tools: Optional[Iterable[AssistantToolParam]] | NotGiven = NOT_GIVEN, + thread_id: str, + event_handler: AsyncAssistantEventHandlerT, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncAssistantStreamManager[AsyncAssistantEventHandlerT]: + """Create a Run stream""" + ... + + def stream( + self, + *, + assistant_id: str, + additional_instructions: Optional[str] | NotGiven = NOT_GIVEN, + instructions: Optional[str] | NotGiven = NOT_GIVEN, + metadata: Optional[object] | NotGiven = NOT_GIVEN, + model: Optional[str] | NotGiven = NOT_GIVEN, + temperature: Optional[float] | NotGiven = NOT_GIVEN, + tools: Optional[Iterable[AssistantToolParam]] | NotGiven = NOT_GIVEN, + thread_id: str, + event_handler: AsyncAssistantEventHandlerT | None = None, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ( + AsyncAssistantStreamManager[AsyncAssistantEventHandler] + | AsyncAssistantStreamManager[AsyncAssistantEventHandlerT] + ): + """Create a Run stream""" + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + + extra_headers = { + "OpenAI-Beta": "assistants=v1", + "X-Stainless-Stream-Helper": "threads.runs.create_and_stream", + "X-Stainless-Custom-Event-Handler": "true" if event_handler else "false", + **(extra_headers or {}), + } + request = self._post( + f"/threads/{thread_id}/runs", + body=maybe_transform( + { + "assistant_id": assistant_id, + "additional_instructions": additional_instructions, + "instructions": instructions, + "metadata": metadata, + "model": model, + "temperature": temperature, + "stream": True, + "tools": tools, + }, + run_create_params.RunCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Run, + stream=True, + stream_cls=AsyncStream[AssistantStreamEvent], + ) + return AsyncAssistantStreamManager(request, event_handler=event_handler or AsyncAssistantEventHandler()) + + @overload + async def submit_tool_outputs( + self, + run_id: str, + *, + thread_id: str, + tool_outputs: Iterable[run_submit_tool_outputs_params.ToolOutput], + stream: Optional[Literal[False]] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Run: + """ + When a run has the `status: "requires_action"` and `required_action.type` is + `submit_tool_outputs`, this endpoint can be used to submit the outputs from the + tool calls once they're all completed. All outputs must be submitted in a single + request. + + Args: + tool_outputs: A list of tools for which the outputs are being submitted. + + stream: If `true`, returns a stream of events that happen during the Run as server-sent + events, terminating when the Run enters a terminal state with a `data: [DONE]` + message. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + ... + + @overload + async def submit_tool_outputs( + self, + run_id: str, + *, + thread_id: str, + stream: Literal[True], + tool_outputs: Iterable[run_submit_tool_outputs_params.ToolOutput], + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncStream[AssistantStreamEvent]: + """ + When a run has the `status: "requires_action"` and `required_action.type` is + `submit_tool_outputs`, this endpoint can be used to submit the outputs from the + tool calls once they're all completed. All outputs must be submitted in a single + request. + + Args: + stream: If `true`, returns a stream of events that happen during the Run as server-sent + events, terminating when the Run enters a terminal state with a `data: [DONE]` + message. + + tool_outputs: A list of tools for which the outputs are being submitted. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + ... + + @overload + async def submit_tool_outputs( + self, + run_id: str, + *, + thread_id: str, + stream: bool, + tool_outputs: Iterable[run_submit_tool_outputs_params.ToolOutput], + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Run | AsyncStream[AssistantStreamEvent]: + """ + When a run has the `status: "requires_action"` and `required_action.type` is + `submit_tool_outputs`, this endpoint can be used to submit the outputs from the + tool calls once they're all completed. All outputs must be submitted in a single + request. + + Args: + stream: If `true`, returns a stream of events that happen during the Run as server-sent + events, terminating when the Run enters a terminal state with a `data: [DONE]` + message. + + tool_outputs: A list of tools for which the outputs are being submitted. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + ... + + @required_args(["thread_id", "tool_outputs"], ["thread_id", "stream", "tool_outputs"]) + async def submit_tool_outputs( + self, + run_id: str, + *, + thread_id: str, + tool_outputs: Iterable[run_submit_tool_outputs_params.ToolOutput], + stream: Optional[Literal[False]] | Literal[True] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Run | AsyncStream[AssistantStreamEvent]: + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + if not run_id: + raise ValueError(f"Expected a non-empty value for `run_id` but received {run_id!r}") + extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})} + return await self._post( + f"/threads/{thread_id}/runs/{run_id}/submit_tool_outputs", + body=await async_maybe_transform( + { + "tool_outputs": tool_outputs, + "stream": stream, + }, + run_submit_tool_outputs_params.RunSubmitToolOutputsParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Run, + stream=stream or False, + stream_cls=AsyncStream[AssistantStreamEvent], + ) + + async def submit_tool_outputs_and_poll( + self, + *, + tool_outputs: Iterable[run_submit_tool_outputs_params.ToolOutput], + run_id: str, + thread_id: str, + poll_interval_ms: int | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Run: + """ + A helper to submit a tool output to a run and poll for a terminal run state. + More information on Run lifecycles can be found here: + https://platform.openai.com/docs/assistants/how-it-works/runs-and-run-steps + """ + run = await self.submit_tool_outputs( + run_id=run_id, + thread_id=thread_id, + tool_outputs=tool_outputs, + stream=False, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ) + return await self.poll( + run_id=run.id, + thread_id=thread_id, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + poll_interval_ms=poll_interval_ms, + ) + + @overload + def submit_tool_outputs_stream( + self, + *, + tool_outputs: Iterable[run_submit_tool_outputs_params.ToolOutput], + run_id: str, + thread_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncAssistantStreamManager[AsyncAssistantEventHandler]: + """ + Submit the tool outputs from a previous run and stream the run to a terminal + state. More information on Run lifecycles can be found here: + https://platform.openai.com/docs/assistants/how-it-works/runs-and-run-steps + """ + ... + + @overload + def submit_tool_outputs_stream( + self, + *, + tool_outputs: Iterable[run_submit_tool_outputs_params.ToolOutput], + run_id: str, + thread_id: str, + event_handler: AsyncAssistantEventHandlerT, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncAssistantStreamManager[AsyncAssistantEventHandlerT]: + """ + Submit the tool outputs from a previous run and stream the run to a terminal + state. More information on Run lifecycles can be found here: + https://platform.openai.com/docs/assistants/how-it-works/runs-and-run-steps + """ + ... + + def submit_tool_outputs_stream( + self, + *, + tool_outputs: Iterable[run_submit_tool_outputs_params.ToolOutput], + run_id: str, + thread_id: str, + event_handler: AsyncAssistantEventHandlerT | None = None, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ( + AsyncAssistantStreamManager[AsyncAssistantEventHandler] + | AsyncAssistantStreamManager[AsyncAssistantEventHandlerT] + ): + """ + Submit the tool outputs from a previous run and stream the run to a terminal + state. More information on Run lifecycles can be found here: + https://platform.openai.com/docs/assistants/how-it-works/runs-and-run-steps + """ + if not run_id: + raise ValueError(f"Expected a non-empty value for `run_id` but received {run_id!r}") + + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + + extra_headers = { + "OpenAI-Beta": "assistants=v1", + "X-Stainless-Stream-Helper": "threads.runs.submit_tool_outputs_stream", + "X-Stainless-Custom-Event-Handler": "true" if event_handler else "false", + **(extra_headers or {}), + } + request = self._post( + f"/threads/{thread_id}/runs/{run_id}/submit_tool_outputs", + body=maybe_transform( + { + "tool_outputs": tool_outputs, + "stream": True, + }, + run_submit_tool_outputs_params.RunSubmitToolOutputsParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Run, + stream=True, + stream_cls=AsyncStream[AssistantStreamEvent], + ) + return AsyncAssistantStreamManager(request, event_handler=event_handler or AsyncAssistantEventHandler()) + + +class RunsWithRawResponse: + def __init__(self, runs: Runs) -> None: + self._runs = runs + + self.create = _legacy_response.to_raw_response_wrapper( + runs.create, + ) + self.retrieve = _legacy_response.to_raw_response_wrapper( + runs.retrieve, + ) + self.update = _legacy_response.to_raw_response_wrapper( + runs.update, + ) + self.list = _legacy_response.to_raw_response_wrapper( + runs.list, + ) + self.cancel = _legacy_response.to_raw_response_wrapper( + runs.cancel, + ) + self.submit_tool_outputs = _legacy_response.to_raw_response_wrapper( + runs.submit_tool_outputs, + ) + + @cached_property + def steps(self) -> StepsWithRawResponse: + return StepsWithRawResponse(self._runs.steps) + + +class AsyncRunsWithRawResponse: + def __init__(self, runs: AsyncRuns) -> None: + self._runs = runs + + self.create = _legacy_response.async_to_raw_response_wrapper( + runs.create, + ) + self.retrieve = _legacy_response.async_to_raw_response_wrapper( + runs.retrieve, + ) + self.update = _legacy_response.async_to_raw_response_wrapper( + runs.update, + ) + self.list = _legacy_response.async_to_raw_response_wrapper( + runs.list, + ) + self.cancel = _legacy_response.async_to_raw_response_wrapper( + runs.cancel, + ) + self.submit_tool_outputs = _legacy_response.async_to_raw_response_wrapper( + runs.submit_tool_outputs, + ) + + @cached_property + def steps(self) -> AsyncStepsWithRawResponse: + return AsyncStepsWithRawResponse(self._runs.steps) + + +class RunsWithStreamingResponse: + def __init__(self, runs: Runs) -> None: + self._runs = runs + + self.create = to_streamed_response_wrapper( + runs.create, + ) + self.retrieve = to_streamed_response_wrapper( + runs.retrieve, + ) + self.update = to_streamed_response_wrapper( + runs.update, + ) + self.list = to_streamed_response_wrapper( + runs.list, + ) + self.cancel = to_streamed_response_wrapper( + runs.cancel, + ) + self.submit_tool_outputs = to_streamed_response_wrapper( + runs.submit_tool_outputs, + ) + + @cached_property + def steps(self) -> StepsWithStreamingResponse: + return StepsWithStreamingResponse(self._runs.steps) + + +class AsyncRunsWithStreamingResponse: + def __init__(self, runs: AsyncRuns) -> None: + self._runs = runs + + self.create = async_to_streamed_response_wrapper( + runs.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + runs.retrieve, + ) + self.update = async_to_streamed_response_wrapper( + runs.update, + ) + self.list = async_to_streamed_response_wrapper( + runs.list, + ) + self.cancel = async_to_streamed_response_wrapper( + runs.cancel, + ) + self.submit_tool_outputs = async_to_streamed_response_wrapper( + runs.submit_tool_outputs, + ) + + @cached_property + def steps(self) -> AsyncStepsWithStreamingResponse: + return AsyncStepsWithStreamingResponse(self._runs.steps) diff --git a/openai/resources/beta/threads/runs/steps.py b/openai/resources/beta/threads/runs/steps.py new file mode 100644 index 00000000..118bd882 --- /dev/null +++ b/openai/resources/beta/threads/runs/steps.py @@ -0,0 +1,310 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal + +import httpx + +from ..... import _legacy_response +from ....._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from ....._utils import maybe_transform +from ....._compat import cached_property +from ....._resource import SyncAPIResource, AsyncAPIResource +from ....._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper +from .....pagination import SyncCursorPage, AsyncCursorPage +from ....._base_client import ( + AsyncPaginator, + make_request_options, +) +from .....types.beta.threads.runs import RunStep, step_list_params + +__all__ = ["Steps", "AsyncSteps"] + + +class Steps(SyncAPIResource): + @cached_property + def with_raw_response(self) -> StepsWithRawResponse: + return StepsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> StepsWithStreamingResponse: + return StepsWithStreamingResponse(self) + + def retrieve( + self, + step_id: str, + *, + thread_id: str, + run_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> RunStep: + """ + Retrieves a run step. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + if not run_id: + raise ValueError(f"Expected a non-empty value for `run_id` but received {run_id!r}") + if not step_id: + raise ValueError(f"Expected a non-empty value for `step_id` but received {step_id!r}") + extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})} + return self._get( + f"/threads/{thread_id}/runs/{run_id}/steps/{step_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=RunStep, + ) + + def list( + self, + run_id: str, + *, + thread_id: str, + after: str | NotGiven = NOT_GIVEN, + before: str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + order: Literal["asc", "desc"] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SyncCursorPage[RunStep]: + """ + Returns a list of run steps belonging to a run. + + Args: + after: A cursor for use in pagination. `after` is an object ID that defines your place + in the list. For instance, if you make a list request and receive 100 objects, + ending with obj_foo, your subsequent call can include after=obj_foo in order to + fetch the next page of the list. + + before: A cursor for use in pagination. `before` is an object ID that defines your place + in the list. For instance, if you make a list request and receive 100 objects, + ending with obj_foo, your subsequent call can include before=obj_foo in order to + fetch the previous page of the list. + + limit: A limit on the number of objects to be returned. Limit can range between 1 and + 100, and the default is 20. + + order: Sort order by the `created_at` timestamp of the objects. `asc` for ascending + order and `desc` for descending order. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + if not run_id: + raise ValueError(f"Expected a non-empty value for `run_id` but received {run_id!r}") + extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})} + return self._get_api_list( + f"/threads/{thread_id}/runs/{run_id}/steps", + page=SyncCursorPage[RunStep], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "after": after, + "before": before, + "limit": limit, + "order": order, + }, + step_list_params.StepListParams, + ), + ), + model=RunStep, + ) + + +class AsyncSteps(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncStepsWithRawResponse: + return AsyncStepsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncStepsWithStreamingResponse: + return AsyncStepsWithStreamingResponse(self) + + async def retrieve( + self, + step_id: str, + *, + thread_id: str, + run_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> RunStep: + """ + Retrieves a run step. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + if not run_id: + raise ValueError(f"Expected a non-empty value for `run_id` but received {run_id!r}") + if not step_id: + raise ValueError(f"Expected a non-empty value for `step_id` but received {step_id!r}") + extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})} + return await self._get( + f"/threads/{thread_id}/runs/{run_id}/steps/{step_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=RunStep, + ) + + def list( + self, + run_id: str, + *, + thread_id: str, + after: str | NotGiven = NOT_GIVEN, + before: str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + order: Literal["asc", "desc"] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncPaginator[RunStep, AsyncCursorPage[RunStep]]: + """ + Returns a list of run steps belonging to a run. + + Args: + after: A cursor for use in pagination. `after` is an object ID that defines your place + in the list. For instance, if you make a list request and receive 100 objects, + ending with obj_foo, your subsequent call can include after=obj_foo in order to + fetch the next page of the list. + + before: A cursor for use in pagination. `before` is an object ID that defines your place + in the list. For instance, if you make a list request and receive 100 objects, + ending with obj_foo, your subsequent call can include before=obj_foo in order to + fetch the previous page of the list. + + limit: A limit on the number of objects to be returned. Limit can range between 1 and + 100, and the default is 20. + + order: Sort order by the `created_at` timestamp of the objects. `asc` for ascending + order and `desc` for descending order. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + if not run_id: + raise ValueError(f"Expected a non-empty value for `run_id` but received {run_id!r}") + extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})} + return self._get_api_list( + f"/threads/{thread_id}/runs/{run_id}/steps", + page=AsyncCursorPage[RunStep], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "after": after, + "before": before, + "limit": limit, + "order": order, + }, + step_list_params.StepListParams, + ), + ), + model=RunStep, + ) + + +class StepsWithRawResponse: + def __init__(self, steps: Steps) -> None: + self._steps = steps + + self.retrieve = _legacy_response.to_raw_response_wrapper( + steps.retrieve, + ) + self.list = _legacy_response.to_raw_response_wrapper( + steps.list, + ) + + +class AsyncStepsWithRawResponse: + def __init__(self, steps: AsyncSteps) -> None: + self._steps = steps + + self.retrieve = _legacy_response.async_to_raw_response_wrapper( + steps.retrieve, + ) + self.list = _legacy_response.async_to_raw_response_wrapper( + steps.list, + ) + + +class StepsWithStreamingResponse: + def __init__(self, steps: Steps) -> None: + self._steps = steps + + self.retrieve = to_streamed_response_wrapper( + steps.retrieve, + ) + self.list = to_streamed_response_wrapper( + steps.list, + ) + + +class AsyncStepsWithStreamingResponse: + def __init__(self, steps: AsyncSteps) -> None: + self._steps = steps + + self.retrieve = async_to_streamed_response_wrapper( + steps.retrieve, + ) + self.list = async_to_streamed_response_wrapper( + steps.list, + ) diff --git a/openai/resources/beta/threads/threads.py b/openai/resources/beta/threads/threads.py new file mode 100644 index 00000000..3509267d --- /dev/null +++ b/openai/resources/beta/threads/threads.py @@ -0,0 +1,1259 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Iterable, Optional, overload +from functools import partial +from typing_extensions import Literal + +import httpx + +from .... import _legacy_response +from .runs import ( + Runs, + AsyncRuns, + RunsWithRawResponse, + AsyncRunsWithRawResponse, + RunsWithStreamingResponse, + AsyncRunsWithStreamingResponse, +) +from .messages import ( + Messages, + AsyncMessages, + MessagesWithRawResponse, + AsyncMessagesWithRawResponse, + MessagesWithStreamingResponse, + AsyncMessagesWithStreamingResponse, +) +from ...._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from ...._utils import ( + required_args, + maybe_transform, + async_maybe_transform, +) +from .runs.runs import Runs, AsyncRuns +from ...._compat import cached_property +from ...._resource import SyncAPIResource, AsyncAPIResource +from ...._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper +from ...._streaming import Stream, AsyncStream +from ....types.beta import ( + Thread, + ThreadDeleted, + AssistantStreamEvent, + thread_create_params, + thread_update_params, + thread_create_and_run_params, +) +from ...._base_client import ( + make_request_options, +) +from ....lib.streaming import ( + AssistantEventHandler, + AssistantEventHandlerT, + AssistantStreamManager, + AsyncAssistantEventHandler, + AsyncAssistantEventHandlerT, + AsyncAssistantStreamManager, +) +from .messages.messages import Messages, AsyncMessages +from ....types.beta.threads import Run + +__all__ = ["Threads", "AsyncThreads"] + + +class Threads(SyncAPIResource): + @cached_property + def runs(self) -> Runs: + return Runs(self._client) + + @cached_property + def messages(self) -> Messages: + return Messages(self._client) + + @cached_property + def with_raw_response(self) -> ThreadsWithRawResponse: + return ThreadsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> ThreadsWithStreamingResponse: + return ThreadsWithStreamingResponse(self) + + def create( + self, + *, + messages: Iterable[thread_create_params.Message] | NotGiven = NOT_GIVEN, + metadata: Optional[object] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Thread: + """ + Create a thread. + + Args: + messages: A list of [messages](https://platform.openai.com/docs/api-reference/messages) to + start the thread with. + + metadata: Set of 16 key-value pairs that can be attached to an object. This can be useful + for storing additional information about the object in a structured format. Keys + can be a maximum of 64 characters long and values can be a maxium of 512 + characters long. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})} + return self._post( + "/threads", + body=maybe_transform( + { + "messages": messages, + "metadata": metadata, + }, + thread_create_params.ThreadCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Thread, + ) + + def retrieve( + self, + thread_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Thread: + """ + Retrieves a thread. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})} + return self._get( + f"/threads/{thread_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Thread, + ) + + def update( + self, + thread_id: str, + *, + metadata: Optional[object] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Thread: + """ + Modifies a thread. + + Args: + metadata: Set of 16 key-value pairs that can be attached to an object. This can be useful + for storing additional information about the object in a structured format. Keys + can be a maximum of 64 characters long and values can be a maxium of 512 + characters long. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})} + return self._post( + f"/threads/{thread_id}", + body=maybe_transform({"metadata": metadata}, thread_update_params.ThreadUpdateParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Thread, + ) + + def delete( + self, + thread_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ThreadDeleted: + """ + Delete a thread. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})} + return self._delete( + f"/threads/{thread_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ThreadDeleted, + ) + + @overload + def create_and_run( + self, + *, + assistant_id: str, + instructions: Optional[str] | NotGiven = NOT_GIVEN, + metadata: Optional[object] | NotGiven = NOT_GIVEN, + model: Optional[str] | NotGiven = NOT_GIVEN, + stream: Optional[Literal[False]] | NotGiven = NOT_GIVEN, + temperature: Optional[float] | NotGiven = NOT_GIVEN, + thread: thread_create_and_run_params.Thread | NotGiven = NOT_GIVEN, + tools: Optional[Iterable[thread_create_and_run_params.Tool]] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Run: + """ + Create a thread and run it in one request. + + Args: + assistant_id: The ID of the + [assistant](https://platform.openai.com/docs/api-reference/assistants) to use to + execute this run. + + instructions: Override the default system message of the assistant. This is useful for + modifying the behavior on a per-run basis. + + metadata: Set of 16 key-value pairs that can be attached to an object. This can be useful + for storing additional information about the object in a structured format. Keys + can be a maximum of 64 characters long and values can be a maxium of 512 + characters long. + + model: The ID of the [Model](https://platform.openai.com/docs/api-reference/models) to + be used to execute this run. If a value is provided here, it will override the + model associated with the assistant. If not, the model associated with the + assistant will be used. + + stream: If `true`, returns a stream of events that happen during the Run as server-sent + events, terminating when the Run enters a terminal state with a `data: [DONE]` + message. + + temperature: What sampling temperature to use, between 0 and 2. Higher values like 0.8 will + make the output more random, while lower values like 0.2 will make it more + focused and deterministic. + + thread: If no thread is provided, an empty thread will be created. + + tools: Override the tools the assistant can use for this run. This is useful for + modifying the behavior on a per-run basis. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + ... + + @overload + def create_and_run( + self, + *, + assistant_id: str, + stream: Literal[True], + instructions: Optional[str] | NotGiven = NOT_GIVEN, + metadata: Optional[object] | NotGiven = NOT_GIVEN, + model: Optional[str] | NotGiven = NOT_GIVEN, + temperature: Optional[float] | NotGiven = NOT_GIVEN, + thread: thread_create_and_run_params.Thread | NotGiven = NOT_GIVEN, + tools: Optional[Iterable[thread_create_and_run_params.Tool]] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Stream[AssistantStreamEvent]: + """ + Create a thread and run it in one request. + + Args: + assistant_id: The ID of the + [assistant](https://platform.openai.com/docs/api-reference/assistants) to use to + execute this run. + + stream: If `true`, returns a stream of events that happen during the Run as server-sent + events, terminating when the Run enters a terminal state with a `data: [DONE]` + message. + + instructions: Override the default system message of the assistant. This is useful for + modifying the behavior on a per-run basis. + + metadata: Set of 16 key-value pairs that can be attached to an object. This can be useful + for storing additional information about the object in a structured format. Keys + can be a maximum of 64 characters long and values can be a maxium of 512 + characters long. + + model: The ID of the [Model](https://platform.openai.com/docs/api-reference/models) to + be used to execute this run. If a value is provided here, it will override the + model associated with the assistant. If not, the model associated with the + assistant will be used. + + temperature: What sampling temperature to use, between 0 and 2. Higher values like 0.8 will + make the output more random, while lower values like 0.2 will make it more + focused and deterministic. + + thread: If no thread is provided, an empty thread will be created. + + tools: Override the tools the assistant can use for this run. This is useful for + modifying the behavior on a per-run basis. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + ... + + @overload + def create_and_run( + self, + *, + assistant_id: str, + stream: bool, + instructions: Optional[str] | NotGiven = NOT_GIVEN, + metadata: Optional[object] | NotGiven = NOT_GIVEN, + model: Optional[str] | NotGiven = NOT_GIVEN, + temperature: Optional[float] | NotGiven = NOT_GIVEN, + thread: thread_create_and_run_params.Thread | NotGiven = NOT_GIVEN, + tools: Optional[Iterable[thread_create_and_run_params.Tool]] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Run | Stream[AssistantStreamEvent]: + """ + Create a thread and run it in one request. + + Args: + assistant_id: The ID of the + [assistant](https://platform.openai.com/docs/api-reference/assistants) to use to + execute this run. + + stream: If `true`, returns a stream of events that happen during the Run as server-sent + events, terminating when the Run enters a terminal state with a `data: [DONE]` + message. + + instructions: Override the default system message of the assistant. This is useful for + modifying the behavior on a per-run basis. + + metadata: Set of 16 key-value pairs that can be attached to an object. This can be useful + for storing additional information about the object in a structured format. Keys + can be a maximum of 64 characters long and values can be a maxium of 512 + characters long. + + model: The ID of the [Model](https://platform.openai.com/docs/api-reference/models) to + be used to execute this run. If a value is provided here, it will override the + model associated with the assistant. If not, the model associated with the + assistant will be used. + + temperature: What sampling temperature to use, between 0 and 2. Higher values like 0.8 will + make the output more random, while lower values like 0.2 will make it more + focused and deterministic. + + thread: If no thread is provided, an empty thread will be created. + + tools: Override the tools the assistant can use for this run. This is useful for + modifying the behavior on a per-run basis. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + ... + + @required_args(["assistant_id"], ["assistant_id", "stream"]) + def create_and_run( + self, + *, + assistant_id: str, + instructions: Optional[str] | NotGiven = NOT_GIVEN, + metadata: Optional[object] | NotGiven = NOT_GIVEN, + model: Optional[str] | NotGiven = NOT_GIVEN, + stream: Optional[Literal[False]] | Literal[True] | NotGiven = NOT_GIVEN, + temperature: Optional[float] | NotGiven = NOT_GIVEN, + thread: thread_create_and_run_params.Thread | NotGiven = NOT_GIVEN, + tools: Optional[Iterable[thread_create_and_run_params.Tool]] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Run | Stream[AssistantStreamEvent]: + extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})} + return self._post( + "/threads/runs", + body=maybe_transform( + { + "assistant_id": assistant_id, + "instructions": instructions, + "metadata": metadata, + "model": model, + "stream": stream, + "temperature": temperature, + "thread": thread, + "tools": tools, + }, + thread_create_and_run_params.ThreadCreateAndRunParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Run, + stream=stream or False, + stream_cls=Stream[AssistantStreamEvent], + ) + + def create_and_run_poll( + self, + *, + assistant_id: str, + instructions: Optional[str] | NotGiven = NOT_GIVEN, + metadata: Optional[object] | NotGiven = NOT_GIVEN, + model: Optional[str] | NotGiven = NOT_GIVEN, + temperature: Optional[float] | NotGiven = NOT_GIVEN, + thread: thread_create_and_run_params.Thread | NotGiven = NOT_GIVEN, + tools: Optional[Iterable[thread_create_and_run_params.Tool]] | NotGiven = NOT_GIVEN, + poll_interval_ms: int | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Run: + """ + A helper to create a thread, start a run and then poll for a terminal state. + More information on Run lifecycles can be found here: + https://platform.openai.com/docs/assistants/how-it-works/runs-and-run-steps + """ + run = self.create_and_run( + assistant_id=assistant_id, + instructions=instructions, + metadata=metadata, + model=model, + temperature=temperature, + stream=False, + thread=thread, + tools=tools, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ) + return self.runs.poll(run.id, run.thread_id, extra_headers, extra_query, extra_body, timeout, poll_interval_ms) + + @overload + def create_and_run_stream( + self, + *, + assistant_id: str, + instructions: Optional[str] | NotGiven = NOT_GIVEN, + metadata: Optional[object] | NotGiven = NOT_GIVEN, + model: Optional[str] | NotGiven = NOT_GIVEN, + temperature: Optional[float] | NotGiven = NOT_GIVEN, + thread: thread_create_and_run_params.Thread | NotGiven = NOT_GIVEN, + tools: Optional[Iterable[thread_create_and_run_params.Tool]] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AssistantStreamManager[AssistantEventHandler]: + """Create a thread and stream the run back""" + ... + + @overload + def create_and_run_stream( + self, + *, + assistant_id: str, + instructions: Optional[str] | NotGiven = NOT_GIVEN, + metadata: Optional[object] | NotGiven = NOT_GIVEN, + model: Optional[str] | NotGiven = NOT_GIVEN, + temperature: Optional[float] | NotGiven = NOT_GIVEN, + thread: thread_create_and_run_params.Thread | NotGiven = NOT_GIVEN, + tools: Optional[Iterable[thread_create_and_run_params.Tool]] | NotGiven = NOT_GIVEN, + event_handler: AssistantEventHandlerT, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AssistantStreamManager[AssistantEventHandlerT]: + """Create a thread and stream the run back""" + ... + + def create_and_run_stream( + self, + *, + assistant_id: str, + instructions: Optional[str] | NotGiven = NOT_GIVEN, + metadata: Optional[object] | NotGiven = NOT_GIVEN, + model: Optional[str] | NotGiven = NOT_GIVEN, + temperature: Optional[float] | NotGiven = NOT_GIVEN, + thread: thread_create_and_run_params.Thread | NotGiven = NOT_GIVEN, + tools: Optional[Iterable[thread_create_and_run_params.Tool]] | NotGiven = NOT_GIVEN, + event_handler: AssistantEventHandlerT | None = None, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AssistantStreamManager[AssistantEventHandler] | AssistantStreamManager[AssistantEventHandlerT]: + """Create a thread and stream the run back""" + extra_headers = { + "OpenAI-Beta": "assistants=v1", + "X-Stainless-Stream-Helper": "threads.create_and_run_stream", + "X-Stainless-Custom-Event-Handler": "true" if event_handler else "false", + **(extra_headers or {}), + } + make_request = partial( + self._post, + "/threads/runs", + body=maybe_transform( + { + "assistant_id": assistant_id, + "instructions": instructions, + "metadata": metadata, + "model": model, + "temperature": temperature, + "stream": True, + "thread": thread, + "tools": tools, + }, + thread_create_and_run_params.ThreadCreateAndRunParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Run, + stream=True, + stream_cls=Stream[AssistantStreamEvent], + ) + return AssistantStreamManager(make_request, event_handler=event_handler or AssistantEventHandler()) + + +class AsyncThreads(AsyncAPIResource): + @cached_property + def runs(self) -> AsyncRuns: + return AsyncRuns(self._client) + + @cached_property + def messages(self) -> AsyncMessages: + return AsyncMessages(self._client) + + @cached_property + def with_raw_response(self) -> AsyncThreadsWithRawResponse: + return AsyncThreadsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncThreadsWithStreamingResponse: + return AsyncThreadsWithStreamingResponse(self) + + async def create( + self, + *, + messages: Iterable[thread_create_params.Message] | NotGiven = NOT_GIVEN, + metadata: Optional[object] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Thread: + """ + Create a thread. + + Args: + messages: A list of [messages](https://platform.openai.com/docs/api-reference/messages) to + start the thread with. + + metadata: Set of 16 key-value pairs that can be attached to an object. This can be useful + for storing additional information about the object in a structured format. Keys + can be a maximum of 64 characters long and values can be a maxium of 512 + characters long. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})} + return await self._post( + "/threads", + body=await async_maybe_transform( + { + "messages": messages, + "metadata": metadata, + }, + thread_create_params.ThreadCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Thread, + ) + + async def retrieve( + self, + thread_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Thread: + """ + Retrieves a thread. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})} + return await self._get( + f"/threads/{thread_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Thread, + ) + + async def update( + self, + thread_id: str, + *, + metadata: Optional[object] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Thread: + """ + Modifies a thread. + + Args: + metadata: Set of 16 key-value pairs that can be attached to an object. This can be useful + for storing additional information about the object in a structured format. Keys + can be a maximum of 64 characters long and values can be a maxium of 512 + characters long. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})} + return await self._post( + f"/threads/{thread_id}", + body=await async_maybe_transform({"metadata": metadata}, thread_update_params.ThreadUpdateParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Thread, + ) + + async def delete( + self, + thread_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ThreadDeleted: + """ + Delete a thread. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})} + return await self._delete( + f"/threads/{thread_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ThreadDeleted, + ) + + @overload + async def create_and_run( + self, + *, + assistant_id: str, + instructions: Optional[str] | NotGiven = NOT_GIVEN, + metadata: Optional[object] | NotGiven = NOT_GIVEN, + model: Optional[str] | NotGiven = NOT_GIVEN, + stream: Optional[Literal[False]] | NotGiven = NOT_GIVEN, + temperature: Optional[float] | NotGiven = NOT_GIVEN, + thread: thread_create_and_run_params.Thread | NotGiven = NOT_GIVEN, + tools: Optional[Iterable[thread_create_and_run_params.Tool]] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Run: + """ + Create a thread and run it in one request. + + Args: + assistant_id: The ID of the + [assistant](https://platform.openai.com/docs/api-reference/assistants) to use to + execute this run. + + instructions: Override the default system message of the assistant. This is useful for + modifying the behavior on a per-run basis. + + metadata: Set of 16 key-value pairs that can be attached to an object. This can be useful + for storing additional information about the object in a structured format. Keys + can be a maximum of 64 characters long and values can be a maxium of 512 + characters long. + + model: The ID of the [Model](https://platform.openai.com/docs/api-reference/models) to + be used to execute this run. If a value is provided here, it will override the + model associated with the assistant. If not, the model associated with the + assistant will be used. + + stream: If `true`, returns a stream of events that happen during the Run as server-sent + events, terminating when the Run enters a terminal state with a `data: [DONE]` + message. + + temperature: What sampling temperature to use, between 0 and 2. Higher values like 0.8 will + make the output more random, while lower values like 0.2 will make it more + focused and deterministic. + + thread: If no thread is provided, an empty thread will be created. + + tools: Override the tools the assistant can use for this run. This is useful for + modifying the behavior on a per-run basis. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + ... + + @overload + async def create_and_run( + self, + *, + assistant_id: str, + stream: Literal[True], + instructions: Optional[str] | NotGiven = NOT_GIVEN, + metadata: Optional[object] | NotGiven = NOT_GIVEN, + model: Optional[str] | NotGiven = NOT_GIVEN, + temperature: Optional[float] | NotGiven = NOT_GIVEN, + thread: thread_create_and_run_params.Thread | NotGiven = NOT_GIVEN, + tools: Optional[Iterable[thread_create_and_run_params.Tool]] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncStream[AssistantStreamEvent]: + """ + Create a thread and run it in one request. + + Args: + assistant_id: The ID of the + [assistant](https://platform.openai.com/docs/api-reference/assistants) to use to + execute this run. + + stream: If `true`, returns a stream of events that happen during the Run as server-sent + events, terminating when the Run enters a terminal state with a `data: [DONE]` + message. + + instructions: Override the default system message of the assistant. This is useful for + modifying the behavior on a per-run basis. + + metadata: Set of 16 key-value pairs that can be attached to an object. This can be useful + for storing additional information about the object in a structured format. Keys + can be a maximum of 64 characters long and values can be a maxium of 512 + characters long. + + model: The ID of the [Model](https://platform.openai.com/docs/api-reference/models) to + be used to execute this run. If a value is provided here, it will override the + model associated with the assistant. If not, the model associated with the + assistant will be used. + + temperature: What sampling temperature to use, between 0 and 2. Higher values like 0.8 will + make the output more random, while lower values like 0.2 will make it more + focused and deterministic. + + thread: If no thread is provided, an empty thread will be created. + + tools: Override the tools the assistant can use for this run. This is useful for + modifying the behavior on a per-run basis. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + ... + + @overload + async def create_and_run( + self, + *, + assistant_id: str, + stream: bool, + instructions: Optional[str] | NotGiven = NOT_GIVEN, + metadata: Optional[object] | NotGiven = NOT_GIVEN, + model: Optional[str] | NotGiven = NOT_GIVEN, + temperature: Optional[float] | NotGiven = NOT_GIVEN, + thread: thread_create_and_run_params.Thread | NotGiven = NOT_GIVEN, + tools: Optional[Iterable[thread_create_and_run_params.Tool]] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Run | AsyncStream[AssistantStreamEvent]: + """ + Create a thread and run it in one request. + + Args: + assistant_id: The ID of the + [assistant](https://platform.openai.com/docs/api-reference/assistants) to use to + execute this run. + + stream: If `true`, returns a stream of events that happen during the Run as server-sent + events, terminating when the Run enters a terminal state with a `data: [DONE]` + message. + + instructions: Override the default system message of the assistant. This is useful for + modifying the behavior on a per-run basis. + + metadata: Set of 16 key-value pairs that can be attached to an object. This can be useful + for storing additional information about the object in a structured format. Keys + can be a maximum of 64 characters long and values can be a maxium of 512 + characters long. + + model: The ID of the [Model](https://platform.openai.com/docs/api-reference/models) to + be used to execute this run. If a value is provided here, it will override the + model associated with the assistant. If not, the model associated with the + assistant will be used. + + temperature: What sampling temperature to use, between 0 and 2. Higher values like 0.8 will + make the output more random, while lower values like 0.2 will make it more + focused and deterministic. + + thread: If no thread is provided, an empty thread will be created. + + tools: Override the tools the assistant can use for this run. This is useful for + modifying the behavior on a per-run basis. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + ... + + @required_args(["assistant_id"], ["assistant_id", "stream"]) + async def create_and_run( + self, + *, + assistant_id: str, + instructions: Optional[str] | NotGiven = NOT_GIVEN, + metadata: Optional[object] | NotGiven = NOT_GIVEN, + model: Optional[str] | NotGiven = NOT_GIVEN, + stream: Optional[Literal[False]] | Literal[True] | NotGiven = NOT_GIVEN, + temperature: Optional[float] | NotGiven = NOT_GIVEN, + thread: thread_create_and_run_params.Thread | NotGiven = NOT_GIVEN, + tools: Optional[Iterable[thread_create_and_run_params.Tool]] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Run | AsyncStream[AssistantStreamEvent]: + extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})} + return await self._post( + "/threads/runs", + body=await async_maybe_transform( + { + "assistant_id": assistant_id, + "instructions": instructions, + "metadata": metadata, + "model": model, + "stream": stream, + "temperature": temperature, + "thread": thread, + "tools": tools, + }, + thread_create_and_run_params.ThreadCreateAndRunParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Run, + stream=stream or False, + stream_cls=AsyncStream[AssistantStreamEvent], + ) + + async def create_and_run_poll( + self, + *, + assistant_id: str, + instructions: Optional[str] | NotGiven = NOT_GIVEN, + metadata: Optional[object] | NotGiven = NOT_GIVEN, + model: Optional[str] | NotGiven = NOT_GIVEN, + temperature: Optional[float] | NotGiven = NOT_GIVEN, + thread: thread_create_and_run_params.Thread | NotGiven = NOT_GIVEN, + tools: Optional[Iterable[thread_create_and_run_params.Tool]] | NotGiven = NOT_GIVEN, + poll_interval_ms: int | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Run: + """ + A helper to create a thread, start a run and then poll for a terminal state. + More information on Run lifecycles can be found here: + https://platform.openai.com/docs/assistants/how-it-works/runs-and-run-steps + """ + run = await self.create_and_run( + assistant_id=assistant_id, + instructions=instructions, + metadata=metadata, + model=model, + temperature=temperature, + stream=False, + thread=thread, + tools=tools, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ) + return await self.runs.poll( + run.id, run.thread_id, extra_headers, extra_query, extra_body, timeout, poll_interval_ms + ) + + @overload + def create_and_run_stream( + self, + *, + assistant_id: str, + instructions: Optional[str] | NotGiven = NOT_GIVEN, + metadata: Optional[object] | NotGiven = NOT_GIVEN, + model: Optional[str] | NotGiven = NOT_GIVEN, + temperature: Optional[float] | NotGiven = NOT_GIVEN, + thread: thread_create_and_run_params.Thread | NotGiven = NOT_GIVEN, + tools: Optional[Iterable[thread_create_and_run_params.Tool]] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncAssistantStreamManager[AsyncAssistantEventHandler]: + """Create a thread and stream the run back""" + ... + + @overload + def create_and_run_stream( + self, + *, + assistant_id: str, + instructions: Optional[str] | NotGiven = NOT_GIVEN, + metadata: Optional[object] | NotGiven = NOT_GIVEN, + model: Optional[str] | NotGiven = NOT_GIVEN, + temperature: Optional[float] | NotGiven = NOT_GIVEN, + thread: thread_create_and_run_params.Thread | NotGiven = NOT_GIVEN, + tools: Optional[Iterable[thread_create_and_run_params.Tool]] | NotGiven = NOT_GIVEN, + event_handler: AsyncAssistantEventHandlerT, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncAssistantStreamManager[AsyncAssistantEventHandlerT]: + """Create a thread and stream the run back""" + ... + + def create_and_run_stream( + self, + *, + assistant_id: str, + instructions: Optional[str] | NotGiven = NOT_GIVEN, + metadata: Optional[object] | NotGiven = NOT_GIVEN, + model: Optional[str] | NotGiven = NOT_GIVEN, + temperature: Optional[float] | NotGiven = NOT_GIVEN, + thread: thread_create_and_run_params.Thread | NotGiven = NOT_GIVEN, + tools: Optional[Iterable[thread_create_and_run_params.Tool]] | NotGiven = NOT_GIVEN, + event_handler: AsyncAssistantEventHandlerT | None = None, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ( + AsyncAssistantStreamManager[AsyncAssistantEventHandler] + | AsyncAssistantStreamManager[AsyncAssistantEventHandlerT] + ): + """Create a thread and stream the run back""" + extra_headers = { + "OpenAI-Beta": "assistants=v1", + "X-Stainless-Stream-Helper": "threads.create_and_run_stream", + "X-Stainless-Custom-Event-Handler": "true" if event_handler else "false", + **(extra_headers or {}), + } + request = self._post( + "/threads/runs", + body=maybe_transform( + { + "assistant_id": assistant_id, + "instructions": instructions, + "metadata": metadata, + "model": model, + "temperature": temperature, + "stream": True, + "thread": thread, + "tools": tools, + }, + thread_create_and_run_params.ThreadCreateAndRunParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Run, + stream=True, + stream_cls=AsyncStream[AssistantStreamEvent], + ) + return AsyncAssistantStreamManager(request, event_handler=event_handler or AsyncAssistantEventHandler()) + + +class ThreadsWithRawResponse: + def __init__(self, threads: Threads) -> None: + self._threads = threads + + self.create = _legacy_response.to_raw_response_wrapper( + threads.create, + ) + self.retrieve = _legacy_response.to_raw_response_wrapper( + threads.retrieve, + ) + self.update = _legacy_response.to_raw_response_wrapper( + threads.update, + ) + self.delete = _legacy_response.to_raw_response_wrapper( + threads.delete, + ) + self.create_and_run = _legacy_response.to_raw_response_wrapper( + threads.create_and_run, + ) + + @cached_property + def runs(self) -> RunsWithRawResponse: + return RunsWithRawResponse(self._threads.runs) + + @cached_property + def messages(self) -> MessagesWithRawResponse: + return MessagesWithRawResponse(self._threads.messages) + + +class AsyncThreadsWithRawResponse: + def __init__(self, threads: AsyncThreads) -> None: + self._threads = threads + + self.create = _legacy_response.async_to_raw_response_wrapper( + threads.create, + ) + self.retrieve = _legacy_response.async_to_raw_response_wrapper( + threads.retrieve, + ) + self.update = _legacy_response.async_to_raw_response_wrapper( + threads.update, + ) + self.delete = _legacy_response.async_to_raw_response_wrapper( + threads.delete, + ) + self.create_and_run = _legacy_response.async_to_raw_response_wrapper( + threads.create_and_run, + ) + + @cached_property + def runs(self) -> AsyncRunsWithRawResponse: + return AsyncRunsWithRawResponse(self._threads.runs) + + @cached_property + def messages(self) -> AsyncMessagesWithRawResponse: + return AsyncMessagesWithRawResponse(self._threads.messages) + + +class ThreadsWithStreamingResponse: + def __init__(self, threads: Threads) -> None: + self._threads = threads + + self.create = to_streamed_response_wrapper( + threads.create, + ) + self.retrieve = to_streamed_response_wrapper( + threads.retrieve, + ) + self.update = to_streamed_response_wrapper( + threads.update, + ) + self.delete = to_streamed_response_wrapper( + threads.delete, + ) + self.create_and_run = to_streamed_response_wrapper( + threads.create_and_run, + ) + + @cached_property + def runs(self) -> RunsWithStreamingResponse: + return RunsWithStreamingResponse(self._threads.runs) + + @cached_property + def messages(self) -> MessagesWithStreamingResponse: + return MessagesWithStreamingResponse(self._threads.messages) + + +class AsyncThreadsWithStreamingResponse: + def __init__(self, threads: AsyncThreads) -> None: + self._threads = threads + + self.create = async_to_streamed_response_wrapper( + threads.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + threads.retrieve, + ) + self.update = async_to_streamed_response_wrapper( + threads.update, + ) + self.delete = async_to_streamed_response_wrapper( + threads.delete, + ) + self.create_and_run = async_to_streamed_response_wrapper( + threads.create_and_run, + ) + + @cached_property + def runs(self) -> AsyncRunsWithStreamingResponse: + return AsyncRunsWithStreamingResponse(self._threads.runs) + + @cached_property + def messages(self) -> AsyncMessagesWithStreamingResponse: + return AsyncMessagesWithStreamingResponse(self._threads.messages) diff --git a/openai/resources/chat/__init__.py b/openai/resources/chat/__init__.py new file mode 100644 index 00000000..52dfdcea --- /dev/null +++ b/openai/resources/chat/__init__.py @@ -0,0 +1,33 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .chat import ( + Chat, + AsyncChat, + ChatWithRawResponse, + AsyncChatWithRawResponse, + ChatWithStreamingResponse, + AsyncChatWithStreamingResponse, +) +from .completions import ( + Completions, + AsyncCompletions, + CompletionsWithRawResponse, + AsyncCompletionsWithRawResponse, + CompletionsWithStreamingResponse, + AsyncCompletionsWithStreamingResponse, +) + +__all__ = [ + "Completions", + "AsyncCompletions", + "CompletionsWithRawResponse", + "AsyncCompletionsWithRawResponse", + "CompletionsWithStreamingResponse", + "AsyncCompletionsWithStreamingResponse", + "Chat", + "AsyncChat", + "ChatWithRawResponse", + "AsyncChatWithRawResponse", + "ChatWithStreamingResponse", + "AsyncChatWithStreamingResponse", +] diff --git a/openai/resources/chat/chat.py b/openai/resources/chat/chat.py new file mode 100644 index 00000000..d14d0555 --- /dev/null +++ b/openai/resources/chat/chat.py @@ -0,0 +1,80 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from ..._compat import cached_property +from ..._resource import SyncAPIResource, AsyncAPIResource +from .completions import ( + Completions, + AsyncCompletions, + CompletionsWithRawResponse, + AsyncCompletionsWithRawResponse, + CompletionsWithStreamingResponse, + AsyncCompletionsWithStreamingResponse, +) + +__all__ = ["Chat", "AsyncChat"] + + +class Chat(SyncAPIResource): + @cached_property + def completions(self) -> Completions: + return Completions(self._client) + + @cached_property + def with_raw_response(self) -> ChatWithRawResponse: + return ChatWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> ChatWithStreamingResponse: + return ChatWithStreamingResponse(self) + + +class AsyncChat(AsyncAPIResource): + @cached_property + def completions(self) -> AsyncCompletions: + return AsyncCompletions(self._client) + + @cached_property + def with_raw_response(self) -> AsyncChatWithRawResponse: + return AsyncChatWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncChatWithStreamingResponse: + return AsyncChatWithStreamingResponse(self) + + +class ChatWithRawResponse: + def __init__(self, chat: Chat) -> None: + self._chat = chat + + @cached_property + def completions(self) -> CompletionsWithRawResponse: + return CompletionsWithRawResponse(self._chat.completions) + + +class AsyncChatWithRawResponse: + def __init__(self, chat: AsyncChat) -> None: + self._chat = chat + + @cached_property + def completions(self) -> AsyncCompletionsWithRawResponse: + return AsyncCompletionsWithRawResponse(self._chat.completions) + + +class ChatWithStreamingResponse: + def __init__(self, chat: Chat) -> None: + self._chat = chat + + @cached_property + def completions(self) -> CompletionsWithStreamingResponse: + return CompletionsWithStreamingResponse(self._chat.completions) + + +class AsyncChatWithStreamingResponse: + def __init__(self, chat: AsyncChat) -> None: + self._chat = chat + + @cached_property + def completions(self) -> AsyncCompletionsWithStreamingResponse: + return AsyncCompletionsWithStreamingResponse(self._chat.completions) diff --git a/openai/resources/chat/completions.py b/openai/resources/chat/completions.py new file mode 100644 index 00000000..30006036 --- /dev/null +++ b/openai/resources/chat/completions.py @@ -0,0 +1,1403 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Dict, List, Union, Iterable, Optional, overload +from typing_extensions import Literal + +import httpx + +from ... import _legacy_response +from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from ..._utils import ( + required_args, + maybe_transform, + async_maybe_transform, +) +from ..._compat import cached_property +from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper +from ..._streaming import Stream, AsyncStream +from ...types.chat import ( + ChatCompletion, + ChatCompletionChunk, + ChatCompletionToolParam, + ChatCompletionMessageParam, + ChatCompletionToolChoiceOptionParam, + completion_create_params, +) +from ..._base_client import ( + make_request_options, +) + +__all__ = ["Completions", "AsyncCompletions"] + + +class Completions(SyncAPIResource): + @cached_property + def with_raw_response(self) -> CompletionsWithRawResponse: + return CompletionsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> CompletionsWithStreamingResponse: + return CompletionsWithStreamingResponse(self) + + @overload + def create( + self, + *, + messages: Iterable[ChatCompletionMessageParam], + model: Union[ + str, + Literal[ + "gpt-4-0125-preview", + "gpt-4-turbo-preview", + "gpt-4-1106-preview", + "gpt-4-vision-preview", + "gpt-4", + "gpt-4-0314", + "gpt-4-0613", + "gpt-4-32k", + "gpt-4-32k-0314", + "gpt-4-32k-0613", + "gpt-3.5-turbo", + "gpt-3.5-turbo-16k", + "gpt-3.5-turbo-0301", + "gpt-3.5-turbo-0613", + "gpt-3.5-turbo-1106", + "gpt-3.5-turbo-0125", + "gpt-3.5-turbo-16k-0613", + ], + ], + frequency_penalty: Optional[float] | NotGiven = NOT_GIVEN, + function_call: completion_create_params.FunctionCall | NotGiven = NOT_GIVEN, + functions: Iterable[completion_create_params.Function] | NotGiven = NOT_GIVEN, + logit_bias: Optional[Dict[str, int]] | NotGiven = NOT_GIVEN, + logprobs: Optional[bool] | NotGiven = NOT_GIVEN, + max_tokens: Optional[int] | NotGiven = NOT_GIVEN, + n: Optional[int] | NotGiven = NOT_GIVEN, + presence_penalty: Optional[float] | NotGiven = NOT_GIVEN, + response_format: completion_create_params.ResponseFormat | NotGiven = NOT_GIVEN, + seed: Optional[int] | NotGiven = NOT_GIVEN, + stop: Union[Optional[str], List[str]] | NotGiven = NOT_GIVEN, + stream: Optional[Literal[False]] | NotGiven = NOT_GIVEN, + temperature: Optional[float] | NotGiven = NOT_GIVEN, + tool_choice: ChatCompletionToolChoiceOptionParam | NotGiven = NOT_GIVEN, + tools: Iterable[ChatCompletionToolParam] | NotGiven = NOT_GIVEN, + top_logprobs: Optional[int] | NotGiven = NOT_GIVEN, + top_p: Optional[float] | NotGiven = NOT_GIVEN, + user: str | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ChatCompletion: + """ + Creates a model response for the given chat conversation. + + Args: + messages: A list of messages comprising the conversation so far. + [Example Python code](https://cookbook.openai.com/examples/how_to_format_inputs_to_chatgpt_models). + + model: ID of the model to use. See the + [model endpoint compatibility](https://platform.openai.com/docs/models/model-endpoint-compatibility) + table for details on which models work with the Chat API. + + frequency_penalty: Number between -2.0 and 2.0. Positive values penalize new tokens based on their + existing frequency in the text so far, decreasing the model's likelihood to + repeat the same line verbatim. + + [See more information about frequency and presence penalties.](https://platform.openai.com/docs/guides/text-generation/parameter-details) + + function_call: Deprecated in favor of `tool_choice`. + + Controls which (if any) function is called by the model. `none` means the model + will not call a function and instead generates a message. `auto` means the model + can pick between generating a message or calling a function. Specifying a + particular function via `{"name": "my_function"}` forces the model to call that + function. + + `none` is the default when no functions are present. `auto` is the default if + functions are present. + + functions: Deprecated in favor of `tools`. + + A list of functions the model may generate JSON inputs for. + + logit_bias: Modify the likelihood of specified tokens appearing in the completion. + + Accepts a JSON object that maps tokens (specified by their token ID in the + tokenizer) to an associated bias value from -100 to 100. Mathematically, the + bias is added to the logits generated by the model prior to sampling. The exact + effect will vary per model, but values between -1 and 1 should decrease or + increase likelihood of selection; values like -100 or 100 should result in a ban + or exclusive selection of the relevant token. + + logprobs: Whether to return log probabilities of the output tokens or not. If true, + returns the log probabilities of each output token returned in the `content` of + `message`. This option is currently not available on the `gpt-4-vision-preview` + model. + + max_tokens: The maximum number of [tokens](/tokenizer) that can be generated in the chat + completion. + + The total length of input tokens and generated tokens is limited by the model's + context length. + [Example Python code](https://cookbook.openai.com/examples/how_to_count_tokens_with_tiktoken) + for counting tokens. + + n: How many chat completion choices to generate for each input message. Note that + you will be charged based on the number of generated tokens across all of the + choices. Keep `n` as `1` to minimize costs. + + presence_penalty: Number between -2.0 and 2.0. Positive values penalize new tokens based on + whether they appear in the text so far, increasing the model's likelihood to + talk about new topics. + + [See more information about frequency and presence penalties.](https://platform.openai.com/docs/guides/text-generation/parameter-details) + + response_format: An object specifying the format that the model must output. Compatible with + [GPT-4 Turbo](https://platform.openai.com/docs/models/gpt-4-and-gpt-4-turbo) and + all GPT-3.5 Turbo models newer than `gpt-3.5-turbo-1106`. + + Setting to `{ "type": "json_object" }` enables JSON mode, which guarantees the + message the model generates is valid JSON. + + **Important:** when using JSON mode, you **must** also instruct the model to + produce JSON yourself via a system or user message. Without this, the model may + generate an unending stream of whitespace until the generation reaches the token + limit, resulting in a long-running and seemingly "stuck" request. Also note that + the message content may be partially cut off if `finish_reason="length"`, which + indicates the generation exceeded `max_tokens` or the conversation exceeded the + max context length. + + seed: This feature is in Beta. If specified, our system will make a best effort to + sample deterministically, such that repeated requests with the same `seed` and + parameters should return the same result. Determinism is not guaranteed, and you + should refer to the `system_fingerprint` response parameter to monitor changes + in the backend. + + stop: Up to 4 sequences where the API will stop generating further tokens. + + stream: If set, partial message deltas will be sent, like in ChatGPT. Tokens will be + sent as data-only + [server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format) + as they become available, with the stream terminated by a `data: [DONE]` + message. + [Example Python code](https://cookbook.openai.com/examples/how_to_stream_completions). + + temperature: What sampling temperature to use, between 0 and 2. Higher values like 0.8 will + make the output more random, while lower values like 0.2 will make it more + focused and deterministic. + + We generally recommend altering this or `top_p` but not both. + + tool_choice: Controls which (if any) function is called by the model. `none` means the model + will not call a function and instead generates a message. `auto` means the model + can pick between generating a message or calling a function. Specifying a + particular function via + `{"type": "function", "function": {"name": "my_function"}}` forces the model to + call that function. + + `none` is the default when no functions are present. `auto` is the default if + functions are present. + + tools: A list of tools the model may call. Currently, only functions are supported as a + tool. Use this to provide a list of functions the model may generate JSON inputs + for. A max of 128 functions are supported. + + top_logprobs: An integer between 0 and 20 specifying the number of most likely tokens to + return at each token position, each with an associated log probability. + `logprobs` must be set to `true` if this parameter is used. + + top_p: An alternative to sampling with temperature, called nucleus sampling, where the + model considers the results of the tokens with top_p probability mass. So 0.1 + means only the tokens comprising the top 10% probability mass are considered. + + We generally recommend altering this or `temperature` but not both. + + user: A unique identifier representing your end-user, which can help OpenAI to monitor + and detect abuse. + [Learn more](https://platform.openai.com/docs/guides/safety-best-practices/end-user-ids). + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + ... + + @overload + def create( + self, + *, + messages: Iterable[ChatCompletionMessageParam], + model: Union[ + str, + Literal[ + "gpt-4-0125-preview", + "gpt-4-turbo-preview", + "gpt-4-1106-preview", + "gpt-4-vision-preview", + "gpt-4", + "gpt-4-0314", + "gpt-4-0613", + "gpt-4-32k", + "gpt-4-32k-0314", + "gpt-4-32k-0613", + "gpt-3.5-turbo", + "gpt-3.5-turbo-16k", + "gpt-3.5-turbo-0301", + "gpt-3.5-turbo-0613", + "gpt-3.5-turbo-1106", + "gpt-3.5-turbo-0125", + "gpt-3.5-turbo-16k-0613", + ], + ], + stream: Literal[True], + frequency_penalty: Optional[float] | NotGiven = NOT_GIVEN, + function_call: completion_create_params.FunctionCall | NotGiven = NOT_GIVEN, + functions: Iterable[completion_create_params.Function] | NotGiven = NOT_GIVEN, + logit_bias: Optional[Dict[str, int]] | NotGiven = NOT_GIVEN, + logprobs: Optional[bool] | NotGiven = NOT_GIVEN, + max_tokens: Optional[int] | NotGiven = NOT_GIVEN, + n: Optional[int] | NotGiven = NOT_GIVEN, + presence_penalty: Optional[float] | NotGiven = NOT_GIVEN, + response_format: completion_create_params.ResponseFormat | NotGiven = NOT_GIVEN, + seed: Optional[int] | NotGiven = NOT_GIVEN, + stop: Union[Optional[str], List[str]] | NotGiven = NOT_GIVEN, + temperature: Optional[float] | NotGiven = NOT_GIVEN, + tool_choice: ChatCompletionToolChoiceOptionParam | NotGiven = NOT_GIVEN, + tools: Iterable[ChatCompletionToolParam] | NotGiven = NOT_GIVEN, + top_logprobs: Optional[int] | NotGiven = NOT_GIVEN, + top_p: Optional[float] | NotGiven = NOT_GIVEN, + user: str | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Stream[ChatCompletionChunk]: + """ + Creates a model response for the given chat conversation. + + Args: + messages: A list of messages comprising the conversation so far. + [Example Python code](https://cookbook.openai.com/examples/how_to_format_inputs_to_chatgpt_models). + + model: ID of the model to use. See the + [model endpoint compatibility](https://platform.openai.com/docs/models/model-endpoint-compatibility) + table for details on which models work with the Chat API. + + stream: If set, partial message deltas will be sent, like in ChatGPT. Tokens will be + sent as data-only + [server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format) + as they become available, with the stream terminated by a `data: [DONE]` + message. + [Example Python code](https://cookbook.openai.com/examples/how_to_stream_completions). + + frequency_penalty: Number between -2.0 and 2.0. Positive values penalize new tokens based on their + existing frequency in the text so far, decreasing the model's likelihood to + repeat the same line verbatim. + + [See more information about frequency and presence penalties.](https://platform.openai.com/docs/guides/text-generation/parameter-details) + + function_call: Deprecated in favor of `tool_choice`. + + Controls which (if any) function is called by the model. `none` means the model + will not call a function and instead generates a message. `auto` means the model + can pick between generating a message or calling a function. Specifying a + particular function via `{"name": "my_function"}` forces the model to call that + function. + + `none` is the default when no functions are present. `auto` is the default if + functions are present. + + functions: Deprecated in favor of `tools`. + + A list of functions the model may generate JSON inputs for. + + logit_bias: Modify the likelihood of specified tokens appearing in the completion. + + Accepts a JSON object that maps tokens (specified by their token ID in the + tokenizer) to an associated bias value from -100 to 100. Mathematically, the + bias is added to the logits generated by the model prior to sampling. The exact + effect will vary per model, but values between -1 and 1 should decrease or + increase likelihood of selection; values like -100 or 100 should result in a ban + or exclusive selection of the relevant token. + + logprobs: Whether to return log probabilities of the output tokens or not. If true, + returns the log probabilities of each output token returned in the `content` of + `message`. This option is currently not available on the `gpt-4-vision-preview` + model. + + max_tokens: The maximum number of [tokens](/tokenizer) that can be generated in the chat + completion. + + The total length of input tokens and generated tokens is limited by the model's + context length. + [Example Python code](https://cookbook.openai.com/examples/how_to_count_tokens_with_tiktoken) + for counting tokens. + + n: How many chat completion choices to generate for each input message. Note that + you will be charged based on the number of generated tokens across all of the + choices. Keep `n` as `1` to minimize costs. + + presence_penalty: Number between -2.0 and 2.0. Positive values penalize new tokens based on + whether they appear in the text so far, increasing the model's likelihood to + talk about new topics. + + [See more information about frequency and presence penalties.](https://platform.openai.com/docs/guides/text-generation/parameter-details) + + response_format: An object specifying the format that the model must output. Compatible with + [GPT-4 Turbo](https://platform.openai.com/docs/models/gpt-4-and-gpt-4-turbo) and + all GPT-3.5 Turbo models newer than `gpt-3.5-turbo-1106`. + + Setting to `{ "type": "json_object" }` enables JSON mode, which guarantees the + message the model generates is valid JSON. + + **Important:** when using JSON mode, you **must** also instruct the model to + produce JSON yourself via a system or user message. Without this, the model may + generate an unending stream of whitespace until the generation reaches the token + limit, resulting in a long-running and seemingly "stuck" request. Also note that + the message content may be partially cut off if `finish_reason="length"`, which + indicates the generation exceeded `max_tokens` or the conversation exceeded the + max context length. + + seed: This feature is in Beta. If specified, our system will make a best effort to + sample deterministically, such that repeated requests with the same `seed` and + parameters should return the same result. Determinism is not guaranteed, and you + should refer to the `system_fingerprint` response parameter to monitor changes + in the backend. + + stop: Up to 4 sequences where the API will stop generating further tokens. + + temperature: What sampling temperature to use, between 0 and 2. Higher values like 0.8 will + make the output more random, while lower values like 0.2 will make it more + focused and deterministic. + + We generally recommend altering this or `top_p` but not both. + + tool_choice: Controls which (if any) function is called by the model. `none` means the model + will not call a function and instead generates a message. `auto` means the model + can pick between generating a message or calling a function. Specifying a + particular function via + `{"type": "function", "function": {"name": "my_function"}}` forces the model to + call that function. + + `none` is the default when no functions are present. `auto` is the default if + functions are present. + + tools: A list of tools the model may call. Currently, only functions are supported as a + tool. Use this to provide a list of functions the model may generate JSON inputs + for. A max of 128 functions are supported. + + top_logprobs: An integer between 0 and 20 specifying the number of most likely tokens to + return at each token position, each with an associated log probability. + `logprobs` must be set to `true` if this parameter is used. + + top_p: An alternative to sampling with temperature, called nucleus sampling, where the + model considers the results of the tokens with top_p probability mass. So 0.1 + means only the tokens comprising the top 10% probability mass are considered. + + We generally recommend altering this or `temperature` but not both. + + user: A unique identifier representing your end-user, which can help OpenAI to monitor + and detect abuse. + [Learn more](https://platform.openai.com/docs/guides/safety-best-practices/end-user-ids). + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + ... + + @overload + def create( + self, + *, + messages: Iterable[ChatCompletionMessageParam], + model: Union[ + str, + Literal[ + "gpt-4-0125-preview", + "gpt-4-turbo-preview", + "gpt-4-1106-preview", + "gpt-4-vision-preview", + "gpt-4", + "gpt-4-0314", + "gpt-4-0613", + "gpt-4-32k", + "gpt-4-32k-0314", + "gpt-4-32k-0613", + "gpt-3.5-turbo", + "gpt-3.5-turbo-16k", + "gpt-3.5-turbo-0301", + "gpt-3.5-turbo-0613", + "gpt-3.5-turbo-1106", + "gpt-3.5-turbo-0125", + "gpt-3.5-turbo-16k-0613", + ], + ], + stream: bool, + frequency_penalty: Optional[float] | NotGiven = NOT_GIVEN, + function_call: completion_create_params.FunctionCall | NotGiven = NOT_GIVEN, + functions: Iterable[completion_create_params.Function] | NotGiven = NOT_GIVEN, + logit_bias: Optional[Dict[str, int]] | NotGiven = NOT_GIVEN, + logprobs: Optional[bool] | NotGiven = NOT_GIVEN, + max_tokens: Optional[int] | NotGiven = NOT_GIVEN, + n: Optional[int] | NotGiven = NOT_GIVEN, + presence_penalty: Optional[float] | NotGiven = NOT_GIVEN, + response_format: completion_create_params.ResponseFormat | NotGiven = NOT_GIVEN, + seed: Optional[int] | NotGiven = NOT_GIVEN, + stop: Union[Optional[str], List[str]] | NotGiven = NOT_GIVEN, + temperature: Optional[float] | NotGiven = NOT_GIVEN, + tool_choice: ChatCompletionToolChoiceOptionParam | NotGiven = NOT_GIVEN, + tools: Iterable[ChatCompletionToolParam] | NotGiven = NOT_GIVEN, + top_logprobs: Optional[int] | NotGiven = NOT_GIVEN, + top_p: Optional[float] | NotGiven = NOT_GIVEN, + user: str | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ChatCompletion | Stream[ChatCompletionChunk]: + """ + Creates a model response for the given chat conversation. + + Args: + messages: A list of messages comprising the conversation so far. + [Example Python code](https://cookbook.openai.com/examples/how_to_format_inputs_to_chatgpt_models). + + model: ID of the model to use. See the + [model endpoint compatibility](https://platform.openai.com/docs/models/model-endpoint-compatibility) + table for details on which models work with the Chat API. + + stream: If set, partial message deltas will be sent, like in ChatGPT. Tokens will be + sent as data-only + [server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format) + as they become available, with the stream terminated by a `data: [DONE]` + message. + [Example Python code](https://cookbook.openai.com/examples/how_to_stream_completions). + + frequency_penalty: Number between -2.0 and 2.0. Positive values penalize new tokens based on their + existing frequency in the text so far, decreasing the model's likelihood to + repeat the same line verbatim. + + [See more information about frequency and presence penalties.](https://platform.openai.com/docs/guides/text-generation/parameter-details) + + function_call: Deprecated in favor of `tool_choice`. + + Controls which (if any) function is called by the model. `none` means the model + will not call a function and instead generates a message. `auto` means the model + can pick between generating a message or calling a function. Specifying a + particular function via `{"name": "my_function"}` forces the model to call that + function. + + `none` is the default when no functions are present. `auto` is the default if + functions are present. + + functions: Deprecated in favor of `tools`. + + A list of functions the model may generate JSON inputs for. + + logit_bias: Modify the likelihood of specified tokens appearing in the completion. + + Accepts a JSON object that maps tokens (specified by their token ID in the + tokenizer) to an associated bias value from -100 to 100. Mathematically, the + bias is added to the logits generated by the model prior to sampling. The exact + effect will vary per model, but values between -1 and 1 should decrease or + increase likelihood of selection; values like -100 or 100 should result in a ban + or exclusive selection of the relevant token. + + logprobs: Whether to return log probabilities of the output tokens or not. If true, + returns the log probabilities of each output token returned in the `content` of + `message`. This option is currently not available on the `gpt-4-vision-preview` + model. + + max_tokens: The maximum number of [tokens](/tokenizer) that can be generated in the chat + completion. + + The total length of input tokens and generated tokens is limited by the model's + context length. + [Example Python code](https://cookbook.openai.com/examples/how_to_count_tokens_with_tiktoken) + for counting tokens. + + n: How many chat completion choices to generate for each input message. Note that + you will be charged based on the number of generated tokens across all of the + choices. Keep `n` as `1` to minimize costs. + + presence_penalty: Number between -2.0 and 2.0. Positive values penalize new tokens based on + whether they appear in the text so far, increasing the model's likelihood to + talk about new topics. + + [See more information about frequency and presence penalties.](https://platform.openai.com/docs/guides/text-generation/parameter-details) + + response_format: An object specifying the format that the model must output. Compatible with + [GPT-4 Turbo](https://platform.openai.com/docs/models/gpt-4-and-gpt-4-turbo) and + all GPT-3.5 Turbo models newer than `gpt-3.5-turbo-1106`. + + Setting to `{ "type": "json_object" }` enables JSON mode, which guarantees the + message the model generates is valid JSON. + + **Important:** when using JSON mode, you **must** also instruct the model to + produce JSON yourself via a system or user message. Without this, the model may + generate an unending stream of whitespace until the generation reaches the token + limit, resulting in a long-running and seemingly "stuck" request. Also note that + the message content may be partially cut off if `finish_reason="length"`, which + indicates the generation exceeded `max_tokens` or the conversation exceeded the + max context length. + + seed: This feature is in Beta. If specified, our system will make a best effort to + sample deterministically, such that repeated requests with the same `seed` and + parameters should return the same result. Determinism is not guaranteed, and you + should refer to the `system_fingerprint` response parameter to monitor changes + in the backend. + + stop: Up to 4 sequences where the API will stop generating further tokens. + + temperature: What sampling temperature to use, between 0 and 2. Higher values like 0.8 will + make the output more random, while lower values like 0.2 will make it more + focused and deterministic. + + We generally recommend altering this or `top_p` but not both. + + tool_choice: Controls which (if any) function is called by the model. `none` means the model + will not call a function and instead generates a message. `auto` means the model + can pick between generating a message or calling a function. Specifying a + particular function via + `{"type": "function", "function": {"name": "my_function"}}` forces the model to + call that function. + + `none` is the default when no functions are present. `auto` is the default if + functions are present. + + tools: A list of tools the model may call. Currently, only functions are supported as a + tool. Use this to provide a list of functions the model may generate JSON inputs + for. A max of 128 functions are supported. + + top_logprobs: An integer between 0 and 20 specifying the number of most likely tokens to + return at each token position, each with an associated log probability. + `logprobs` must be set to `true` if this parameter is used. + + top_p: An alternative to sampling with temperature, called nucleus sampling, where the + model considers the results of the tokens with top_p probability mass. So 0.1 + means only the tokens comprising the top 10% probability mass are considered. + + We generally recommend altering this or `temperature` but not both. + + user: A unique identifier representing your end-user, which can help OpenAI to monitor + and detect abuse. + [Learn more](https://platform.openai.com/docs/guides/safety-best-practices/end-user-ids). + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + ... + + @required_args(["messages", "model"], ["messages", "model", "stream"]) + def create( + self, + *, + messages: Iterable[ChatCompletionMessageParam], + model: Union[ + str, + Literal[ + "gpt-4-0125-preview", + "gpt-4-turbo-preview", + "gpt-4-1106-preview", + "gpt-4-vision-preview", + "gpt-4", + "gpt-4-0314", + "gpt-4-0613", + "gpt-4-32k", + "gpt-4-32k-0314", + "gpt-4-32k-0613", + "gpt-3.5-turbo", + "gpt-3.5-turbo-16k", + "gpt-3.5-turbo-0301", + "gpt-3.5-turbo-0613", + "gpt-3.5-turbo-1106", + "gpt-3.5-turbo-0125", + "gpt-3.5-turbo-16k-0613", + ], + ], + frequency_penalty: Optional[float] | NotGiven = NOT_GIVEN, + function_call: completion_create_params.FunctionCall | NotGiven = NOT_GIVEN, + functions: Iterable[completion_create_params.Function] | NotGiven = NOT_GIVEN, + logit_bias: Optional[Dict[str, int]] | NotGiven = NOT_GIVEN, + logprobs: Optional[bool] | NotGiven = NOT_GIVEN, + max_tokens: Optional[int] | NotGiven = NOT_GIVEN, + n: Optional[int] | NotGiven = NOT_GIVEN, + presence_penalty: Optional[float] | NotGiven = NOT_GIVEN, + response_format: completion_create_params.ResponseFormat | NotGiven = NOT_GIVEN, + seed: Optional[int] | NotGiven = NOT_GIVEN, + stop: Union[Optional[str], List[str]] | NotGiven = NOT_GIVEN, + stream: Optional[Literal[False]] | Literal[True] | NotGiven = NOT_GIVEN, + temperature: Optional[float] | NotGiven = NOT_GIVEN, + tool_choice: ChatCompletionToolChoiceOptionParam | NotGiven = NOT_GIVEN, + tools: Iterable[ChatCompletionToolParam] | NotGiven = NOT_GIVEN, + top_logprobs: Optional[int] | NotGiven = NOT_GIVEN, + top_p: Optional[float] | NotGiven = NOT_GIVEN, + user: str | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ChatCompletion | Stream[ChatCompletionChunk]: + return self._post( + "/chat/completions", + body=maybe_transform( + { + "messages": messages, + "model": model, + "frequency_penalty": frequency_penalty, + "function_call": function_call, + "functions": functions, + "logit_bias": logit_bias, + "logprobs": logprobs, + "max_tokens": max_tokens, + "n": n, + "presence_penalty": presence_penalty, + "response_format": response_format, + "seed": seed, + "stop": stop, + "stream": stream, + "temperature": temperature, + "tool_choice": tool_choice, + "tools": tools, + "top_logprobs": top_logprobs, + "top_p": top_p, + "user": user, + }, + completion_create_params.CompletionCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ChatCompletion, + stream=stream or False, + stream_cls=Stream[ChatCompletionChunk], + ) + + +class AsyncCompletions(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncCompletionsWithRawResponse: + return AsyncCompletionsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncCompletionsWithStreamingResponse: + return AsyncCompletionsWithStreamingResponse(self) + + @overload + async def create( + self, + *, + messages: Iterable[ChatCompletionMessageParam], + model: Union[ + str, + Literal[ + "gpt-4-0125-preview", + "gpt-4-turbo-preview", + "gpt-4-1106-preview", + "gpt-4-vision-preview", + "gpt-4", + "gpt-4-0314", + "gpt-4-0613", + "gpt-4-32k", + "gpt-4-32k-0314", + "gpt-4-32k-0613", + "gpt-3.5-turbo", + "gpt-3.5-turbo-16k", + "gpt-3.5-turbo-0301", + "gpt-3.5-turbo-0613", + "gpt-3.5-turbo-1106", + "gpt-3.5-turbo-0125", + "gpt-3.5-turbo-16k-0613", + ], + ], + frequency_penalty: Optional[float] | NotGiven = NOT_GIVEN, + function_call: completion_create_params.FunctionCall | NotGiven = NOT_GIVEN, + functions: Iterable[completion_create_params.Function] | NotGiven = NOT_GIVEN, + logit_bias: Optional[Dict[str, int]] | NotGiven = NOT_GIVEN, + logprobs: Optional[bool] | NotGiven = NOT_GIVEN, + max_tokens: Optional[int] | NotGiven = NOT_GIVEN, + n: Optional[int] | NotGiven = NOT_GIVEN, + presence_penalty: Optional[float] | NotGiven = NOT_GIVEN, + response_format: completion_create_params.ResponseFormat | NotGiven = NOT_GIVEN, + seed: Optional[int] | NotGiven = NOT_GIVEN, + stop: Union[Optional[str], List[str]] | NotGiven = NOT_GIVEN, + stream: Optional[Literal[False]] | NotGiven = NOT_GIVEN, + temperature: Optional[float] | NotGiven = NOT_GIVEN, + tool_choice: ChatCompletionToolChoiceOptionParam | NotGiven = NOT_GIVEN, + tools: Iterable[ChatCompletionToolParam] | NotGiven = NOT_GIVEN, + top_logprobs: Optional[int] | NotGiven = NOT_GIVEN, + top_p: Optional[float] | NotGiven = NOT_GIVEN, + user: str | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ChatCompletion: + """ + Creates a model response for the given chat conversation. + + Args: + messages: A list of messages comprising the conversation so far. + [Example Python code](https://cookbook.openai.com/examples/how_to_format_inputs_to_chatgpt_models). + + model: ID of the model to use. See the + [model endpoint compatibility](https://platform.openai.com/docs/models/model-endpoint-compatibility) + table for details on which models work with the Chat API. + + frequency_penalty: Number between -2.0 and 2.0. Positive values penalize new tokens based on their + existing frequency in the text so far, decreasing the model's likelihood to + repeat the same line verbatim. + + [See more information about frequency and presence penalties.](https://platform.openai.com/docs/guides/text-generation/parameter-details) + + function_call: Deprecated in favor of `tool_choice`. + + Controls which (if any) function is called by the model. `none` means the model + will not call a function and instead generates a message. `auto` means the model + can pick between generating a message or calling a function. Specifying a + particular function via `{"name": "my_function"}` forces the model to call that + function. + + `none` is the default when no functions are present. `auto` is the default if + functions are present. + + functions: Deprecated in favor of `tools`. + + A list of functions the model may generate JSON inputs for. + + logit_bias: Modify the likelihood of specified tokens appearing in the completion. + + Accepts a JSON object that maps tokens (specified by their token ID in the + tokenizer) to an associated bias value from -100 to 100. Mathematically, the + bias is added to the logits generated by the model prior to sampling. The exact + effect will vary per model, but values between -1 and 1 should decrease or + increase likelihood of selection; values like -100 or 100 should result in a ban + or exclusive selection of the relevant token. + + logprobs: Whether to return log probabilities of the output tokens or not. If true, + returns the log probabilities of each output token returned in the `content` of + `message`. This option is currently not available on the `gpt-4-vision-preview` + model. + + max_tokens: The maximum number of [tokens](/tokenizer) that can be generated in the chat + completion. + + The total length of input tokens and generated tokens is limited by the model's + context length. + [Example Python code](https://cookbook.openai.com/examples/how_to_count_tokens_with_tiktoken) + for counting tokens. + + n: How many chat completion choices to generate for each input message. Note that + you will be charged based on the number of generated tokens across all of the + choices. Keep `n` as `1` to minimize costs. + + presence_penalty: Number between -2.0 and 2.0. Positive values penalize new tokens based on + whether they appear in the text so far, increasing the model's likelihood to + talk about new topics. + + [See more information about frequency and presence penalties.](https://platform.openai.com/docs/guides/text-generation/parameter-details) + + response_format: An object specifying the format that the model must output. Compatible with + [GPT-4 Turbo](https://platform.openai.com/docs/models/gpt-4-and-gpt-4-turbo) and + all GPT-3.5 Turbo models newer than `gpt-3.5-turbo-1106`. + + Setting to `{ "type": "json_object" }` enables JSON mode, which guarantees the + message the model generates is valid JSON. + + **Important:** when using JSON mode, you **must** also instruct the model to + produce JSON yourself via a system or user message. Without this, the model may + generate an unending stream of whitespace until the generation reaches the token + limit, resulting in a long-running and seemingly "stuck" request. Also note that + the message content may be partially cut off if `finish_reason="length"`, which + indicates the generation exceeded `max_tokens` or the conversation exceeded the + max context length. + + seed: This feature is in Beta. If specified, our system will make a best effort to + sample deterministically, such that repeated requests with the same `seed` and + parameters should return the same result. Determinism is not guaranteed, and you + should refer to the `system_fingerprint` response parameter to monitor changes + in the backend. + + stop: Up to 4 sequences where the API will stop generating further tokens. + + stream: If set, partial message deltas will be sent, like in ChatGPT. Tokens will be + sent as data-only + [server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format) + as they become available, with the stream terminated by a `data: [DONE]` + message. + [Example Python code](https://cookbook.openai.com/examples/how_to_stream_completions). + + temperature: What sampling temperature to use, between 0 and 2. Higher values like 0.8 will + make the output more random, while lower values like 0.2 will make it more + focused and deterministic. + + We generally recommend altering this or `top_p` but not both. + + tool_choice: Controls which (if any) function is called by the model. `none` means the model + will not call a function and instead generates a message. `auto` means the model + can pick between generating a message or calling a function. Specifying a + particular function via + `{"type": "function", "function": {"name": "my_function"}}` forces the model to + call that function. + + `none` is the default when no functions are present. `auto` is the default if + functions are present. + + tools: A list of tools the model may call. Currently, only functions are supported as a + tool. Use this to provide a list of functions the model may generate JSON inputs + for. A max of 128 functions are supported. + + top_logprobs: An integer between 0 and 20 specifying the number of most likely tokens to + return at each token position, each with an associated log probability. + `logprobs` must be set to `true` if this parameter is used. + + top_p: An alternative to sampling with temperature, called nucleus sampling, where the + model considers the results of the tokens with top_p probability mass. So 0.1 + means only the tokens comprising the top 10% probability mass are considered. + + We generally recommend altering this or `temperature` but not both. + + user: A unique identifier representing your end-user, which can help OpenAI to monitor + and detect abuse. + [Learn more](https://platform.openai.com/docs/guides/safety-best-practices/end-user-ids). + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + ... + + @overload + async def create( + self, + *, + messages: Iterable[ChatCompletionMessageParam], + model: Union[ + str, + Literal[ + "gpt-4-0125-preview", + "gpt-4-turbo-preview", + "gpt-4-1106-preview", + "gpt-4-vision-preview", + "gpt-4", + "gpt-4-0314", + "gpt-4-0613", + "gpt-4-32k", + "gpt-4-32k-0314", + "gpt-4-32k-0613", + "gpt-3.5-turbo", + "gpt-3.5-turbo-16k", + "gpt-3.5-turbo-0301", + "gpt-3.5-turbo-0613", + "gpt-3.5-turbo-1106", + "gpt-3.5-turbo-0125", + "gpt-3.5-turbo-16k-0613", + ], + ], + stream: Literal[True], + frequency_penalty: Optional[float] | NotGiven = NOT_GIVEN, + function_call: completion_create_params.FunctionCall | NotGiven = NOT_GIVEN, + functions: Iterable[completion_create_params.Function] | NotGiven = NOT_GIVEN, + logit_bias: Optional[Dict[str, int]] | NotGiven = NOT_GIVEN, + logprobs: Optional[bool] | NotGiven = NOT_GIVEN, + max_tokens: Optional[int] | NotGiven = NOT_GIVEN, + n: Optional[int] | NotGiven = NOT_GIVEN, + presence_penalty: Optional[float] | NotGiven = NOT_GIVEN, + response_format: completion_create_params.ResponseFormat | NotGiven = NOT_GIVEN, + seed: Optional[int] | NotGiven = NOT_GIVEN, + stop: Union[Optional[str], List[str]] | NotGiven = NOT_GIVEN, + temperature: Optional[float] | NotGiven = NOT_GIVEN, + tool_choice: ChatCompletionToolChoiceOptionParam | NotGiven = NOT_GIVEN, + tools: Iterable[ChatCompletionToolParam] | NotGiven = NOT_GIVEN, + top_logprobs: Optional[int] | NotGiven = NOT_GIVEN, + top_p: Optional[float] | NotGiven = NOT_GIVEN, + user: str | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncStream[ChatCompletionChunk]: + """ + Creates a model response for the given chat conversation. + + Args: + messages: A list of messages comprising the conversation so far. + [Example Python code](https://cookbook.openai.com/examples/how_to_format_inputs_to_chatgpt_models). + + model: ID of the model to use. See the + [model endpoint compatibility](https://platform.openai.com/docs/models/model-endpoint-compatibility) + table for details on which models work with the Chat API. + + stream: If set, partial message deltas will be sent, like in ChatGPT. Tokens will be + sent as data-only + [server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format) + as they become available, with the stream terminated by a `data: [DONE]` + message. + [Example Python code](https://cookbook.openai.com/examples/how_to_stream_completions). + + frequency_penalty: Number between -2.0 and 2.0. Positive values penalize new tokens based on their + existing frequency in the text so far, decreasing the model's likelihood to + repeat the same line verbatim. + + [See more information about frequency and presence penalties.](https://platform.openai.com/docs/guides/text-generation/parameter-details) + + function_call: Deprecated in favor of `tool_choice`. + + Controls which (if any) function is called by the model. `none` means the model + will not call a function and instead generates a message. `auto` means the model + can pick between generating a message or calling a function. Specifying a + particular function via `{"name": "my_function"}` forces the model to call that + function. + + `none` is the default when no functions are present. `auto` is the default if + functions are present. + + functions: Deprecated in favor of `tools`. + + A list of functions the model may generate JSON inputs for. + + logit_bias: Modify the likelihood of specified tokens appearing in the completion. + + Accepts a JSON object that maps tokens (specified by their token ID in the + tokenizer) to an associated bias value from -100 to 100. Mathematically, the + bias is added to the logits generated by the model prior to sampling. The exact + effect will vary per model, but values between -1 and 1 should decrease or + increase likelihood of selection; values like -100 or 100 should result in a ban + or exclusive selection of the relevant token. + + logprobs: Whether to return log probabilities of the output tokens or not. If true, + returns the log probabilities of each output token returned in the `content` of + `message`. This option is currently not available on the `gpt-4-vision-preview` + model. + + max_tokens: The maximum number of [tokens](/tokenizer) that can be generated in the chat + completion. + + The total length of input tokens and generated tokens is limited by the model's + context length. + [Example Python code](https://cookbook.openai.com/examples/how_to_count_tokens_with_tiktoken) + for counting tokens. + + n: How many chat completion choices to generate for each input message. Note that + you will be charged based on the number of generated tokens across all of the + choices. Keep `n` as `1` to minimize costs. + + presence_penalty: Number between -2.0 and 2.0. Positive values penalize new tokens based on + whether they appear in the text so far, increasing the model's likelihood to + talk about new topics. + + [See more information about frequency and presence penalties.](https://platform.openai.com/docs/guides/text-generation/parameter-details) + + response_format: An object specifying the format that the model must output. Compatible with + [GPT-4 Turbo](https://platform.openai.com/docs/models/gpt-4-and-gpt-4-turbo) and + all GPT-3.5 Turbo models newer than `gpt-3.5-turbo-1106`. + + Setting to `{ "type": "json_object" }` enables JSON mode, which guarantees the + message the model generates is valid JSON. + + **Important:** when using JSON mode, you **must** also instruct the model to + produce JSON yourself via a system or user message. Without this, the model may + generate an unending stream of whitespace until the generation reaches the token + limit, resulting in a long-running and seemingly "stuck" request. Also note that + the message content may be partially cut off if `finish_reason="length"`, which + indicates the generation exceeded `max_tokens` or the conversation exceeded the + max context length. + + seed: This feature is in Beta. If specified, our system will make a best effort to + sample deterministically, such that repeated requests with the same `seed` and + parameters should return the same result. Determinism is not guaranteed, and you + should refer to the `system_fingerprint` response parameter to monitor changes + in the backend. + + stop: Up to 4 sequences where the API will stop generating further tokens. + + temperature: What sampling temperature to use, between 0 and 2. Higher values like 0.8 will + make the output more random, while lower values like 0.2 will make it more + focused and deterministic. + + We generally recommend altering this or `top_p` but not both. + + tool_choice: Controls which (if any) function is called by the model. `none` means the model + will not call a function and instead generates a message. `auto` means the model + can pick between generating a message or calling a function. Specifying a + particular function via + `{"type": "function", "function": {"name": "my_function"}}` forces the model to + call that function. + + `none` is the default when no functions are present. `auto` is the default if + functions are present. + + tools: A list of tools the model may call. Currently, only functions are supported as a + tool. Use this to provide a list of functions the model may generate JSON inputs + for. A max of 128 functions are supported. + + top_logprobs: An integer between 0 and 20 specifying the number of most likely tokens to + return at each token position, each with an associated log probability. + `logprobs` must be set to `true` if this parameter is used. + + top_p: An alternative to sampling with temperature, called nucleus sampling, where the + model considers the results of the tokens with top_p probability mass. So 0.1 + means only the tokens comprising the top 10% probability mass are considered. + + We generally recommend altering this or `temperature` but not both. + + user: A unique identifier representing your end-user, which can help OpenAI to monitor + and detect abuse. + [Learn more](https://platform.openai.com/docs/guides/safety-best-practices/end-user-ids). + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + ... + + @overload + async def create( + self, + *, + messages: Iterable[ChatCompletionMessageParam], + model: Union[ + str, + Literal[ + "gpt-4-0125-preview", + "gpt-4-turbo-preview", + "gpt-4-1106-preview", + "gpt-4-vision-preview", + "gpt-4", + "gpt-4-0314", + "gpt-4-0613", + "gpt-4-32k", + "gpt-4-32k-0314", + "gpt-4-32k-0613", + "gpt-3.5-turbo", + "gpt-3.5-turbo-16k", + "gpt-3.5-turbo-0301", + "gpt-3.5-turbo-0613", + "gpt-3.5-turbo-1106", + "gpt-3.5-turbo-0125", + "gpt-3.5-turbo-16k-0613", + ], + ], + stream: bool, + frequency_penalty: Optional[float] | NotGiven = NOT_GIVEN, + function_call: completion_create_params.FunctionCall | NotGiven = NOT_GIVEN, + functions: Iterable[completion_create_params.Function] | NotGiven = NOT_GIVEN, + logit_bias: Optional[Dict[str, int]] | NotGiven = NOT_GIVEN, + logprobs: Optional[bool] | NotGiven = NOT_GIVEN, + max_tokens: Optional[int] | NotGiven = NOT_GIVEN, + n: Optional[int] | NotGiven = NOT_GIVEN, + presence_penalty: Optional[float] | NotGiven = NOT_GIVEN, + response_format: completion_create_params.ResponseFormat | NotGiven = NOT_GIVEN, + seed: Optional[int] | NotGiven = NOT_GIVEN, + stop: Union[Optional[str], List[str]] | NotGiven = NOT_GIVEN, + temperature: Optional[float] | NotGiven = NOT_GIVEN, + tool_choice: ChatCompletionToolChoiceOptionParam | NotGiven = NOT_GIVEN, + tools: Iterable[ChatCompletionToolParam] | NotGiven = NOT_GIVEN, + top_logprobs: Optional[int] | NotGiven = NOT_GIVEN, + top_p: Optional[float] | NotGiven = NOT_GIVEN, + user: str | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ChatCompletion | AsyncStream[ChatCompletionChunk]: + """ + Creates a model response for the given chat conversation. + + Args: + messages: A list of messages comprising the conversation so far. + [Example Python code](https://cookbook.openai.com/examples/how_to_format_inputs_to_chatgpt_models). + + model: ID of the model to use. See the + [model endpoint compatibility](https://platform.openai.com/docs/models/model-endpoint-compatibility) + table for details on which models work with the Chat API. + + stream: If set, partial message deltas will be sent, like in ChatGPT. Tokens will be + sent as data-only + [server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format) + as they become available, with the stream terminated by a `data: [DONE]` + message. + [Example Python code](https://cookbook.openai.com/examples/how_to_stream_completions). + + frequency_penalty: Number between -2.0 and 2.0. Positive values penalize new tokens based on their + existing frequency in the text so far, decreasing the model's likelihood to + repeat the same line verbatim. + + [See more information about frequency and presence penalties.](https://platform.openai.com/docs/guides/text-generation/parameter-details) + + function_call: Deprecated in favor of `tool_choice`. + + Controls which (if any) function is called by the model. `none` means the model + will not call a function and instead generates a message. `auto` means the model + can pick between generating a message or calling a function. Specifying a + particular function via `{"name": "my_function"}` forces the model to call that + function. + + `none` is the default when no functions are present. `auto` is the default if + functions are present. + + functions: Deprecated in favor of `tools`. + + A list of functions the model may generate JSON inputs for. + + logit_bias: Modify the likelihood of specified tokens appearing in the completion. + + Accepts a JSON object that maps tokens (specified by their token ID in the + tokenizer) to an associated bias value from -100 to 100. Mathematically, the + bias is added to the logits generated by the model prior to sampling. The exact + effect will vary per model, but values between -1 and 1 should decrease or + increase likelihood of selection; values like -100 or 100 should result in a ban + or exclusive selection of the relevant token. + + logprobs: Whether to return log probabilities of the output tokens or not. If true, + returns the log probabilities of each output token returned in the `content` of + `message`. This option is currently not available on the `gpt-4-vision-preview` + model. + + max_tokens: The maximum number of [tokens](/tokenizer) that can be generated in the chat + completion. + + The total length of input tokens and generated tokens is limited by the model's + context length. + [Example Python code](https://cookbook.openai.com/examples/how_to_count_tokens_with_tiktoken) + for counting tokens. + + n: How many chat completion choices to generate for each input message. Note that + you will be charged based on the number of generated tokens across all of the + choices. Keep `n` as `1` to minimize costs. + + presence_penalty: Number between -2.0 and 2.0. Positive values penalize new tokens based on + whether they appear in the text so far, increasing the model's likelihood to + talk about new topics. + + [See more information about frequency and presence penalties.](https://platform.openai.com/docs/guides/text-generation/parameter-details) + + response_format: An object specifying the format that the model must output. Compatible with + [GPT-4 Turbo](https://platform.openai.com/docs/models/gpt-4-and-gpt-4-turbo) and + all GPT-3.5 Turbo models newer than `gpt-3.5-turbo-1106`. + + Setting to `{ "type": "json_object" }` enables JSON mode, which guarantees the + message the model generates is valid JSON. + + **Important:** when using JSON mode, you **must** also instruct the model to + produce JSON yourself via a system or user message. Without this, the model may + generate an unending stream of whitespace until the generation reaches the token + limit, resulting in a long-running and seemingly "stuck" request. Also note that + the message content may be partially cut off if `finish_reason="length"`, which + indicates the generation exceeded `max_tokens` or the conversation exceeded the + max context length. + + seed: This feature is in Beta. If specified, our system will make a best effort to + sample deterministically, such that repeated requests with the same `seed` and + parameters should return the same result. Determinism is not guaranteed, and you + should refer to the `system_fingerprint` response parameter to monitor changes + in the backend. + + stop: Up to 4 sequences where the API will stop generating further tokens. + + temperature: What sampling temperature to use, between 0 and 2. Higher values like 0.8 will + make the output more random, while lower values like 0.2 will make it more + focused and deterministic. + + We generally recommend altering this or `top_p` but not both. + + tool_choice: Controls which (if any) function is called by the model. `none` means the model + will not call a function and instead generates a message. `auto` means the model + can pick between generating a message or calling a function. Specifying a + particular function via + `{"type": "function", "function": {"name": "my_function"}}` forces the model to + call that function. + + `none` is the default when no functions are present. `auto` is the default if + functions are present. + + tools: A list of tools the model may call. Currently, only functions are supported as a + tool. Use this to provide a list of functions the model may generate JSON inputs + for. A max of 128 functions are supported. + + top_logprobs: An integer between 0 and 20 specifying the number of most likely tokens to + return at each token position, each with an associated log probability. + `logprobs` must be set to `true` if this parameter is used. + + top_p: An alternative to sampling with temperature, called nucleus sampling, where the + model considers the results of the tokens with top_p probability mass. So 0.1 + means only the tokens comprising the top 10% probability mass are considered. + + We generally recommend altering this or `temperature` but not both. + + user: A unique identifier representing your end-user, which can help OpenAI to monitor + and detect abuse. + [Learn more](https://platform.openai.com/docs/guides/safety-best-practices/end-user-ids). + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + ... + + @required_args(["messages", "model"], ["messages", "model", "stream"]) + async def create( + self, + *, + messages: Iterable[ChatCompletionMessageParam], + model: Union[ + str, + Literal[ + "gpt-4-0125-preview", + "gpt-4-turbo-preview", + "gpt-4-1106-preview", + "gpt-4-vision-preview", + "gpt-4", + "gpt-4-0314", + "gpt-4-0613", + "gpt-4-32k", + "gpt-4-32k-0314", + "gpt-4-32k-0613", + "gpt-3.5-turbo", + "gpt-3.5-turbo-16k", + "gpt-3.5-turbo-0301", + "gpt-3.5-turbo-0613", + "gpt-3.5-turbo-1106", + "gpt-3.5-turbo-0125", + "gpt-3.5-turbo-16k-0613", + ], + ], + frequency_penalty: Optional[float] | NotGiven = NOT_GIVEN, + function_call: completion_create_params.FunctionCall | NotGiven = NOT_GIVEN, + functions: Iterable[completion_create_params.Function] | NotGiven = NOT_GIVEN, + logit_bias: Optional[Dict[str, int]] | NotGiven = NOT_GIVEN, + logprobs: Optional[bool] | NotGiven = NOT_GIVEN, + max_tokens: Optional[int] | NotGiven = NOT_GIVEN, + n: Optional[int] | NotGiven = NOT_GIVEN, + presence_penalty: Optional[float] | NotGiven = NOT_GIVEN, + response_format: completion_create_params.ResponseFormat | NotGiven = NOT_GIVEN, + seed: Optional[int] | NotGiven = NOT_GIVEN, + stop: Union[Optional[str], List[str]] | NotGiven = NOT_GIVEN, + stream: Optional[Literal[False]] | Literal[True] | NotGiven = NOT_GIVEN, + temperature: Optional[float] | NotGiven = NOT_GIVEN, + tool_choice: ChatCompletionToolChoiceOptionParam | NotGiven = NOT_GIVEN, + tools: Iterable[ChatCompletionToolParam] | NotGiven = NOT_GIVEN, + top_logprobs: Optional[int] | NotGiven = NOT_GIVEN, + top_p: Optional[float] | NotGiven = NOT_GIVEN, + user: str | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ChatCompletion | AsyncStream[ChatCompletionChunk]: + return await self._post( + "/chat/completions", + body=await async_maybe_transform( + { + "messages": messages, + "model": model, + "frequency_penalty": frequency_penalty, + "function_call": function_call, + "functions": functions, + "logit_bias": logit_bias, + "logprobs": logprobs, + "max_tokens": max_tokens, + "n": n, + "presence_penalty": presence_penalty, + "response_format": response_format, + "seed": seed, + "stop": stop, + "stream": stream, + "temperature": temperature, + "tool_choice": tool_choice, + "tools": tools, + "top_logprobs": top_logprobs, + "top_p": top_p, + "user": user, + }, + completion_create_params.CompletionCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ChatCompletion, + stream=stream or False, + stream_cls=AsyncStream[ChatCompletionChunk], + ) + + +class CompletionsWithRawResponse: + def __init__(self, completions: Completions) -> None: + self._completions = completions + + self.create = _legacy_response.to_raw_response_wrapper( + completions.create, + ) + + +class AsyncCompletionsWithRawResponse: + def __init__(self, completions: AsyncCompletions) -> None: + self._completions = completions + + self.create = _legacy_response.async_to_raw_response_wrapper( + completions.create, + ) + + +class CompletionsWithStreamingResponse: + def __init__(self, completions: Completions) -> None: + self._completions = completions + + self.create = to_streamed_response_wrapper( + completions.create, + ) + + +class AsyncCompletionsWithStreamingResponse: + def __init__(self, completions: AsyncCompletions) -> None: + self._completions = completions + + self.create = async_to_streamed_response_wrapper( + completions.create, + ) diff --git a/openai/resources/completions.py b/openai/resources/completions.py new file mode 100644 index 00000000..db87c83c --- /dev/null +++ b/openai/resources/completions.py @@ -0,0 +1,1102 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Dict, List, Union, Iterable, Optional, overload +from typing_extensions import Literal + +import httpx + +from .. import _legacy_response +from ..types import Completion, completion_create_params +from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from .._utils import ( + required_args, + maybe_transform, + async_maybe_transform, +) +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper +from .._streaming import Stream, AsyncStream +from .._base_client import ( + make_request_options, +) + +__all__ = ["Completions", "AsyncCompletions"] + + +class Completions(SyncAPIResource): + @cached_property + def with_raw_response(self) -> CompletionsWithRawResponse: + return CompletionsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> CompletionsWithStreamingResponse: + return CompletionsWithStreamingResponse(self) + + @overload + def create( + self, + *, + model: Union[str, Literal["gpt-3.5-turbo-instruct", "davinci-002", "babbage-002"]], + prompt: Union[str, List[str], Iterable[int], Iterable[Iterable[int]], None], + best_of: Optional[int] | NotGiven = NOT_GIVEN, + echo: Optional[bool] | NotGiven = NOT_GIVEN, + frequency_penalty: Optional[float] | NotGiven = NOT_GIVEN, + logit_bias: Optional[Dict[str, int]] | NotGiven = NOT_GIVEN, + logprobs: Optional[int] | NotGiven = NOT_GIVEN, + max_tokens: Optional[int] | NotGiven = NOT_GIVEN, + n: Optional[int] | NotGiven = NOT_GIVEN, + presence_penalty: Optional[float] | NotGiven = NOT_GIVEN, + seed: Optional[int] | NotGiven = NOT_GIVEN, + stop: Union[Optional[str], List[str], None] | NotGiven = NOT_GIVEN, + stream: Optional[Literal[False]] | NotGiven = NOT_GIVEN, + suffix: Optional[str] | NotGiven = NOT_GIVEN, + temperature: Optional[float] | NotGiven = NOT_GIVEN, + top_p: Optional[float] | NotGiven = NOT_GIVEN, + user: str | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Completion: + """ + Creates a completion for the provided prompt and parameters. + + Args: + model: ID of the model to use. You can use the + [List models](https://platform.openai.com/docs/api-reference/models/list) API to + see all of your available models, or see our + [Model overview](https://platform.openai.com/docs/models/overview) for + descriptions of them. + + prompt: The prompt(s) to generate completions for, encoded as a string, array of + strings, array of tokens, or array of token arrays. + + Note that <|endoftext|> is the document separator that the model sees during + training, so if a prompt is not specified the model will generate as if from the + beginning of a new document. + + best_of: Generates `best_of` completions server-side and returns the "best" (the one with + the highest log probability per token). Results cannot be streamed. + + When used with `n`, `best_of` controls the number of candidate completions and + `n` specifies how many to return – `best_of` must be greater than `n`. + + **Note:** Because this parameter generates many completions, it can quickly + consume your token quota. Use carefully and ensure that you have reasonable + settings for `max_tokens` and `stop`. + + echo: Echo back the prompt in addition to the completion + + frequency_penalty: Number between -2.0 and 2.0. Positive values penalize new tokens based on their + existing frequency in the text so far, decreasing the model's likelihood to + repeat the same line verbatim. + + [See more information about frequency and presence penalties.](https://platform.openai.com/docs/guides/text-generation/parameter-details) + + logit_bias: Modify the likelihood of specified tokens appearing in the completion. + + Accepts a JSON object that maps tokens (specified by their token ID in the GPT + tokenizer) to an associated bias value from -100 to 100. You can use this + [tokenizer tool](/tokenizer?view=bpe) to convert text to token IDs. + Mathematically, the bias is added to the logits generated by the model prior to + sampling. The exact effect will vary per model, but values between -1 and 1 + should decrease or increase likelihood of selection; values like -100 or 100 + should result in a ban or exclusive selection of the relevant token. + + As an example, you can pass `{"50256": -100}` to prevent the <|endoftext|> token + from being generated. + + logprobs: Include the log probabilities on the `logprobs` most likely output tokens, as + well the chosen tokens. For example, if `logprobs` is 5, the API will return a + list of the 5 most likely tokens. The API will always return the `logprob` of + the sampled token, so there may be up to `logprobs+1` elements in the response. + + The maximum value for `logprobs` is 5. + + max_tokens: The maximum number of [tokens](/tokenizer) that can be generated in the + completion. + + The token count of your prompt plus `max_tokens` cannot exceed the model's + context length. + [Example Python code](https://cookbook.openai.com/examples/how_to_count_tokens_with_tiktoken) + for counting tokens. + + n: How many completions to generate for each prompt. + + **Note:** Because this parameter generates many completions, it can quickly + consume your token quota. Use carefully and ensure that you have reasonable + settings for `max_tokens` and `stop`. + + presence_penalty: Number between -2.0 and 2.0. Positive values penalize new tokens based on + whether they appear in the text so far, increasing the model's likelihood to + talk about new topics. + + [See more information about frequency and presence penalties.](https://platform.openai.com/docs/guides/text-generation/parameter-details) + + seed: If specified, our system will make a best effort to sample deterministically, + such that repeated requests with the same `seed` and parameters should return + the same result. + + Determinism is not guaranteed, and you should refer to the `system_fingerprint` + response parameter to monitor changes in the backend. + + stop: Up to 4 sequences where the API will stop generating further tokens. The + returned text will not contain the stop sequence. + + stream: Whether to stream back partial progress. If set, tokens will be sent as + data-only + [server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format) + as they become available, with the stream terminated by a `data: [DONE]` + message. + [Example Python code](https://cookbook.openai.com/examples/how_to_stream_completions). + + suffix: The suffix that comes after a completion of inserted text. + + This parameter is only supported for `gpt-3.5-turbo-instruct`. + + temperature: What sampling temperature to use, between 0 and 2. Higher values like 0.8 will + make the output more random, while lower values like 0.2 will make it more + focused and deterministic. + + We generally recommend altering this or `top_p` but not both. + + top_p: An alternative to sampling with temperature, called nucleus sampling, where the + model considers the results of the tokens with top_p probability mass. So 0.1 + means only the tokens comprising the top 10% probability mass are considered. + + We generally recommend altering this or `temperature` but not both. + + user: A unique identifier representing your end-user, which can help OpenAI to monitor + and detect abuse. + [Learn more](https://platform.openai.com/docs/guides/safety-best-practices/end-user-ids). + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + ... + + @overload + def create( + self, + *, + model: Union[str, Literal["gpt-3.5-turbo-instruct", "davinci-002", "babbage-002"]], + prompt: Union[str, List[str], Iterable[int], Iterable[Iterable[int]], None], + stream: Literal[True], + best_of: Optional[int] | NotGiven = NOT_GIVEN, + echo: Optional[bool] | NotGiven = NOT_GIVEN, + frequency_penalty: Optional[float] | NotGiven = NOT_GIVEN, + logit_bias: Optional[Dict[str, int]] | NotGiven = NOT_GIVEN, + logprobs: Optional[int] | NotGiven = NOT_GIVEN, + max_tokens: Optional[int] | NotGiven = NOT_GIVEN, + n: Optional[int] | NotGiven = NOT_GIVEN, + presence_penalty: Optional[float] | NotGiven = NOT_GIVEN, + seed: Optional[int] | NotGiven = NOT_GIVEN, + stop: Union[Optional[str], List[str], None] | NotGiven = NOT_GIVEN, + suffix: Optional[str] | NotGiven = NOT_GIVEN, + temperature: Optional[float] | NotGiven = NOT_GIVEN, + top_p: Optional[float] | NotGiven = NOT_GIVEN, + user: str | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Stream[Completion]: + """ + Creates a completion for the provided prompt and parameters. + + Args: + model: ID of the model to use. You can use the + [List models](https://platform.openai.com/docs/api-reference/models/list) API to + see all of your available models, or see our + [Model overview](https://platform.openai.com/docs/models/overview) for + descriptions of them. + + prompt: The prompt(s) to generate completions for, encoded as a string, array of + strings, array of tokens, or array of token arrays. + + Note that <|endoftext|> is the document separator that the model sees during + training, so if a prompt is not specified the model will generate as if from the + beginning of a new document. + + stream: Whether to stream back partial progress. If set, tokens will be sent as + data-only + [server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format) + as they become available, with the stream terminated by a `data: [DONE]` + message. + [Example Python code](https://cookbook.openai.com/examples/how_to_stream_completions). + + best_of: Generates `best_of` completions server-side and returns the "best" (the one with + the highest log probability per token). Results cannot be streamed. + + When used with `n`, `best_of` controls the number of candidate completions and + `n` specifies how many to return – `best_of` must be greater than `n`. + + **Note:** Because this parameter generates many completions, it can quickly + consume your token quota. Use carefully and ensure that you have reasonable + settings for `max_tokens` and `stop`. + + echo: Echo back the prompt in addition to the completion + + frequency_penalty: Number between -2.0 and 2.0. Positive values penalize new tokens based on their + existing frequency in the text so far, decreasing the model's likelihood to + repeat the same line verbatim. + + [See more information about frequency and presence penalties.](https://platform.openai.com/docs/guides/text-generation/parameter-details) + + logit_bias: Modify the likelihood of specified tokens appearing in the completion. + + Accepts a JSON object that maps tokens (specified by their token ID in the GPT + tokenizer) to an associated bias value from -100 to 100. You can use this + [tokenizer tool](/tokenizer?view=bpe) to convert text to token IDs. + Mathematically, the bias is added to the logits generated by the model prior to + sampling. The exact effect will vary per model, but values between -1 and 1 + should decrease or increase likelihood of selection; values like -100 or 100 + should result in a ban or exclusive selection of the relevant token. + + As an example, you can pass `{"50256": -100}` to prevent the <|endoftext|> token + from being generated. + + logprobs: Include the log probabilities on the `logprobs` most likely output tokens, as + well the chosen tokens. For example, if `logprobs` is 5, the API will return a + list of the 5 most likely tokens. The API will always return the `logprob` of + the sampled token, so there may be up to `logprobs+1` elements in the response. + + The maximum value for `logprobs` is 5. + + max_tokens: The maximum number of [tokens](/tokenizer) that can be generated in the + completion. + + The token count of your prompt plus `max_tokens` cannot exceed the model's + context length. + [Example Python code](https://cookbook.openai.com/examples/how_to_count_tokens_with_tiktoken) + for counting tokens. + + n: How many completions to generate for each prompt. + + **Note:** Because this parameter generates many completions, it can quickly + consume your token quota. Use carefully and ensure that you have reasonable + settings for `max_tokens` and `stop`. + + presence_penalty: Number between -2.0 and 2.0. Positive values penalize new tokens based on + whether they appear in the text so far, increasing the model's likelihood to + talk about new topics. + + [See more information about frequency and presence penalties.](https://platform.openai.com/docs/guides/text-generation/parameter-details) + + seed: If specified, our system will make a best effort to sample deterministically, + such that repeated requests with the same `seed` and parameters should return + the same result. + + Determinism is not guaranteed, and you should refer to the `system_fingerprint` + response parameter to monitor changes in the backend. + + stop: Up to 4 sequences where the API will stop generating further tokens. The + returned text will not contain the stop sequence. + + suffix: The suffix that comes after a completion of inserted text. + + This parameter is only supported for `gpt-3.5-turbo-instruct`. + + temperature: What sampling temperature to use, between 0 and 2. Higher values like 0.8 will + make the output more random, while lower values like 0.2 will make it more + focused and deterministic. + + We generally recommend altering this or `top_p` but not both. + + top_p: An alternative to sampling with temperature, called nucleus sampling, where the + model considers the results of the tokens with top_p probability mass. So 0.1 + means only the tokens comprising the top 10% probability mass are considered. + + We generally recommend altering this or `temperature` but not both. + + user: A unique identifier representing your end-user, which can help OpenAI to monitor + and detect abuse. + [Learn more](https://platform.openai.com/docs/guides/safety-best-practices/end-user-ids). + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + ... + + @overload + def create( + self, + *, + model: Union[str, Literal["gpt-3.5-turbo-instruct", "davinci-002", "babbage-002"]], + prompt: Union[str, List[str], Iterable[int], Iterable[Iterable[int]], None], + stream: bool, + best_of: Optional[int] | NotGiven = NOT_GIVEN, + echo: Optional[bool] | NotGiven = NOT_GIVEN, + frequency_penalty: Optional[float] | NotGiven = NOT_GIVEN, + logit_bias: Optional[Dict[str, int]] | NotGiven = NOT_GIVEN, + logprobs: Optional[int] | NotGiven = NOT_GIVEN, + max_tokens: Optional[int] | NotGiven = NOT_GIVEN, + n: Optional[int] | NotGiven = NOT_GIVEN, + presence_penalty: Optional[float] | NotGiven = NOT_GIVEN, + seed: Optional[int] | NotGiven = NOT_GIVEN, + stop: Union[Optional[str], List[str], None] | NotGiven = NOT_GIVEN, + suffix: Optional[str] | NotGiven = NOT_GIVEN, + temperature: Optional[float] | NotGiven = NOT_GIVEN, + top_p: Optional[float] | NotGiven = NOT_GIVEN, + user: str | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Completion | Stream[Completion]: + """ + Creates a completion for the provided prompt and parameters. + + Args: + model: ID of the model to use. You can use the + [List models](https://platform.openai.com/docs/api-reference/models/list) API to + see all of your available models, or see our + [Model overview](https://platform.openai.com/docs/models/overview) for + descriptions of them. + + prompt: The prompt(s) to generate completions for, encoded as a string, array of + strings, array of tokens, or array of token arrays. + + Note that <|endoftext|> is the document separator that the model sees during + training, so if a prompt is not specified the model will generate as if from the + beginning of a new document. + + stream: Whether to stream back partial progress. If set, tokens will be sent as + data-only + [server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format) + as they become available, with the stream terminated by a `data: [DONE]` + message. + [Example Python code](https://cookbook.openai.com/examples/how_to_stream_completions). + + best_of: Generates `best_of` completions server-side and returns the "best" (the one with + the highest log probability per token). Results cannot be streamed. + + When used with `n`, `best_of` controls the number of candidate completions and + `n` specifies how many to return – `best_of` must be greater than `n`. + + **Note:** Because this parameter generates many completions, it can quickly + consume your token quota. Use carefully and ensure that you have reasonable + settings for `max_tokens` and `stop`. + + echo: Echo back the prompt in addition to the completion + + frequency_penalty: Number between -2.0 and 2.0. Positive values penalize new tokens based on their + existing frequency in the text so far, decreasing the model's likelihood to + repeat the same line verbatim. + + [See more information about frequency and presence penalties.](https://platform.openai.com/docs/guides/text-generation/parameter-details) + + logit_bias: Modify the likelihood of specified tokens appearing in the completion. + + Accepts a JSON object that maps tokens (specified by their token ID in the GPT + tokenizer) to an associated bias value from -100 to 100. You can use this + [tokenizer tool](/tokenizer?view=bpe) to convert text to token IDs. + Mathematically, the bias is added to the logits generated by the model prior to + sampling. The exact effect will vary per model, but values between -1 and 1 + should decrease or increase likelihood of selection; values like -100 or 100 + should result in a ban or exclusive selection of the relevant token. + + As an example, you can pass `{"50256": -100}` to prevent the <|endoftext|> token + from being generated. + + logprobs: Include the log probabilities on the `logprobs` most likely output tokens, as + well the chosen tokens. For example, if `logprobs` is 5, the API will return a + list of the 5 most likely tokens. The API will always return the `logprob` of + the sampled token, so there may be up to `logprobs+1` elements in the response. + + The maximum value for `logprobs` is 5. + + max_tokens: The maximum number of [tokens](/tokenizer) that can be generated in the + completion. + + The token count of your prompt plus `max_tokens` cannot exceed the model's + context length. + [Example Python code](https://cookbook.openai.com/examples/how_to_count_tokens_with_tiktoken) + for counting tokens. + + n: How many completions to generate for each prompt. + + **Note:** Because this parameter generates many completions, it can quickly + consume your token quota. Use carefully and ensure that you have reasonable + settings for `max_tokens` and `stop`. + + presence_penalty: Number between -2.0 and 2.0. Positive values penalize new tokens based on + whether they appear in the text so far, increasing the model's likelihood to + talk about new topics. + + [See more information about frequency and presence penalties.](https://platform.openai.com/docs/guides/text-generation/parameter-details) + + seed: If specified, our system will make a best effort to sample deterministically, + such that repeated requests with the same `seed` and parameters should return + the same result. + + Determinism is not guaranteed, and you should refer to the `system_fingerprint` + response parameter to monitor changes in the backend. + + stop: Up to 4 sequences where the API will stop generating further tokens. The + returned text will not contain the stop sequence. + + suffix: The suffix that comes after a completion of inserted text. + + This parameter is only supported for `gpt-3.5-turbo-instruct`. + + temperature: What sampling temperature to use, between 0 and 2. Higher values like 0.8 will + make the output more random, while lower values like 0.2 will make it more + focused and deterministic. + + We generally recommend altering this or `top_p` but not both. + + top_p: An alternative to sampling with temperature, called nucleus sampling, where the + model considers the results of the tokens with top_p probability mass. So 0.1 + means only the tokens comprising the top 10% probability mass are considered. + + We generally recommend altering this or `temperature` but not both. + + user: A unique identifier representing your end-user, which can help OpenAI to monitor + and detect abuse. + [Learn more](https://platform.openai.com/docs/guides/safety-best-practices/end-user-ids). + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + ... + + @required_args(["model", "prompt"], ["model", "prompt", "stream"]) + def create( + self, + *, + model: Union[str, Literal["gpt-3.5-turbo-instruct", "davinci-002", "babbage-002"]], + prompt: Union[str, List[str], Iterable[int], Iterable[Iterable[int]], None], + best_of: Optional[int] | NotGiven = NOT_GIVEN, + echo: Optional[bool] | NotGiven = NOT_GIVEN, + frequency_penalty: Optional[float] | NotGiven = NOT_GIVEN, + logit_bias: Optional[Dict[str, int]] | NotGiven = NOT_GIVEN, + logprobs: Optional[int] | NotGiven = NOT_GIVEN, + max_tokens: Optional[int] | NotGiven = NOT_GIVEN, + n: Optional[int] | NotGiven = NOT_GIVEN, + presence_penalty: Optional[float] | NotGiven = NOT_GIVEN, + seed: Optional[int] | NotGiven = NOT_GIVEN, + stop: Union[Optional[str], List[str], None] | NotGiven = NOT_GIVEN, + stream: Optional[Literal[False]] | Literal[True] | NotGiven = NOT_GIVEN, + suffix: Optional[str] | NotGiven = NOT_GIVEN, + temperature: Optional[float] | NotGiven = NOT_GIVEN, + top_p: Optional[float] | NotGiven = NOT_GIVEN, + user: str | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Completion | Stream[Completion]: + return self._post( + "/completions", + body=maybe_transform( + { + "model": model, + "prompt": prompt, + "best_of": best_of, + "echo": echo, + "frequency_penalty": frequency_penalty, + "logit_bias": logit_bias, + "logprobs": logprobs, + "max_tokens": max_tokens, + "n": n, + "presence_penalty": presence_penalty, + "seed": seed, + "stop": stop, + "stream": stream, + "suffix": suffix, + "temperature": temperature, + "top_p": top_p, + "user": user, + }, + completion_create_params.CompletionCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Completion, + stream=stream or False, + stream_cls=Stream[Completion], + ) + + +class AsyncCompletions(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncCompletionsWithRawResponse: + return AsyncCompletionsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncCompletionsWithStreamingResponse: + return AsyncCompletionsWithStreamingResponse(self) + + @overload + async def create( + self, + *, + model: Union[str, Literal["gpt-3.5-turbo-instruct", "davinci-002", "babbage-002"]], + prompt: Union[str, List[str], Iterable[int], Iterable[Iterable[int]], None], + best_of: Optional[int] | NotGiven = NOT_GIVEN, + echo: Optional[bool] | NotGiven = NOT_GIVEN, + frequency_penalty: Optional[float] | NotGiven = NOT_GIVEN, + logit_bias: Optional[Dict[str, int]] | NotGiven = NOT_GIVEN, + logprobs: Optional[int] | NotGiven = NOT_GIVEN, + max_tokens: Optional[int] | NotGiven = NOT_GIVEN, + n: Optional[int] | NotGiven = NOT_GIVEN, + presence_penalty: Optional[float] | NotGiven = NOT_GIVEN, + seed: Optional[int] | NotGiven = NOT_GIVEN, + stop: Union[Optional[str], List[str], None] | NotGiven = NOT_GIVEN, + stream: Optional[Literal[False]] | NotGiven = NOT_GIVEN, + suffix: Optional[str] | NotGiven = NOT_GIVEN, + temperature: Optional[float] | NotGiven = NOT_GIVEN, + top_p: Optional[float] | NotGiven = NOT_GIVEN, + user: str | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Completion: + """ + Creates a completion for the provided prompt and parameters. + + Args: + model: ID of the model to use. You can use the + [List models](https://platform.openai.com/docs/api-reference/models/list) API to + see all of your available models, or see our + [Model overview](https://platform.openai.com/docs/models/overview) for + descriptions of them. + + prompt: The prompt(s) to generate completions for, encoded as a string, array of + strings, array of tokens, or array of token arrays. + + Note that <|endoftext|> is the document separator that the model sees during + training, so if a prompt is not specified the model will generate as if from the + beginning of a new document. + + best_of: Generates `best_of` completions server-side and returns the "best" (the one with + the highest log probability per token). Results cannot be streamed. + + When used with `n`, `best_of` controls the number of candidate completions and + `n` specifies how many to return – `best_of` must be greater than `n`. + + **Note:** Because this parameter generates many completions, it can quickly + consume your token quota. Use carefully and ensure that you have reasonable + settings for `max_tokens` and `stop`. + + echo: Echo back the prompt in addition to the completion + + frequency_penalty: Number between -2.0 and 2.0. Positive values penalize new tokens based on their + existing frequency in the text so far, decreasing the model's likelihood to + repeat the same line verbatim. + + [See more information about frequency and presence penalties.](https://platform.openai.com/docs/guides/text-generation/parameter-details) + + logit_bias: Modify the likelihood of specified tokens appearing in the completion. + + Accepts a JSON object that maps tokens (specified by their token ID in the GPT + tokenizer) to an associated bias value from -100 to 100. You can use this + [tokenizer tool](/tokenizer?view=bpe) to convert text to token IDs. + Mathematically, the bias is added to the logits generated by the model prior to + sampling. The exact effect will vary per model, but values between -1 and 1 + should decrease or increase likelihood of selection; values like -100 or 100 + should result in a ban or exclusive selection of the relevant token. + + As an example, you can pass `{"50256": -100}` to prevent the <|endoftext|> token + from being generated. + + logprobs: Include the log probabilities on the `logprobs` most likely output tokens, as + well the chosen tokens. For example, if `logprobs` is 5, the API will return a + list of the 5 most likely tokens. The API will always return the `logprob` of + the sampled token, so there may be up to `logprobs+1` elements in the response. + + The maximum value for `logprobs` is 5. + + max_tokens: The maximum number of [tokens](/tokenizer) that can be generated in the + completion. + + The token count of your prompt plus `max_tokens` cannot exceed the model's + context length. + [Example Python code](https://cookbook.openai.com/examples/how_to_count_tokens_with_tiktoken) + for counting tokens. + + n: How many completions to generate for each prompt. + + **Note:** Because this parameter generates many completions, it can quickly + consume your token quota. Use carefully and ensure that you have reasonable + settings for `max_tokens` and `stop`. + + presence_penalty: Number between -2.0 and 2.0. Positive values penalize new tokens based on + whether they appear in the text so far, increasing the model's likelihood to + talk about new topics. + + [See more information about frequency and presence penalties.](https://platform.openai.com/docs/guides/text-generation/parameter-details) + + seed: If specified, our system will make a best effort to sample deterministically, + such that repeated requests with the same `seed` and parameters should return + the same result. + + Determinism is not guaranteed, and you should refer to the `system_fingerprint` + response parameter to monitor changes in the backend. + + stop: Up to 4 sequences where the API will stop generating further tokens. The + returned text will not contain the stop sequence. + + stream: Whether to stream back partial progress. If set, tokens will be sent as + data-only + [server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format) + as they become available, with the stream terminated by a `data: [DONE]` + message. + [Example Python code](https://cookbook.openai.com/examples/how_to_stream_completions). + + suffix: The suffix that comes after a completion of inserted text. + + This parameter is only supported for `gpt-3.5-turbo-instruct`. + + temperature: What sampling temperature to use, between 0 and 2. Higher values like 0.8 will + make the output more random, while lower values like 0.2 will make it more + focused and deterministic. + + We generally recommend altering this or `top_p` but not both. + + top_p: An alternative to sampling with temperature, called nucleus sampling, where the + model considers the results of the tokens with top_p probability mass. So 0.1 + means only the tokens comprising the top 10% probability mass are considered. + + We generally recommend altering this or `temperature` but not both. + + user: A unique identifier representing your end-user, which can help OpenAI to monitor + and detect abuse. + [Learn more](https://platform.openai.com/docs/guides/safety-best-practices/end-user-ids). + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + ... + + @overload + async def create( + self, + *, + model: Union[str, Literal["gpt-3.5-turbo-instruct", "davinci-002", "babbage-002"]], + prompt: Union[str, List[str], Iterable[int], Iterable[Iterable[int]], None], + stream: Literal[True], + best_of: Optional[int] | NotGiven = NOT_GIVEN, + echo: Optional[bool] | NotGiven = NOT_GIVEN, + frequency_penalty: Optional[float] | NotGiven = NOT_GIVEN, + logit_bias: Optional[Dict[str, int]] | NotGiven = NOT_GIVEN, + logprobs: Optional[int] | NotGiven = NOT_GIVEN, + max_tokens: Optional[int] | NotGiven = NOT_GIVEN, + n: Optional[int] | NotGiven = NOT_GIVEN, + presence_penalty: Optional[float] | NotGiven = NOT_GIVEN, + seed: Optional[int] | NotGiven = NOT_GIVEN, + stop: Union[Optional[str], List[str], None] | NotGiven = NOT_GIVEN, + suffix: Optional[str] | NotGiven = NOT_GIVEN, + temperature: Optional[float] | NotGiven = NOT_GIVEN, + top_p: Optional[float] | NotGiven = NOT_GIVEN, + user: str | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncStream[Completion]: + """ + Creates a completion for the provided prompt and parameters. + + Args: + model: ID of the model to use. You can use the + [List models](https://platform.openai.com/docs/api-reference/models/list) API to + see all of your available models, or see our + [Model overview](https://platform.openai.com/docs/models/overview) for + descriptions of them. + + prompt: The prompt(s) to generate completions for, encoded as a string, array of + strings, array of tokens, or array of token arrays. + + Note that <|endoftext|> is the document separator that the model sees during + training, so if a prompt is not specified the model will generate as if from the + beginning of a new document. + + stream: Whether to stream back partial progress. If set, tokens will be sent as + data-only + [server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format) + as they become available, with the stream terminated by a `data: [DONE]` + message. + [Example Python code](https://cookbook.openai.com/examples/how_to_stream_completions). + + best_of: Generates `best_of` completions server-side and returns the "best" (the one with + the highest log probability per token). Results cannot be streamed. + + When used with `n`, `best_of` controls the number of candidate completions and + `n` specifies how many to return – `best_of` must be greater than `n`. + + **Note:** Because this parameter generates many completions, it can quickly + consume your token quota. Use carefully and ensure that you have reasonable + settings for `max_tokens` and `stop`. + + echo: Echo back the prompt in addition to the completion + + frequency_penalty: Number between -2.0 and 2.0. Positive values penalize new tokens based on their + existing frequency in the text so far, decreasing the model's likelihood to + repeat the same line verbatim. + + [See more information about frequency and presence penalties.](https://platform.openai.com/docs/guides/text-generation/parameter-details) + + logit_bias: Modify the likelihood of specified tokens appearing in the completion. + + Accepts a JSON object that maps tokens (specified by their token ID in the GPT + tokenizer) to an associated bias value from -100 to 100. You can use this + [tokenizer tool](/tokenizer?view=bpe) to convert text to token IDs. + Mathematically, the bias is added to the logits generated by the model prior to + sampling. The exact effect will vary per model, but values between -1 and 1 + should decrease or increase likelihood of selection; values like -100 or 100 + should result in a ban or exclusive selection of the relevant token. + + As an example, you can pass `{"50256": -100}` to prevent the <|endoftext|> token + from being generated. + + logprobs: Include the log probabilities on the `logprobs` most likely output tokens, as + well the chosen tokens. For example, if `logprobs` is 5, the API will return a + list of the 5 most likely tokens. The API will always return the `logprob` of + the sampled token, so there may be up to `logprobs+1` elements in the response. + + The maximum value for `logprobs` is 5. + + max_tokens: The maximum number of [tokens](/tokenizer) that can be generated in the + completion. + + The token count of your prompt plus `max_tokens` cannot exceed the model's + context length. + [Example Python code](https://cookbook.openai.com/examples/how_to_count_tokens_with_tiktoken) + for counting tokens. + + n: How many completions to generate for each prompt. + + **Note:** Because this parameter generates many completions, it can quickly + consume your token quota. Use carefully and ensure that you have reasonable + settings for `max_tokens` and `stop`. + + presence_penalty: Number between -2.0 and 2.0. Positive values penalize new tokens based on + whether they appear in the text so far, increasing the model's likelihood to + talk about new topics. + + [See more information about frequency and presence penalties.](https://platform.openai.com/docs/guides/text-generation/parameter-details) + + seed: If specified, our system will make a best effort to sample deterministically, + such that repeated requests with the same `seed` and parameters should return + the same result. + + Determinism is not guaranteed, and you should refer to the `system_fingerprint` + response parameter to monitor changes in the backend. + + stop: Up to 4 sequences where the API will stop generating further tokens. The + returned text will not contain the stop sequence. + + suffix: The suffix that comes after a completion of inserted text. + + This parameter is only supported for `gpt-3.5-turbo-instruct`. + + temperature: What sampling temperature to use, between 0 and 2. Higher values like 0.8 will + make the output more random, while lower values like 0.2 will make it more + focused and deterministic. + + We generally recommend altering this or `top_p` but not both. + + top_p: An alternative to sampling with temperature, called nucleus sampling, where the + model considers the results of the tokens with top_p probability mass. So 0.1 + means only the tokens comprising the top 10% probability mass are considered. + + We generally recommend altering this or `temperature` but not both. + + user: A unique identifier representing your end-user, which can help OpenAI to monitor + and detect abuse. + [Learn more](https://platform.openai.com/docs/guides/safety-best-practices/end-user-ids). + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + ... + + @overload + async def create( + self, + *, + model: Union[str, Literal["gpt-3.5-turbo-instruct", "davinci-002", "babbage-002"]], + prompt: Union[str, List[str], Iterable[int], Iterable[Iterable[int]], None], + stream: bool, + best_of: Optional[int] | NotGiven = NOT_GIVEN, + echo: Optional[bool] | NotGiven = NOT_GIVEN, + frequency_penalty: Optional[float] | NotGiven = NOT_GIVEN, + logit_bias: Optional[Dict[str, int]] | NotGiven = NOT_GIVEN, + logprobs: Optional[int] | NotGiven = NOT_GIVEN, + max_tokens: Optional[int] | NotGiven = NOT_GIVEN, + n: Optional[int] | NotGiven = NOT_GIVEN, + presence_penalty: Optional[float] | NotGiven = NOT_GIVEN, + seed: Optional[int] | NotGiven = NOT_GIVEN, + stop: Union[Optional[str], List[str], None] | NotGiven = NOT_GIVEN, + suffix: Optional[str] | NotGiven = NOT_GIVEN, + temperature: Optional[float] | NotGiven = NOT_GIVEN, + top_p: Optional[float] | NotGiven = NOT_GIVEN, + user: str | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Completion | AsyncStream[Completion]: + """ + Creates a completion for the provided prompt and parameters. + + Args: + model: ID of the model to use. You can use the + [List models](https://platform.openai.com/docs/api-reference/models/list) API to + see all of your available models, or see our + [Model overview](https://platform.openai.com/docs/models/overview) for + descriptions of them. + + prompt: The prompt(s) to generate completions for, encoded as a string, array of + strings, array of tokens, or array of token arrays. + + Note that <|endoftext|> is the document separator that the model sees during + training, so if a prompt is not specified the model will generate as if from the + beginning of a new document. + + stream: Whether to stream back partial progress. If set, tokens will be sent as + data-only + [server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format) + as they become available, with the stream terminated by a `data: [DONE]` + message. + [Example Python code](https://cookbook.openai.com/examples/how_to_stream_completions). + + best_of: Generates `best_of` completions server-side and returns the "best" (the one with + the highest log probability per token). Results cannot be streamed. + + When used with `n`, `best_of` controls the number of candidate completions and + `n` specifies how many to return – `best_of` must be greater than `n`. + + **Note:** Because this parameter generates many completions, it can quickly + consume your token quota. Use carefully and ensure that you have reasonable + settings for `max_tokens` and `stop`. + + echo: Echo back the prompt in addition to the completion + + frequency_penalty: Number between -2.0 and 2.0. Positive values penalize new tokens based on their + existing frequency in the text so far, decreasing the model's likelihood to + repeat the same line verbatim. + + [See more information about frequency and presence penalties.](https://platform.openai.com/docs/guides/text-generation/parameter-details) + + logit_bias: Modify the likelihood of specified tokens appearing in the completion. + + Accepts a JSON object that maps tokens (specified by their token ID in the GPT + tokenizer) to an associated bias value from -100 to 100. You can use this + [tokenizer tool](/tokenizer?view=bpe) to convert text to token IDs. + Mathematically, the bias is added to the logits generated by the model prior to + sampling. The exact effect will vary per model, but values between -1 and 1 + should decrease or increase likelihood of selection; values like -100 or 100 + should result in a ban or exclusive selection of the relevant token. + + As an example, you can pass `{"50256": -100}` to prevent the <|endoftext|> token + from being generated. + + logprobs: Include the log probabilities on the `logprobs` most likely output tokens, as + well the chosen tokens. For example, if `logprobs` is 5, the API will return a + list of the 5 most likely tokens. The API will always return the `logprob` of + the sampled token, so there may be up to `logprobs+1` elements in the response. + + The maximum value for `logprobs` is 5. + + max_tokens: The maximum number of [tokens](/tokenizer) that can be generated in the + completion. + + The token count of your prompt plus `max_tokens` cannot exceed the model's + context length. + [Example Python code](https://cookbook.openai.com/examples/how_to_count_tokens_with_tiktoken) + for counting tokens. + + n: How many completions to generate for each prompt. + + **Note:** Because this parameter generates many completions, it can quickly + consume your token quota. Use carefully and ensure that you have reasonable + settings for `max_tokens` and `stop`. + + presence_penalty: Number between -2.0 and 2.0. Positive values penalize new tokens based on + whether they appear in the text so far, increasing the model's likelihood to + talk about new topics. + + [See more information about frequency and presence penalties.](https://platform.openai.com/docs/guides/text-generation/parameter-details) + + seed: If specified, our system will make a best effort to sample deterministically, + such that repeated requests with the same `seed` and parameters should return + the same result. + + Determinism is not guaranteed, and you should refer to the `system_fingerprint` + response parameter to monitor changes in the backend. + + stop: Up to 4 sequences where the API will stop generating further tokens. The + returned text will not contain the stop sequence. + + suffix: The suffix that comes after a completion of inserted text. + + This parameter is only supported for `gpt-3.5-turbo-instruct`. + + temperature: What sampling temperature to use, between 0 and 2. Higher values like 0.8 will + make the output more random, while lower values like 0.2 will make it more + focused and deterministic. + + We generally recommend altering this or `top_p` but not both. + + top_p: An alternative to sampling with temperature, called nucleus sampling, where the + model considers the results of the tokens with top_p probability mass. So 0.1 + means only the tokens comprising the top 10% probability mass are considered. + + We generally recommend altering this or `temperature` but not both. + + user: A unique identifier representing your end-user, which can help OpenAI to monitor + and detect abuse. + [Learn more](https://platform.openai.com/docs/guides/safety-best-practices/end-user-ids). + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + ... + + @required_args(["model", "prompt"], ["model", "prompt", "stream"]) + async def create( + self, + *, + model: Union[str, Literal["gpt-3.5-turbo-instruct", "davinci-002", "babbage-002"]], + prompt: Union[str, List[str], Iterable[int], Iterable[Iterable[int]], None], + best_of: Optional[int] | NotGiven = NOT_GIVEN, + echo: Optional[bool] | NotGiven = NOT_GIVEN, + frequency_penalty: Optional[float] | NotGiven = NOT_GIVEN, + logit_bias: Optional[Dict[str, int]] | NotGiven = NOT_GIVEN, + logprobs: Optional[int] | NotGiven = NOT_GIVEN, + max_tokens: Optional[int] | NotGiven = NOT_GIVEN, + n: Optional[int] | NotGiven = NOT_GIVEN, + presence_penalty: Optional[float] | NotGiven = NOT_GIVEN, + seed: Optional[int] | NotGiven = NOT_GIVEN, + stop: Union[Optional[str], List[str], None] | NotGiven = NOT_GIVEN, + stream: Optional[Literal[False]] | Literal[True] | NotGiven = NOT_GIVEN, + suffix: Optional[str] | NotGiven = NOT_GIVEN, + temperature: Optional[float] | NotGiven = NOT_GIVEN, + top_p: Optional[float] | NotGiven = NOT_GIVEN, + user: str | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Completion | AsyncStream[Completion]: + return await self._post( + "/completions", + body=await async_maybe_transform( + { + "model": model, + "prompt": prompt, + "best_of": best_of, + "echo": echo, + "frequency_penalty": frequency_penalty, + "logit_bias": logit_bias, + "logprobs": logprobs, + "max_tokens": max_tokens, + "n": n, + "presence_penalty": presence_penalty, + "seed": seed, + "stop": stop, + "stream": stream, + "suffix": suffix, + "temperature": temperature, + "top_p": top_p, + "user": user, + }, + completion_create_params.CompletionCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Completion, + stream=stream or False, + stream_cls=AsyncStream[Completion], + ) + + +class CompletionsWithRawResponse: + def __init__(self, completions: Completions) -> None: + self._completions = completions + + self.create = _legacy_response.to_raw_response_wrapper( + completions.create, + ) + + +class AsyncCompletionsWithRawResponse: + def __init__(self, completions: AsyncCompletions) -> None: + self._completions = completions + + self.create = _legacy_response.async_to_raw_response_wrapper( + completions.create, + ) + + +class CompletionsWithStreamingResponse: + def __init__(self, completions: Completions) -> None: + self._completions = completions + + self.create = to_streamed_response_wrapper( + completions.create, + ) + + +class AsyncCompletionsWithStreamingResponse: + def __init__(self, completions: AsyncCompletions) -> None: + self._completions = completions + + self.create = async_to_streamed_response_wrapper( + completions.create, + ) diff --git a/openai/resources/embeddings.py b/openai/resources/embeddings.py new file mode 100644 index 00000000..a083b626 --- /dev/null +++ b/openai/resources/embeddings.py @@ -0,0 +1,261 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import base64 +from typing import List, Union, Iterable, cast +from typing_extensions import Literal + +import httpx + +from .. import _legacy_response +from ..types import CreateEmbeddingResponse, embedding_create_params +from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from .._utils import is_given, maybe_transform +from .._compat import cached_property +from .._extras import numpy as np, has_numpy +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper +from .._base_client import ( + make_request_options, +) + +__all__ = ["Embeddings", "AsyncEmbeddings"] + + +class Embeddings(SyncAPIResource): + @cached_property + def with_raw_response(self) -> EmbeddingsWithRawResponse: + return EmbeddingsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> EmbeddingsWithStreamingResponse: + return EmbeddingsWithStreamingResponse(self) + + def create( + self, + *, + input: Union[str, List[str], Iterable[int], Iterable[Iterable[int]]], + model: Union[str, Literal["text-embedding-ada-002", "text-embedding-3-small", "text-embedding-3-large"]], + dimensions: int | NotGiven = NOT_GIVEN, + encoding_format: Literal["float", "base64"] | NotGiven = NOT_GIVEN, + user: str | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CreateEmbeddingResponse: + """ + Creates an embedding vector representing the input text. + + Args: + input: Input text to embed, encoded as a string or array of tokens. To embed multiple + inputs in a single request, pass an array of strings or array of token arrays. + The input must not exceed the max input tokens for the model (8192 tokens for + `text-embedding-ada-002`), cannot be an empty string, and any array must be 2048 + dimensions or less. + [Example Python code](https://cookbook.openai.com/examples/how_to_count_tokens_with_tiktoken) + for counting tokens. + + model: ID of the model to use. You can use the + [List models](https://platform.openai.com/docs/api-reference/models/list) API to + see all of your available models, or see our + [Model overview](https://platform.openai.com/docs/models/overview) for + descriptions of them. + + dimensions: The number of dimensions the resulting output embeddings should have. Only + supported in `text-embedding-3` and later models. + + encoding_format: The format to return the embeddings in. Can be either `float` or + [`base64`](https://pypi.org/project/pybase64/). + + user: A unique identifier representing your end-user, which can help OpenAI to monitor + and detect abuse. + [Learn more](https://platform.openai.com/docs/guides/safety-best-practices/end-user-ids). + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + params = { + "input": input, + "model": model, + "user": user, + "dimensions": dimensions, + "encoding_format": encoding_format, + } + if not is_given(encoding_format) and has_numpy(): + params["encoding_format"] = "base64" + + def parser(obj: CreateEmbeddingResponse) -> CreateEmbeddingResponse: + if is_given(encoding_format): + # don't modify the response object if a user explicitly asked for a format + return obj + + for embedding in obj.data: + data = cast(object, embedding.embedding) + if not isinstance(data, str): + # numpy is not installed / base64 optimisation isn't enabled for this model yet + continue + + embedding.embedding = np.frombuffer( # type: ignore[no-untyped-call] + base64.b64decode(data), dtype="float32" + ).tolist() + + return obj + + return self._post( + "/embeddings", + body=maybe_transform(params, embedding_create_params.EmbeddingCreateParams), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + post_parser=parser, + ), + cast_to=CreateEmbeddingResponse, + ) + + +class AsyncEmbeddings(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncEmbeddingsWithRawResponse: + return AsyncEmbeddingsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncEmbeddingsWithStreamingResponse: + return AsyncEmbeddingsWithStreamingResponse(self) + + async def create( + self, + *, + input: Union[str, List[str], Iterable[int], Iterable[Iterable[int]]], + model: Union[str, Literal["text-embedding-ada-002", "text-embedding-3-small", "text-embedding-3-large"]], + dimensions: int | NotGiven = NOT_GIVEN, + encoding_format: Literal["float", "base64"] | NotGiven = NOT_GIVEN, + user: str | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CreateEmbeddingResponse: + """ + Creates an embedding vector representing the input text. + + Args: + input: Input text to embed, encoded as a string or array of tokens. To embed multiple + inputs in a single request, pass an array of strings or array of token arrays. + The input must not exceed the max input tokens for the model (8192 tokens for + `text-embedding-ada-002`), cannot be an empty string, and any array must be 2048 + dimensions or less. + [Example Python code](https://cookbook.openai.com/examples/how_to_count_tokens_with_tiktoken) + for counting tokens. + + model: ID of the model to use. You can use the + [List models](https://platform.openai.com/docs/api-reference/models/list) API to + see all of your available models, or see our + [Model overview](https://platform.openai.com/docs/models/overview) for + descriptions of them. + + dimensions: The number of dimensions the resulting output embeddings should have. Only + supported in `text-embedding-3` and later models. + + encoding_format: The format to return the embeddings in. Can be either `float` or + [`base64`](https://pypi.org/project/pybase64/). + + user: A unique identifier representing your end-user, which can help OpenAI to monitor + and detect abuse. + [Learn more](https://platform.openai.com/docs/guides/safety-best-practices/end-user-ids). + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + params = { + "input": input, + "model": model, + "user": user, + "dimensions": dimensions, + "encoding_format": encoding_format, + } + if not is_given(encoding_format) and has_numpy(): + params["encoding_format"] = "base64" + + def parser(obj: CreateEmbeddingResponse) -> CreateEmbeddingResponse: + if is_given(encoding_format): + # don't modify the response object if a user explicitly asked for a format + return obj + + for embedding in obj.data: + data = cast(object, embedding.embedding) + if not isinstance(data, str): + # numpy is not installed / base64 optimisation isn't enabled for this model yet + continue + + embedding.embedding = np.frombuffer( # type: ignore[no-untyped-call] + base64.b64decode(data), dtype="float32" + ).tolist() + + return obj + + return await self._post( + "/embeddings", + body=maybe_transform(params, embedding_create_params.EmbeddingCreateParams), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + post_parser=parser, + ), + cast_to=CreateEmbeddingResponse, + ) + + +class EmbeddingsWithRawResponse: + def __init__(self, embeddings: Embeddings) -> None: + self._embeddings = embeddings + + self.create = _legacy_response.to_raw_response_wrapper( + embeddings.create, + ) + + +class AsyncEmbeddingsWithRawResponse: + def __init__(self, embeddings: AsyncEmbeddings) -> None: + self._embeddings = embeddings + + self.create = _legacy_response.async_to_raw_response_wrapper( + embeddings.create, + ) + + +class EmbeddingsWithStreamingResponse: + def __init__(self, embeddings: Embeddings) -> None: + self._embeddings = embeddings + + self.create = to_streamed_response_wrapper( + embeddings.create, + ) + + +class AsyncEmbeddingsWithStreamingResponse: + def __init__(self, embeddings: AsyncEmbeddings) -> None: + self._embeddings = embeddings + + self.create = async_to_streamed_response_wrapper( + embeddings.create, + ) diff --git a/openai/resources/files.py b/openai/resources/files.py new file mode 100644 index 00000000..33860ada --- /dev/null +++ b/openai/resources/files.py @@ -0,0 +1,689 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import time +import typing_extensions +from typing import Mapping, cast +from typing_extensions import Literal + +import httpx + +from .. import _legacy_response +from ..types import FileObject, FileDeleted, file_list_params, file_create_params +from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven, FileTypes +from .._utils import ( + extract_files, + maybe_transform, + deepcopy_minimal, + async_maybe_transform, +) +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + StreamedBinaryAPIResponse, + AsyncStreamedBinaryAPIResponse, + to_streamed_response_wrapper, + async_to_streamed_response_wrapper, + to_custom_streamed_response_wrapper, + async_to_custom_streamed_response_wrapper, +) +from ..pagination import SyncPage, AsyncPage +from .._base_client import ( + AsyncPaginator, + make_request_options, +) + +__all__ = ["Files", "AsyncFiles"] + + +class Files(SyncAPIResource): + @cached_property + def with_raw_response(self) -> FilesWithRawResponse: + return FilesWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> FilesWithStreamingResponse: + return FilesWithStreamingResponse(self) + + def create( + self, + *, + file: FileTypes, + purpose: Literal["fine-tune", "assistants"], + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> FileObject: + """Upload a file that can be used across various endpoints. + + The size of all the + files uploaded by one organization can be up to 100 GB. + + The size of individual files can be a maximum of 512 MB or 2 million tokens for + Assistants. See the + [Assistants Tools guide](https://platform.openai.com/docs/assistants/tools) to + learn more about the types of files supported. The Fine-tuning API only supports + `.jsonl` files. + + Please [contact us](https://help.openai.com/) if you need to increase these + storage limits. + + Args: + file: The File object (not file name) to be uploaded. + + purpose: The intended purpose of the uploaded file. + + Use "fine-tune" for + [Fine-tuning](https://platform.openai.com/docs/api-reference/fine-tuning) and + "assistants" for + [Assistants](https://platform.openai.com/docs/api-reference/assistants) and + [Messages](https://platform.openai.com/docs/api-reference/messages). This allows + us to validate the format of the uploaded file is correct for fine-tuning. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + body = deepcopy_minimal( + { + "file": file, + "purpose": purpose, + } + ) + files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) + if files: + # It should be noted that the actual Content-Type header that will be + # sent to the server will contain a `boundary` parameter, e.g. + # multipart/form-data; boundary=---abc-- + extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} + return self._post( + "/files", + body=maybe_transform(body, file_create_params.FileCreateParams), + files=files, + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=FileObject, + ) + + def retrieve( + self, + file_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> FileObject: + """ + Returns information about a specific file. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not file_id: + raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") + return self._get( + f"/files/{file_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=FileObject, + ) + + def list( + self, + *, + purpose: str | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SyncPage[FileObject]: + """ + Returns a list of files that belong to the user's organization. + + Args: + purpose: Only return files with the given purpose. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get_api_list( + "/files", + page=SyncPage[FileObject], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform({"purpose": purpose}, file_list_params.FileListParams), + ), + model=FileObject, + ) + + def delete( + self, + file_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> FileDeleted: + """ + Delete a file. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not file_id: + raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") + return self._delete( + f"/files/{file_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=FileDeleted, + ) + + def content( + self, + file_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> _legacy_response.HttpxBinaryResponseContent: + """ + Returns the contents of the specified file. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not file_id: + raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") + extra_headers = {"Accept": "application/binary", **(extra_headers or {})} + return self._get( + f"/files/{file_id}/content", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=_legacy_response.HttpxBinaryResponseContent, + ) + + @typing_extensions.deprecated("The `.content()` method should be used instead") + def retrieve_content( + self, + file_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> str: + """ + Returns the contents of the specified file. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not file_id: + raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") + return self._get( + f"/files/{file_id}/content", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=str, + ) + + def wait_for_processing( + self, + id: str, + *, + poll_interval: float = 5.0, + max_wait_seconds: float = 30 * 60, + ) -> FileObject: + """Waits for the given file to be processed, default timeout is 30 mins.""" + TERMINAL_STATES = {"processed", "error", "deleted"} + + start = time.time() + file = self.retrieve(id) + while file.status not in TERMINAL_STATES: + self._sleep(poll_interval) + + file = self.retrieve(id) + if time.time() - start > max_wait_seconds: + raise RuntimeError( + f"Giving up on waiting for file {id} to finish processing after {max_wait_seconds} seconds." + ) + + return file + + +class AsyncFiles(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncFilesWithRawResponse: + return AsyncFilesWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncFilesWithStreamingResponse: + return AsyncFilesWithStreamingResponse(self) + + async def create( + self, + *, + file: FileTypes, + purpose: Literal["fine-tune", "assistants"], + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> FileObject: + """Upload a file that can be used across various endpoints. + + The size of all the + files uploaded by one organization can be up to 100 GB. + + The size of individual files can be a maximum of 512 MB or 2 million tokens for + Assistants. See the + [Assistants Tools guide](https://platform.openai.com/docs/assistants/tools) to + learn more about the types of files supported. The Fine-tuning API only supports + `.jsonl` files. + + Please [contact us](https://help.openai.com/) if you need to increase these + storage limits. + + Args: + file: The File object (not file name) to be uploaded. + + purpose: The intended purpose of the uploaded file. + + Use "fine-tune" for + [Fine-tuning](https://platform.openai.com/docs/api-reference/fine-tuning) and + "assistants" for + [Assistants](https://platform.openai.com/docs/api-reference/assistants) and + [Messages](https://platform.openai.com/docs/api-reference/messages). This allows + us to validate the format of the uploaded file is correct for fine-tuning. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + body = deepcopy_minimal( + { + "file": file, + "purpose": purpose, + } + ) + files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) + if files: + # It should be noted that the actual Content-Type header that will be + # sent to the server will contain a `boundary` parameter, e.g. + # multipart/form-data; boundary=---abc-- + extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} + return await self._post( + "/files", + body=await async_maybe_transform(body, file_create_params.FileCreateParams), + files=files, + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=FileObject, + ) + + async def retrieve( + self, + file_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> FileObject: + """ + Returns information about a specific file. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not file_id: + raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") + return await self._get( + f"/files/{file_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=FileObject, + ) + + def list( + self, + *, + purpose: str | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncPaginator[FileObject, AsyncPage[FileObject]]: + """ + Returns a list of files that belong to the user's organization. + + Args: + purpose: Only return files with the given purpose. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get_api_list( + "/files", + page=AsyncPage[FileObject], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform({"purpose": purpose}, file_list_params.FileListParams), + ), + model=FileObject, + ) + + async def delete( + self, + file_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> FileDeleted: + """ + Delete a file. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not file_id: + raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") + return await self._delete( + f"/files/{file_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=FileDeleted, + ) + + async def content( + self, + file_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> _legacy_response.HttpxBinaryResponseContent: + """ + Returns the contents of the specified file. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not file_id: + raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") + extra_headers = {"Accept": "application/binary", **(extra_headers or {})} + return await self._get( + f"/files/{file_id}/content", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=_legacy_response.HttpxBinaryResponseContent, + ) + + @typing_extensions.deprecated("The `.content()` method should be used instead") + async def retrieve_content( + self, + file_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> str: + """ + Returns the contents of the specified file. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not file_id: + raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") + return await self._get( + f"/files/{file_id}/content", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=str, + ) + + async def wait_for_processing( + self, + id: str, + *, + poll_interval: float = 5.0, + max_wait_seconds: float = 30 * 60, + ) -> FileObject: + """Waits for the given file to be processed, default timeout is 30 mins.""" + TERMINAL_STATES = {"processed", "error", "deleted"} + + start = time.time() + file = await self.retrieve(id) + while file.status not in TERMINAL_STATES: + await self._sleep(poll_interval) + + file = await self.retrieve(id) + if time.time() - start > max_wait_seconds: + raise RuntimeError( + f"Giving up on waiting for file {id} to finish processing after {max_wait_seconds} seconds." + ) + + return file + + +class FilesWithRawResponse: + def __init__(self, files: Files) -> None: + self._files = files + + self.create = _legacy_response.to_raw_response_wrapper( + files.create, + ) + self.retrieve = _legacy_response.to_raw_response_wrapper( + files.retrieve, + ) + self.list = _legacy_response.to_raw_response_wrapper( + files.list, + ) + self.delete = _legacy_response.to_raw_response_wrapper( + files.delete, + ) + self.content = _legacy_response.to_raw_response_wrapper( + files.content, + ) + self.retrieve_content = ( # pyright: ignore[reportDeprecated] + _legacy_response.to_raw_response_wrapper( + files.retrieve_content # pyright: ignore[reportDeprecated], + ) + ) + + +class AsyncFilesWithRawResponse: + def __init__(self, files: AsyncFiles) -> None: + self._files = files + + self.create = _legacy_response.async_to_raw_response_wrapper( + files.create, + ) + self.retrieve = _legacy_response.async_to_raw_response_wrapper( + files.retrieve, + ) + self.list = _legacy_response.async_to_raw_response_wrapper( + files.list, + ) + self.delete = _legacy_response.async_to_raw_response_wrapper( + files.delete, + ) + self.content = _legacy_response.async_to_raw_response_wrapper( + files.content, + ) + self.retrieve_content = ( # pyright: ignore[reportDeprecated] + _legacy_response.async_to_raw_response_wrapper( + files.retrieve_content # pyright: ignore[reportDeprecated], + ) + ) + + +class FilesWithStreamingResponse: + def __init__(self, files: Files) -> None: + self._files = files + + self.create = to_streamed_response_wrapper( + files.create, + ) + self.retrieve = to_streamed_response_wrapper( + files.retrieve, + ) + self.list = to_streamed_response_wrapper( + files.list, + ) + self.delete = to_streamed_response_wrapper( + files.delete, + ) + self.content = to_custom_streamed_response_wrapper( + files.content, + StreamedBinaryAPIResponse, + ) + self.retrieve_content = ( # pyright: ignore[reportDeprecated] + to_streamed_response_wrapper( + files.retrieve_content # pyright: ignore[reportDeprecated], + ) + ) + + +class AsyncFilesWithStreamingResponse: + def __init__(self, files: AsyncFiles) -> None: + self._files = files + + self.create = async_to_streamed_response_wrapper( + files.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + files.retrieve, + ) + self.list = async_to_streamed_response_wrapper( + files.list, + ) + self.delete = async_to_streamed_response_wrapper( + files.delete, + ) + self.content = async_to_custom_streamed_response_wrapper( + files.content, + AsyncStreamedBinaryAPIResponse, + ) + self.retrieve_content = ( # pyright: ignore[reportDeprecated] + async_to_streamed_response_wrapper( + files.retrieve_content # pyright: ignore[reportDeprecated], + ) + ) diff --git a/openai/resources/fine_tuning/__init__.py b/openai/resources/fine_tuning/__init__.py new file mode 100644 index 00000000..7765231f --- /dev/null +++ b/openai/resources/fine_tuning/__init__.py @@ -0,0 +1,33 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .jobs import ( + Jobs, + AsyncJobs, + JobsWithRawResponse, + AsyncJobsWithRawResponse, + JobsWithStreamingResponse, + AsyncJobsWithStreamingResponse, +) +from .fine_tuning import ( + FineTuning, + AsyncFineTuning, + FineTuningWithRawResponse, + AsyncFineTuningWithRawResponse, + FineTuningWithStreamingResponse, + AsyncFineTuningWithStreamingResponse, +) + +__all__ = [ + "Jobs", + "AsyncJobs", + "JobsWithRawResponse", + "AsyncJobsWithRawResponse", + "JobsWithStreamingResponse", + "AsyncJobsWithStreamingResponse", + "FineTuning", + "AsyncFineTuning", + "FineTuningWithRawResponse", + "AsyncFineTuningWithRawResponse", + "FineTuningWithStreamingResponse", + "AsyncFineTuningWithStreamingResponse", +] diff --git a/openai/resources/fine_tuning/fine_tuning.py b/openai/resources/fine_tuning/fine_tuning.py new file mode 100644 index 00000000..659b3e85 --- /dev/null +++ b/openai/resources/fine_tuning/fine_tuning.py @@ -0,0 +1,80 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from .jobs import ( + Jobs, + AsyncJobs, + JobsWithRawResponse, + AsyncJobsWithRawResponse, + JobsWithStreamingResponse, + AsyncJobsWithStreamingResponse, +) +from ..._compat import cached_property +from ..._resource import SyncAPIResource, AsyncAPIResource + +__all__ = ["FineTuning", "AsyncFineTuning"] + + +class FineTuning(SyncAPIResource): + @cached_property + def jobs(self) -> Jobs: + return Jobs(self._client) + + @cached_property + def with_raw_response(self) -> FineTuningWithRawResponse: + return FineTuningWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> FineTuningWithStreamingResponse: + return FineTuningWithStreamingResponse(self) + + +class AsyncFineTuning(AsyncAPIResource): + @cached_property + def jobs(self) -> AsyncJobs: + return AsyncJobs(self._client) + + @cached_property + def with_raw_response(self) -> AsyncFineTuningWithRawResponse: + return AsyncFineTuningWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncFineTuningWithStreamingResponse: + return AsyncFineTuningWithStreamingResponse(self) + + +class FineTuningWithRawResponse: + def __init__(self, fine_tuning: FineTuning) -> None: + self._fine_tuning = fine_tuning + + @cached_property + def jobs(self) -> JobsWithRawResponse: + return JobsWithRawResponse(self._fine_tuning.jobs) + + +class AsyncFineTuningWithRawResponse: + def __init__(self, fine_tuning: AsyncFineTuning) -> None: + self._fine_tuning = fine_tuning + + @cached_property + def jobs(self) -> AsyncJobsWithRawResponse: + return AsyncJobsWithRawResponse(self._fine_tuning.jobs) + + +class FineTuningWithStreamingResponse: + def __init__(self, fine_tuning: FineTuning) -> None: + self._fine_tuning = fine_tuning + + @cached_property + def jobs(self) -> JobsWithStreamingResponse: + return JobsWithStreamingResponse(self._fine_tuning.jobs) + + +class AsyncFineTuningWithStreamingResponse: + def __init__(self, fine_tuning: AsyncFineTuning) -> None: + self._fine_tuning = fine_tuning + + @cached_property + def jobs(self) -> AsyncJobsWithStreamingResponse: + return AsyncJobsWithStreamingResponse(self._fine_tuning.jobs) diff --git a/openai/resources/fine_tuning/jobs.py b/openai/resources/fine_tuning/jobs.py new file mode 100644 index 00000000..a0c3e24d --- /dev/null +++ b/openai/resources/fine_tuning/jobs.py @@ -0,0 +1,638 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Union, Optional +from typing_extensions import Literal + +import httpx + +from ... import _legacy_response +from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from ..._utils import ( + maybe_transform, + async_maybe_transform, +) +from ..._compat import cached_property +from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper +from ...pagination import SyncCursorPage, AsyncCursorPage +from ..._base_client import ( + AsyncPaginator, + make_request_options, +) +from ...types.fine_tuning import ( + FineTuningJob, + FineTuningJobEvent, + job_list_params, + job_create_params, + job_list_events_params, +) + +__all__ = ["Jobs", "AsyncJobs"] + + +class Jobs(SyncAPIResource): + @cached_property + def with_raw_response(self) -> JobsWithRawResponse: + return JobsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> JobsWithStreamingResponse: + return JobsWithStreamingResponse(self) + + def create( + self, + *, + model: Union[str, Literal["babbage-002", "davinci-002", "gpt-3.5-turbo"]], + training_file: str, + hyperparameters: job_create_params.Hyperparameters | NotGiven = NOT_GIVEN, + suffix: Optional[str] | NotGiven = NOT_GIVEN, + validation_file: Optional[str] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> FineTuningJob: + """ + Creates a fine-tuning job which begins the process of creating a new model from + a given dataset. + + Response includes details of the enqueued job including job status and the name + of the fine-tuned models once complete. + + [Learn more about fine-tuning](https://platform.openai.com/docs/guides/fine-tuning) + + Args: + model: The name of the model to fine-tune. You can select one of the + [supported models](https://platform.openai.com/docs/guides/fine-tuning/what-models-can-be-fine-tuned). + + training_file: The ID of an uploaded file that contains training data. + + See [upload file](https://platform.openai.com/docs/api-reference/files/upload) + for how to upload a file. + + Your dataset must be formatted as a JSONL file. Additionally, you must upload + your file with the purpose `fine-tune`. + + See the [fine-tuning guide](https://platform.openai.com/docs/guides/fine-tuning) + for more details. + + hyperparameters: The hyperparameters used for the fine-tuning job. + + suffix: A string of up to 18 characters that will be added to your fine-tuned model + name. + + For example, a `suffix` of "custom-model-name" would produce a model name like + `ft:gpt-3.5-turbo:openai:custom-model-name:7p4lURel`. + + validation_file: The ID of an uploaded file that contains validation data. + + If you provide this file, the data is used to generate validation metrics + periodically during fine-tuning. These metrics can be viewed in the fine-tuning + results file. The same data should not be present in both train and validation + files. + + Your dataset must be formatted as a JSONL file. You must upload your file with + the purpose `fine-tune`. + + See the [fine-tuning guide](https://platform.openai.com/docs/guides/fine-tuning) + for more details. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._post( + "/fine_tuning/jobs", + body=maybe_transform( + { + "model": model, + "training_file": training_file, + "hyperparameters": hyperparameters, + "suffix": suffix, + "validation_file": validation_file, + }, + job_create_params.JobCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=FineTuningJob, + ) + + def retrieve( + self, + fine_tuning_job_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> FineTuningJob: + """ + Get info about a fine-tuning job. + + [Learn more about fine-tuning](https://platform.openai.com/docs/guides/fine-tuning) + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not fine_tuning_job_id: + raise ValueError(f"Expected a non-empty value for `fine_tuning_job_id` but received {fine_tuning_job_id!r}") + return self._get( + f"/fine_tuning/jobs/{fine_tuning_job_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=FineTuningJob, + ) + + def list( + self, + *, + after: str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SyncCursorPage[FineTuningJob]: + """ + List your organization's fine-tuning jobs + + Args: + after: Identifier for the last job from the previous pagination request. + + limit: Number of fine-tuning jobs to retrieve. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get_api_list( + "/fine_tuning/jobs", + page=SyncCursorPage[FineTuningJob], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "after": after, + "limit": limit, + }, + job_list_params.JobListParams, + ), + ), + model=FineTuningJob, + ) + + def cancel( + self, + fine_tuning_job_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> FineTuningJob: + """ + Immediately cancel a fine-tune job. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not fine_tuning_job_id: + raise ValueError(f"Expected a non-empty value for `fine_tuning_job_id` but received {fine_tuning_job_id!r}") + return self._post( + f"/fine_tuning/jobs/{fine_tuning_job_id}/cancel", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=FineTuningJob, + ) + + def list_events( + self, + fine_tuning_job_id: str, + *, + after: str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SyncCursorPage[FineTuningJobEvent]: + """ + Get status updates for a fine-tuning job. + + Args: + after: Identifier for the last event from the previous pagination request. + + limit: Number of events to retrieve. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not fine_tuning_job_id: + raise ValueError(f"Expected a non-empty value for `fine_tuning_job_id` but received {fine_tuning_job_id!r}") + return self._get_api_list( + f"/fine_tuning/jobs/{fine_tuning_job_id}/events", + page=SyncCursorPage[FineTuningJobEvent], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "after": after, + "limit": limit, + }, + job_list_events_params.JobListEventsParams, + ), + ), + model=FineTuningJobEvent, + ) + + +class AsyncJobs(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncJobsWithRawResponse: + return AsyncJobsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncJobsWithStreamingResponse: + return AsyncJobsWithStreamingResponse(self) + + async def create( + self, + *, + model: Union[str, Literal["babbage-002", "davinci-002", "gpt-3.5-turbo"]], + training_file: str, + hyperparameters: job_create_params.Hyperparameters | NotGiven = NOT_GIVEN, + suffix: Optional[str] | NotGiven = NOT_GIVEN, + validation_file: Optional[str] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> FineTuningJob: + """ + Creates a fine-tuning job which begins the process of creating a new model from + a given dataset. + + Response includes details of the enqueued job including job status and the name + of the fine-tuned models once complete. + + [Learn more about fine-tuning](https://platform.openai.com/docs/guides/fine-tuning) + + Args: + model: The name of the model to fine-tune. You can select one of the + [supported models](https://platform.openai.com/docs/guides/fine-tuning/what-models-can-be-fine-tuned). + + training_file: The ID of an uploaded file that contains training data. + + See [upload file](https://platform.openai.com/docs/api-reference/files/upload) + for how to upload a file. + + Your dataset must be formatted as a JSONL file. Additionally, you must upload + your file with the purpose `fine-tune`. + + See the [fine-tuning guide](https://platform.openai.com/docs/guides/fine-tuning) + for more details. + + hyperparameters: The hyperparameters used for the fine-tuning job. + + suffix: A string of up to 18 characters that will be added to your fine-tuned model + name. + + For example, a `suffix` of "custom-model-name" would produce a model name like + `ft:gpt-3.5-turbo:openai:custom-model-name:7p4lURel`. + + validation_file: The ID of an uploaded file that contains validation data. + + If you provide this file, the data is used to generate validation metrics + periodically during fine-tuning. These metrics can be viewed in the fine-tuning + results file. The same data should not be present in both train and validation + files. + + Your dataset must be formatted as a JSONL file. You must upload your file with + the purpose `fine-tune`. + + See the [fine-tuning guide](https://platform.openai.com/docs/guides/fine-tuning) + for more details. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._post( + "/fine_tuning/jobs", + body=await async_maybe_transform( + { + "model": model, + "training_file": training_file, + "hyperparameters": hyperparameters, + "suffix": suffix, + "validation_file": validation_file, + }, + job_create_params.JobCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=FineTuningJob, + ) + + async def retrieve( + self, + fine_tuning_job_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> FineTuningJob: + """ + Get info about a fine-tuning job. + + [Learn more about fine-tuning](https://platform.openai.com/docs/guides/fine-tuning) + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not fine_tuning_job_id: + raise ValueError(f"Expected a non-empty value for `fine_tuning_job_id` but received {fine_tuning_job_id!r}") + return await self._get( + f"/fine_tuning/jobs/{fine_tuning_job_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=FineTuningJob, + ) + + def list( + self, + *, + after: str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncPaginator[FineTuningJob, AsyncCursorPage[FineTuningJob]]: + """ + List your organization's fine-tuning jobs + + Args: + after: Identifier for the last job from the previous pagination request. + + limit: Number of fine-tuning jobs to retrieve. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get_api_list( + "/fine_tuning/jobs", + page=AsyncCursorPage[FineTuningJob], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "after": after, + "limit": limit, + }, + job_list_params.JobListParams, + ), + ), + model=FineTuningJob, + ) + + async def cancel( + self, + fine_tuning_job_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> FineTuningJob: + """ + Immediately cancel a fine-tune job. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not fine_tuning_job_id: + raise ValueError(f"Expected a non-empty value for `fine_tuning_job_id` but received {fine_tuning_job_id!r}") + return await self._post( + f"/fine_tuning/jobs/{fine_tuning_job_id}/cancel", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=FineTuningJob, + ) + + def list_events( + self, + fine_tuning_job_id: str, + *, + after: str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncPaginator[FineTuningJobEvent, AsyncCursorPage[FineTuningJobEvent]]: + """ + Get status updates for a fine-tuning job. + + Args: + after: Identifier for the last event from the previous pagination request. + + limit: Number of events to retrieve. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not fine_tuning_job_id: + raise ValueError(f"Expected a non-empty value for `fine_tuning_job_id` but received {fine_tuning_job_id!r}") + return self._get_api_list( + f"/fine_tuning/jobs/{fine_tuning_job_id}/events", + page=AsyncCursorPage[FineTuningJobEvent], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "after": after, + "limit": limit, + }, + job_list_events_params.JobListEventsParams, + ), + ), + model=FineTuningJobEvent, + ) + + +class JobsWithRawResponse: + def __init__(self, jobs: Jobs) -> None: + self._jobs = jobs + + self.create = _legacy_response.to_raw_response_wrapper( + jobs.create, + ) + self.retrieve = _legacy_response.to_raw_response_wrapper( + jobs.retrieve, + ) + self.list = _legacy_response.to_raw_response_wrapper( + jobs.list, + ) + self.cancel = _legacy_response.to_raw_response_wrapper( + jobs.cancel, + ) + self.list_events = _legacy_response.to_raw_response_wrapper( + jobs.list_events, + ) + + +class AsyncJobsWithRawResponse: + def __init__(self, jobs: AsyncJobs) -> None: + self._jobs = jobs + + self.create = _legacy_response.async_to_raw_response_wrapper( + jobs.create, + ) + self.retrieve = _legacy_response.async_to_raw_response_wrapper( + jobs.retrieve, + ) + self.list = _legacy_response.async_to_raw_response_wrapper( + jobs.list, + ) + self.cancel = _legacy_response.async_to_raw_response_wrapper( + jobs.cancel, + ) + self.list_events = _legacy_response.async_to_raw_response_wrapper( + jobs.list_events, + ) + + +class JobsWithStreamingResponse: + def __init__(self, jobs: Jobs) -> None: + self._jobs = jobs + + self.create = to_streamed_response_wrapper( + jobs.create, + ) + self.retrieve = to_streamed_response_wrapper( + jobs.retrieve, + ) + self.list = to_streamed_response_wrapper( + jobs.list, + ) + self.cancel = to_streamed_response_wrapper( + jobs.cancel, + ) + self.list_events = to_streamed_response_wrapper( + jobs.list_events, + ) + + +class AsyncJobsWithStreamingResponse: + def __init__(self, jobs: AsyncJobs) -> None: + self._jobs = jobs + + self.create = async_to_streamed_response_wrapper( + jobs.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + jobs.retrieve, + ) + self.list = async_to_streamed_response_wrapper( + jobs.list, + ) + self.cancel = async_to_streamed_response_wrapper( + jobs.cancel, + ) + self.list_events = async_to_streamed_response_wrapper( + jobs.list_events, + ) diff --git a/openai/resources/images.py b/openai/resources/images.py new file mode 100644 index 00000000..e12fa51b --- /dev/null +++ b/openai/resources/images.py @@ -0,0 +1,587 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Union, Mapping, Optional, cast +from typing_extensions import Literal + +import httpx + +from .. import _legacy_response +from ..types import ( + ImagesResponse, + image_edit_params, + image_generate_params, + image_create_variation_params, +) +from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven, FileTypes +from .._utils import ( + extract_files, + maybe_transform, + deepcopy_minimal, + async_maybe_transform, +) +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper +from .._base_client import ( + make_request_options, +) + +__all__ = ["Images", "AsyncImages"] + + +class Images(SyncAPIResource): + @cached_property + def with_raw_response(self) -> ImagesWithRawResponse: + return ImagesWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> ImagesWithStreamingResponse: + return ImagesWithStreamingResponse(self) + + def create_variation( + self, + *, + image: FileTypes, + model: Union[str, Literal["dall-e-2"], None] | NotGiven = NOT_GIVEN, + n: Optional[int] | NotGiven = NOT_GIVEN, + response_format: Optional[Literal["url", "b64_json"]] | NotGiven = NOT_GIVEN, + size: Optional[Literal["256x256", "512x512", "1024x1024"]] | NotGiven = NOT_GIVEN, + user: str | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ImagesResponse: + """ + Creates a variation of a given image. + + Args: + image: The image to use as the basis for the variation(s). Must be a valid PNG file, + less than 4MB, and square. + + model: The model to use for image generation. Only `dall-e-2` is supported at this + time. + + n: The number of images to generate. Must be between 1 and 10. For `dall-e-3`, only + `n=1` is supported. + + response_format: The format in which the generated images are returned. Must be one of `url` or + `b64_json`. URLs are only valid for 60 minutes after the image has been + generated. + + size: The size of the generated images. Must be one of `256x256`, `512x512`, or + `1024x1024`. + + user: A unique identifier representing your end-user, which can help OpenAI to monitor + and detect abuse. + [Learn more](https://platform.openai.com/docs/guides/safety-best-practices/end-user-ids). + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + body = deepcopy_minimal( + { + "image": image, + "model": model, + "n": n, + "response_format": response_format, + "size": size, + "user": user, + } + ) + files = extract_files(cast(Mapping[str, object], body), paths=[["image"]]) + if files: + # It should be noted that the actual Content-Type header that will be + # sent to the server will contain a `boundary` parameter, e.g. + # multipart/form-data; boundary=---abc-- + extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} + return self._post( + "/images/variations", + body=maybe_transform(body, image_create_variation_params.ImageCreateVariationParams), + files=files, + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ImagesResponse, + ) + + def edit( + self, + *, + image: FileTypes, + prompt: str, + mask: FileTypes | NotGiven = NOT_GIVEN, + model: Union[str, Literal["dall-e-2"], None] | NotGiven = NOT_GIVEN, + n: Optional[int] | NotGiven = NOT_GIVEN, + response_format: Optional[Literal["url", "b64_json"]] | NotGiven = NOT_GIVEN, + size: Optional[Literal["256x256", "512x512", "1024x1024"]] | NotGiven = NOT_GIVEN, + user: str | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ImagesResponse: + """ + Creates an edited or extended image given an original image and a prompt. + + Args: + image: The image to edit. Must be a valid PNG file, less than 4MB, and square. If mask + is not provided, image must have transparency, which will be used as the mask. + + prompt: A text description of the desired image(s). The maximum length is 1000 + characters. + + mask: An additional image whose fully transparent areas (e.g. where alpha is zero) + indicate where `image` should be edited. Must be a valid PNG file, less than + 4MB, and have the same dimensions as `image`. + + model: The model to use for image generation. Only `dall-e-2` is supported at this + time. + + n: The number of images to generate. Must be between 1 and 10. + + response_format: The format in which the generated images are returned. Must be one of `url` or + `b64_json`. URLs are only valid for 60 minutes after the image has been + generated. + + size: The size of the generated images. Must be one of `256x256`, `512x512`, or + `1024x1024`. + + user: A unique identifier representing your end-user, which can help OpenAI to monitor + and detect abuse. + [Learn more](https://platform.openai.com/docs/guides/safety-best-practices/end-user-ids). + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + body = deepcopy_minimal( + { + "image": image, + "prompt": prompt, + "mask": mask, + "model": model, + "n": n, + "response_format": response_format, + "size": size, + "user": user, + } + ) + files = extract_files(cast(Mapping[str, object], body), paths=[["image"], ["mask"]]) + if files: + # It should be noted that the actual Content-Type header that will be + # sent to the server will contain a `boundary` parameter, e.g. + # multipart/form-data; boundary=---abc-- + extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} + return self._post( + "/images/edits", + body=maybe_transform(body, image_edit_params.ImageEditParams), + files=files, + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ImagesResponse, + ) + + def generate( + self, + *, + prompt: str, + model: Union[str, Literal["dall-e-2", "dall-e-3"], None] | NotGiven = NOT_GIVEN, + n: Optional[int] | NotGiven = NOT_GIVEN, + quality: Literal["standard", "hd"] | NotGiven = NOT_GIVEN, + response_format: Optional[Literal["url", "b64_json"]] | NotGiven = NOT_GIVEN, + size: Optional[Literal["256x256", "512x512", "1024x1024", "1792x1024", "1024x1792"]] | NotGiven = NOT_GIVEN, + style: Optional[Literal["vivid", "natural"]] | NotGiven = NOT_GIVEN, + user: str | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ImagesResponse: + """ + Creates an image given a prompt. + + Args: + prompt: A text description of the desired image(s). The maximum length is 1000 + characters for `dall-e-2` and 4000 characters for `dall-e-3`. + + model: The model to use for image generation. + + n: The number of images to generate. Must be between 1 and 10. For `dall-e-3`, only + `n=1` is supported. + + quality: The quality of the image that will be generated. `hd` creates images with finer + details and greater consistency across the image. This param is only supported + for `dall-e-3`. + + response_format: The format in which the generated images are returned. Must be one of `url` or + `b64_json`. URLs are only valid for 60 minutes after the image has been + generated. + + size: The size of the generated images. Must be one of `256x256`, `512x512`, or + `1024x1024` for `dall-e-2`. Must be one of `1024x1024`, `1792x1024`, or + `1024x1792` for `dall-e-3` models. + + style: The style of the generated images. Must be one of `vivid` or `natural`. Vivid + causes the model to lean towards generating hyper-real and dramatic images. + Natural causes the model to produce more natural, less hyper-real looking + images. This param is only supported for `dall-e-3`. + + user: A unique identifier representing your end-user, which can help OpenAI to monitor + and detect abuse. + [Learn more](https://platform.openai.com/docs/guides/safety-best-practices/end-user-ids). + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._post( + "/images/generations", + body=maybe_transform( + { + "prompt": prompt, + "model": model, + "n": n, + "quality": quality, + "response_format": response_format, + "size": size, + "style": style, + "user": user, + }, + image_generate_params.ImageGenerateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ImagesResponse, + ) + + +class AsyncImages(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncImagesWithRawResponse: + return AsyncImagesWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncImagesWithStreamingResponse: + return AsyncImagesWithStreamingResponse(self) + + async def create_variation( + self, + *, + image: FileTypes, + model: Union[str, Literal["dall-e-2"], None] | NotGiven = NOT_GIVEN, + n: Optional[int] | NotGiven = NOT_GIVEN, + response_format: Optional[Literal["url", "b64_json"]] | NotGiven = NOT_GIVEN, + size: Optional[Literal["256x256", "512x512", "1024x1024"]] | NotGiven = NOT_GIVEN, + user: str | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ImagesResponse: + """ + Creates a variation of a given image. + + Args: + image: The image to use as the basis for the variation(s). Must be a valid PNG file, + less than 4MB, and square. + + model: The model to use for image generation. Only `dall-e-2` is supported at this + time. + + n: The number of images to generate. Must be between 1 and 10. For `dall-e-3`, only + `n=1` is supported. + + response_format: The format in which the generated images are returned. Must be one of `url` or + `b64_json`. URLs are only valid for 60 minutes after the image has been + generated. + + size: The size of the generated images. Must be one of `256x256`, `512x512`, or + `1024x1024`. + + user: A unique identifier representing your end-user, which can help OpenAI to monitor + and detect abuse. + [Learn more](https://platform.openai.com/docs/guides/safety-best-practices/end-user-ids). + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + body = deepcopy_minimal( + { + "image": image, + "model": model, + "n": n, + "response_format": response_format, + "size": size, + "user": user, + } + ) + files = extract_files(cast(Mapping[str, object], body), paths=[["image"]]) + if files: + # It should be noted that the actual Content-Type header that will be + # sent to the server will contain a `boundary` parameter, e.g. + # multipart/form-data; boundary=---abc-- + extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} + return await self._post( + "/images/variations", + body=await async_maybe_transform(body, image_create_variation_params.ImageCreateVariationParams), + files=files, + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ImagesResponse, + ) + + async def edit( + self, + *, + image: FileTypes, + prompt: str, + mask: FileTypes | NotGiven = NOT_GIVEN, + model: Union[str, Literal["dall-e-2"], None] | NotGiven = NOT_GIVEN, + n: Optional[int] | NotGiven = NOT_GIVEN, + response_format: Optional[Literal["url", "b64_json"]] | NotGiven = NOT_GIVEN, + size: Optional[Literal["256x256", "512x512", "1024x1024"]] | NotGiven = NOT_GIVEN, + user: str | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ImagesResponse: + """ + Creates an edited or extended image given an original image and a prompt. + + Args: + image: The image to edit. Must be a valid PNG file, less than 4MB, and square. If mask + is not provided, image must have transparency, which will be used as the mask. + + prompt: A text description of the desired image(s). The maximum length is 1000 + characters. + + mask: An additional image whose fully transparent areas (e.g. where alpha is zero) + indicate where `image` should be edited. Must be a valid PNG file, less than + 4MB, and have the same dimensions as `image`. + + model: The model to use for image generation. Only `dall-e-2` is supported at this + time. + + n: The number of images to generate. Must be between 1 and 10. + + response_format: The format in which the generated images are returned. Must be one of `url` or + `b64_json`. URLs are only valid for 60 minutes after the image has been + generated. + + size: The size of the generated images. Must be one of `256x256`, `512x512`, or + `1024x1024`. + + user: A unique identifier representing your end-user, which can help OpenAI to monitor + and detect abuse. + [Learn more](https://platform.openai.com/docs/guides/safety-best-practices/end-user-ids). + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + body = deepcopy_minimal( + { + "image": image, + "prompt": prompt, + "mask": mask, + "model": model, + "n": n, + "response_format": response_format, + "size": size, + "user": user, + } + ) + files = extract_files(cast(Mapping[str, object], body), paths=[["image"], ["mask"]]) + if files: + # It should be noted that the actual Content-Type header that will be + # sent to the server will contain a `boundary` parameter, e.g. + # multipart/form-data; boundary=---abc-- + extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} + return await self._post( + "/images/edits", + body=await async_maybe_transform(body, image_edit_params.ImageEditParams), + files=files, + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ImagesResponse, + ) + + async def generate( + self, + *, + prompt: str, + model: Union[str, Literal["dall-e-2", "dall-e-3"], None] | NotGiven = NOT_GIVEN, + n: Optional[int] | NotGiven = NOT_GIVEN, + quality: Literal["standard", "hd"] | NotGiven = NOT_GIVEN, + response_format: Optional[Literal["url", "b64_json"]] | NotGiven = NOT_GIVEN, + size: Optional[Literal["256x256", "512x512", "1024x1024", "1792x1024", "1024x1792"]] | NotGiven = NOT_GIVEN, + style: Optional[Literal["vivid", "natural"]] | NotGiven = NOT_GIVEN, + user: str | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ImagesResponse: + """ + Creates an image given a prompt. + + Args: + prompt: A text description of the desired image(s). The maximum length is 1000 + characters for `dall-e-2` and 4000 characters for `dall-e-3`. + + model: The model to use for image generation. + + n: The number of images to generate. Must be between 1 and 10. For `dall-e-3`, only + `n=1` is supported. + + quality: The quality of the image that will be generated. `hd` creates images with finer + details and greater consistency across the image. This param is only supported + for `dall-e-3`. + + response_format: The format in which the generated images are returned. Must be one of `url` or + `b64_json`. URLs are only valid for 60 minutes after the image has been + generated. + + size: The size of the generated images. Must be one of `256x256`, `512x512`, or + `1024x1024` for `dall-e-2`. Must be one of `1024x1024`, `1792x1024`, or + `1024x1792` for `dall-e-3` models. + + style: The style of the generated images. Must be one of `vivid` or `natural`. Vivid + causes the model to lean towards generating hyper-real and dramatic images. + Natural causes the model to produce more natural, less hyper-real looking + images. This param is only supported for `dall-e-3`. + + user: A unique identifier representing your end-user, which can help OpenAI to monitor + and detect abuse. + [Learn more](https://platform.openai.com/docs/guides/safety-best-practices/end-user-ids). + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._post( + "/images/generations", + body=await async_maybe_transform( + { + "prompt": prompt, + "model": model, + "n": n, + "quality": quality, + "response_format": response_format, + "size": size, + "style": style, + "user": user, + }, + image_generate_params.ImageGenerateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ImagesResponse, + ) + + +class ImagesWithRawResponse: + def __init__(self, images: Images) -> None: + self._images = images + + self.create_variation = _legacy_response.to_raw_response_wrapper( + images.create_variation, + ) + self.edit = _legacy_response.to_raw_response_wrapper( + images.edit, + ) + self.generate = _legacy_response.to_raw_response_wrapper( + images.generate, + ) + + +class AsyncImagesWithRawResponse: + def __init__(self, images: AsyncImages) -> None: + self._images = images + + self.create_variation = _legacy_response.async_to_raw_response_wrapper( + images.create_variation, + ) + self.edit = _legacy_response.async_to_raw_response_wrapper( + images.edit, + ) + self.generate = _legacy_response.async_to_raw_response_wrapper( + images.generate, + ) + + +class ImagesWithStreamingResponse: + def __init__(self, images: Images) -> None: + self._images = images + + self.create_variation = to_streamed_response_wrapper( + images.create_variation, + ) + self.edit = to_streamed_response_wrapper( + images.edit, + ) + self.generate = to_streamed_response_wrapper( + images.generate, + ) + + +class AsyncImagesWithStreamingResponse: + def __init__(self, images: AsyncImages) -> None: + self._images = images + + self.create_variation = async_to_streamed_response_wrapper( + images.create_variation, + ) + self.edit = async_to_streamed_response_wrapper( + images.edit, + ) + self.generate = async_to_streamed_response_wrapper( + images.generate, + ) diff --git a/openai/resources/models.py b/openai/resources/models.py new file mode 100644 index 00000000..4e36e208 --- /dev/null +++ b/openai/resources/models.py @@ -0,0 +1,283 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import httpx + +from .. import _legacy_response +from ..types import Model, ModelDeleted +from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper +from ..pagination import SyncPage, AsyncPage +from .._base_client import ( + AsyncPaginator, + make_request_options, +) + +__all__ = ["Models", "AsyncModels"] + + +class Models(SyncAPIResource): + @cached_property + def with_raw_response(self) -> ModelsWithRawResponse: + return ModelsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> ModelsWithStreamingResponse: + return ModelsWithStreamingResponse(self) + + def retrieve( + self, + model: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Model: + """ + Retrieves a model instance, providing basic information about the model such as + the owner and permissioning. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not model: + raise ValueError(f"Expected a non-empty value for `model` but received {model!r}") + return self._get( + f"/models/{model}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Model, + ) + + def list( + self, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SyncPage[Model]: + """ + Lists the currently available models, and provides basic information about each + one such as the owner and availability. + """ + return self._get_api_list( + "/models", + page=SyncPage[Model], + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + model=Model, + ) + + def delete( + self, + model: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ModelDeleted: + """Delete a fine-tuned model. + + You must have the Owner role in your organization to + delete a model. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not model: + raise ValueError(f"Expected a non-empty value for `model` but received {model!r}") + return self._delete( + f"/models/{model}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ModelDeleted, + ) + + +class AsyncModels(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncModelsWithRawResponse: + return AsyncModelsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncModelsWithStreamingResponse: + return AsyncModelsWithStreamingResponse(self) + + async def retrieve( + self, + model: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Model: + """ + Retrieves a model instance, providing basic information about the model such as + the owner and permissioning. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not model: + raise ValueError(f"Expected a non-empty value for `model` but received {model!r}") + return await self._get( + f"/models/{model}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Model, + ) + + def list( + self, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncPaginator[Model, AsyncPage[Model]]: + """ + Lists the currently available models, and provides basic information about each + one such as the owner and availability. + """ + return self._get_api_list( + "/models", + page=AsyncPage[Model], + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + model=Model, + ) + + async def delete( + self, + model: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ModelDeleted: + """Delete a fine-tuned model. + + You must have the Owner role in your organization to + delete a model. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not model: + raise ValueError(f"Expected a non-empty value for `model` but received {model!r}") + return await self._delete( + f"/models/{model}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ModelDeleted, + ) + + +class ModelsWithRawResponse: + def __init__(self, models: Models) -> None: + self._models = models + + self.retrieve = _legacy_response.to_raw_response_wrapper( + models.retrieve, + ) + self.list = _legacy_response.to_raw_response_wrapper( + models.list, + ) + self.delete = _legacy_response.to_raw_response_wrapper( + models.delete, + ) + + +class AsyncModelsWithRawResponse: + def __init__(self, models: AsyncModels) -> None: + self._models = models + + self.retrieve = _legacy_response.async_to_raw_response_wrapper( + models.retrieve, + ) + self.list = _legacy_response.async_to_raw_response_wrapper( + models.list, + ) + self.delete = _legacy_response.async_to_raw_response_wrapper( + models.delete, + ) + + +class ModelsWithStreamingResponse: + def __init__(self, models: Models) -> None: + self._models = models + + self.retrieve = to_streamed_response_wrapper( + models.retrieve, + ) + self.list = to_streamed_response_wrapper( + models.list, + ) + self.delete = to_streamed_response_wrapper( + models.delete, + ) + + +class AsyncModelsWithStreamingResponse: + def __init__(self, models: AsyncModels) -> None: + self._models = models + + self.retrieve = async_to_streamed_response_wrapper( + models.retrieve, + ) + self.list = async_to_streamed_response_wrapper( + models.list, + ) + self.delete = async_to_streamed_response_wrapper( + models.delete, + ) diff --git a/openai/resources/moderations.py b/openai/resources/moderations.py new file mode 100644 index 00000000..385b672f --- /dev/null +++ b/openai/resources/moderations.py @@ -0,0 +1,180 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List, Union +from typing_extensions import Literal + +import httpx + +from .. import _legacy_response +from ..types import ModerationCreateResponse, moderation_create_params +from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from .._utils import ( + maybe_transform, + async_maybe_transform, +) +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper +from .._base_client import ( + make_request_options, +) + +__all__ = ["Moderations", "AsyncModerations"] + + +class Moderations(SyncAPIResource): + @cached_property + def with_raw_response(self) -> ModerationsWithRawResponse: + return ModerationsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> ModerationsWithStreamingResponse: + return ModerationsWithStreamingResponse(self) + + def create( + self, + *, + input: Union[str, List[str]], + model: Union[str, Literal["text-moderation-latest", "text-moderation-stable"]] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ModerationCreateResponse: + """ + Classifies if text is potentially harmful. + + Args: + input: The input text to classify + + model: Two content moderations models are available: `text-moderation-stable` and + `text-moderation-latest`. + + The default is `text-moderation-latest` which will be automatically upgraded + over time. This ensures you are always using our most accurate model. If you use + `text-moderation-stable`, we will provide advanced notice before updating the + model. Accuracy of `text-moderation-stable` may be slightly lower than for + `text-moderation-latest`. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._post( + "/moderations", + body=maybe_transform( + { + "input": input, + "model": model, + }, + moderation_create_params.ModerationCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ModerationCreateResponse, + ) + + +class AsyncModerations(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncModerationsWithRawResponse: + return AsyncModerationsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncModerationsWithStreamingResponse: + return AsyncModerationsWithStreamingResponse(self) + + async def create( + self, + *, + input: Union[str, List[str]], + model: Union[str, Literal["text-moderation-latest", "text-moderation-stable"]] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ModerationCreateResponse: + """ + Classifies if text is potentially harmful. + + Args: + input: The input text to classify + + model: Two content moderations models are available: `text-moderation-stable` and + `text-moderation-latest`. + + The default is `text-moderation-latest` which will be automatically upgraded + over time. This ensures you are always using our most accurate model. If you use + `text-moderation-stable`, we will provide advanced notice before updating the + model. Accuracy of `text-moderation-stable` may be slightly lower than for + `text-moderation-latest`. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._post( + "/moderations", + body=await async_maybe_transform( + { + "input": input, + "model": model, + }, + moderation_create_params.ModerationCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ModerationCreateResponse, + ) + + +class ModerationsWithRawResponse: + def __init__(self, moderations: Moderations) -> None: + self._moderations = moderations + + self.create = _legacy_response.to_raw_response_wrapper( + moderations.create, + ) + + +class AsyncModerationsWithRawResponse: + def __init__(self, moderations: AsyncModerations) -> None: + self._moderations = moderations + + self.create = _legacy_response.async_to_raw_response_wrapper( + moderations.create, + ) + + +class ModerationsWithStreamingResponse: + def __init__(self, moderations: Moderations) -> None: + self._moderations = moderations + + self.create = to_streamed_response_wrapper( + moderations.create, + ) + + +class AsyncModerationsWithStreamingResponse: + def __init__(self, moderations: AsyncModerations) -> None: + self._moderations = moderations + + self.create = async_to_streamed_response_wrapper( + moderations.create, + ) diff --git a/openai/types/__init__.py b/openai/types/__init__.py new file mode 100644 index 00000000..0917e22a --- /dev/null +++ b/openai/types/__init__.py @@ -0,0 +1,31 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from .image import Image as Image +from .model import Model as Model +from .shared import ( + ErrorObject as ErrorObject, + FunctionDefinition as FunctionDefinition, + FunctionParameters as FunctionParameters, +) +from .embedding import Embedding as Embedding +from .completion import Completion as Completion +from .moderation import Moderation as Moderation +from .file_object import FileObject as FileObject +from .file_content import FileContent as FileContent +from .file_deleted import FileDeleted as FileDeleted +from .model_deleted import ModelDeleted as ModelDeleted +from .images_response import ImagesResponse as ImagesResponse +from .completion_usage import CompletionUsage as CompletionUsage +from .file_list_params import FileListParams as FileListParams +from .completion_choice import CompletionChoice as CompletionChoice +from .image_edit_params import ImageEditParams as ImageEditParams +from .file_create_params import FileCreateParams as FileCreateParams +from .image_generate_params import ImageGenerateParams as ImageGenerateParams +from .embedding_create_params import EmbeddingCreateParams as EmbeddingCreateParams +from .completion_create_params import CompletionCreateParams as CompletionCreateParams +from .moderation_create_params import ModerationCreateParams as ModerationCreateParams +from .create_embedding_response import CreateEmbeddingResponse as CreateEmbeddingResponse +from .moderation_create_response import ModerationCreateResponse as ModerationCreateResponse +from .image_create_variation_params import ImageCreateVariationParams as ImageCreateVariationParams diff --git a/openai/types/audio/__init__.py b/openai/types/audio/__init__.py new file mode 100644 index 00000000..8d2c44c8 --- /dev/null +++ b/openai/types/audio/__init__.py @@ -0,0 +1,9 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from .translation import Translation as Translation +from .transcription import Transcription as Transcription +from .speech_create_params import SpeechCreateParams as SpeechCreateParams +from .translation_create_params import TranslationCreateParams as TranslationCreateParams +from .transcription_create_params import TranscriptionCreateParams as TranscriptionCreateParams diff --git a/openai/types/audio/speech_create_params.py b/openai/types/audio/speech_create_params.py new file mode 100644 index 00000000..8d75ec4c --- /dev/null +++ b/openai/types/audio/speech_create_params.py @@ -0,0 +1,39 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Union +from typing_extensions import Literal, Required, TypedDict + +__all__ = ["SpeechCreateParams"] + + +class SpeechCreateParams(TypedDict, total=False): + input: Required[str] + """The text to generate audio for. The maximum length is 4096 characters.""" + + model: Required[Union[str, Literal["tts-1", "tts-1-hd"]]] + """ + One of the available [TTS models](https://platform.openai.com/docs/models/tts): + `tts-1` or `tts-1-hd` + """ + + voice: Required[Literal["alloy", "echo", "fable", "onyx", "nova", "shimmer"]] + """The voice to use when generating the audio. + + Supported voices are `alloy`, `echo`, `fable`, `onyx`, `nova`, and `shimmer`. + Previews of the voices are available in the + [Text to speech guide](https://platform.openai.com/docs/guides/text-to-speech/voice-options). + """ + + response_format: Literal["mp3", "opus", "aac", "flac", "wav", "pcm"] + """The format to audio in. + + Supported formats are `mp3`, `opus`, `aac`, `flac`, `wav`, and `pcm`. + """ + + speed: float + """The speed of the generated audio. + + Select a value from `0.25` to `4.0`. `1.0` is the default. + """ diff --git a/openai/types/audio/transcription.py b/openai/types/audio/transcription.py new file mode 100644 index 00000000..fa512e27 --- /dev/null +++ b/openai/types/audio/transcription.py @@ -0,0 +1,10 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from ..._models import BaseModel + +__all__ = ["Transcription"] + + +class Transcription(BaseModel): + text: str + """The transcribed text.""" diff --git a/openai/types/audio/transcription_create_params.py b/openai/types/audio/transcription_create_params.py new file mode 100644 index 00000000..6b2d5bae --- /dev/null +++ b/openai/types/audio/transcription_create_params.py @@ -0,0 +1,65 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List, Union +from typing_extensions import Literal, Required, TypedDict + +from ..._types import FileTypes + +__all__ = ["TranscriptionCreateParams"] + + +class TranscriptionCreateParams(TypedDict, total=False): + file: Required[FileTypes] + """ + The audio file object (not file name) to transcribe, in one of these formats: + flac, mp3, mp4, mpeg, mpga, m4a, ogg, wav, or webm. + """ + + model: Required[Union[str, Literal["whisper-1"]]] + """ID of the model to use. + + Only `whisper-1` (which is powered by our open source Whisper V2 model) is + currently available. + """ + + language: str + """The language of the input audio. + + Supplying the input language in + [ISO-639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) format will + improve accuracy and latency. + """ + + prompt: str + """An optional text to guide the model's style or continue a previous audio + segment. + + The [prompt](https://platform.openai.com/docs/guides/speech-to-text/prompting) + should match the audio language. + """ + + response_format: Literal["json", "text", "srt", "verbose_json", "vtt"] + """ + The format of the transcript output, in one of these options: `json`, `text`, + `srt`, `verbose_json`, or `vtt`. + """ + + temperature: float + """The sampling temperature, between 0 and 1. + + Higher values like 0.8 will make the output more random, while lower values like + 0.2 will make it more focused and deterministic. If set to 0, the model will use + [log probability](https://en.wikipedia.org/wiki/Log_probability) to + automatically increase the temperature until certain thresholds are hit. + """ + + timestamp_granularities: List[Literal["word", "segment"]] + """The timestamp granularities to populate for this transcription. + + `response_format` must be set `verbose_json` to use timestamp granularities. + Either or both of these options are supported: `word`, or `segment`. Note: There + is no additional latency for segment timestamps, but generating word timestamps + incurs additional latency. + """ diff --git a/openai/types/audio/translation.py b/openai/types/audio/translation.py new file mode 100644 index 00000000..efc56f7f --- /dev/null +++ b/openai/types/audio/translation.py @@ -0,0 +1,9 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from ..._models import BaseModel + +__all__ = ["Translation"] + + +class Translation(BaseModel): + text: str diff --git a/openai/types/audio/translation_create_params.py b/openai/types/audio/translation_create_params.py new file mode 100644 index 00000000..f23a41ed --- /dev/null +++ b/openai/types/audio/translation_create_params.py @@ -0,0 +1,48 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Union +from typing_extensions import Literal, Required, TypedDict + +from ..._types import FileTypes + +__all__ = ["TranslationCreateParams"] + + +class TranslationCreateParams(TypedDict, total=False): + file: Required[FileTypes] + """ + The audio file object (not file name) translate, in one of these formats: flac, + mp3, mp4, mpeg, mpga, m4a, ogg, wav, or webm. + """ + + model: Required[Union[str, Literal["whisper-1"]]] + """ID of the model to use. + + Only `whisper-1` (which is powered by our open source Whisper V2 model) is + currently available. + """ + + prompt: str + """An optional text to guide the model's style or continue a previous audio + segment. + + The [prompt](https://platform.openai.com/docs/guides/speech-to-text/prompting) + should be in English. + """ + + response_format: str + """ + The format of the transcript output, in one of these options: `json`, `text`, + `srt`, `verbose_json`, or `vtt`. + """ + + temperature: float + """The sampling temperature, between 0 and 1. + + Higher values like 0.8 will make the output more random, while lower values like + 0.2 will make it more focused and deterministic. If set to 0, the model will use + [log probability](https://en.wikipedia.org/wiki/Log_probability) to + automatically increase the temperature until certain thresholds are hit. + """ diff --git a/openai/types/beta/__init__.py b/openai/types/beta/__init__.py new file mode 100644 index 00000000..a7de0272 --- /dev/null +++ b/openai/types/beta/__init__.py @@ -0,0 +1,23 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from .thread import Thread as Thread +from .assistant import Assistant as Assistant +from .function_tool import FunctionTool as FunctionTool +from .assistant_tool import AssistantTool as AssistantTool +from .retrieval_tool import RetrievalTool as RetrievalTool +from .thread_deleted import ThreadDeleted as ThreadDeleted +from .assistant_deleted import AssistantDeleted as AssistantDeleted +from .function_tool_param import FunctionToolParam as FunctionToolParam +from .assistant_tool_param import AssistantToolParam as AssistantToolParam +from .retrieval_tool_param import RetrievalToolParam as RetrievalToolParam +from .thread_create_params import ThreadCreateParams as ThreadCreateParams +from .thread_update_params import ThreadUpdateParams as ThreadUpdateParams +from .assistant_list_params import AssistantListParams as AssistantListParams +from .code_interpreter_tool import CodeInterpreterTool as CodeInterpreterTool +from .assistant_stream_event import AssistantStreamEvent as AssistantStreamEvent +from .assistant_create_params import AssistantCreateParams as AssistantCreateParams +from .assistant_update_params import AssistantUpdateParams as AssistantUpdateParams +from .code_interpreter_tool_param import CodeInterpreterToolParam as CodeInterpreterToolParam +from .thread_create_and_run_params import ThreadCreateAndRunParams as ThreadCreateAndRunParams diff --git a/openai/types/beta/assistant.py b/openai/types/beta/assistant.py new file mode 100644 index 00000000..32561a9a --- /dev/null +++ b/openai/types/beta/assistant.py @@ -0,0 +1,64 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional +from typing_extensions import Literal + +from ..._models import BaseModel +from .assistant_tool import AssistantTool + +__all__ = ["Assistant"] + + +class Assistant(BaseModel): + id: str + """The identifier, which can be referenced in API endpoints.""" + + created_at: int + """The Unix timestamp (in seconds) for when the assistant was created.""" + + description: Optional[str] = None + """The description of the assistant. The maximum length is 512 characters.""" + + file_ids: List[str] + """ + A list of [file](https://platform.openai.com/docs/api-reference/files) IDs + attached to this assistant. There can be a maximum of 20 files attached to the + assistant. Files are ordered by their creation date in ascending order. + """ + + instructions: Optional[str] = None + """The system instructions that the assistant uses. + + The maximum length is 32768 characters. + """ + + metadata: Optional[object] = None + """Set of 16 key-value pairs that can be attached to an object. + + This can be useful for storing additional information about the object in a + structured format. Keys can be a maximum of 64 characters long and values can be + a maxium of 512 characters long. + """ + + model: str + """ID of the model to use. + + You can use the + [List models](https://platform.openai.com/docs/api-reference/models/list) API to + see all of your available models, or see our + [Model overview](https://platform.openai.com/docs/models/overview) for + descriptions of them. + """ + + name: Optional[str] = None + """The name of the assistant. The maximum length is 256 characters.""" + + object: Literal["assistant"] + """The object type, which is always `assistant`.""" + + tools: List[AssistantTool] + """A list of tool enabled on the assistant. + + There can be a maximum of 128 tools per assistant. Tools can be of types + `code_interpreter`, `retrieval`, or `function`. + """ diff --git a/openai/types/beta/assistant_create_params.py b/openai/types/beta/assistant_create_params.py new file mode 100644 index 00000000..8bad3236 --- /dev/null +++ b/openai/types/beta/assistant_create_params.py @@ -0,0 +1,56 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List, Iterable, Optional +from typing_extensions import Required, TypedDict + +from .assistant_tool_param import AssistantToolParam + +__all__ = ["AssistantCreateParams"] + + +class AssistantCreateParams(TypedDict, total=False): + model: Required[str] + """ID of the model to use. + + You can use the + [List models](https://platform.openai.com/docs/api-reference/models/list) API to + see all of your available models, or see our + [Model overview](https://platform.openai.com/docs/models/overview) for + descriptions of them. + """ + + description: Optional[str] + """The description of the assistant. The maximum length is 512 characters.""" + + file_ids: List[str] + """ + A list of [file](https://platform.openai.com/docs/api-reference/files) IDs + attached to this assistant. There can be a maximum of 20 files attached to the + assistant. Files are ordered by their creation date in ascending order. + """ + + instructions: Optional[str] + """The system instructions that the assistant uses. + + The maximum length is 32768 characters. + """ + + metadata: Optional[object] + """Set of 16 key-value pairs that can be attached to an object. + + This can be useful for storing additional information about the object in a + structured format. Keys can be a maximum of 64 characters long and values can be + a maxium of 512 characters long. + """ + + name: Optional[str] + """The name of the assistant. The maximum length is 256 characters.""" + + tools: Iterable[AssistantToolParam] + """A list of tool enabled on the assistant. + + There can be a maximum of 128 tools per assistant. Tools can be of types + `code_interpreter`, `retrieval`, or `function`. + """ diff --git a/openai/types/beta/assistant_deleted.py b/openai/types/beta/assistant_deleted.py new file mode 100644 index 00000000..3be40cd6 --- /dev/null +++ b/openai/types/beta/assistant_deleted.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing_extensions import Literal + +from ..._models import BaseModel + +__all__ = ["AssistantDeleted"] + + +class AssistantDeleted(BaseModel): + id: str + + deleted: bool + + object: Literal["assistant.deleted"] diff --git a/openai/types/beta/assistant_list_params.py b/openai/types/beta/assistant_list_params.py new file mode 100644 index 00000000..f54f6312 --- /dev/null +++ b/openai/types/beta/assistant_list_params.py @@ -0,0 +1,39 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, TypedDict + +__all__ = ["AssistantListParams"] + + +class AssistantListParams(TypedDict, total=False): + after: str + """A cursor for use in pagination. + + `after` is an object ID that defines your place in the list. For instance, if + you make a list request and receive 100 objects, ending with obj_foo, your + subsequent call can include after=obj_foo in order to fetch the next page of the + list. + """ + + before: str + """A cursor for use in pagination. + + `before` is an object ID that defines your place in the list. For instance, if + you make a list request and receive 100 objects, ending with obj_foo, your + subsequent call can include before=obj_foo in order to fetch the previous page + of the list. + """ + + limit: int + """A limit on the number of objects to be returned. + + Limit can range between 1 and 100, and the default is 20. + """ + + order: Literal["asc", "desc"] + """Sort order by the `created_at` timestamp of the objects. + + `asc` for ascending order and `desc` for descending order. + """ diff --git a/openai/types/beta/assistant_stream_event.py b/openai/types/beta/assistant_stream_event.py new file mode 100644 index 00000000..90471f7d --- /dev/null +++ b/openai/types/beta/assistant_stream_event.py @@ -0,0 +1,276 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Union +from typing_extensions import Literal, Annotated + +from .thread import Thread +from ..shared import ErrorObject +from .threads import Run, Message, MessageDeltaEvent +from ..._utils import PropertyInfo +from ..._models import BaseModel +from .threads.runs import RunStep, RunStepDeltaEvent + +__all__ = [ + "AssistantStreamEvent", + "ThreadCreated", + "ThreadRunCreated", + "ThreadRunQueued", + "ThreadRunInProgress", + "ThreadRunRequiresAction", + "ThreadRunCompleted", + "ThreadRunFailed", + "ThreadRunCancelling", + "ThreadRunCancelled", + "ThreadRunExpired", + "ThreadRunStepCreated", + "ThreadRunStepInProgress", + "ThreadRunStepDelta", + "ThreadRunStepCompleted", + "ThreadRunStepFailed", + "ThreadRunStepCancelled", + "ThreadRunStepExpired", + "ThreadMessageCreated", + "ThreadMessageInProgress", + "ThreadMessageDelta", + "ThreadMessageCompleted", + "ThreadMessageIncomplete", + "ErrorEvent", +] + + +class ThreadCreated(BaseModel): + data: Thread + """ + Represents a thread that contains + [messages](https://platform.openai.com/docs/api-reference/messages). + """ + + event: Literal["thread.created"] + + +class ThreadRunCreated(BaseModel): + data: Run + """ + Represents an execution run on a + [thread](https://platform.openai.com/docs/api-reference/threads). + """ + + event: Literal["thread.run.created"] + + +class ThreadRunQueued(BaseModel): + data: Run + """ + Represents an execution run on a + [thread](https://platform.openai.com/docs/api-reference/threads). + """ + + event: Literal["thread.run.queued"] + + +class ThreadRunInProgress(BaseModel): + data: Run + """ + Represents an execution run on a + [thread](https://platform.openai.com/docs/api-reference/threads). + """ + + event: Literal["thread.run.in_progress"] + + +class ThreadRunRequiresAction(BaseModel): + data: Run + """ + Represents an execution run on a + [thread](https://platform.openai.com/docs/api-reference/threads). + """ + + event: Literal["thread.run.requires_action"] + + +class ThreadRunCompleted(BaseModel): + data: Run + """ + Represents an execution run on a + [thread](https://platform.openai.com/docs/api-reference/threads). + """ + + event: Literal["thread.run.completed"] + + +class ThreadRunFailed(BaseModel): + data: Run + """ + Represents an execution run on a + [thread](https://platform.openai.com/docs/api-reference/threads). + """ + + event: Literal["thread.run.failed"] + + +class ThreadRunCancelling(BaseModel): + data: Run + """ + Represents an execution run on a + [thread](https://platform.openai.com/docs/api-reference/threads). + """ + + event: Literal["thread.run.cancelling"] + + +class ThreadRunCancelled(BaseModel): + data: Run + """ + Represents an execution run on a + [thread](https://platform.openai.com/docs/api-reference/threads). + """ + + event: Literal["thread.run.cancelled"] + + +class ThreadRunExpired(BaseModel): + data: Run + """ + Represents an execution run on a + [thread](https://platform.openai.com/docs/api-reference/threads). + """ + + event: Literal["thread.run.expired"] + + +class ThreadRunStepCreated(BaseModel): + data: RunStep + """Represents a step in execution of a run.""" + + event: Literal["thread.run.step.created"] + + +class ThreadRunStepInProgress(BaseModel): + data: RunStep + """Represents a step in execution of a run.""" + + event: Literal["thread.run.step.in_progress"] + + +class ThreadRunStepDelta(BaseModel): + data: RunStepDeltaEvent + """Represents a run step delta i.e. + + any changed fields on a run step during streaming. + """ + + event: Literal["thread.run.step.delta"] + + +class ThreadRunStepCompleted(BaseModel): + data: RunStep + """Represents a step in execution of a run.""" + + event: Literal["thread.run.step.completed"] + + +class ThreadRunStepFailed(BaseModel): + data: RunStep + """Represents a step in execution of a run.""" + + event: Literal["thread.run.step.failed"] + + +class ThreadRunStepCancelled(BaseModel): + data: RunStep + """Represents a step in execution of a run.""" + + event: Literal["thread.run.step.cancelled"] + + +class ThreadRunStepExpired(BaseModel): + data: RunStep + """Represents a step in execution of a run.""" + + event: Literal["thread.run.step.expired"] + + +class ThreadMessageCreated(BaseModel): + data: Message + """ + Represents a message within a + [thread](https://platform.openai.com/docs/api-reference/threads). + """ + + event: Literal["thread.message.created"] + + +class ThreadMessageInProgress(BaseModel): + data: Message + """ + Represents a message within a + [thread](https://platform.openai.com/docs/api-reference/threads). + """ + + event: Literal["thread.message.in_progress"] + + +class ThreadMessageDelta(BaseModel): + data: MessageDeltaEvent + """Represents a message delta i.e. + + any changed fields on a message during streaming. + """ + + event: Literal["thread.message.delta"] + + +class ThreadMessageCompleted(BaseModel): + data: Message + """ + Represents a message within a + [thread](https://platform.openai.com/docs/api-reference/threads). + """ + + event: Literal["thread.message.completed"] + + +class ThreadMessageIncomplete(BaseModel): + data: Message + """ + Represents a message within a + [thread](https://platform.openai.com/docs/api-reference/threads). + """ + + event: Literal["thread.message.incomplete"] + + +class ErrorEvent(BaseModel): + data: ErrorObject + + event: Literal["error"] + + +AssistantStreamEvent = Annotated[ + Union[ + ThreadCreated, + ThreadRunCreated, + ThreadRunQueued, + ThreadRunInProgress, + ThreadRunRequiresAction, + ThreadRunCompleted, + ThreadRunFailed, + ThreadRunCancelling, + ThreadRunCancelled, + ThreadRunExpired, + ThreadRunStepCreated, + ThreadRunStepInProgress, + ThreadRunStepDelta, + ThreadRunStepCompleted, + ThreadRunStepFailed, + ThreadRunStepCancelled, + ThreadRunStepExpired, + ThreadMessageCreated, + ThreadMessageInProgress, + ThreadMessageDelta, + ThreadMessageCompleted, + ThreadMessageIncomplete, + ErrorEvent, + ], + PropertyInfo(discriminator="event"), +] diff --git a/openai/types/beta/assistant_tool.py b/openai/types/beta/assistant_tool.py new file mode 100644 index 00000000..a4420385 --- /dev/null +++ b/openai/types/beta/assistant_tool.py @@ -0,0 +1,13 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Union +from typing_extensions import Annotated + +from ..._utils import PropertyInfo +from .function_tool import FunctionTool +from .retrieval_tool import RetrievalTool +from .code_interpreter_tool import CodeInterpreterTool + +__all__ = ["AssistantTool"] + +AssistantTool = Annotated[Union[CodeInterpreterTool, RetrievalTool, FunctionTool], PropertyInfo(discriminator="type")] diff --git a/openai/types/beta/assistant_tool_param.py b/openai/types/beta/assistant_tool_param.py new file mode 100644 index 00000000..d5758f16 --- /dev/null +++ b/openai/types/beta/assistant_tool_param.py @@ -0,0 +1,13 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Union + +from .function_tool_param import FunctionToolParam +from .retrieval_tool_param import RetrievalToolParam +from .code_interpreter_tool_param import CodeInterpreterToolParam + +__all__ = ["AssistantToolParam"] + +AssistantToolParam = Union[CodeInterpreterToolParam, RetrievalToolParam, FunctionToolParam] diff --git a/openai/types/beta/assistant_update_params.py b/openai/types/beta/assistant_update_params.py new file mode 100644 index 00000000..7c96aca8 --- /dev/null +++ b/openai/types/beta/assistant_update_params.py @@ -0,0 +1,58 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List, Iterable, Optional +from typing_extensions import TypedDict + +from .assistant_tool_param import AssistantToolParam + +__all__ = ["AssistantUpdateParams"] + + +class AssistantUpdateParams(TypedDict, total=False): + description: Optional[str] + """The description of the assistant. The maximum length is 512 characters.""" + + file_ids: List[str] + """ + A list of [File](https://platform.openai.com/docs/api-reference/files) IDs + attached to this assistant. There can be a maximum of 20 files attached to the + assistant. Files are ordered by their creation date in ascending order. If a + file was previously attached to the list but does not show up in the list, it + will be deleted from the assistant. + """ + + instructions: Optional[str] + """The system instructions that the assistant uses. + + The maximum length is 32768 characters. + """ + + metadata: Optional[object] + """Set of 16 key-value pairs that can be attached to an object. + + This can be useful for storing additional information about the object in a + structured format. Keys can be a maximum of 64 characters long and values can be + a maxium of 512 characters long. + """ + + model: str + """ID of the model to use. + + You can use the + [List models](https://platform.openai.com/docs/api-reference/models/list) API to + see all of your available models, or see our + [Model overview](https://platform.openai.com/docs/models/overview) for + descriptions of them. + """ + + name: Optional[str] + """The name of the assistant. The maximum length is 256 characters.""" + + tools: Iterable[AssistantToolParam] + """A list of tool enabled on the assistant. + + There can be a maximum of 128 tools per assistant. Tools can be of types + `code_interpreter`, `retrieval`, or `function`. + """ diff --git a/openai/types/beta/assistants/__init__.py b/openai/types/beta/assistants/__init__.py new file mode 100644 index 00000000..d4dd2de0 --- /dev/null +++ b/openai/types/beta/assistants/__init__.py @@ -0,0 +1,8 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from .assistant_file import AssistantFile as AssistantFile +from .file_list_params import FileListParams as FileListParams +from .file_create_params import FileCreateParams as FileCreateParams +from .file_delete_response import FileDeleteResponse as FileDeleteResponse diff --git a/openai/types/beta/assistants/assistant_file.py b/openai/types/beta/assistants/assistant_file.py new file mode 100644 index 00000000..25aec07b --- /dev/null +++ b/openai/types/beta/assistants/assistant_file.py @@ -0,0 +1,21 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing_extensions import Literal + +from ...._models import BaseModel + +__all__ = ["AssistantFile"] + + +class AssistantFile(BaseModel): + id: str + """The identifier, which can be referenced in API endpoints.""" + + assistant_id: str + """The assistant ID that the file is attached to.""" + + created_at: int + """The Unix timestamp (in seconds) for when the assistant file was created.""" + + object: Literal["assistant.file"] + """The object type, which is always `assistant.file`.""" diff --git a/openai/types/beta/assistants/file_create_params.py b/openai/types/beta/assistants/file_create_params.py new file mode 100644 index 00000000..55f0e8cd --- /dev/null +++ b/openai/types/beta/assistants/file_create_params.py @@ -0,0 +1,16 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["FileCreateParams"] + + +class FileCreateParams(TypedDict, total=False): + file_id: Required[str] + """ + A [File](https://platform.openai.com/docs/api-reference/files) ID (with + `purpose="assistants"`) that the assistant should use. Useful for tools like + `retrieval` and `code_interpreter` that can access files. + """ diff --git a/openai/types/beta/assistants/file_delete_response.py b/openai/types/beta/assistants/file_delete_response.py new file mode 100644 index 00000000..685fb2a7 --- /dev/null +++ b/openai/types/beta/assistants/file_delete_response.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing_extensions import Literal + +from ...._models import BaseModel + +__all__ = ["FileDeleteResponse"] + + +class FileDeleteResponse(BaseModel): + id: str + + deleted: bool + + object: Literal["assistant.file.deleted"] diff --git a/openai/types/beta/assistants/file_list_params.py b/openai/types/beta/assistants/file_list_params.py new file mode 100644 index 00000000..53c493b3 --- /dev/null +++ b/openai/types/beta/assistants/file_list_params.py @@ -0,0 +1,39 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, TypedDict + +__all__ = ["FileListParams"] + + +class FileListParams(TypedDict, total=False): + after: str + """A cursor for use in pagination. + + `after` is an object ID that defines your place in the list. For instance, if + you make a list request and receive 100 objects, ending with obj_foo, your + subsequent call can include after=obj_foo in order to fetch the next page of the + list. + """ + + before: str + """A cursor for use in pagination. + + `before` is an object ID that defines your place in the list. For instance, if + you make a list request and receive 100 objects, ending with obj_foo, your + subsequent call can include before=obj_foo in order to fetch the previous page + of the list. + """ + + limit: int + """A limit on the number of objects to be returned. + + Limit can range between 1 and 100, and the default is 20. + """ + + order: Literal["asc", "desc"] + """Sort order by the `created_at` timestamp of the objects. + + `asc` for ascending order and `desc` for descending order. + """ diff --git a/openai/types/beta/chat/__init__.py b/openai/types/beta/chat/__init__.py new file mode 100644 index 00000000..f8ee8b14 --- /dev/null +++ b/openai/types/beta/chat/__init__.py @@ -0,0 +1,3 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations diff --git a/openai/types/beta/code_interpreter_tool.py b/openai/types/beta/code_interpreter_tool.py new file mode 100644 index 00000000..17ab3de6 --- /dev/null +++ b/openai/types/beta/code_interpreter_tool.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing_extensions import Literal + +from ..._models import BaseModel + +__all__ = ["CodeInterpreterTool"] + + +class CodeInterpreterTool(BaseModel): + type: Literal["code_interpreter"] + """The type of tool being defined: `code_interpreter`""" diff --git a/openai/types/beta/code_interpreter_tool_param.py b/openai/types/beta/code_interpreter_tool_param.py new file mode 100644 index 00000000..4f6916d7 --- /dev/null +++ b/openai/types/beta/code_interpreter_tool_param.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, Required, TypedDict + +__all__ = ["CodeInterpreterToolParam"] + + +class CodeInterpreterToolParam(TypedDict, total=False): + type: Required[Literal["code_interpreter"]] + """The type of tool being defined: `code_interpreter`""" diff --git a/openai/types/beta/function_tool.py b/openai/types/beta/function_tool.py new file mode 100644 index 00000000..5d278e74 --- /dev/null +++ b/openai/types/beta/function_tool.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing_extensions import Literal + +from ..shared import FunctionDefinition +from ..._models import BaseModel + +__all__ = ["FunctionTool"] + + +class FunctionTool(BaseModel): + function: FunctionDefinition + + type: Literal["function"] + """The type of tool being defined: `function`""" diff --git a/openai/types/beta/function_tool_param.py b/openai/types/beta/function_tool_param.py new file mode 100644 index 00000000..b44c0d47 --- /dev/null +++ b/openai/types/beta/function_tool_param.py @@ -0,0 +1,16 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, Required, TypedDict + +from ...types import shared_params + +__all__ = ["FunctionToolParam"] + + +class FunctionToolParam(TypedDict, total=False): + function: Required[shared_params.FunctionDefinition] + + type: Required[Literal["function"]] + """The type of tool being defined: `function`""" diff --git a/openai/types/beta/retrieval_tool.py b/openai/types/beta/retrieval_tool.py new file mode 100644 index 00000000..b07b785c --- /dev/null +++ b/openai/types/beta/retrieval_tool.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing_extensions import Literal + +from ..._models import BaseModel + +__all__ = ["RetrievalTool"] + + +class RetrievalTool(BaseModel): + type: Literal["retrieval"] + """The type of tool being defined: `retrieval`""" diff --git a/openai/types/beta/retrieval_tool_param.py b/openai/types/beta/retrieval_tool_param.py new file mode 100644 index 00000000..d76c0bee --- /dev/null +++ b/openai/types/beta/retrieval_tool_param.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, Required, TypedDict + +__all__ = ["RetrievalToolParam"] + + +class RetrievalToolParam(TypedDict, total=False): + type: Required[Literal["retrieval"]] + """The type of tool being defined: `retrieval`""" diff --git a/openai/types/beta/thread.py b/openai/types/beta/thread.py new file mode 100644 index 00000000..8fd14230 --- /dev/null +++ b/openai/types/beta/thread.py @@ -0,0 +1,27 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from ..._models import BaseModel + +__all__ = ["Thread"] + + +class Thread(BaseModel): + id: str + """The identifier, which can be referenced in API endpoints.""" + + created_at: int + """The Unix timestamp (in seconds) for when the thread was created.""" + + metadata: Optional[object] = None + """Set of 16 key-value pairs that can be attached to an object. + + This can be useful for storing additional information about the object in a + structured format. Keys can be a maximum of 64 characters long and values can be + a maxium of 512 characters long. + """ + + object: Literal["thread"] + """The object type, which is always `thread`.""" diff --git a/openai/types/beta/thread_create_and_run_params.py b/openai/types/beta/thread_create_and_run_params.py new file mode 100644 index 00000000..d4266fc4 --- /dev/null +++ b/openai/types/beta/thread_create_and_run_params.py @@ -0,0 +1,136 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List, Union, Iterable, Optional +from typing_extensions import Literal, Required, TypedDict + +from .function_tool_param import FunctionToolParam +from .retrieval_tool_param import RetrievalToolParam +from .code_interpreter_tool_param import CodeInterpreterToolParam + +__all__ = [ + "ThreadCreateAndRunParamsBase", + "Thread", + "ThreadMessage", + "Tool", + "ThreadCreateAndRunParamsNonStreaming", + "ThreadCreateAndRunParamsStreaming", +] + + +class ThreadCreateAndRunParamsBase(TypedDict, total=False): + assistant_id: Required[str] + """ + The ID of the + [assistant](https://platform.openai.com/docs/api-reference/assistants) to use to + execute this run. + """ + + instructions: Optional[str] + """Override the default system message of the assistant. + + This is useful for modifying the behavior on a per-run basis. + """ + + metadata: Optional[object] + """Set of 16 key-value pairs that can be attached to an object. + + This can be useful for storing additional information about the object in a + structured format. Keys can be a maximum of 64 characters long and values can be + a maxium of 512 characters long. + """ + + model: Optional[str] + """ + The ID of the [Model](https://platform.openai.com/docs/api-reference/models) to + be used to execute this run. If a value is provided here, it will override the + model associated with the assistant. If not, the model associated with the + assistant will be used. + """ + + temperature: Optional[float] + """What sampling temperature to use, between 0 and 2. + + Higher values like 0.8 will make the output more random, while lower values like + 0.2 will make it more focused and deterministic. + """ + + thread: Thread + """If no thread is provided, an empty thread will be created.""" + + tools: Optional[Iterable[Tool]] + """Override the tools the assistant can use for this run. + + This is useful for modifying the behavior on a per-run basis. + """ + + +class ThreadMessage(TypedDict, total=False): + content: Required[str] + """The content of the message.""" + + role: Required[Literal["user", "assistant"]] + """The role of the entity that is creating the message. Allowed values include: + + - `user`: Indicates the message is sent by an actual user and should be used in + most cases to represent user-generated messages. + - `assistant`: Indicates the message is generated by the assistant. Use this + value to insert messages from the assistant into the conversation. + """ + + file_ids: List[str] + """ + A list of [File](https://platform.openai.com/docs/api-reference/files) IDs that + the message should use. There can be a maximum of 10 files attached to a + message. Useful for tools like `retrieval` and `code_interpreter` that can + access and use files. + """ + + metadata: Optional[object] + """Set of 16 key-value pairs that can be attached to an object. + + This can be useful for storing additional information about the object in a + structured format. Keys can be a maximum of 64 characters long and values can be + a maxium of 512 characters long. + """ + + +class Thread(TypedDict, total=False): + messages: Iterable[ThreadMessage] + """ + A list of [messages](https://platform.openai.com/docs/api-reference/messages) to + start the thread with. + """ + + metadata: Optional[object] + """Set of 16 key-value pairs that can be attached to an object. + + This can be useful for storing additional information about the object in a + structured format. Keys can be a maximum of 64 characters long and values can be + a maxium of 512 characters long. + """ + + +Tool = Union[CodeInterpreterToolParam, RetrievalToolParam, FunctionToolParam] + + +class ThreadCreateAndRunParamsNonStreaming(ThreadCreateAndRunParamsBase): + stream: Optional[Literal[False]] + """ + If `true`, returns a stream of events that happen during the Run as server-sent + events, terminating when the Run enters a terminal state with a `data: [DONE]` + message. + """ + + +class ThreadCreateAndRunParamsStreaming(ThreadCreateAndRunParamsBase): + stream: Required[Literal[True]] + """ + If `true`, returns a stream of events that happen during the Run as server-sent + events, terminating when the Run enters a terminal state with a `data: [DONE]` + message. + """ + + +ThreadCreateAndRunParams = Union[ThreadCreateAndRunParamsNonStreaming, ThreadCreateAndRunParamsStreaming] diff --git a/openai/types/beta/thread_create_params.py b/openai/types/beta/thread_create_params.py new file mode 100644 index 00000000..1b382186 --- /dev/null +++ b/openai/types/beta/thread_create_params.py @@ -0,0 +1,54 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List, Iterable, Optional +from typing_extensions import Literal, Required, TypedDict + +__all__ = ["ThreadCreateParams", "Message"] + + +class ThreadCreateParams(TypedDict, total=False): + messages: Iterable[Message] + """ + A list of [messages](https://platform.openai.com/docs/api-reference/messages) to + start the thread with. + """ + + metadata: Optional[object] + """Set of 16 key-value pairs that can be attached to an object. + + This can be useful for storing additional information about the object in a + structured format. Keys can be a maximum of 64 characters long and values can be + a maxium of 512 characters long. + """ + + +class Message(TypedDict, total=False): + content: Required[str] + """The content of the message.""" + + role: Required[Literal["user", "assistant"]] + """The role of the entity that is creating the message. Allowed values include: + + - `user`: Indicates the message is sent by an actual user and should be used in + most cases to represent user-generated messages. + - `assistant`: Indicates the message is generated by the assistant. Use this + value to insert messages from the assistant into the conversation. + """ + + file_ids: List[str] + """ + A list of [File](https://platform.openai.com/docs/api-reference/files) IDs that + the message should use. There can be a maximum of 10 files attached to a + message. Useful for tools like `retrieval` and `code_interpreter` that can + access and use files. + """ + + metadata: Optional[object] + """Set of 16 key-value pairs that can be attached to an object. + + This can be useful for storing additional information about the object in a + structured format. Keys can be a maximum of 64 characters long and values can be + a maxium of 512 characters long. + """ diff --git a/openai/types/beta/thread_deleted.py b/openai/types/beta/thread_deleted.py new file mode 100644 index 00000000..d3856263 --- /dev/null +++ b/openai/types/beta/thread_deleted.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing_extensions import Literal + +from ..._models import BaseModel + +__all__ = ["ThreadDeleted"] + + +class ThreadDeleted(BaseModel): + id: str + + deleted: bool + + object: Literal["thread.deleted"] diff --git a/openai/types/beta/thread_update_params.py b/openai/types/beta/thread_update_params.py new file mode 100644 index 00000000..94f1b1e2 --- /dev/null +++ b/openai/types/beta/thread_update_params.py @@ -0,0 +1,18 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Optional +from typing_extensions import TypedDict + +__all__ = ["ThreadUpdateParams"] + + +class ThreadUpdateParams(TypedDict, total=False): + metadata: Optional[object] + """Set of 16 key-value pairs that can be attached to an object. + + This can be useful for storing additional information about the object in a + structured format. Keys can be a maximum of 64 characters long and values can be + a maxium of 512 characters long. + """ diff --git a/openai/types/beta/threads/__init__.py b/openai/types/beta/threads/__init__.py new file mode 100644 index 00000000..b57ebccb --- /dev/null +++ b/openai/types/beta/threads/__init__.py @@ -0,0 +1,33 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from .run import Run as Run +from .text import Text as Text +from .message import Message as Message +from .annotation import Annotation as Annotation +from .image_file import ImageFile as ImageFile +from .run_status import RunStatus as RunStatus +from .text_delta import TextDelta as TextDelta +from .message_delta import MessageDelta as MessageDelta +from .message_content import MessageContent as MessageContent +from .run_list_params import RunListParams as RunListParams +from .annotation_delta import AnnotationDelta as AnnotationDelta +from .image_file_delta import ImageFileDelta as ImageFileDelta +from .text_delta_block import TextDeltaBlock as TextDeltaBlock +from .run_create_params import RunCreateParams as RunCreateParams +from .run_update_params import RunUpdateParams as RunUpdateParams +from .text_content_block import TextContentBlock as TextContentBlock +from .message_delta_event import MessageDeltaEvent as MessageDeltaEvent +from .message_list_params import MessageListParams as MessageListParams +from .file_path_annotation import FilePathAnnotation as FilePathAnnotation +from .message_content_delta import MessageContentDelta as MessageContentDelta +from .message_create_params import MessageCreateParams as MessageCreateParams +from .message_update_params import MessageUpdateParams as MessageUpdateParams +from .image_file_delta_block import ImageFileDeltaBlock as ImageFileDeltaBlock +from .file_citation_annotation import FileCitationAnnotation as FileCitationAnnotation +from .image_file_content_block import ImageFileContentBlock as ImageFileContentBlock +from .file_path_delta_annotation import FilePathDeltaAnnotation as FilePathDeltaAnnotation +from .file_citation_delta_annotation import FileCitationDeltaAnnotation as FileCitationDeltaAnnotation +from .run_submit_tool_outputs_params import RunSubmitToolOutputsParams as RunSubmitToolOutputsParams +from .required_action_function_tool_call import RequiredActionFunctionToolCall as RequiredActionFunctionToolCall diff --git a/openai/types/beta/threads/annotation.py b/openai/types/beta/threads/annotation.py new file mode 100644 index 00000000..31e228c8 --- /dev/null +++ b/openai/types/beta/threads/annotation.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Union +from typing_extensions import Annotated + +from ...._utils import PropertyInfo +from .file_path_annotation import FilePathAnnotation +from .file_citation_annotation import FileCitationAnnotation + +__all__ = ["Annotation"] + +Annotation = Annotated[Union[FileCitationAnnotation, FilePathAnnotation], PropertyInfo(discriminator="type")] diff --git a/openai/types/beta/threads/annotation_delta.py b/openai/types/beta/threads/annotation_delta.py new file mode 100644 index 00000000..91242967 --- /dev/null +++ b/openai/types/beta/threads/annotation_delta.py @@ -0,0 +1,14 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Union +from typing_extensions import Annotated + +from ...._utils import PropertyInfo +from .file_path_delta_annotation import FilePathDeltaAnnotation +from .file_citation_delta_annotation import FileCitationDeltaAnnotation + +__all__ = ["AnnotationDelta"] + +AnnotationDelta = Annotated[ + Union[FileCitationDeltaAnnotation, FilePathDeltaAnnotation], PropertyInfo(discriminator="type") +] diff --git a/openai/types/beta/threads/file_citation_annotation.py b/openai/types/beta/threads/file_citation_annotation.py new file mode 100644 index 00000000..68571cd4 --- /dev/null +++ b/openai/types/beta/threads/file_citation_annotation.py @@ -0,0 +1,29 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing_extensions import Literal + +from ...._models import BaseModel + +__all__ = ["FileCitationAnnotation", "FileCitation"] + + +class FileCitation(BaseModel): + file_id: str + """The ID of the specific File the citation is from.""" + + quote: str + """The specific quote in the file.""" + + +class FileCitationAnnotation(BaseModel): + end_index: int + + file_citation: FileCitation + + start_index: int + + text: str + """The text in the message content that needs to be replaced.""" + + type: Literal["file_citation"] + """Always `file_citation`.""" diff --git a/openai/types/beta/threads/file_citation_delta_annotation.py b/openai/types/beta/threads/file_citation_delta_annotation.py new file mode 100644 index 00000000..b40c0d12 --- /dev/null +++ b/openai/types/beta/threads/file_citation_delta_annotation.py @@ -0,0 +1,33 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from ...._models import BaseModel + +__all__ = ["FileCitationDeltaAnnotation", "FileCitation"] + + +class FileCitation(BaseModel): + file_id: Optional[str] = None + """The ID of the specific File the citation is from.""" + + quote: Optional[str] = None + """The specific quote in the file.""" + + +class FileCitationDeltaAnnotation(BaseModel): + index: int + """The index of the annotation in the text content part.""" + + type: Literal["file_citation"] + """Always `file_citation`.""" + + end_index: Optional[int] = None + + file_citation: Optional[FileCitation] = None + + start_index: Optional[int] = None + + text: Optional[str] = None + """The text in the message content that needs to be replaced.""" diff --git a/openai/types/beta/threads/file_path_annotation.py b/openai/types/beta/threads/file_path_annotation.py new file mode 100644 index 00000000..9812737e --- /dev/null +++ b/openai/types/beta/threads/file_path_annotation.py @@ -0,0 +1,26 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing_extensions import Literal + +from ...._models import BaseModel + +__all__ = ["FilePathAnnotation", "FilePath"] + + +class FilePath(BaseModel): + file_id: str + """The ID of the file that was generated.""" + + +class FilePathAnnotation(BaseModel): + end_index: int + + file_path: FilePath + + start_index: int + + text: str + """The text in the message content that needs to be replaced.""" + + type: Literal["file_path"] + """Always `file_path`.""" diff --git a/openai/types/beta/threads/file_path_delta_annotation.py b/openai/types/beta/threads/file_path_delta_annotation.py new file mode 100644 index 00000000..0cbb445e --- /dev/null +++ b/openai/types/beta/threads/file_path_delta_annotation.py @@ -0,0 +1,30 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from ...._models import BaseModel + +__all__ = ["FilePathDeltaAnnotation", "FilePath"] + + +class FilePath(BaseModel): + file_id: Optional[str] = None + """The ID of the file that was generated.""" + + +class FilePathDeltaAnnotation(BaseModel): + index: int + """The index of the annotation in the text content part.""" + + type: Literal["file_path"] + """Always `file_path`.""" + + end_index: Optional[int] = None + + file_path: Optional[FilePath] = None + + start_index: Optional[int] = None + + text: Optional[str] = None + """The text in the message content that needs to be replaced.""" diff --git a/openai/types/beta/threads/image_file.py b/openai/types/beta/threads/image_file.py new file mode 100644 index 00000000..db0d6e82 --- /dev/null +++ b/openai/types/beta/threads/image_file.py @@ -0,0 +1,13 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from ...._models import BaseModel + +__all__ = ["ImageFile"] + + +class ImageFile(BaseModel): + file_id: str + """ + The [File](https://platform.openai.com/docs/api-reference/files) ID of the image + in the message content. + """ diff --git a/openai/types/beta/threads/image_file_content_block.py b/openai/types/beta/threads/image_file_content_block.py new file mode 100644 index 00000000..a9099990 --- /dev/null +++ b/openai/types/beta/threads/image_file_content_block.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing_extensions import Literal + +from ...._models import BaseModel +from .image_file import ImageFile + +__all__ = ["ImageFileContentBlock"] + + +class ImageFileContentBlock(BaseModel): + image_file: ImageFile + + type: Literal["image_file"] + """Always `image_file`.""" diff --git a/openai/types/beta/threads/image_file_delta.py b/openai/types/beta/threads/image_file_delta.py new file mode 100644 index 00000000..b0b1d32f --- /dev/null +++ b/openai/types/beta/threads/image_file_delta.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from ...._models import BaseModel + +__all__ = ["ImageFileDelta"] + + +class ImageFileDelta(BaseModel): + file_id: Optional[str] = None + """ + The [File](https://platform.openai.com/docs/api-reference/files) ID of the image + in the message content. + """ diff --git a/openai/types/beta/threads/image_file_delta_block.py b/openai/types/beta/threads/image_file_delta_block.py new file mode 100644 index 00000000..0a5a2e8a --- /dev/null +++ b/openai/types/beta/threads/image_file_delta_block.py @@ -0,0 +1,19 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from ...._models import BaseModel +from .image_file_delta import ImageFileDelta + +__all__ = ["ImageFileDeltaBlock"] + + +class ImageFileDeltaBlock(BaseModel): + index: int + """The index of the content part in the message.""" + + type: Literal["image_file"] + """Always `image_file`.""" + + image_file: Optional[ImageFileDelta] = None diff --git a/openai/types/beta/threads/message.py b/openai/types/beta/threads/message.py new file mode 100644 index 00000000..bde02639 --- /dev/null +++ b/openai/types/beta/threads/message.py @@ -0,0 +1,81 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional +from typing_extensions import Literal + +from ...._models import BaseModel +from .message_content import MessageContent + +__all__ = ["Message", "IncompleteDetails"] + + +class IncompleteDetails(BaseModel): + reason: Literal["content_filter", "max_tokens", "run_cancelled", "run_expired", "run_failed"] + """The reason the message is incomplete.""" + + +class Message(BaseModel): + id: str + """The identifier, which can be referenced in API endpoints.""" + + assistant_id: Optional[str] = None + """ + If applicable, the ID of the + [assistant](https://platform.openai.com/docs/api-reference/assistants) that + authored this message. + """ + + completed_at: Optional[int] = None + """The Unix timestamp (in seconds) for when the message was completed.""" + + content: List[MessageContent] + """The content of the message in array of text and/or images.""" + + created_at: int + """The Unix timestamp (in seconds) for when the message was created.""" + + file_ids: List[str] + """ + A list of [file](https://platform.openai.com/docs/api-reference/files) IDs that + the assistant should use. Useful for tools like retrieval and code_interpreter + that can access files. A maximum of 10 files can be attached to a message. + """ + + incomplete_at: Optional[int] = None + """The Unix timestamp (in seconds) for when the message was marked as incomplete.""" + + incomplete_details: Optional[IncompleteDetails] = None + """On an incomplete message, details about why the message is incomplete.""" + + metadata: Optional[object] = None + """Set of 16 key-value pairs that can be attached to an object. + + This can be useful for storing additional information about the object in a + structured format. Keys can be a maximum of 64 characters long and values can be + a maxium of 512 characters long. + """ + + object: Literal["thread.message"] + """The object type, which is always `thread.message`.""" + + role: Literal["user", "assistant"] + """The entity that produced the message. One of `user` or `assistant`.""" + + run_id: Optional[str] = None + """ + The ID of the [run](https://platform.openai.com/docs/api-reference/runs) + associated with the creation of this message. Value is `null` when messages are + created manually using the create message or create thread endpoints. + """ + + status: Literal["in_progress", "incomplete", "completed"] + """ + The status of the message, which can be either `in_progress`, `incomplete`, or + `completed`. + """ + + thread_id: str + """ + The [thread](https://platform.openai.com/docs/api-reference/threads) ID that + this message belongs to. + """ diff --git a/openai/types/beta/threads/message_content.py b/openai/types/beta/threads/message_content.py new file mode 100644 index 00000000..bc79b39f --- /dev/null +++ b/openai/types/beta/threads/message_content.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Union +from typing_extensions import Annotated + +from ...._utils import PropertyInfo +from .text_content_block import TextContentBlock +from .image_file_content_block import ImageFileContentBlock + +__all__ = ["MessageContent"] + +MessageContent = Annotated[Union[ImageFileContentBlock, TextContentBlock], PropertyInfo(discriminator="type")] diff --git a/openai/types/beta/threads/message_content_delta.py b/openai/types/beta/threads/message_content_delta.py new file mode 100644 index 00000000..3cbc22c9 --- /dev/null +++ b/openai/types/beta/threads/message_content_delta.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Union +from typing_extensions import Annotated + +from ...._utils import PropertyInfo +from .text_delta_block import TextDeltaBlock +from .image_file_delta_block import ImageFileDeltaBlock + +__all__ = ["MessageContentDelta"] + +MessageContentDelta = Annotated[Union[ImageFileDeltaBlock, TextDeltaBlock], PropertyInfo(discriminator="type")] diff --git a/openai/types/beta/threads/message_create_params.py b/openai/types/beta/threads/message_create_params.py new file mode 100644 index 00000000..9b9467ef --- /dev/null +++ b/openai/types/beta/threads/message_create_params.py @@ -0,0 +1,38 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List, Optional +from typing_extensions import Literal, Required, TypedDict + +__all__ = ["MessageCreateParams"] + + +class MessageCreateParams(TypedDict, total=False): + content: Required[str] + """The content of the message.""" + + role: Required[Literal["user", "assistant"]] + """The role of the entity that is creating the message. Allowed values include: + + - `user`: Indicates the message is sent by an actual user and should be used in + most cases to represent user-generated messages. + - `assistant`: Indicates the message is generated by the assistant. Use this + value to insert messages from the assistant into the conversation. + """ + + file_ids: List[str] + """ + A list of [File](https://platform.openai.com/docs/api-reference/files) IDs that + the message should use. There can be a maximum of 10 files attached to a + message. Useful for tools like `retrieval` and `code_interpreter` that can + access and use files. + """ + + metadata: Optional[object] + """Set of 16 key-value pairs that can be attached to an object. + + This can be useful for storing additional information about the object in a + structured format. Keys can be a maximum of 64 characters long and values can be + a maxium of 512 characters long. + """ diff --git a/openai/types/beta/threads/message_delta.py b/openai/types/beta/threads/message_delta.py new file mode 100644 index 00000000..3a55e144 --- /dev/null +++ b/openai/types/beta/threads/message_delta.py @@ -0,0 +1,24 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional +from typing_extensions import Literal + +from ...._models import BaseModel +from .message_content_delta import MessageContentDelta + +__all__ = ["MessageDelta"] + + +class MessageDelta(BaseModel): + content: Optional[List[MessageContentDelta]] = None + """The content of the message in array of text and/or images.""" + + file_ids: Optional[List[str]] = None + """ + A list of [file](https://platform.openai.com/docs/api-reference/files) IDs that + the assistant should use. Useful for tools like retrieval and code_interpreter + that can access files. A maximum of 10 files can be attached to a message. + """ + + role: Optional[Literal["user", "assistant"]] = None + """The entity that produced the message. One of `user` or `assistant`.""" diff --git a/openai/types/beta/threads/message_delta_event.py b/openai/types/beta/threads/message_delta_event.py new file mode 100644 index 00000000..3811cef6 --- /dev/null +++ b/openai/types/beta/threads/message_delta_event.py @@ -0,0 +1,19 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing_extensions import Literal + +from ...._models import BaseModel +from .message_delta import MessageDelta + +__all__ = ["MessageDeltaEvent"] + + +class MessageDeltaEvent(BaseModel): + id: str + """The identifier of the message, which can be referenced in API endpoints.""" + + delta: MessageDelta + """The delta containing the fields that have changed on the Message.""" + + object: Literal["thread.message.delta"] + """The object type, which is always `thread.message.delta`.""" diff --git a/openai/types/beta/threads/message_list_params.py b/openai/types/beta/threads/message_list_params.py new file mode 100644 index 00000000..18c2442f --- /dev/null +++ b/openai/types/beta/threads/message_list_params.py @@ -0,0 +1,42 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, TypedDict + +__all__ = ["MessageListParams"] + + +class MessageListParams(TypedDict, total=False): + after: str + """A cursor for use in pagination. + + `after` is an object ID that defines your place in the list. For instance, if + you make a list request and receive 100 objects, ending with obj_foo, your + subsequent call can include after=obj_foo in order to fetch the next page of the + list. + """ + + before: str + """A cursor for use in pagination. + + `before` is an object ID that defines your place in the list. For instance, if + you make a list request and receive 100 objects, ending with obj_foo, your + subsequent call can include before=obj_foo in order to fetch the previous page + of the list. + """ + + limit: int + """A limit on the number of objects to be returned. + + Limit can range between 1 and 100, and the default is 20. + """ + + order: Literal["asc", "desc"] + """Sort order by the `created_at` timestamp of the objects. + + `asc` for ascending order and `desc` for descending order. + """ + + run_id: str + """Filter messages by the run ID that generated them.""" diff --git a/openai/types/beta/threads/message_update_params.py b/openai/types/beta/threads/message_update_params.py new file mode 100644 index 00000000..7000f331 --- /dev/null +++ b/openai/types/beta/threads/message_update_params.py @@ -0,0 +1,20 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Optional +from typing_extensions import Required, TypedDict + +__all__ = ["MessageUpdateParams"] + + +class MessageUpdateParams(TypedDict, total=False): + thread_id: Required[str] + + metadata: Optional[object] + """Set of 16 key-value pairs that can be attached to an object. + + This can be useful for storing additional information about the object in a + structured format. Keys can be a maximum of 64 characters long and values can be + a maxium of 512 characters long. + """ diff --git a/openai/types/beta/threads/messages/__init__.py b/openai/types/beta/threads/messages/__init__.py new file mode 100644 index 00000000..d1292976 --- /dev/null +++ b/openai/types/beta/threads/messages/__init__.py @@ -0,0 +1,6 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from .message_file import MessageFile as MessageFile +from .file_list_params import FileListParams as FileListParams diff --git a/openai/types/beta/threads/messages/file_list_params.py b/openai/types/beta/threads/messages/file_list_params.py new file mode 100644 index 00000000..7e2d6136 --- /dev/null +++ b/openai/types/beta/threads/messages/file_list_params.py @@ -0,0 +1,41 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, Required, TypedDict + +__all__ = ["FileListParams"] + + +class FileListParams(TypedDict, total=False): + thread_id: Required[str] + + after: str + """A cursor for use in pagination. + + `after` is an object ID that defines your place in the list. For instance, if + you make a list request and receive 100 objects, ending with obj_foo, your + subsequent call can include after=obj_foo in order to fetch the next page of the + list. + """ + + before: str + """A cursor for use in pagination. + + `before` is an object ID that defines your place in the list. For instance, if + you make a list request and receive 100 objects, ending with obj_foo, your + subsequent call can include before=obj_foo in order to fetch the previous page + of the list. + """ + + limit: int + """A limit on the number of objects to be returned. + + Limit can range between 1 and 100, and the default is 20. + """ + + order: Literal["asc", "desc"] + """Sort order by the `created_at` timestamp of the objects. + + `asc` for ascending order and `desc` for descending order. + """ diff --git a/openai/types/beta/threads/messages/message_file.py b/openai/types/beta/threads/messages/message_file.py new file mode 100644 index 00000000..342479ab --- /dev/null +++ b/openai/types/beta/threads/messages/message_file.py @@ -0,0 +1,25 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing_extensions import Literal + +from ....._models import BaseModel + +__all__ = ["MessageFile"] + + +class MessageFile(BaseModel): + id: str + """The identifier, which can be referenced in API endpoints.""" + + created_at: int + """The Unix timestamp (in seconds) for when the message file was created.""" + + message_id: str + """ + The ID of the [message](https://platform.openai.com/docs/api-reference/messages) + that the [File](https://platform.openai.com/docs/api-reference/files) is + attached to. + """ + + object: Literal["thread.message.file"] + """The object type, which is always `thread.message.file`.""" diff --git a/openai/types/beta/threads/required_action_function_tool_call.py b/openai/types/beta/threads/required_action_function_tool_call.py new file mode 100644 index 00000000..a24dfd06 --- /dev/null +++ b/openai/types/beta/threads/required_action_function_tool_call.py @@ -0,0 +1,34 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing_extensions import Literal + +from ...._models import BaseModel + +__all__ = ["RequiredActionFunctionToolCall", "Function"] + + +class Function(BaseModel): + arguments: str + """The arguments that the model expects you to pass to the function.""" + + name: str + """The name of the function.""" + + +class RequiredActionFunctionToolCall(BaseModel): + id: str + """The ID of the tool call. + + This ID must be referenced when you submit the tool outputs in using the + [Submit tool outputs to run](https://platform.openai.com/docs/api-reference/runs/submitToolOutputs) + endpoint. + """ + + function: Function + """The function definition.""" + + type: Literal["function"] + """The type of tool call the output is required for. + + For now, this is always `function`. + """ diff --git a/openai/types/beta/threads/run.py b/openai/types/beta/threads/run.py new file mode 100644 index 00000000..3ab27624 --- /dev/null +++ b/openai/types/beta/threads/run.py @@ -0,0 +1,144 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional +from typing_extensions import Literal + +from ...._models import BaseModel +from .run_status import RunStatus +from ..assistant_tool import AssistantTool +from .required_action_function_tool_call import RequiredActionFunctionToolCall + +__all__ = ["Run", "LastError", "RequiredAction", "RequiredActionSubmitToolOutputs", "Usage"] + + +class LastError(BaseModel): + code: Literal["server_error", "rate_limit_exceeded", "invalid_prompt"] + """One of `server_error`, `rate_limit_exceeded`, or `invalid_prompt`.""" + + message: str + """A human-readable description of the error.""" + + +class RequiredActionSubmitToolOutputs(BaseModel): + tool_calls: List[RequiredActionFunctionToolCall] + """A list of the relevant tool calls.""" + + +class RequiredAction(BaseModel): + submit_tool_outputs: RequiredActionSubmitToolOutputs + """Details on the tool outputs needed for this run to continue.""" + + type: Literal["submit_tool_outputs"] + """For now, this is always `submit_tool_outputs`.""" + + +class Usage(BaseModel): + completion_tokens: int + """Number of completion tokens used over the course of the run.""" + + prompt_tokens: int + """Number of prompt tokens used over the course of the run.""" + + total_tokens: int + """Total number of tokens used (prompt + completion).""" + + +class Run(BaseModel): + id: str + """The identifier, which can be referenced in API endpoints.""" + + assistant_id: str + """ + The ID of the + [assistant](https://platform.openai.com/docs/api-reference/assistants) used for + execution of this run. + """ + + cancelled_at: Optional[int] = None + """The Unix timestamp (in seconds) for when the run was cancelled.""" + + completed_at: Optional[int] = None + """The Unix timestamp (in seconds) for when the run was completed.""" + + created_at: int + """The Unix timestamp (in seconds) for when the run was created.""" + + expires_at: Optional[int] = None + """The Unix timestamp (in seconds) for when the run will expire.""" + + failed_at: Optional[int] = None + """The Unix timestamp (in seconds) for when the run failed.""" + + file_ids: List[str] + """ + The list of [File](https://platform.openai.com/docs/api-reference/files) IDs the + [assistant](https://platform.openai.com/docs/api-reference/assistants) used for + this run. + """ + + instructions: str + """ + The instructions that the + [assistant](https://platform.openai.com/docs/api-reference/assistants) used for + this run. + """ + + last_error: Optional[LastError] = None + """The last error associated with this run. Will be `null` if there are no errors.""" + + metadata: Optional[object] = None + """Set of 16 key-value pairs that can be attached to an object. + + This can be useful for storing additional information about the object in a + structured format. Keys can be a maximum of 64 characters long and values can be + a maxium of 512 characters long. + """ + + model: str + """ + The model that the + [assistant](https://platform.openai.com/docs/api-reference/assistants) used for + this run. + """ + + object: Literal["thread.run"] + """The object type, which is always `thread.run`.""" + + required_action: Optional[RequiredAction] = None + """Details on the action required to continue the run. + + Will be `null` if no action is required. + """ + + started_at: Optional[int] = None + """The Unix timestamp (in seconds) for when the run was started.""" + + status: RunStatus + """ + The status of the run, which can be either `queued`, `in_progress`, + `requires_action`, `cancelling`, `cancelled`, `failed`, `completed`, or + `expired`. + """ + + thread_id: str + """ + The ID of the [thread](https://platform.openai.com/docs/api-reference/threads) + that was executed on as a part of this run. + """ + + tools: List[AssistantTool] + """ + The list of tools that the + [assistant](https://platform.openai.com/docs/api-reference/assistants) used for + this run. + """ + + usage: Optional[Usage] = None + """Usage statistics related to the run. + + This value will be `null` if the run is not in a terminal state (i.e. + `in_progress`, `queued`, etc.). + """ + + temperature: Optional[float] = None + """The sampling temperature used for this run. If not set, defaults to 1.""" diff --git a/openai/types/beta/threads/run_create_params.py b/openai/types/beta/threads/run_create_params.py new file mode 100644 index 00000000..ac185973 --- /dev/null +++ b/openai/types/beta/threads/run_create_params.py @@ -0,0 +1,83 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Union, Iterable, Optional +from typing_extensions import Literal, Required, TypedDict + +from ..assistant_tool_param import AssistantToolParam + +__all__ = ["RunCreateParamsBase", "RunCreateParamsNonStreaming", "RunCreateParamsStreaming"] + + +class RunCreateParamsBase(TypedDict, total=False): + assistant_id: Required[str] + """ + The ID of the + [assistant](https://platform.openai.com/docs/api-reference/assistants) to use to + execute this run. + """ + + additional_instructions: Optional[str] + """Appends additional instructions at the end of the instructions for the run. + + This is useful for modifying the behavior on a per-run basis without overriding + other instructions. + """ + + instructions: Optional[str] + """ + Overrides the + [instructions](https://platform.openai.com/docs/api-reference/assistants/createAssistant) + of the assistant. This is useful for modifying the behavior on a per-run basis. + """ + + metadata: Optional[object] + """Set of 16 key-value pairs that can be attached to an object. + + This can be useful for storing additional information about the object in a + structured format. Keys can be a maximum of 64 characters long and values can be + a maxium of 512 characters long. + """ + + model: Optional[str] + """ + The ID of the [Model](https://platform.openai.com/docs/api-reference/models) to + be used to execute this run. If a value is provided here, it will override the + model associated with the assistant. If not, the model associated with the + assistant will be used. + """ + + temperature: Optional[float] + """What sampling temperature to use, between 0 and 2. + + Higher values like 0.8 will make the output more random, while lower values like + 0.2 will make it more focused and deterministic. + """ + + tools: Optional[Iterable[AssistantToolParam]] + """Override the tools the assistant can use for this run. + + This is useful for modifying the behavior on a per-run basis. + """ + + +class RunCreateParamsNonStreaming(RunCreateParamsBase): + stream: Optional[Literal[False]] + """ + If `true`, returns a stream of events that happen during the Run as server-sent + events, terminating when the Run enters a terminal state with a `data: [DONE]` + message. + """ + + +class RunCreateParamsStreaming(RunCreateParamsBase): + stream: Required[Literal[True]] + """ + If `true`, returns a stream of events that happen during the Run as server-sent + events, terminating when the Run enters a terminal state with a `data: [DONE]` + message. + """ + + +RunCreateParams = Union[RunCreateParamsNonStreaming, RunCreateParamsStreaming] diff --git a/openai/types/beta/threads/run_list_params.py b/openai/types/beta/threads/run_list_params.py new file mode 100644 index 00000000..1e32bca4 --- /dev/null +++ b/openai/types/beta/threads/run_list_params.py @@ -0,0 +1,39 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, TypedDict + +__all__ = ["RunListParams"] + + +class RunListParams(TypedDict, total=False): + after: str + """A cursor for use in pagination. + + `after` is an object ID that defines your place in the list. For instance, if + you make a list request and receive 100 objects, ending with obj_foo, your + subsequent call can include after=obj_foo in order to fetch the next page of the + list. + """ + + before: str + """A cursor for use in pagination. + + `before` is an object ID that defines your place in the list. For instance, if + you make a list request and receive 100 objects, ending with obj_foo, your + subsequent call can include before=obj_foo in order to fetch the previous page + of the list. + """ + + limit: int + """A limit on the number of objects to be returned. + + Limit can range between 1 and 100, and the default is 20. + """ + + order: Literal["asc", "desc"] + """Sort order by the `created_at` timestamp of the objects. + + `asc` for ascending order and `desc` for descending order. + """ diff --git a/openai/types/beta/threads/run_status.py b/openai/types/beta/threads/run_status.py new file mode 100644 index 00000000..bf9b4e7b --- /dev/null +++ b/openai/types/beta/threads/run_status.py @@ -0,0 +1,9 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing_extensions import Literal + +__all__ = ["RunStatus"] + +RunStatus = Literal[ + "queued", "in_progress", "requires_action", "cancelling", "cancelled", "failed", "completed", "expired" +] diff --git a/openai/types/beta/threads/run_submit_tool_outputs_params.py b/openai/types/beta/threads/run_submit_tool_outputs_params.py new file mode 100644 index 00000000..ccb5e5e9 --- /dev/null +++ b/openai/types/beta/threads/run_submit_tool_outputs_params.py @@ -0,0 +1,52 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Union, Iterable, Optional +from typing_extensions import Literal, Required, TypedDict + +__all__ = [ + "RunSubmitToolOutputsParamsBase", + "ToolOutput", + "RunSubmitToolOutputsParamsNonStreaming", + "RunSubmitToolOutputsParamsStreaming", +] + + +class RunSubmitToolOutputsParamsBase(TypedDict, total=False): + thread_id: Required[str] + + tool_outputs: Required[Iterable[ToolOutput]] + """A list of tools for which the outputs are being submitted.""" + + +class ToolOutput(TypedDict, total=False): + output: str + """The output of the tool call to be submitted to continue the run.""" + + tool_call_id: str + """ + The ID of the tool call in the `required_action` object within the run object + the output is being submitted for. + """ + + +class RunSubmitToolOutputsParamsNonStreaming(RunSubmitToolOutputsParamsBase): + stream: Optional[Literal[False]] + """ + If `true`, returns a stream of events that happen during the Run as server-sent + events, terminating when the Run enters a terminal state with a `data: [DONE]` + message. + """ + + +class RunSubmitToolOutputsParamsStreaming(RunSubmitToolOutputsParamsBase): + stream: Required[Literal[True]] + """ + If `true`, returns a stream of events that happen during the Run as server-sent + events, terminating when the Run enters a terminal state with a `data: [DONE]` + message. + """ + + +RunSubmitToolOutputsParams = Union[RunSubmitToolOutputsParamsNonStreaming, RunSubmitToolOutputsParamsStreaming] diff --git a/openai/types/beta/threads/run_update_params.py b/openai/types/beta/threads/run_update_params.py new file mode 100644 index 00000000..e595eac8 --- /dev/null +++ b/openai/types/beta/threads/run_update_params.py @@ -0,0 +1,20 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Optional +from typing_extensions import Required, TypedDict + +__all__ = ["RunUpdateParams"] + + +class RunUpdateParams(TypedDict, total=False): + thread_id: Required[str] + + metadata: Optional[object] + """Set of 16 key-value pairs that can be attached to an object. + + This can be useful for storing additional information about the object in a + structured format. Keys can be a maximum of 64 characters long and values can be + a maxium of 512 characters long. + """ diff --git a/openai/types/beta/threads/runs/__init__.py b/openai/types/beta/threads/runs/__init__.py new file mode 100644 index 00000000..256510dc --- /dev/null +++ b/openai/types/beta/threads/runs/__init__.py @@ -0,0 +1,22 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from .run_step import RunStep as RunStep +from .tool_call import ToolCall as ToolCall +from .run_step_delta import RunStepDelta as RunStepDelta +from .tool_call_delta import ToolCallDelta as ToolCallDelta +from .step_list_params import StepListParams as StepListParams +from .function_tool_call import FunctionToolCall as FunctionToolCall +from .retrieval_tool_call import RetrievalToolCall as RetrievalToolCall +from .run_step_delta_event import RunStepDeltaEvent as RunStepDeltaEvent +from .code_interpreter_logs import CodeInterpreterLogs as CodeInterpreterLogs +from .tool_call_delta_object import ToolCallDeltaObject as ToolCallDeltaObject +from .tool_calls_step_details import ToolCallsStepDetails as ToolCallsStepDetails +from .function_tool_call_delta import FunctionToolCallDelta as FunctionToolCallDelta +from .retrieval_tool_call_delta import RetrievalToolCallDelta as RetrievalToolCallDelta +from .code_interpreter_tool_call import CodeInterpreterToolCall as CodeInterpreterToolCall +from .run_step_delta_message_delta import RunStepDeltaMessageDelta as RunStepDeltaMessageDelta +from .code_interpreter_output_image import CodeInterpreterOutputImage as CodeInterpreterOutputImage +from .message_creation_step_details import MessageCreationStepDetails as MessageCreationStepDetails +from .code_interpreter_tool_call_delta import CodeInterpreterToolCallDelta as CodeInterpreterToolCallDelta diff --git a/openai/types/beta/threads/runs/code_interpreter_logs.py b/openai/types/beta/threads/runs/code_interpreter_logs.py new file mode 100644 index 00000000..0bf8c1da --- /dev/null +++ b/openai/types/beta/threads/runs/code_interpreter_logs.py @@ -0,0 +1,19 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from ....._models import BaseModel + +__all__ = ["CodeInterpreterLogs"] + + +class CodeInterpreterLogs(BaseModel): + index: int + """The index of the output in the outputs array.""" + + type: Literal["logs"] + """Always `logs`.""" + + logs: Optional[str] = None + """The text output from the Code Interpreter tool call.""" diff --git a/openai/types/beta/threads/runs/code_interpreter_output_image.py b/openai/types/beta/threads/runs/code_interpreter_output_image.py new file mode 100644 index 00000000..2257f37e --- /dev/null +++ b/openai/types/beta/threads/runs/code_interpreter_output_image.py @@ -0,0 +1,26 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from ....._models import BaseModel + +__all__ = ["CodeInterpreterOutputImage", "Image"] + + +class Image(BaseModel): + file_id: Optional[str] = None + """ + The [file](https://platform.openai.com/docs/api-reference/files) ID of the + image. + """ + + +class CodeInterpreterOutputImage(BaseModel): + index: int + """The index of the output in the outputs array.""" + + type: Literal["image"] + """Always `image`.""" + + image: Optional[Image] = None diff --git a/openai/types/beta/threads/runs/code_interpreter_tool_call.py b/openai/types/beta/threads/runs/code_interpreter_tool_call.py new file mode 100644 index 00000000..2f072436 --- /dev/null +++ b/openai/types/beta/threads/runs/code_interpreter_tool_call.py @@ -0,0 +1,70 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Union +from typing_extensions import Literal, Annotated + +from ....._utils import PropertyInfo +from ....._models import BaseModel + +__all__ = [ + "CodeInterpreterToolCall", + "CodeInterpreter", + "CodeInterpreterOutput", + "CodeInterpreterOutputLogs", + "CodeInterpreterOutputImage", + "CodeInterpreterOutputImageImage", +] + + +class CodeInterpreterOutputLogs(BaseModel): + logs: str + """The text output from the Code Interpreter tool call.""" + + type: Literal["logs"] + """Always `logs`.""" + + +class CodeInterpreterOutputImageImage(BaseModel): + file_id: str + """ + The [file](https://platform.openai.com/docs/api-reference/files) ID of the + image. + """ + + +class CodeInterpreterOutputImage(BaseModel): + image: CodeInterpreterOutputImageImage + + type: Literal["image"] + """Always `image`.""" + + +CodeInterpreterOutput = Annotated[ + Union[CodeInterpreterOutputLogs, CodeInterpreterOutputImage], PropertyInfo(discriminator="type") +] + + +class CodeInterpreter(BaseModel): + input: str + """The input to the Code Interpreter tool call.""" + + outputs: List[CodeInterpreterOutput] + """The outputs from the Code Interpreter tool call. + + Code Interpreter can output one or more items, including text (`logs`) or images + (`image`). Each of these are represented by a different object type. + """ + + +class CodeInterpreterToolCall(BaseModel): + id: str + """The ID of the tool call.""" + + code_interpreter: CodeInterpreter + """The Code Interpreter tool call definition.""" + + type: Literal["code_interpreter"] + """The type of tool call. + + This is always going to be `code_interpreter` for this type of tool call. + """ diff --git a/openai/types/beta/threads/runs/code_interpreter_tool_call_delta.py b/openai/types/beta/threads/runs/code_interpreter_tool_call_delta.py new file mode 100644 index 00000000..eff76355 --- /dev/null +++ b/openai/types/beta/threads/runs/code_interpreter_tool_call_delta.py @@ -0,0 +1,44 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Union, Optional +from typing_extensions import Literal, Annotated + +from ....._utils import PropertyInfo +from ....._models import BaseModel +from .code_interpreter_logs import CodeInterpreterLogs +from .code_interpreter_output_image import CodeInterpreterOutputImage + +__all__ = ["CodeInterpreterToolCallDelta", "CodeInterpreter", "CodeInterpreterOutput"] + +CodeInterpreterOutput = Annotated[ + Union[CodeInterpreterLogs, CodeInterpreterOutputImage], PropertyInfo(discriminator="type") +] + + +class CodeInterpreter(BaseModel): + input: Optional[str] = None + """The input to the Code Interpreter tool call.""" + + outputs: Optional[List[CodeInterpreterOutput]] = None + """The outputs from the Code Interpreter tool call. + + Code Interpreter can output one or more items, including text (`logs`) or images + (`image`). Each of these are represented by a different object type. + """ + + +class CodeInterpreterToolCallDelta(BaseModel): + index: int + """The index of the tool call in the tool calls array.""" + + type: Literal["code_interpreter"] + """The type of tool call. + + This is always going to be `code_interpreter` for this type of tool call. + """ + + id: Optional[str] = None + """The ID of the tool call.""" + + code_interpreter: Optional[CodeInterpreter] = None + """The Code Interpreter tool call definition.""" diff --git a/openai/types/beta/threads/runs/function_tool_call.py b/openai/types/beta/threads/runs/function_tool_call.py new file mode 100644 index 00000000..b1d354f8 --- /dev/null +++ b/openai/types/beta/threads/runs/function_tool_call.py @@ -0,0 +1,38 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from ....._models import BaseModel + +__all__ = ["FunctionToolCall", "Function"] + + +class Function(BaseModel): + arguments: str + """The arguments passed to the function.""" + + name: str + """The name of the function.""" + + output: Optional[str] = None + """The output of the function. + + This will be `null` if the outputs have not been + [submitted](https://platform.openai.com/docs/api-reference/runs/submitToolOutputs) + yet. + """ + + +class FunctionToolCall(BaseModel): + id: str + """The ID of the tool call object.""" + + function: Function + """The definition of the function that was called.""" + + type: Literal["function"] + """The type of tool call. + + This is always going to be `function` for this type of tool call. + """ diff --git a/openai/types/beta/threads/runs/function_tool_call_delta.py b/openai/types/beta/threads/runs/function_tool_call_delta.py new file mode 100644 index 00000000..faaf026f --- /dev/null +++ b/openai/types/beta/threads/runs/function_tool_call_delta.py @@ -0,0 +1,41 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from ....._models import BaseModel + +__all__ = ["FunctionToolCallDelta", "Function"] + + +class Function(BaseModel): + arguments: Optional[str] = None + """The arguments passed to the function.""" + + name: Optional[str] = None + """The name of the function.""" + + output: Optional[str] = None + """The output of the function. + + This will be `null` if the outputs have not been + [submitted](https://platform.openai.com/docs/api-reference/runs/submitToolOutputs) + yet. + """ + + +class FunctionToolCallDelta(BaseModel): + index: int + """The index of the tool call in the tool calls array.""" + + type: Literal["function"] + """The type of tool call. + + This is always going to be `function` for this type of tool call. + """ + + id: Optional[str] = None + """The ID of the tool call object.""" + + function: Optional[Function] = None + """The definition of the function that was called.""" diff --git a/openai/types/beta/threads/runs/message_creation_step_details.py b/openai/types/beta/threads/runs/message_creation_step_details.py new file mode 100644 index 00000000..73439079 --- /dev/null +++ b/openai/types/beta/threads/runs/message_creation_step_details.py @@ -0,0 +1,19 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing_extensions import Literal + +from ....._models import BaseModel + +__all__ = ["MessageCreationStepDetails", "MessageCreation"] + + +class MessageCreation(BaseModel): + message_id: str + """The ID of the message that was created by this run step.""" + + +class MessageCreationStepDetails(BaseModel): + message_creation: MessageCreation + + type: Literal["message_creation"] + """Always `message_creation`.""" diff --git a/openai/types/beta/threads/runs/retrieval_tool_call.py b/openai/types/beta/threads/runs/retrieval_tool_call.py new file mode 100644 index 00000000..48704ed3 --- /dev/null +++ b/openai/types/beta/threads/runs/retrieval_tool_call.py @@ -0,0 +1,21 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing_extensions import Literal + +from ....._models import BaseModel + +__all__ = ["RetrievalToolCall"] + + +class RetrievalToolCall(BaseModel): + id: str + """The ID of the tool call object.""" + + retrieval: object + """For now, this is always going to be an empty object.""" + + type: Literal["retrieval"] + """The type of tool call. + + This is always going to be `retrieval` for this type of tool call. + """ diff --git a/openai/types/beta/threads/runs/retrieval_tool_call_delta.py b/openai/types/beta/threads/runs/retrieval_tool_call_delta.py new file mode 100644 index 00000000..33100793 --- /dev/null +++ b/openai/types/beta/threads/runs/retrieval_tool_call_delta.py @@ -0,0 +1,25 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from ....._models import BaseModel + +__all__ = ["RetrievalToolCallDelta"] + + +class RetrievalToolCallDelta(BaseModel): + index: int + """The index of the tool call in the tool calls array.""" + + type: Literal["retrieval"] + """The type of tool call. + + This is always going to be `retrieval` for this type of tool call. + """ + + id: Optional[str] = None + """The ID of the tool call object.""" + + retrieval: Optional[object] = None + """For now, this is always going to be an empty object.""" diff --git a/openai/types/beta/threads/runs/run_step.py b/openai/types/beta/threads/runs/run_step.py new file mode 100644 index 00000000..7c81dcac --- /dev/null +++ b/openai/types/beta/threads/runs/run_step.py @@ -0,0 +1,110 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Union, Optional +from typing_extensions import Literal, Annotated + +from ....._utils import PropertyInfo +from ....._models import BaseModel +from .tool_calls_step_details import ToolCallsStepDetails +from .message_creation_step_details import MessageCreationStepDetails + +__all__ = ["RunStep", "LastError", "StepDetails", "Usage"] + + +class LastError(BaseModel): + code: Literal["server_error", "rate_limit_exceeded"] + """One of `server_error` or `rate_limit_exceeded`.""" + + message: str + """A human-readable description of the error.""" + + +StepDetails = Annotated[Union[MessageCreationStepDetails, ToolCallsStepDetails], PropertyInfo(discriminator="type")] + + +class Usage(BaseModel): + completion_tokens: int + """Number of completion tokens used over the course of the run step.""" + + prompt_tokens: int + """Number of prompt tokens used over the course of the run step.""" + + total_tokens: int + """Total number of tokens used (prompt + completion).""" + + +class RunStep(BaseModel): + id: str + """The identifier of the run step, which can be referenced in API endpoints.""" + + assistant_id: str + """ + The ID of the + [assistant](https://platform.openai.com/docs/api-reference/assistants) + associated with the run step. + """ + + cancelled_at: Optional[int] = None + """The Unix timestamp (in seconds) for when the run step was cancelled.""" + + completed_at: Optional[int] = None + """The Unix timestamp (in seconds) for when the run step completed.""" + + created_at: int + """The Unix timestamp (in seconds) for when the run step was created.""" + + expired_at: Optional[int] = None + """The Unix timestamp (in seconds) for when the run step expired. + + A step is considered expired if the parent run is expired. + """ + + failed_at: Optional[int] = None + """The Unix timestamp (in seconds) for when the run step failed.""" + + last_error: Optional[LastError] = None + """The last error associated with this run step. + + Will be `null` if there are no errors. + """ + + metadata: Optional[object] = None + """Set of 16 key-value pairs that can be attached to an object. + + This can be useful for storing additional information about the object in a + structured format. Keys can be a maximum of 64 characters long and values can be + a maxium of 512 characters long. + """ + + object: Literal["thread.run.step"] + """The object type, which is always `thread.run.step`.""" + + run_id: str + """ + The ID of the [run](https://platform.openai.com/docs/api-reference/runs) that + this run step is a part of. + """ + + status: Literal["in_progress", "cancelled", "failed", "completed", "expired"] + """ + The status of the run step, which can be either `in_progress`, `cancelled`, + `failed`, `completed`, or `expired`. + """ + + step_details: StepDetails + """The details of the run step.""" + + thread_id: str + """ + The ID of the [thread](https://platform.openai.com/docs/api-reference/threads) + that was run. + """ + + type: Literal["message_creation", "tool_calls"] + """The type of run step, which can be either `message_creation` or `tool_calls`.""" + + usage: Optional[Usage] = None + """Usage statistics related to the run step. + + This value will be `null` while the run step's status is `in_progress`. + """ diff --git a/openai/types/beta/threads/runs/run_step_delta.py b/openai/types/beta/threads/runs/run_step_delta.py new file mode 100644 index 00000000..d6b4aefe --- /dev/null +++ b/openai/types/beta/threads/runs/run_step_delta.py @@ -0,0 +1,18 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Union, Optional +from typing_extensions import Annotated + +from ....._utils import PropertyInfo +from ....._models import BaseModel +from .tool_call_delta_object import ToolCallDeltaObject +from .run_step_delta_message_delta import RunStepDeltaMessageDelta + +__all__ = ["RunStepDelta", "StepDetails"] + +StepDetails = Annotated[Union[RunStepDeltaMessageDelta, ToolCallDeltaObject], PropertyInfo(discriminator="type")] + + +class RunStepDelta(BaseModel): + step_details: Optional[StepDetails] = None + """The details of the run step.""" diff --git a/openai/types/beta/threads/runs/run_step_delta_event.py b/openai/types/beta/threads/runs/run_step_delta_event.py new file mode 100644 index 00000000..7f3f92aa --- /dev/null +++ b/openai/types/beta/threads/runs/run_step_delta_event.py @@ -0,0 +1,19 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing_extensions import Literal + +from ....._models import BaseModel +from .run_step_delta import RunStepDelta + +__all__ = ["RunStepDeltaEvent"] + + +class RunStepDeltaEvent(BaseModel): + id: str + """The identifier of the run step, which can be referenced in API endpoints.""" + + delta: RunStepDelta + """The delta containing the fields that have changed on the run step.""" + + object: Literal["thread.run.step.delta"] + """The object type, which is always `thread.run.step.delta`.""" diff --git a/openai/types/beta/threads/runs/run_step_delta_message_delta.py b/openai/types/beta/threads/runs/run_step_delta_message_delta.py new file mode 100644 index 00000000..f58ed3d9 --- /dev/null +++ b/openai/types/beta/threads/runs/run_step_delta_message_delta.py @@ -0,0 +1,20 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from ....._models import BaseModel + +__all__ = ["RunStepDeltaMessageDelta", "MessageCreation"] + + +class MessageCreation(BaseModel): + message_id: Optional[str] = None + """The ID of the message that was created by this run step.""" + + +class RunStepDeltaMessageDelta(BaseModel): + type: Literal["message_creation"] + """Always `message_creation`.""" + + message_creation: Optional[MessageCreation] = None diff --git a/openai/types/beta/threads/runs/step_list_params.py b/openai/types/beta/threads/runs/step_list_params.py new file mode 100644 index 00000000..606d4445 --- /dev/null +++ b/openai/types/beta/threads/runs/step_list_params.py @@ -0,0 +1,41 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, Required, TypedDict + +__all__ = ["StepListParams"] + + +class StepListParams(TypedDict, total=False): + thread_id: Required[str] + + after: str + """A cursor for use in pagination. + + `after` is an object ID that defines your place in the list. For instance, if + you make a list request and receive 100 objects, ending with obj_foo, your + subsequent call can include after=obj_foo in order to fetch the next page of the + list. + """ + + before: str + """A cursor for use in pagination. + + `before` is an object ID that defines your place in the list. For instance, if + you make a list request and receive 100 objects, ending with obj_foo, your + subsequent call can include before=obj_foo in order to fetch the previous page + of the list. + """ + + limit: int + """A limit on the number of objects to be returned. + + Limit can range between 1 and 100, and the default is 20. + """ + + order: Literal["asc", "desc"] + """Sort order by the `created_at` timestamp of the objects. + + `asc` for ascending order and `desc` for descending order. + """ diff --git a/openai/types/beta/threads/runs/tool_call.py b/openai/types/beta/threads/runs/tool_call.py new file mode 100644 index 00000000..dcca797b --- /dev/null +++ b/openai/types/beta/threads/runs/tool_call.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Union +from typing_extensions import Annotated + +from ....._utils import PropertyInfo +from .function_tool_call import FunctionToolCall +from .retrieval_tool_call import RetrievalToolCall +from .code_interpreter_tool_call import CodeInterpreterToolCall + +__all__ = ["ToolCall"] + +ToolCall = Annotated[ + Union[CodeInterpreterToolCall, RetrievalToolCall, FunctionToolCall], PropertyInfo(discriminator="type") +] diff --git a/openai/types/beta/threads/runs/tool_call_delta.py b/openai/types/beta/threads/runs/tool_call_delta.py new file mode 100644 index 00000000..fc98981a --- /dev/null +++ b/openai/types/beta/threads/runs/tool_call_delta.py @@ -0,0 +1,16 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Union +from typing_extensions import Annotated + +from ....._utils import PropertyInfo +from .function_tool_call_delta import FunctionToolCallDelta +from .retrieval_tool_call_delta import RetrievalToolCallDelta +from .code_interpreter_tool_call_delta import CodeInterpreterToolCallDelta + +__all__ = ["ToolCallDelta"] + +ToolCallDelta = Annotated[ + Union[CodeInterpreterToolCallDelta, RetrievalToolCallDelta, FunctionToolCallDelta], + PropertyInfo(discriminator="type"), +] diff --git a/openai/types/beta/threads/runs/tool_call_delta_object.py b/openai/types/beta/threads/runs/tool_call_delta_object.py new file mode 100644 index 00000000..9cd59a6e --- /dev/null +++ b/openai/types/beta/threads/runs/tool_call_delta_object.py @@ -0,0 +1,21 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional +from typing_extensions import Literal + +from ....._models import BaseModel +from .tool_call_delta import ToolCallDelta + +__all__ = ["ToolCallDeltaObject"] + + +class ToolCallDeltaObject(BaseModel): + type: Literal["tool_calls"] + """Always `tool_calls`.""" + + tool_calls: Optional[List[ToolCallDelta]] = None + """An array of tool calls the run step was involved in. + + These can be associated with one of three types of tools: `code_interpreter`, + `retrieval`, or `function`. + """ diff --git a/openai/types/beta/threads/runs/tool_calls_step_details.py b/openai/types/beta/threads/runs/tool_calls_step_details.py new file mode 100644 index 00000000..ca08fabd --- /dev/null +++ b/openai/types/beta/threads/runs/tool_calls_step_details.py @@ -0,0 +1,21 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List +from typing_extensions import Literal + +from .tool_call import ToolCall +from ....._models import BaseModel + +__all__ = ["ToolCallsStepDetails"] + + +class ToolCallsStepDetails(BaseModel): + tool_calls: List[ToolCall] + """An array of tool calls the run step was involved in. + + These can be associated with one of three types of tools: `code_interpreter`, + `retrieval`, or `function`. + """ + + type: Literal["tool_calls"] + """Always `tool_calls`.""" diff --git a/openai/types/beta/threads/text.py b/openai/types/beta/threads/text.py new file mode 100644 index 00000000..853bec29 --- /dev/null +++ b/openai/types/beta/threads/text.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List + +from ...._models import BaseModel +from .annotation import Annotation + +__all__ = ["Text"] + + +class Text(BaseModel): + annotations: List[Annotation] + + value: str + """The data that makes up the text.""" diff --git a/openai/types/beta/threads/text_content_block.py b/openai/types/beta/threads/text_content_block.py new file mode 100644 index 00000000..3706d6b9 --- /dev/null +++ b/openai/types/beta/threads/text_content_block.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing_extensions import Literal + +from .text import Text +from ...._models import BaseModel + +__all__ = ["TextContentBlock"] + + +class TextContentBlock(BaseModel): + text: Text + + type: Literal["text"] + """Always `text`.""" diff --git a/openai/types/beta/threads/text_delta.py b/openai/types/beta/threads/text_delta.py new file mode 100644 index 00000000..09cd3570 --- /dev/null +++ b/openai/types/beta/threads/text_delta.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from ...._models import BaseModel +from .annotation_delta import AnnotationDelta + +__all__ = ["TextDelta"] + + +class TextDelta(BaseModel): + annotations: Optional[List[AnnotationDelta]] = None + + value: Optional[str] = None + """The data that makes up the text.""" diff --git a/openai/types/beta/threads/text_delta_block.py b/openai/types/beta/threads/text_delta_block.py new file mode 100644 index 00000000..586116e0 --- /dev/null +++ b/openai/types/beta/threads/text_delta_block.py @@ -0,0 +1,19 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from ...._models import BaseModel +from .text_delta import TextDelta + +__all__ = ["TextDeltaBlock"] + + +class TextDeltaBlock(BaseModel): + index: int + """The index of the content part in the message.""" + + type: Literal["text"] + """Always `text`.""" + + text: Optional[TextDelta] = None diff --git a/openai/types/chat/__init__.py b/openai/types/chat/__init__.py new file mode 100644 index 00000000..5d122d20 --- /dev/null +++ b/openai/types/chat/__init__.py @@ -0,0 +1,41 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from .chat_completion import ChatCompletion as ChatCompletion +from .chat_completion_role import ChatCompletionRole as ChatCompletionRole +from .chat_completion_chunk import ChatCompletionChunk as ChatCompletionChunk +from .chat_completion_message import ChatCompletionMessage as ChatCompletionMessage +from .completion_create_params import CompletionCreateParams as CompletionCreateParams +from .chat_completion_tool_param import ChatCompletionToolParam as ChatCompletionToolParam +from .chat_completion_message_param import ChatCompletionMessageParam as ChatCompletionMessageParam +from .chat_completion_token_logprob import ChatCompletionTokenLogprob as ChatCompletionTokenLogprob +from .chat_completion_message_tool_call import ChatCompletionMessageToolCall as ChatCompletionMessageToolCall +from .chat_completion_content_part_param import ChatCompletionContentPartParam as ChatCompletionContentPartParam +from .chat_completion_tool_message_param import ChatCompletionToolMessageParam as ChatCompletionToolMessageParam +from .chat_completion_user_message_param import ChatCompletionUserMessageParam as ChatCompletionUserMessageParam +from .chat_completion_system_message_param import ChatCompletionSystemMessageParam as ChatCompletionSystemMessageParam +from .chat_completion_function_message_param import ( + ChatCompletionFunctionMessageParam as ChatCompletionFunctionMessageParam, +) +from .chat_completion_assistant_message_param import ( + ChatCompletionAssistantMessageParam as ChatCompletionAssistantMessageParam, +) +from .chat_completion_content_part_text_param import ( + ChatCompletionContentPartTextParam as ChatCompletionContentPartTextParam, +) +from .chat_completion_message_tool_call_param import ( + ChatCompletionMessageToolCallParam as ChatCompletionMessageToolCallParam, +) +from .chat_completion_named_tool_choice_param import ( + ChatCompletionNamedToolChoiceParam as ChatCompletionNamedToolChoiceParam, +) +from .chat_completion_content_part_image_param import ( + ChatCompletionContentPartImageParam as ChatCompletionContentPartImageParam, +) +from .chat_completion_tool_choice_option_param import ( + ChatCompletionToolChoiceOptionParam as ChatCompletionToolChoiceOptionParam, +) +from .chat_completion_function_call_option_param import ( + ChatCompletionFunctionCallOptionParam as ChatCompletionFunctionCallOptionParam, +) diff --git a/openai/types/chat/chat_completion.py b/openai/types/chat/chat_completion.py new file mode 100644 index 00000000..61a94a25 --- /dev/null +++ b/openai/types/chat/chat_completion.py @@ -0,0 +1,67 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional +from typing_extensions import Literal + +from ..._models import BaseModel +from ..completion_usage import CompletionUsage +from .chat_completion_message import ChatCompletionMessage +from .chat_completion_token_logprob import ChatCompletionTokenLogprob + +__all__ = ["ChatCompletion", "Choice", "ChoiceLogprobs"] + + +class ChoiceLogprobs(BaseModel): + content: Optional[List[ChatCompletionTokenLogprob]] = None + """A list of message content tokens with log probability information.""" + + +class Choice(BaseModel): + finish_reason: Literal["stop", "length", "tool_calls", "content_filter", "function_call"] + """The reason the model stopped generating tokens. + + This will be `stop` if the model hit a natural stop point or a provided stop + sequence, `length` if the maximum number of tokens specified in the request was + reached, `content_filter` if content was omitted due to a flag from our content + filters, `tool_calls` if the model called a tool, or `function_call` + (deprecated) if the model called a function. + """ + + index: int + """The index of the choice in the list of choices.""" + + logprobs: Optional[ChoiceLogprobs] = None + """Log probability information for the choice.""" + + message: ChatCompletionMessage + """A chat completion message generated by the model.""" + + +class ChatCompletion(BaseModel): + id: str + """A unique identifier for the chat completion.""" + + choices: List[Choice] + """A list of chat completion choices. + + Can be more than one if `n` is greater than 1. + """ + + created: int + """The Unix timestamp (in seconds) of when the chat completion was created.""" + + model: str + """The model used for the chat completion.""" + + object: Literal["chat.completion"] + """The object type, which is always `chat.completion`.""" + + system_fingerprint: Optional[str] = None + """This fingerprint represents the backend configuration that the model runs with. + + Can be used in conjunction with the `seed` request parameter to understand when + backend changes have been made that might impact determinism. + """ + + usage: Optional[CompletionUsage] = None + """Usage statistics for the completion request.""" diff --git a/openai/types/chat/chat_completion_assistant_message_param.py b/openai/types/chat/chat_completion_assistant_message_param.py new file mode 100644 index 00000000..e1e39948 --- /dev/null +++ b/openai/types/chat/chat_completion_assistant_message_param.py @@ -0,0 +1,51 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Iterable, Optional +from typing_extensions import Literal, Required, TypedDict + +from .chat_completion_message_tool_call_param import ChatCompletionMessageToolCallParam + +__all__ = ["ChatCompletionAssistantMessageParam", "FunctionCall"] + + +class FunctionCall(TypedDict, total=False): + arguments: Required[str] + """ + The arguments to call the function with, as generated by the model in JSON + format. Note that the model does not always generate valid JSON, and may + hallucinate parameters not defined by your function schema. Validate the + arguments in your code before calling your function. + """ + + name: Required[str] + """The name of the function to call.""" + + +class ChatCompletionAssistantMessageParam(TypedDict, total=False): + role: Required[Literal["assistant"]] + """The role of the messages author, in this case `assistant`.""" + + content: Optional[str] + """The contents of the assistant message. + + Required unless `tool_calls` or `function_call` is specified. + """ + + function_call: FunctionCall + """Deprecated and replaced by `tool_calls`. + + The name and arguments of a function that should be called, as generated by the + model. + """ + + name: str + """An optional name for the participant. + + Provides the model information to differentiate between participants of the same + role. + """ + + tool_calls: Iterable[ChatCompletionMessageToolCallParam] + """The tool calls generated by the model, such as function calls.""" diff --git a/openai/types/chat/chat_completion_chunk.py b/openai/types/chat/chat_completion_chunk.py new file mode 100644 index 00000000..c2f18bcb --- /dev/null +++ b/openai/types/chat/chat_completion_chunk.py @@ -0,0 +1,128 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional +from typing_extensions import Literal + +from ..._models import BaseModel +from .chat_completion_token_logprob import ChatCompletionTokenLogprob + +__all__ = [ + "ChatCompletionChunk", + "Choice", + "ChoiceDelta", + "ChoiceDeltaFunctionCall", + "ChoiceDeltaToolCall", + "ChoiceDeltaToolCallFunction", + "ChoiceLogprobs", +] + + +class ChoiceDeltaFunctionCall(BaseModel): + arguments: Optional[str] = None + """ + The arguments to call the function with, as generated by the model in JSON + format. Note that the model does not always generate valid JSON, and may + hallucinate parameters not defined by your function schema. Validate the + arguments in your code before calling your function. + """ + + name: Optional[str] = None + """The name of the function to call.""" + + +class ChoiceDeltaToolCallFunction(BaseModel): + arguments: Optional[str] = None + """ + The arguments to call the function with, as generated by the model in JSON + format. Note that the model does not always generate valid JSON, and may + hallucinate parameters not defined by your function schema. Validate the + arguments in your code before calling your function. + """ + + name: Optional[str] = None + """The name of the function to call.""" + + +class ChoiceDeltaToolCall(BaseModel): + index: int + + id: Optional[str] = None + """The ID of the tool call.""" + + function: Optional[ChoiceDeltaToolCallFunction] = None + + type: Optional[Literal["function"]] = None + """The type of the tool. Currently, only `function` is supported.""" + + +class ChoiceDelta(BaseModel): + content: Optional[str] = None + """The contents of the chunk message.""" + + function_call: Optional[ChoiceDeltaFunctionCall] = None + """Deprecated and replaced by `tool_calls`. + + The name and arguments of a function that should be called, as generated by the + model. + """ + + role: Optional[Literal["system", "user", "assistant", "tool"]] = None + """The role of the author of this message.""" + + tool_calls: Optional[List[ChoiceDeltaToolCall]] = None + + +class ChoiceLogprobs(BaseModel): + content: Optional[List[ChatCompletionTokenLogprob]] = None + """A list of message content tokens with log probability information.""" + + +class Choice(BaseModel): + delta: ChoiceDelta + """A chat completion delta generated by streamed model responses.""" + + finish_reason: Optional[Literal["stop", "length", "tool_calls", "content_filter", "function_call"]] = None + """The reason the model stopped generating tokens. + + This will be `stop` if the model hit a natural stop point or a provided stop + sequence, `length` if the maximum number of tokens specified in the request was + reached, `content_filter` if content was omitted due to a flag from our content + filters, `tool_calls` if the model called a tool, or `function_call` + (deprecated) if the model called a function. + """ + + index: int + """The index of the choice in the list of choices.""" + + logprobs: Optional[ChoiceLogprobs] = None + """Log probability information for the choice.""" + + +class ChatCompletionChunk(BaseModel): + id: str + """A unique identifier for the chat completion. Each chunk has the same ID.""" + + choices: List[Choice] + """A list of chat completion choices. + + Can be more than one if `n` is greater than 1. + """ + + created: int + """The Unix timestamp (in seconds) of when the chat completion was created. + + Each chunk has the same timestamp. + """ + + model: str + """The model to generate the completion.""" + + object: Literal["chat.completion.chunk"] + """The object type, which is always `chat.completion.chunk`.""" + + system_fingerprint: Optional[str] = None + """ + This fingerprint represents the backend configuration that the model runs with. + Can be used in conjunction with the `seed` request parameter to understand when + backend changes have been made that might impact determinism. + """ diff --git a/openai/types/chat/chat_completion_content_part_image_param.py b/openai/types/chat/chat_completion_content_part_image_param.py new file mode 100644 index 00000000..b1a186aa --- /dev/null +++ b/openai/types/chat/chat_completion_content_part_image_param.py @@ -0,0 +1,26 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, Required, TypedDict + +__all__ = ["ChatCompletionContentPartImageParam", "ImageURL"] + + +class ImageURL(TypedDict, total=False): + url: Required[str] + """Either a URL of the image or the base64 encoded image data.""" + + detail: Literal["auto", "low", "high"] + """Specifies the detail level of the image. + + Learn more in the + [Vision guide](https://platform.openai.com/docs/guides/vision/low-or-high-fidelity-image-understanding). + """ + + +class ChatCompletionContentPartImageParam(TypedDict, total=False): + image_url: Required[ImageURL] + + type: Required[Literal["image_url"]] + """The type of the content part.""" diff --git a/openai/types/chat/chat_completion_content_part_param.py b/openai/types/chat/chat_completion_content_part_param.py new file mode 100644 index 00000000..f9b5f71e --- /dev/null +++ b/openai/types/chat/chat_completion_content_part_param.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Union + +from .chat_completion_content_part_text_param import ChatCompletionContentPartTextParam +from .chat_completion_content_part_image_param import ChatCompletionContentPartImageParam + +__all__ = ["ChatCompletionContentPartParam"] + +ChatCompletionContentPartParam = Union[ChatCompletionContentPartTextParam, ChatCompletionContentPartImageParam] diff --git a/openai/types/chat/chat_completion_content_part_text_param.py b/openai/types/chat/chat_completion_content_part_text_param.py new file mode 100644 index 00000000..a2707444 --- /dev/null +++ b/openai/types/chat/chat_completion_content_part_text_param.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, Required, TypedDict + +__all__ = ["ChatCompletionContentPartTextParam"] + + +class ChatCompletionContentPartTextParam(TypedDict, total=False): + text: Required[str] + """The text content.""" + + type: Required[Literal["text"]] + """The type of the content part.""" diff --git a/openai/types/chat/chat_completion_function_call_option_param.py b/openai/types/chat/chat_completion_function_call_option_param.py new file mode 100644 index 00000000..2bc014af --- /dev/null +++ b/openai/types/chat/chat_completion_function_call_option_param.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["ChatCompletionFunctionCallOptionParam"] + + +class ChatCompletionFunctionCallOptionParam(TypedDict, total=False): + name: Required[str] + """The name of the function to call.""" diff --git a/openai/types/chat/chat_completion_function_message_param.py b/openai/types/chat/chat_completion_function_message_param.py new file mode 100644 index 00000000..5af12bf9 --- /dev/null +++ b/openai/types/chat/chat_completion_function_message_param.py @@ -0,0 +1,19 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Optional +from typing_extensions import Literal, Required, TypedDict + +__all__ = ["ChatCompletionFunctionMessageParam"] + + +class ChatCompletionFunctionMessageParam(TypedDict, total=False): + content: Required[Optional[str]] + """The contents of the function message.""" + + name: Required[str] + """The name of the function to call.""" + + role: Required[Literal["function"]] + """The role of the messages author, in this case `function`.""" diff --git a/openai/types/chat/chat_completion_message.py b/openai/types/chat/chat_completion_message.py new file mode 100644 index 00000000..8db7d17d --- /dev/null +++ b/openai/types/chat/chat_completion_message.py @@ -0,0 +1,40 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional +from typing_extensions import Literal + +from ..._models import BaseModel +from .chat_completion_message_tool_call import ChatCompletionMessageToolCall + +__all__ = ["ChatCompletionMessage", "FunctionCall"] + + +class FunctionCall(BaseModel): + arguments: str + """ + The arguments to call the function with, as generated by the model in JSON + format. Note that the model does not always generate valid JSON, and may + hallucinate parameters not defined by your function schema. Validate the + arguments in your code before calling your function. + """ + + name: str + """The name of the function to call.""" + + +class ChatCompletionMessage(BaseModel): + content: Optional[str] = None + """The contents of the message.""" + + role: Literal["assistant"] + """The role of the author of this message.""" + + function_call: Optional[FunctionCall] = None + """Deprecated and replaced by `tool_calls`. + + The name and arguments of a function that should be called, as generated by the + model. + """ + + tool_calls: Optional[List[ChatCompletionMessageToolCall]] = None + """The tool calls generated by the model, such as function calls.""" diff --git a/openai/types/chat/chat_completion_message_param.py b/openai/types/chat/chat_completion_message_param.py new file mode 100644 index 00000000..a3644a53 --- /dev/null +++ b/openai/types/chat/chat_completion_message_param.py @@ -0,0 +1,21 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Union + +from .chat_completion_tool_message_param import ChatCompletionToolMessageParam +from .chat_completion_user_message_param import ChatCompletionUserMessageParam +from .chat_completion_system_message_param import ChatCompletionSystemMessageParam +from .chat_completion_function_message_param import ChatCompletionFunctionMessageParam +from .chat_completion_assistant_message_param import ChatCompletionAssistantMessageParam + +__all__ = ["ChatCompletionMessageParam"] + +ChatCompletionMessageParam = Union[ + ChatCompletionSystemMessageParam, + ChatCompletionUserMessageParam, + ChatCompletionAssistantMessageParam, + ChatCompletionToolMessageParam, + ChatCompletionFunctionMessageParam, +] diff --git a/openai/types/chat/chat_completion_message_tool_call.py b/openai/types/chat/chat_completion_message_tool_call.py new file mode 100644 index 00000000..4fec6670 --- /dev/null +++ b/openai/types/chat/chat_completion_message_tool_call.py @@ -0,0 +1,31 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing_extensions import Literal + +from ..._models import BaseModel + +__all__ = ["ChatCompletionMessageToolCall", "Function"] + + +class Function(BaseModel): + arguments: str + """ + The arguments to call the function with, as generated by the model in JSON + format. Note that the model does not always generate valid JSON, and may + hallucinate parameters not defined by your function schema. Validate the + arguments in your code before calling your function. + """ + + name: str + """The name of the function to call.""" + + +class ChatCompletionMessageToolCall(BaseModel): + id: str + """The ID of the tool call.""" + + function: Function + """The function that the model called.""" + + type: Literal["function"] + """The type of the tool. Currently, only `function` is supported.""" diff --git a/openai/types/chat/chat_completion_message_tool_call_param.py b/openai/types/chat/chat_completion_message_tool_call_param.py new file mode 100644 index 00000000..f616c363 --- /dev/null +++ b/openai/types/chat/chat_completion_message_tool_call_param.py @@ -0,0 +1,31 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, Required, TypedDict + +__all__ = ["ChatCompletionMessageToolCallParam", "Function"] + + +class Function(TypedDict, total=False): + arguments: Required[str] + """ + The arguments to call the function with, as generated by the model in JSON + format. Note that the model does not always generate valid JSON, and may + hallucinate parameters not defined by your function schema. Validate the + arguments in your code before calling your function. + """ + + name: Required[str] + """The name of the function to call.""" + + +class ChatCompletionMessageToolCallParam(TypedDict, total=False): + id: Required[str] + """The ID of the tool call.""" + + function: Required[Function] + """The function that the model called.""" + + type: Required[Literal["function"]] + """The type of the tool. Currently, only `function` is supported.""" diff --git a/openai/types/chat/chat_completion_named_tool_choice_param.py b/openai/types/chat/chat_completion_named_tool_choice_param.py new file mode 100644 index 00000000..369f8b42 --- /dev/null +++ b/openai/types/chat/chat_completion_named_tool_choice_param.py @@ -0,0 +1,19 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, Required, TypedDict + +__all__ = ["ChatCompletionNamedToolChoiceParam", "Function"] + + +class Function(TypedDict, total=False): + name: Required[str] + """The name of the function to call.""" + + +class ChatCompletionNamedToolChoiceParam(TypedDict, total=False): + function: Required[Function] + + type: Required[Literal["function"]] + """The type of the tool. Currently, only `function` is supported.""" diff --git a/openai/types/chat/chat_completion_role.py b/openai/types/chat/chat_completion_role.py new file mode 100644 index 00000000..1fd83888 --- /dev/null +++ b/openai/types/chat/chat_completion_role.py @@ -0,0 +1,7 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing_extensions import Literal + +__all__ = ["ChatCompletionRole"] + +ChatCompletionRole = Literal["system", "user", "assistant", "tool", "function"] diff --git a/openai/types/chat/chat_completion_system_message_param.py b/openai/types/chat/chat_completion_system_message_param.py new file mode 100644 index 00000000..94bb3f63 --- /dev/null +++ b/openai/types/chat/chat_completion_system_message_param.py @@ -0,0 +1,22 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, Required, TypedDict + +__all__ = ["ChatCompletionSystemMessageParam"] + + +class ChatCompletionSystemMessageParam(TypedDict, total=False): + content: Required[str] + """The contents of the system message.""" + + role: Required[Literal["system"]] + """The role of the messages author, in this case `system`.""" + + name: str + """An optional name for the participant. + + Provides the model information to differentiate between participants of the same + role. + """ diff --git a/openai/types/chat/chat_completion_token_logprob.py b/openai/types/chat/chat_completion_token_logprob.py new file mode 100644 index 00000000..c69e2589 --- /dev/null +++ b/openai/types/chat/chat_completion_token_logprob.py @@ -0,0 +1,57 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from ..._models import BaseModel + +__all__ = ["ChatCompletionTokenLogprob", "TopLogprob"] + + +class TopLogprob(BaseModel): + token: str + """The token.""" + + bytes: Optional[List[int]] = None + """A list of integers representing the UTF-8 bytes representation of the token. + + Useful in instances where characters are represented by multiple tokens and + their byte representations must be combined to generate the correct text + representation. Can be `null` if there is no bytes representation for the token. + """ + + logprob: float + """The log probability of this token, if it is within the top 20 most likely + tokens. + + Otherwise, the value `-9999.0` is used to signify that the token is very + unlikely. + """ + + +class ChatCompletionTokenLogprob(BaseModel): + token: str + """The token.""" + + bytes: Optional[List[int]] = None + """A list of integers representing the UTF-8 bytes representation of the token. + + Useful in instances where characters are represented by multiple tokens and + their byte representations must be combined to generate the correct text + representation. Can be `null` if there is no bytes representation for the token. + """ + + logprob: float + """The log probability of this token, if it is within the top 20 most likely + tokens. + + Otherwise, the value `-9999.0` is used to signify that the token is very + unlikely. + """ + + top_logprobs: List[TopLogprob] + """List of the most likely tokens and their log probability, at this token + position. + + In rare cases, there may be fewer than the number of requested `top_logprobs` + returned. + """ diff --git a/openai/types/chat/chat_completion_tool_choice_option_param.py b/openai/types/chat/chat_completion_tool_choice_option_param.py new file mode 100644 index 00000000..9c0ae225 --- /dev/null +++ b/openai/types/chat/chat_completion_tool_choice_option_param.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Union +from typing_extensions import Literal + +from .chat_completion_named_tool_choice_param import ChatCompletionNamedToolChoiceParam + +__all__ = ["ChatCompletionToolChoiceOptionParam"] + +ChatCompletionToolChoiceOptionParam = Union[Literal["none", "auto"], ChatCompletionNamedToolChoiceParam] diff --git a/openai/types/chat/chat_completion_tool_message_param.py b/openai/types/chat/chat_completion_tool_message_param.py new file mode 100644 index 00000000..5c590e03 --- /dev/null +++ b/openai/types/chat/chat_completion_tool_message_param.py @@ -0,0 +1,18 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, Required, TypedDict + +__all__ = ["ChatCompletionToolMessageParam"] + + +class ChatCompletionToolMessageParam(TypedDict, total=False): + content: Required[str] + """The contents of the tool message.""" + + role: Required[Literal["tool"]] + """The role of the messages author, in this case `tool`.""" + + tool_call_id: Required[str] + """Tool call that this message is responding to.""" diff --git a/openai/types/chat/chat_completion_tool_param.py b/openai/types/chat/chat_completion_tool_param.py new file mode 100644 index 00000000..0cf6ea72 --- /dev/null +++ b/openai/types/chat/chat_completion_tool_param.py @@ -0,0 +1,16 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, Required, TypedDict + +from ...types import shared_params + +__all__ = ["ChatCompletionToolParam"] + + +class ChatCompletionToolParam(TypedDict, total=False): + function: Required[shared_params.FunctionDefinition] + + type: Required[Literal["function"]] + """The type of the tool. Currently, only `function` is supported.""" diff --git a/openai/types/chat/chat_completion_user_message_param.py b/openai/types/chat/chat_completion_user_message_param.py new file mode 100644 index 00000000..5c15322a --- /dev/null +++ b/openai/types/chat/chat_completion_user_message_param.py @@ -0,0 +1,25 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Union, Iterable +from typing_extensions import Literal, Required, TypedDict + +from .chat_completion_content_part_param import ChatCompletionContentPartParam + +__all__ = ["ChatCompletionUserMessageParam"] + + +class ChatCompletionUserMessageParam(TypedDict, total=False): + content: Required[Union[str, Iterable[ChatCompletionContentPartParam]]] + """The contents of the user message.""" + + role: Required[Literal["user"]] + """The role of the messages author, in this case `user`.""" + + name: str + """An optional name for the participant. + + Provides the model information to differentiate between participants of the same + role. + """ diff --git a/openai/types/chat/completion_create_params.py b/openai/types/chat/completion_create_params.py new file mode 100644 index 00000000..ab6a7470 --- /dev/null +++ b/openai/types/chat/completion_create_params.py @@ -0,0 +1,280 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Dict, List, Union, Iterable, Optional +from typing_extensions import Literal, Required, TypedDict + +from ...types import shared_params +from .chat_completion_tool_param import ChatCompletionToolParam +from .chat_completion_message_param import ChatCompletionMessageParam +from .chat_completion_tool_choice_option_param import ChatCompletionToolChoiceOptionParam +from .chat_completion_function_call_option_param import ChatCompletionFunctionCallOptionParam + +__all__ = [ + "CompletionCreateParamsBase", + "FunctionCall", + "Function", + "ResponseFormat", + "CompletionCreateParamsNonStreaming", + "CompletionCreateParamsStreaming", +] + + +class CompletionCreateParamsBase(TypedDict, total=False): + messages: Required[Iterable[ChatCompletionMessageParam]] + """A list of messages comprising the conversation so far. + + [Example Python code](https://cookbook.openai.com/examples/how_to_format_inputs_to_chatgpt_models). + """ + + model: Required[ + Union[ + str, + Literal[ + "gpt-4-0125-preview", + "gpt-4-turbo-preview", + "gpt-4-1106-preview", + "gpt-4-vision-preview", + "gpt-4", + "gpt-4-0314", + "gpt-4-0613", + "gpt-4-32k", + "gpt-4-32k-0314", + "gpt-4-32k-0613", + "gpt-3.5-turbo", + "gpt-3.5-turbo-16k", + "gpt-3.5-turbo-0301", + "gpt-3.5-turbo-0613", + "gpt-3.5-turbo-1106", + "gpt-3.5-turbo-0125", + "gpt-3.5-turbo-16k-0613", + ], + ] + ] + """ID of the model to use. + + See the + [model endpoint compatibility](https://platform.openai.com/docs/models/model-endpoint-compatibility) + table for details on which models work with the Chat API. + """ + + frequency_penalty: Optional[float] + """Number between -2.0 and 2.0. + + Positive values penalize new tokens based on their existing frequency in the + text so far, decreasing the model's likelihood to repeat the same line verbatim. + + [See more information about frequency and presence penalties.](https://platform.openai.com/docs/guides/text-generation/parameter-details) + """ + + function_call: FunctionCall + """Deprecated in favor of `tool_choice`. + + Controls which (if any) function is called by the model. `none` means the model + will not call a function and instead generates a message. `auto` means the model + can pick between generating a message or calling a function. Specifying a + particular function via `{"name": "my_function"}` forces the model to call that + function. + + `none` is the default when no functions are present. `auto` is the default if + functions are present. + """ + + functions: Iterable[Function] + """Deprecated in favor of `tools`. + + A list of functions the model may generate JSON inputs for. + """ + + logit_bias: Optional[Dict[str, int]] + """Modify the likelihood of specified tokens appearing in the completion. + + Accepts a JSON object that maps tokens (specified by their token ID in the + tokenizer) to an associated bias value from -100 to 100. Mathematically, the + bias is added to the logits generated by the model prior to sampling. The exact + effect will vary per model, but values between -1 and 1 should decrease or + increase likelihood of selection; values like -100 or 100 should result in a ban + or exclusive selection of the relevant token. + """ + + logprobs: Optional[bool] + """Whether to return log probabilities of the output tokens or not. + + If true, returns the log probabilities of each output token returned in the + `content` of `message`. This option is currently not available on the + `gpt-4-vision-preview` model. + """ + + max_tokens: Optional[int] + """ + The maximum number of [tokens](/tokenizer) that can be generated in the chat + completion. + + The total length of input tokens and generated tokens is limited by the model's + context length. + [Example Python code](https://cookbook.openai.com/examples/how_to_count_tokens_with_tiktoken) + for counting tokens. + """ + + n: Optional[int] + """How many chat completion choices to generate for each input message. + + Note that you will be charged based on the number of generated tokens across all + of the choices. Keep `n` as `1` to minimize costs. + """ + + presence_penalty: Optional[float] + """Number between -2.0 and 2.0. + + Positive values penalize new tokens based on whether they appear in the text so + far, increasing the model's likelihood to talk about new topics. + + [See more information about frequency and presence penalties.](https://platform.openai.com/docs/guides/text-generation/parameter-details) + """ + + response_format: ResponseFormat + """An object specifying the format that the model must output. + + Compatible with + [GPT-4 Turbo](https://platform.openai.com/docs/models/gpt-4-and-gpt-4-turbo) and + all GPT-3.5 Turbo models newer than `gpt-3.5-turbo-1106`. + + Setting to `{ "type": "json_object" }` enables JSON mode, which guarantees the + message the model generates is valid JSON. + + **Important:** when using JSON mode, you **must** also instruct the model to + produce JSON yourself via a system or user message. Without this, the model may + generate an unending stream of whitespace until the generation reaches the token + limit, resulting in a long-running and seemingly "stuck" request. Also note that + the message content may be partially cut off if `finish_reason="length"`, which + indicates the generation exceeded `max_tokens` or the conversation exceeded the + max context length. + """ + + seed: Optional[int] + """ + This feature is in Beta. If specified, our system will make a best effort to + sample deterministically, such that repeated requests with the same `seed` and + parameters should return the same result. Determinism is not guaranteed, and you + should refer to the `system_fingerprint` response parameter to monitor changes + in the backend. + """ + + stop: Union[Optional[str], List[str]] + """Up to 4 sequences where the API will stop generating further tokens.""" + + temperature: Optional[float] + """What sampling temperature to use, between 0 and 2. + + Higher values like 0.8 will make the output more random, while lower values like + 0.2 will make it more focused and deterministic. + + We generally recommend altering this or `top_p` but not both. + """ + + tool_choice: ChatCompletionToolChoiceOptionParam + """ + Controls which (if any) function is called by the model. `none` means the model + will not call a function and instead generates a message. `auto` means the model + can pick between generating a message or calling a function. Specifying a + particular function via + `{"type": "function", "function": {"name": "my_function"}}` forces the model to + call that function. + + `none` is the default when no functions are present. `auto` is the default if + functions are present. + """ + + tools: Iterable[ChatCompletionToolParam] + """A list of tools the model may call. + + Currently, only functions are supported as a tool. Use this to provide a list of + functions the model may generate JSON inputs for. A max of 128 functions are + supported. + """ + + top_logprobs: Optional[int] + """ + An integer between 0 and 20 specifying the number of most likely tokens to + return at each token position, each with an associated log probability. + `logprobs` must be set to `true` if this parameter is used. + """ + + top_p: Optional[float] + """ + An alternative to sampling with temperature, called nucleus sampling, where the + model considers the results of the tokens with top_p probability mass. So 0.1 + means only the tokens comprising the top 10% probability mass are considered. + + We generally recommend altering this or `temperature` but not both. + """ + + user: str + """ + A unique identifier representing your end-user, which can help OpenAI to monitor + and detect abuse. + [Learn more](https://platform.openai.com/docs/guides/safety-best-practices/end-user-ids). + """ + + +FunctionCall = Union[Literal["none", "auto"], ChatCompletionFunctionCallOptionParam] + + +class Function(TypedDict, total=False): + name: Required[str] + """The name of the function to be called. + + Must be a-z, A-Z, 0-9, or contain underscores and dashes, with a maximum length + of 64. + """ + + description: str + """ + A description of what the function does, used by the model to choose when and + how to call the function. + """ + + parameters: shared_params.FunctionParameters + """The parameters the functions accepts, described as a JSON Schema object. + + See the + [guide](https://platform.openai.com/docs/guides/text-generation/function-calling) + for examples, and the + [JSON Schema reference](https://json-schema.org/understanding-json-schema/) for + documentation about the format. + + Omitting `parameters` defines a function with an empty parameter list. + """ + + +class ResponseFormat(TypedDict, total=False): + type: Literal["text", "json_object"] + """Must be one of `text` or `json_object`.""" + + +class CompletionCreateParamsNonStreaming(CompletionCreateParamsBase): + stream: Optional[Literal[False]] + """If set, partial message deltas will be sent, like in ChatGPT. + + Tokens will be sent as data-only + [server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format) + as they become available, with the stream terminated by a `data: [DONE]` + message. + [Example Python code](https://cookbook.openai.com/examples/how_to_stream_completions). + """ + + +class CompletionCreateParamsStreaming(CompletionCreateParamsBase): + stream: Required[Literal[True]] + """If set, partial message deltas will be sent, like in ChatGPT. + + Tokens will be sent as data-only + [server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format) + as they become available, with the stream terminated by a `data: [DONE]` + message. + [Example Python code](https://cookbook.openai.com/examples/how_to_stream_completions). + """ + + +CompletionCreateParams = Union[CompletionCreateParamsNonStreaming, CompletionCreateParamsStreaming] diff --git a/openai/types/completion.py b/openai/types/completion.py new file mode 100644 index 00000000..d3b3102a --- /dev/null +++ b/openai/types/completion.py @@ -0,0 +1,37 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional +from typing_extensions import Literal + +from .._models import BaseModel +from .completion_usage import CompletionUsage +from .completion_choice import CompletionChoice + +__all__ = ["Completion"] + + +class Completion(BaseModel): + id: str + """A unique identifier for the completion.""" + + choices: List[CompletionChoice] + """The list of completion choices the model generated for the input prompt.""" + + created: int + """The Unix timestamp (in seconds) of when the completion was created.""" + + model: str + """The model used for completion.""" + + object: Literal["text_completion"] + """The object type, which is always "text_completion" """ + + system_fingerprint: Optional[str] = None + """This fingerprint represents the backend configuration that the model runs with. + + Can be used in conjunction with the `seed` request parameter to understand when + backend changes have been made that might impact determinism. + """ + + usage: Optional[CompletionUsage] = None + """Usage statistics for the completion request.""" diff --git a/openai/types/completion_choice.py b/openai/types/completion_choice.py new file mode 100644 index 00000000..d948ebc9 --- /dev/null +++ b/openai/types/completion_choice.py @@ -0,0 +1,35 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Dict, List, Optional +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = ["CompletionChoice", "Logprobs"] + + +class Logprobs(BaseModel): + text_offset: Optional[List[int]] = None + + token_logprobs: Optional[List[float]] = None + + tokens: Optional[List[str]] = None + + top_logprobs: Optional[List[Dict[str, float]]] = None + + +class CompletionChoice(BaseModel): + finish_reason: Literal["stop", "length", "content_filter"] + """The reason the model stopped generating tokens. + + This will be `stop` if the model hit a natural stop point or a provided stop + sequence, `length` if the maximum number of tokens specified in the request was + reached, or `content_filter` if content was omitted due to a flag from our + content filters. + """ + + index: int + + logprobs: Optional[Logprobs] = None + + text: str diff --git a/openai/types/completion_create_params.py b/openai/types/completion_create_params.py new file mode 100644 index 00000000..36267e90 --- /dev/null +++ b/openai/types/completion_create_params.py @@ -0,0 +1,182 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Dict, List, Union, Iterable, Optional +from typing_extensions import Literal, Required, TypedDict + +__all__ = ["CompletionCreateParamsBase", "CompletionCreateParamsNonStreaming", "CompletionCreateParamsStreaming"] + + +class CompletionCreateParamsBase(TypedDict, total=False): + model: Required[Union[str, Literal["gpt-3.5-turbo-instruct", "davinci-002", "babbage-002"]]] + """ID of the model to use. + + You can use the + [List models](https://platform.openai.com/docs/api-reference/models/list) API to + see all of your available models, or see our + [Model overview](https://platform.openai.com/docs/models/overview) for + descriptions of them. + """ + + prompt: Required[Union[str, List[str], Iterable[int], Iterable[Iterable[int]], None]] + """ + The prompt(s) to generate completions for, encoded as a string, array of + strings, array of tokens, or array of token arrays. + + Note that <|endoftext|> is the document separator that the model sees during + training, so if a prompt is not specified the model will generate as if from the + beginning of a new document. + """ + + best_of: Optional[int] + """ + Generates `best_of` completions server-side and returns the "best" (the one with + the highest log probability per token). Results cannot be streamed. + + When used with `n`, `best_of` controls the number of candidate completions and + `n` specifies how many to return – `best_of` must be greater than `n`. + + **Note:** Because this parameter generates many completions, it can quickly + consume your token quota. Use carefully and ensure that you have reasonable + settings for `max_tokens` and `stop`. + """ + + echo: Optional[bool] + """Echo back the prompt in addition to the completion""" + + frequency_penalty: Optional[float] + """Number between -2.0 and 2.0. + + Positive values penalize new tokens based on their existing frequency in the + text so far, decreasing the model's likelihood to repeat the same line verbatim. + + [See more information about frequency and presence penalties.](https://platform.openai.com/docs/guides/text-generation/parameter-details) + """ + + logit_bias: Optional[Dict[str, int]] + """Modify the likelihood of specified tokens appearing in the completion. + + Accepts a JSON object that maps tokens (specified by their token ID in the GPT + tokenizer) to an associated bias value from -100 to 100. You can use this + [tokenizer tool](/tokenizer?view=bpe) to convert text to token IDs. + Mathematically, the bias is added to the logits generated by the model prior to + sampling. The exact effect will vary per model, but values between -1 and 1 + should decrease or increase likelihood of selection; values like -100 or 100 + should result in a ban or exclusive selection of the relevant token. + + As an example, you can pass `{"50256": -100}` to prevent the <|endoftext|> token + from being generated. + """ + + logprobs: Optional[int] + """ + Include the log probabilities on the `logprobs` most likely output tokens, as + well the chosen tokens. For example, if `logprobs` is 5, the API will return a + list of the 5 most likely tokens. The API will always return the `logprob` of + the sampled token, so there may be up to `logprobs+1` elements in the response. + + The maximum value for `logprobs` is 5. + """ + + max_tokens: Optional[int] + """ + The maximum number of [tokens](/tokenizer) that can be generated in the + completion. + + The token count of your prompt plus `max_tokens` cannot exceed the model's + context length. + [Example Python code](https://cookbook.openai.com/examples/how_to_count_tokens_with_tiktoken) + for counting tokens. + """ + + n: Optional[int] + """How many completions to generate for each prompt. + + **Note:** Because this parameter generates many completions, it can quickly + consume your token quota. Use carefully and ensure that you have reasonable + settings for `max_tokens` and `stop`. + """ + + presence_penalty: Optional[float] + """Number between -2.0 and 2.0. + + Positive values penalize new tokens based on whether they appear in the text so + far, increasing the model's likelihood to talk about new topics. + + [See more information about frequency and presence penalties.](https://platform.openai.com/docs/guides/text-generation/parameter-details) + """ + + seed: Optional[int] + """ + If specified, our system will make a best effort to sample deterministically, + such that repeated requests with the same `seed` and parameters should return + the same result. + + Determinism is not guaranteed, and you should refer to the `system_fingerprint` + response parameter to monitor changes in the backend. + """ + + stop: Union[Optional[str], List[str], None] + """Up to 4 sequences where the API will stop generating further tokens. + + The returned text will not contain the stop sequence. + """ + + suffix: Optional[str] + """The suffix that comes after a completion of inserted text. + + This parameter is only supported for `gpt-3.5-turbo-instruct`. + """ + + temperature: Optional[float] + """What sampling temperature to use, between 0 and 2. + + Higher values like 0.8 will make the output more random, while lower values like + 0.2 will make it more focused and deterministic. + + We generally recommend altering this or `top_p` but not both. + """ + + top_p: Optional[float] + """ + An alternative to sampling with temperature, called nucleus sampling, where the + model considers the results of the tokens with top_p probability mass. So 0.1 + means only the tokens comprising the top 10% probability mass are considered. + + We generally recommend altering this or `temperature` but not both. + """ + + user: str + """ + A unique identifier representing your end-user, which can help OpenAI to monitor + and detect abuse. + [Learn more](https://platform.openai.com/docs/guides/safety-best-practices/end-user-ids). + """ + + +class CompletionCreateParamsNonStreaming(CompletionCreateParamsBase): + stream: Optional[Literal[False]] + """Whether to stream back partial progress. + + If set, tokens will be sent as data-only + [server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format) + as they become available, with the stream terminated by a `data: [DONE]` + message. + [Example Python code](https://cookbook.openai.com/examples/how_to_stream_completions). + """ + + +class CompletionCreateParamsStreaming(CompletionCreateParamsBase): + stream: Required[Literal[True]] + """Whether to stream back partial progress. + + If set, tokens will be sent as data-only + [server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format) + as they become available, with the stream terminated by a `data: [DONE]` + message. + [Example Python code](https://cookbook.openai.com/examples/how_to_stream_completions). + """ + + +CompletionCreateParams = Union[CompletionCreateParamsNonStreaming, CompletionCreateParamsStreaming] diff --git a/openai/types/completion_usage.py b/openai/types/completion_usage.py new file mode 100644 index 00000000..e185a5cc --- /dev/null +++ b/openai/types/completion_usage.py @@ -0,0 +1,16 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .._models import BaseModel + +__all__ = ["CompletionUsage"] + + +class CompletionUsage(BaseModel): + completion_tokens: int + """Number of tokens in the generated completion.""" + + prompt_tokens: int + """Number of tokens in the prompt.""" + + total_tokens: int + """Total number of tokens used in the request (prompt + completion).""" diff --git a/openai/types/create_embedding_response.py b/openai/types/create_embedding_response.py new file mode 100644 index 00000000..eff247a1 --- /dev/null +++ b/openai/types/create_embedding_response.py @@ -0,0 +1,31 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List +from typing_extensions import Literal + +from .._models import BaseModel +from .embedding import Embedding + +__all__ = ["CreateEmbeddingResponse", "Usage"] + + +class Usage(BaseModel): + prompt_tokens: int + """The number of tokens used by the prompt.""" + + total_tokens: int + """The total number of tokens used by the request.""" + + +class CreateEmbeddingResponse(BaseModel): + data: List[Embedding] + """The list of embeddings generated by the model.""" + + model: str + """The name of the model used to generate the embedding.""" + + object: Literal["list"] + """The object type, which is always "list".""" + + usage: Usage + """The usage information for the request.""" diff --git a/openai/types/embedding.py b/openai/types/embedding.py new file mode 100644 index 00000000..769b1d16 --- /dev/null +++ b/openai/types/embedding.py @@ -0,0 +1,23 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = ["Embedding"] + + +class Embedding(BaseModel): + embedding: List[float] + """The embedding vector, which is a list of floats. + + The length of vector depends on the model as listed in the + [embedding guide](https://platform.openai.com/docs/guides/embeddings). + """ + + index: int + """The index of the embedding in the list of embeddings.""" + + object: Literal["embedding"] + """The object type, which is always "embedding".""" diff --git a/openai/types/embedding_create_params.py b/openai/types/embedding_create_params.py new file mode 100644 index 00000000..930b3b79 --- /dev/null +++ b/openai/types/embedding_create_params.py @@ -0,0 +1,50 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List, Union, Iterable +from typing_extensions import Literal, Required, TypedDict + +__all__ = ["EmbeddingCreateParams"] + + +class EmbeddingCreateParams(TypedDict, total=False): + input: Required[Union[str, List[str], Iterable[int], Iterable[Iterable[int]]]] + """Input text to embed, encoded as a string or array of tokens. + + To embed multiple inputs in a single request, pass an array of strings or array + of token arrays. The input must not exceed the max input tokens for the model + (8192 tokens for `text-embedding-ada-002`), cannot be an empty string, and any + array must be 2048 dimensions or less. + [Example Python code](https://cookbook.openai.com/examples/how_to_count_tokens_with_tiktoken) + for counting tokens. + """ + + model: Required[Union[str, Literal["text-embedding-ada-002", "text-embedding-3-small", "text-embedding-3-large"]]] + """ID of the model to use. + + You can use the + [List models](https://platform.openai.com/docs/api-reference/models/list) API to + see all of your available models, or see our + [Model overview](https://platform.openai.com/docs/models/overview) for + descriptions of them. + """ + + dimensions: int + """The number of dimensions the resulting output embeddings should have. + + Only supported in `text-embedding-3` and later models. + """ + + encoding_format: Literal["float", "base64"] + """The format to return the embeddings in. + + Can be either `float` or [`base64`](https://pypi.org/project/pybase64/). + """ + + user: str + """ + A unique identifier representing your end-user, which can help OpenAI to monitor + and detect abuse. + [Learn more](https://platform.openai.com/docs/guides/safety-best-practices/end-user-ids). + """ diff --git a/openai/types/file_content.py b/openai/types/file_content.py new file mode 100644 index 00000000..b4aa08a9 --- /dev/null +++ b/openai/types/file_content.py @@ -0,0 +1,6 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + + +__all__ = ["FileContent"] + +FileContent = str diff --git a/openai/types/file_create_params.py b/openai/types/file_create_params.py new file mode 100644 index 00000000..26e2da33 --- /dev/null +++ b/openai/types/file_create_params.py @@ -0,0 +1,25 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, Required, TypedDict + +from .._types import FileTypes + +__all__ = ["FileCreateParams"] + + +class FileCreateParams(TypedDict, total=False): + file: Required[FileTypes] + """The File object (not file name) to be uploaded.""" + + purpose: Required[Literal["fine-tune", "assistants"]] + """The intended purpose of the uploaded file. + + Use "fine-tune" for + [Fine-tuning](https://platform.openai.com/docs/api-reference/fine-tuning) and + "assistants" for + [Assistants](https://platform.openai.com/docs/api-reference/assistants) and + [Messages](https://platform.openai.com/docs/api-reference/messages). This allows + us to validate the format of the uploaded file is correct for fine-tuning. + """ diff --git a/openai/types/file_deleted.py b/openai/types/file_deleted.py new file mode 100644 index 00000000..f25fa87a --- /dev/null +++ b/openai/types/file_deleted.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = ["FileDeleted"] + + +class FileDeleted(BaseModel): + id: str + + deleted: bool + + object: Literal["file"] diff --git a/openai/types/file_list_params.py b/openai/types/file_list_params.py new file mode 100644 index 00000000..212eca13 --- /dev/null +++ b/openai/types/file_list_params.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["FileListParams"] + + +class FileListParams(TypedDict, total=False): + purpose: str + """Only return files with the given purpose.""" diff --git a/openai/types/file_object.py b/openai/types/file_object.py new file mode 100644 index 00000000..589a1faf --- /dev/null +++ b/openai/types/file_object.py @@ -0,0 +1,46 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = ["FileObject"] + + +class FileObject(BaseModel): + id: str + """The file identifier, which can be referenced in the API endpoints.""" + + bytes: int + """The size of the file, in bytes.""" + + created_at: int + """The Unix timestamp (in seconds) for when the file was created.""" + + filename: str + """The name of the file.""" + + object: Literal["file"] + """The object type, which is always `file`.""" + + purpose: Literal["fine-tune", "fine-tune-results", "assistants", "assistants_output"] + """The intended purpose of the file. + + Supported values are `fine-tune`, `fine-tune-results`, `assistants`, and + `assistants_output`. + """ + + status: Literal["uploaded", "processed", "error"] + """Deprecated. + + The current status of the file, which can be either `uploaded`, `processed`, or + `error`. + """ + + status_details: Optional[str] = None + """Deprecated. + + For details on why a fine-tuning training file failed validation, see the + `error` field on `fine_tuning.job`. + """ diff --git a/openai/types/fine_tuning/__init__.py b/openai/types/fine_tuning/__init__.py new file mode 100644 index 00000000..0bb2b904 --- /dev/null +++ b/openai/types/fine_tuning/__init__.py @@ -0,0 +1,9 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from .fine_tuning_job import FineTuningJob as FineTuningJob +from .job_list_params import JobListParams as JobListParams +from .job_create_params import JobCreateParams as JobCreateParams +from .fine_tuning_job_event import FineTuningJobEvent as FineTuningJobEvent +from .job_list_events_params import JobListEventsParams as JobListEventsParams diff --git a/openai/types/fine_tuning/fine_tuning_job.py b/openai/types/fine_tuning/fine_tuning_job.py new file mode 100644 index 00000000..23fe96d1 --- /dev/null +++ b/openai/types/fine_tuning/fine_tuning_job.py @@ -0,0 +1,107 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Union, Optional +from typing_extensions import Literal + +from ..._models import BaseModel + +__all__ = ["FineTuningJob", "Error", "Hyperparameters"] + + +class Error(BaseModel): + code: str + """A machine-readable error code.""" + + message: str + """A human-readable error message.""" + + param: Optional[str] = None + """The parameter that was invalid, usually `training_file` or `validation_file`. + + This field will be null if the failure was not parameter-specific. + """ + + +class Hyperparameters(BaseModel): + n_epochs: Union[Literal["auto"], int] + """The number of epochs to train the model for. + + An epoch refers to one full cycle through the training dataset. "auto" decides + the optimal number of epochs based on the size of the dataset. If setting the + number manually, we support any number between 1 and 50 epochs. + """ + + +class FineTuningJob(BaseModel): + id: str + """The object identifier, which can be referenced in the API endpoints.""" + + created_at: int + """The Unix timestamp (in seconds) for when the fine-tuning job was created.""" + + error: Optional[Error] = None + """ + For fine-tuning jobs that have `failed`, this will contain more information on + the cause of the failure. + """ + + fine_tuned_model: Optional[str] = None + """The name of the fine-tuned model that is being created. + + The value will be null if the fine-tuning job is still running. + """ + + finished_at: Optional[int] = None + """The Unix timestamp (in seconds) for when the fine-tuning job was finished. + + The value will be null if the fine-tuning job is still running. + """ + + hyperparameters: Hyperparameters + """The hyperparameters used for the fine-tuning job. + + See the [fine-tuning guide](https://platform.openai.com/docs/guides/fine-tuning) + for more details. + """ + + model: str + """The base model that is being fine-tuned.""" + + object: Literal["fine_tuning.job"] + """The object type, which is always "fine_tuning.job".""" + + organization_id: str + """The organization that owns the fine-tuning job.""" + + result_files: List[str] + """The compiled results file ID(s) for the fine-tuning job. + + You can retrieve the results with the + [Files API](https://platform.openai.com/docs/api-reference/files/retrieve-contents). + """ + + status: Literal["validating_files", "queued", "running", "succeeded", "failed", "cancelled"] + """ + The current status of the fine-tuning job, which can be either + `validating_files`, `queued`, `running`, `succeeded`, `failed`, or `cancelled`. + """ + + trained_tokens: Optional[int] = None + """The total number of billable tokens processed by this fine-tuning job. + + The value will be null if the fine-tuning job is still running. + """ + + training_file: str + """The file ID used for training. + + You can retrieve the training data with the + [Files API](https://platform.openai.com/docs/api-reference/files/retrieve-contents). + """ + + validation_file: Optional[str] = None + """The file ID used for validation. + + You can retrieve the validation results with the + [Files API](https://platform.openai.com/docs/api-reference/files/retrieve-contents). + """ diff --git a/openai/types/fine_tuning/fine_tuning_job_event.py b/openai/types/fine_tuning/fine_tuning_job_event.py new file mode 100644 index 00000000..2d204bb9 --- /dev/null +++ b/openai/types/fine_tuning/fine_tuning_job_event.py @@ -0,0 +1,19 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing_extensions import Literal + +from ..._models import BaseModel + +__all__ = ["FineTuningJobEvent"] + + +class FineTuningJobEvent(BaseModel): + id: str + + created_at: int + + level: Literal["info", "warn", "error"] + + message: str + + object: Literal["fine_tuning.job.event"] diff --git a/openai/types/fine_tuning/job_create_params.py b/openai/types/fine_tuning/job_create_params.py new file mode 100644 index 00000000..79e0b67e --- /dev/null +++ b/openai/types/fine_tuning/job_create_params.py @@ -0,0 +1,78 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Union, Optional +from typing_extensions import Literal, Required, TypedDict + +__all__ = ["JobCreateParams", "Hyperparameters"] + + +class JobCreateParams(TypedDict, total=False): + model: Required[Union[str, Literal["babbage-002", "davinci-002", "gpt-3.5-turbo"]]] + """The name of the model to fine-tune. + + You can select one of the + [supported models](https://platform.openai.com/docs/guides/fine-tuning/what-models-can-be-fine-tuned). + """ + + training_file: Required[str] + """The ID of an uploaded file that contains training data. + + See [upload file](https://platform.openai.com/docs/api-reference/files/upload) + for how to upload a file. + + Your dataset must be formatted as a JSONL file. Additionally, you must upload + your file with the purpose `fine-tune`. + + See the [fine-tuning guide](https://platform.openai.com/docs/guides/fine-tuning) + for more details. + """ + + hyperparameters: Hyperparameters + """The hyperparameters used for the fine-tuning job.""" + + suffix: Optional[str] + """ + A string of up to 18 characters that will be added to your fine-tuned model + name. + + For example, a `suffix` of "custom-model-name" would produce a model name like + `ft:gpt-3.5-turbo:openai:custom-model-name:7p4lURel`. + """ + + validation_file: Optional[str] + """The ID of an uploaded file that contains validation data. + + If you provide this file, the data is used to generate validation metrics + periodically during fine-tuning. These metrics can be viewed in the fine-tuning + results file. The same data should not be present in both train and validation + files. + + Your dataset must be formatted as a JSONL file. You must upload your file with + the purpose `fine-tune`. + + See the [fine-tuning guide](https://platform.openai.com/docs/guides/fine-tuning) + for more details. + """ + + +class Hyperparameters(TypedDict, total=False): + batch_size: Union[Literal["auto"], int] + """Number of examples in each batch. + + A larger batch size means that model parameters are updated less frequently, but + with lower variance. + """ + + learning_rate_multiplier: Union[Literal["auto"], float] + """Scaling factor for the learning rate. + + A smaller learning rate may be useful to avoid overfitting. + """ + + n_epochs: Union[Literal["auto"], int] + """The number of epochs to train the model for. + + An epoch refers to one full cycle through the training dataset. + """ diff --git a/openai/types/fine_tuning/job_list_events_params.py b/openai/types/fine_tuning/job_list_events_params.py new file mode 100644 index 00000000..e1c9a64d --- /dev/null +++ b/openai/types/fine_tuning/job_list_events_params.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["JobListEventsParams"] + + +class JobListEventsParams(TypedDict, total=False): + after: str + """Identifier for the last event from the previous pagination request.""" + + limit: int + """Number of events to retrieve.""" diff --git a/openai/types/fine_tuning/job_list_params.py b/openai/types/fine_tuning/job_list_params.py new file mode 100644 index 00000000..5c075ca3 --- /dev/null +++ b/openai/types/fine_tuning/job_list_params.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["JobListParams"] + + +class JobListParams(TypedDict, total=False): + after: str + """Identifier for the last job from the previous pagination request.""" + + limit: int + """Number of fine-tuning jobs to retrieve.""" diff --git a/openai/types/image.py b/openai/types/image.py new file mode 100644 index 00000000..f48aa2c7 --- /dev/null +++ b/openai/types/image.py @@ -0,0 +1,24 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from .._models import BaseModel + +__all__ = ["Image"] + + +class Image(BaseModel): + b64_json: Optional[str] = None + """ + The base64-encoded JSON of the generated image, if `response_format` is + `b64_json`. + """ + + revised_prompt: Optional[str] = None + """ + The prompt that was used to generate the image, if there was any revision to the + prompt. + """ + + url: Optional[str] = None + """The URL of the generated image, if `response_format` is `url` (default).""" diff --git a/openai/types/image_create_variation_params.py b/openai/types/image_create_variation_params.py new file mode 100644 index 00000000..25493073 --- /dev/null +++ b/openai/types/image_create_variation_params.py @@ -0,0 +1,50 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Union, Optional +from typing_extensions import Literal, Required, TypedDict + +from .._types import FileTypes + +__all__ = ["ImageCreateVariationParams"] + + +class ImageCreateVariationParams(TypedDict, total=False): + image: Required[FileTypes] + """The image to use as the basis for the variation(s). + + Must be a valid PNG file, less than 4MB, and square. + """ + + model: Union[str, Literal["dall-e-2"], None] + """The model to use for image generation. + + Only `dall-e-2` is supported at this time. + """ + + n: Optional[int] + """The number of images to generate. + + Must be between 1 and 10. For `dall-e-3`, only `n=1` is supported. + """ + + response_format: Optional[Literal["url", "b64_json"]] + """The format in which the generated images are returned. + + Must be one of `url` or `b64_json`. URLs are only valid for 60 minutes after the + image has been generated. + """ + + size: Optional[Literal["256x256", "512x512", "1024x1024"]] + """The size of the generated images. + + Must be one of `256x256`, `512x512`, or `1024x1024`. + """ + + user: str + """ + A unique identifier representing your end-user, which can help OpenAI to monitor + and detect abuse. + [Learn more](https://platform.openai.com/docs/guides/safety-best-practices/end-user-ids). + """ diff --git a/openai/types/image_edit_params.py b/openai/types/image_edit_params.py new file mode 100644 index 00000000..073456e3 --- /dev/null +++ b/openai/types/image_edit_params.py @@ -0,0 +1,61 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Union, Optional +from typing_extensions import Literal, Required, TypedDict + +from .._types import FileTypes + +__all__ = ["ImageEditParams"] + + +class ImageEditParams(TypedDict, total=False): + image: Required[FileTypes] + """The image to edit. + + Must be a valid PNG file, less than 4MB, and square. If mask is not provided, + image must have transparency, which will be used as the mask. + """ + + prompt: Required[str] + """A text description of the desired image(s). + + The maximum length is 1000 characters. + """ + + mask: FileTypes + """An additional image whose fully transparent areas (e.g. + + where alpha is zero) indicate where `image` should be edited. Must be a valid + PNG file, less than 4MB, and have the same dimensions as `image`. + """ + + model: Union[str, Literal["dall-e-2"], None] + """The model to use for image generation. + + Only `dall-e-2` is supported at this time. + """ + + n: Optional[int] + """The number of images to generate. Must be between 1 and 10.""" + + response_format: Optional[Literal["url", "b64_json"]] + """The format in which the generated images are returned. + + Must be one of `url` or `b64_json`. URLs are only valid for 60 minutes after the + image has been generated. + """ + + size: Optional[Literal["256x256", "512x512", "1024x1024"]] + """The size of the generated images. + + Must be one of `256x256`, `512x512`, or `1024x1024`. + """ + + user: str + """ + A unique identifier representing your end-user, which can help OpenAI to monitor + and detect abuse. + [Learn more](https://platform.openai.com/docs/guides/safety-best-practices/end-user-ids). + """ diff --git a/openai/types/image_generate_params.py b/openai/types/image_generate_params.py new file mode 100644 index 00000000..18c56f8e --- /dev/null +++ b/openai/types/image_generate_params.py @@ -0,0 +1,63 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Union, Optional +from typing_extensions import Literal, Required, TypedDict + +__all__ = ["ImageGenerateParams"] + + +class ImageGenerateParams(TypedDict, total=False): + prompt: Required[str] + """A text description of the desired image(s). + + The maximum length is 1000 characters for `dall-e-2` and 4000 characters for + `dall-e-3`. + """ + + model: Union[str, Literal["dall-e-2", "dall-e-3"], None] + """The model to use for image generation.""" + + n: Optional[int] + """The number of images to generate. + + Must be between 1 and 10. For `dall-e-3`, only `n=1` is supported. + """ + + quality: Literal["standard", "hd"] + """The quality of the image that will be generated. + + `hd` creates images with finer details and greater consistency across the image. + This param is only supported for `dall-e-3`. + """ + + response_format: Optional[Literal["url", "b64_json"]] + """The format in which the generated images are returned. + + Must be one of `url` or `b64_json`. URLs are only valid for 60 minutes after the + image has been generated. + """ + + size: Optional[Literal["256x256", "512x512", "1024x1024", "1792x1024", "1024x1792"]] + """The size of the generated images. + + Must be one of `256x256`, `512x512`, or `1024x1024` for `dall-e-2`. Must be one + of `1024x1024`, `1792x1024`, or `1024x1792` for `dall-e-3` models. + """ + + style: Optional[Literal["vivid", "natural"]] + """The style of the generated images. + + Must be one of `vivid` or `natural`. Vivid causes the model to lean towards + generating hyper-real and dramatic images. Natural causes the model to produce + more natural, less hyper-real looking images. This param is only supported for + `dall-e-3`. + """ + + user: str + """ + A unique identifier representing your end-user, which can help OpenAI to monitor + and detect abuse. + [Learn more](https://platform.openai.com/docs/guides/safety-best-practices/end-user-ids). + """ diff --git a/openai/types/images_response.py b/openai/types/images_response.py new file mode 100644 index 00000000..7cee8131 --- /dev/null +++ b/openai/types/images_response.py @@ -0,0 +1,14 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List + +from .image import Image +from .._models import BaseModel + +__all__ = ["ImagesResponse"] + + +class ImagesResponse(BaseModel): + created: int + + data: List[Image] diff --git a/openai/types/model.py b/openai/types/model.py new file mode 100644 index 00000000..2631ee8d --- /dev/null +++ b/openai/types/model.py @@ -0,0 +1,21 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = ["Model"] + + +class Model(BaseModel): + id: str + """The model identifier, which can be referenced in the API endpoints.""" + + created: int + """The Unix timestamp (in seconds) when the model was created.""" + + object: Literal["model"] + """The object type, which is always "model".""" + + owned_by: str + """The organization that owns the model.""" diff --git a/openai/types/model_deleted.py b/openai/types/model_deleted.py new file mode 100644 index 00000000..e7601f74 --- /dev/null +++ b/openai/types/model_deleted.py @@ -0,0 +1,13 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .._models import BaseModel + +__all__ = ["ModelDeleted"] + + +class ModelDeleted(BaseModel): + id: str + + deleted: bool + + object: str diff --git a/openai/types/moderation.py b/openai/types/moderation.py new file mode 100644 index 00000000..2a2e5c5d --- /dev/null +++ b/openai/types/moderation.py @@ -0,0 +1,117 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from pydantic import Field as FieldInfo + +from .._models import BaseModel + +__all__ = ["Moderation", "Categories", "CategoryScores"] + + +class Categories(BaseModel): + harassment: bool + """ + Content that expresses, incites, or promotes harassing language towards any + target. + """ + + harassment_threatening: bool = FieldInfo(alias="harassment/threatening") + """ + Harassment content that also includes violence or serious harm towards any + target. + """ + + hate: bool + """ + Content that expresses, incites, or promotes hate based on race, gender, + ethnicity, religion, nationality, sexual orientation, disability status, or + caste. Hateful content aimed at non-protected groups (e.g., chess players) is + harassment. + """ + + hate_threatening: bool = FieldInfo(alias="hate/threatening") + """ + Hateful content that also includes violence or serious harm towards the targeted + group based on race, gender, ethnicity, religion, nationality, sexual + orientation, disability status, or caste. + """ + + self_harm: bool = FieldInfo(alias="self-harm") + """ + Content that promotes, encourages, or depicts acts of self-harm, such as + suicide, cutting, and eating disorders. + """ + + self_harm_instructions: bool = FieldInfo(alias="self-harm/instructions") + """ + Content that encourages performing acts of self-harm, such as suicide, cutting, + and eating disorders, or that gives instructions or advice on how to commit such + acts. + """ + + self_harm_intent: bool = FieldInfo(alias="self-harm/intent") + """ + Content where the speaker expresses that they are engaging or intend to engage + in acts of self-harm, such as suicide, cutting, and eating disorders. + """ + + sexual: bool + """ + Content meant to arouse sexual excitement, such as the description of sexual + activity, or that promotes sexual services (excluding sex education and + wellness). + """ + + sexual_minors: bool = FieldInfo(alias="sexual/minors") + """Sexual content that includes an individual who is under 18 years old.""" + + violence: bool + """Content that depicts death, violence, or physical injury.""" + + violence_graphic: bool = FieldInfo(alias="violence/graphic") + """Content that depicts death, violence, or physical injury in graphic detail.""" + + +class CategoryScores(BaseModel): + harassment: float + """The score for the category 'harassment'.""" + + harassment_threatening: float = FieldInfo(alias="harassment/threatening") + """The score for the category 'harassment/threatening'.""" + + hate: float + """The score for the category 'hate'.""" + + hate_threatening: float = FieldInfo(alias="hate/threatening") + """The score for the category 'hate/threatening'.""" + + self_harm: float = FieldInfo(alias="self-harm") + """The score for the category 'self-harm'.""" + + self_harm_instructions: float = FieldInfo(alias="self-harm/instructions") + """The score for the category 'self-harm/instructions'.""" + + self_harm_intent: float = FieldInfo(alias="self-harm/intent") + """The score for the category 'self-harm/intent'.""" + + sexual: float + """The score for the category 'sexual'.""" + + sexual_minors: float = FieldInfo(alias="sexual/minors") + """The score for the category 'sexual/minors'.""" + + violence: float + """The score for the category 'violence'.""" + + violence_graphic: float = FieldInfo(alias="violence/graphic") + """The score for the category 'violence/graphic'.""" + + +class Moderation(BaseModel): + categories: Categories + """A list of the categories, and whether they are flagged or not.""" + + category_scores: CategoryScores + """A list of the categories along with their scores as predicted by model.""" + + flagged: bool + """Whether any of the below categories are flagged.""" diff --git a/openai/types/moderation_create_params.py b/openai/types/moderation_create_params.py new file mode 100644 index 00000000..d4608def --- /dev/null +++ b/openai/types/moderation_create_params.py @@ -0,0 +1,25 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List, Union +from typing_extensions import Literal, Required, TypedDict + +__all__ = ["ModerationCreateParams"] + + +class ModerationCreateParams(TypedDict, total=False): + input: Required[Union[str, List[str]]] + """The input text to classify""" + + model: Union[str, Literal["text-moderation-latest", "text-moderation-stable"]] + """ + Two content moderations models are available: `text-moderation-stable` and + `text-moderation-latest`. + + The default is `text-moderation-latest` which will be automatically upgraded + over time. This ensures you are always using our most accurate model. If you use + `text-moderation-stable`, we will provide advanced notice before updating the + model. Accuracy of `text-moderation-stable` may be slightly lower than for + `text-moderation-latest`. + """ diff --git a/openai/types/moderation_create_response.py b/openai/types/moderation_create_response.py new file mode 100644 index 00000000..79684f8a --- /dev/null +++ b/openai/types/moderation_create_response.py @@ -0,0 +1,19 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List + +from .._models import BaseModel +from .moderation import Moderation + +__all__ = ["ModerationCreateResponse"] + + +class ModerationCreateResponse(BaseModel): + id: str + """The unique identifier for the moderation request.""" + + model: str + """The model used to generate the moderation results.""" + + results: List[Moderation] + """A list of moderation objects.""" diff --git a/openai/types/shared/__init__.py b/openai/types/shared/__init__.py new file mode 100644 index 00000000..e085744e --- /dev/null +++ b/openai/types/shared/__init__.py @@ -0,0 +1,5 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .error_object import ErrorObject as ErrorObject +from .function_definition import FunctionDefinition as FunctionDefinition +from .function_parameters import FunctionParameters as FunctionParameters diff --git a/openai/types/shared/error_object.py b/openai/types/shared/error_object.py new file mode 100644 index 00000000..32d7045e --- /dev/null +++ b/openai/types/shared/error_object.py @@ -0,0 +1,17 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from ..._models import BaseModel + +__all__ = ["ErrorObject"] + + +class ErrorObject(BaseModel): + code: Optional[str] = None + + message: str + + param: Optional[str] = None + + type: str diff --git a/openai/types/shared/function_definition.py b/openai/types/shared/function_definition.py new file mode 100644 index 00000000..a39116d6 --- /dev/null +++ b/openai/types/shared/function_definition.py @@ -0,0 +1,35 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from ..._models import BaseModel +from .function_parameters import FunctionParameters + +__all__ = ["FunctionDefinition"] + + +class FunctionDefinition(BaseModel): + name: str + """The name of the function to be called. + + Must be a-z, A-Z, 0-9, or contain underscores and dashes, with a maximum length + of 64. + """ + + description: Optional[str] = None + """ + A description of what the function does, used by the model to choose when and + how to call the function. + """ + + parameters: Optional[FunctionParameters] = None + """The parameters the functions accepts, described as a JSON Schema object. + + See the + [guide](https://platform.openai.com/docs/guides/text-generation/function-calling) + for examples, and the + [JSON Schema reference](https://json-schema.org/understanding-json-schema/) for + documentation about the format. + + Omitting `parameters` defines a function with an empty parameter list. + """ diff --git a/openai/types/shared/function_parameters.py b/openai/types/shared/function_parameters.py new file mode 100644 index 00000000..c9524e4c --- /dev/null +++ b/openai/types/shared/function_parameters.py @@ -0,0 +1,7 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Dict + +__all__ = ["FunctionParameters"] + +FunctionParameters = Dict[str, object] diff --git a/openai/types/shared_params/__init__.py b/openai/types/shared_params/__init__.py new file mode 100644 index 00000000..ef638cb2 --- /dev/null +++ b/openai/types/shared_params/__init__.py @@ -0,0 +1,4 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .function_definition import FunctionDefinition as FunctionDefinition +from .function_parameters import FunctionParameters as FunctionParameters diff --git a/openai/types/shared_params/function_definition.py b/openai/types/shared_params/function_definition.py new file mode 100644 index 00000000..58d0203b --- /dev/null +++ b/openai/types/shared_params/function_definition.py @@ -0,0 +1,36 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +from ...types import shared_params + +__all__ = ["FunctionDefinition"] + + +class FunctionDefinition(TypedDict, total=False): + name: Required[str] + """The name of the function to be called. + + Must be a-z, A-Z, 0-9, or contain underscores and dashes, with a maximum length + of 64. + """ + + description: str + """ + A description of what the function does, used by the model to choose when and + how to call the function. + """ + + parameters: shared_params.FunctionParameters + """The parameters the functions accepts, described as a JSON Schema object. + + See the + [guide](https://platform.openai.com/docs/guides/text-generation/function-calling) + for examples, and the + [JSON Schema reference](https://json-schema.org/understanding-json-schema/) for + documentation about the format. + + Omitting `parameters` defines a function with an empty parameter list. + """ diff --git a/openai/types/shared_params/function_parameters.py b/openai/types/shared_params/function_parameters.py new file mode 100644 index 00000000..5b40efb7 --- /dev/null +++ b/openai/types/shared_params/function_parameters.py @@ -0,0 +1,9 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Dict + +__all__ = ["FunctionParameters"] + +FunctionParameters = Dict[str, object] diff --git a/openai/version.py b/openai/version.py new file mode 100644 index 00000000..01a08ab5 --- /dev/null +++ b/openai/version.py @@ -0,0 +1,3 @@ +from ._version import __version__ + +VERSION: str = __version__