diff --git a/.castiron.stats.yml b/.castiron.stats.yml index cbf8b0fb52..5837be57a6 100644 --- a/.castiron.stats.yml +++ b/.castiron.stats.yml @@ -1,8 +1,8 @@ schema_version: 1 -generation_id: 8f7e098f-e353-41cb-b58d-e4f29fe93695 -openapi_spec_hash: 040cdc82e15feee4aff18579e53dbaa4 -openapi_transformed_spec_hash: 122a138e15209cbe9b31f91cdaccd592 -config_hash: 84cdcc959ee7140cc9a65e9b642c097b -codegen_sha: ad8ea5ac6f75d45952618ec57d96ba0285961aee -codegen_hash: 67b5ab20d81c80689a8a857466838278fb0d428d48b3261d2479bab43396d589 -public_codegen_sha: 7c66cc09bbb86486567a7cee32f3048ebb85175e +generation_id: 333859af-62cd-49b7-9d68-fdc65b83bcef +openapi_spec_hash: 190a5881eaed8f8bfed8eac62c68302a +openapi_transformed_spec_hash: 1e89b4f0b784ea36e6aa81a6622b35ae +config_hash: 38c35bf82be025557d8a7dc727060f2f +codegen_sha: d59ac118482823565bbdca847051357ed3e4f6e1 +codegen_hash: 210a69f518d2f337946ef9339996d6d741400fee9d80e2e009fec34efeeff626 +public_codegen_sha: d91884e54ca755e5851b5f8cd7fa91b1c39121ab diff --git a/api_reference/openapi.transformed.yml b/api_reference/openapi.transformed.yml index c2e045786d..761b694419 100644 --- a/api_reference/openapi.transformed.yml +++ b/api_reference/openapi.transformed.yml @@ -601,6 +601,30 @@ paths: schema: type: string format: binary + audio/mpeg: + schema: + type: string + format: binary + audio/aac: + schema: + type: string + format: binary + audio/opus: + schema: + type: string + format: binary + audio/flac: + schema: + type: string + format: binary + audio/pcm: + schema: + type: string + format: binary + audio/wav: + schema: + type: string + format: binary text/event-stream: schema: $ref: '#/components/schemas/CreateSpeechResponseStreamEvent' @@ -723,7 +747,8 @@ paths: Transcribes audio into the input language. Returns a transcription object in `json`, `diarized_json`, or `verbose_json` - format, or a stream of transcript events. + format, plain text in `text`, `srt`, or `vtt` format, or a stream of + transcript events. Supported formats depend on the model. requestBody: required: true content: @@ -744,6 +769,9 @@ paths: - $ref: '#/components/schemas/CreateTranscriptionResponseVerboseJson' discriminator: propertyName: task + text/plain: + schema: + type: string text/event-stream: schema: $ref: '#/components/schemas/CreateTranscriptionResponseStreamEvent' @@ -1341,6 +1369,9 @@ paths: - $ref: '#/components/schemas/CreateTranslationResponseVerboseJson' x-stainless-skip: - go + text/plain: + schema: + type: string '400': description: Invalid audio input or request parameters. content: @@ -1818,7 +1849,7 @@ paths: }, "metadata": { "customer_id": "user_123456789", - "batch_description": "Nightly eval job", + "batch_description": "Nightly eval job" } } get: @@ -2025,7 +2056,7 @@ paths: }, "metadata": { "customer_id": "user_123456789", - "batch_description": "Nightly eval job", + "batch_description": "Nightly eval job" } } /batches/{batch_id}/cancel: @@ -2126,7 +2157,7 @@ paths: }, "metadata": { "customer_id": "user_123456789", - "batch_description": "Nightly eval job", + "batch_description": "Nightly eval job" } } /chat/completions: @@ -3679,8 +3710,8 @@ paths: "finish_reason": null } ], - "model": "gpt-3.5-turbo-instruct" - "system_fingerprint": "fp_44709d6fcb", + "model": "gpt-3.5-turbo-instruct", + "system_fingerprint": "fp_44709d6fcb" } /containers: get: @@ -5116,6 +5147,7 @@ paths: "item", "sample" ] + } }, "testing_criteria": [ { @@ -5250,7 +5282,7 @@ paths: ], "name": "External Data Eval", "created_at": 1739314509, - "metadata": {}, + "metadata": {} } post: operationId: updateEval @@ -5375,7 +5407,7 @@ paths: ], "name": "Updated Eval", "created_at": 1739314509, - "metadata": {"description": "Updated description"}, + "metadata": {"description": "Updated description"} } delete: operationId: deleteEval @@ -6744,7 +6776,7 @@ paths: "created_at": 1613677385, "expires_at": 1677614202, "filename": "salesOverview.pdf", - "purpose": "assistants", + "purpose": "assistants" }, { "id": "file-abc456", @@ -6753,7 +6785,7 @@ paths: "created_at": 1613779121, "expires_at": 1677614202, "filename": "puppy.jsonl", - "purpose": "fine-tune", + "purpose": "fine-tune" } ], "first_id": "file-abc123", @@ -6873,7 +6905,7 @@ paths: "created_at": 1677610602, "expires_at": 1677614202, "filename": "mydata.jsonl", - "purpose": "fine-tune", + "purpose": "fine-tune" } /files/{file_id}: delete: @@ -7003,7 +7035,7 @@ paths: "created_at": 1677610602, "expires_at": 1677614202, "filename": "mydata.jsonl", - "purpose": "fine-tune", + "purpose": "fine-tune" } /files/{file_id}/content: get: @@ -7023,9 +7055,10 @@ paths: '200': description: OK content: - application/json: + application/octet-stream: schema: type: string + format: binary '400': description: The file cannot be downloaded for its purpose or the account does not have permission to download it. content: @@ -7468,7 +7501,7 @@ paths: "id": "cp_enQCFmOTGj3syEpYVhBRLTSy", "created_at": 1721764800, "project_id": "proj_iqGMw1llN8IrBb6SvvY5A1oF" - }, + } ], "first_id": "cp_zc4Q7MP6XxulcVzj4MZdwsAB", "last_id": "cp_enQCFmOTGj3syEpYVhBRLTSy", @@ -7701,7 +7734,7 @@ paths: "hyperparameters": { "batch_size": "auto", "learning_rate_multiplier": "auto", - "n_epochs": "auto", + "n_epochs": "auto" } } }, @@ -7964,7 +7997,7 @@ paths: "hyperparameters": { "batch_size": "auto", "learning_rate_multiplier": "auto", - "n_epochs": "auto", + "n_epochs": "auto" } } }, @@ -8010,7 +8043,7 @@ paths: "type": "wandb", "wandb": { "project": "my-wandb-project", - "entity": None, + "entity": null, "run_id": "ftjob-abc123" } } @@ -8021,7 +8054,7 @@ paths: "hyperparameters": { "batch_size": "auto", "learning_rate_multiplier": "auto", - "n_epochs": "auto", + "n_epochs": "auto" } } }, @@ -9563,7 +9596,7 @@ paths: "created": 1686935002, "owned_by": "openai", "shutdown_date": "2026-10-23" - }, + } ] } /models/{model}: @@ -10407,7 +10440,7 @@ paths: }, "project.archived": { "id": "proj_abc" - }, + } }, { "id": "audit_log-yyy__20240101", @@ -10440,7 +10473,7 @@ paths: "data": { "scopes": ["resource_2.operation_2"] } - }, + } } ], "first_id": "audit_log-xxx__20240101", @@ -10524,7 +10557,7 @@ paths: "valid_at": 12345667, "expires_at": 12345678 } - }, + } ], "first_id": "cert_abc", "last_id": "cert_abc", @@ -10680,8 +10713,8 @@ paths: "valid_at": 12345667, "expires_at": 12345678 } - }, - ], + } + ] } /organization/certificates/deactivate: post: @@ -10771,8 +10804,8 @@ paths: "valid_at": 12345667, "expires_at": 12345678 } - }, - ], + } + ] } /organization/certificates/{certificate_id}: get: @@ -13174,7 +13207,7 @@ paths: "valid_at": 12345667, "expires_at": 12345678 } - }, + } ], "first_id": "cert_abc", "last_id": "cert_abc", @@ -13275,8 +13308,8 @@ paths: "valid_at": 12345667, "expires_at": 12345678 } - }, - ], + } + ] } /organization/projects/{project_id}/certificates/deactivate: post: @@ -13370,8 +13403,8 @@ paths: "valid_at": 12345667, "expires_at": 12345678 } - }, - ], + } + ] } /organization/projects/{project_id}/data_retention: get: @@ -19709,7 +19742,7 @@ paths: "transcription": null, "noise_reduction": null, "turn_detection": { - "type": "server_vad", + "type": "server_vad" } }, "output": { @@ -24347,7 +24380,7 @@ paths: "created_at": 1719186911, "expires_at": 1719127296, "filename": "training_examples.jsonl", - "purpose": "fine-tune", + "purpose": "fine-tune" } } /uploads/{upload_id}/parts: @@ -24860,9 +24893,9 @@ paths: main(); response: | { - id: "vs_abc123", - object: "vector_store.deleted", - deleted: true + "id": "vs_abc123", + "object": "vector_store.deleted", + "deleted": true } /vector_stores/{vector_store_id}/file_batches: post: @@ -25012,7 +25045,7 @@ paths: "completed": 1, "failed": 0, "cancelled": 0, - "total": 0, + "total": 0 } } /vector_stores/{vector_store_id}/file_batches/{batch_id}: @@ -25101,7 +25134,7 @@ paths: "completed": 1, "failed": 0, "cancelled": 0, - "total": 0, + "total": 0 } } /vector_stores/{vector_store_id}/file_batches/{batch_id}/cancel: @@ -25201,7 +25234,7 @@ paths: "completed": 3, "failed": 0, "cancelled": 0, - "total": 15, + "total": 15 } } /vector_stores/{vector_store_id}/file_batches/{batch_id}/files: @@ -25743,9 +25776,9 @@ paths: main(); response: | { - id: "file-abc123", - object: "vector_store.file.deleted", - deleted: true + "id": "file-abc123", + "object": "vector_store.file.deleted", + "deleted": true } post: operationId: updateVectorStoreFileAttributes @@ -25956,7 +25989,7 @@ paths: response: | { "object": "vector_store.search_results.page", - "search_query": "What is the return policy?", + "search_query": ["What is the return policy?"], "data": [ { "file_id": "file_123", @@ -26625,10 +26658,27 @@ paths: "geography": "US", "status": "validated", "created_at": 1711471500 + }, + { + "object": "organization.external_storage", + "id": "extstorage_gcp123", + "project_id": "proj_gcp123", + "provider": { + "type": "gcp", + "bucket": "customer-gcp-retention", + "workload_identity_project_number": "123456789012", + "workload_identity_pool_id": "customer-pool", + "workload_identity_provider_id": "customer-provider", + "region": "us-central1", + "audience": "proj_gcp123" + }, + "geography": "US", + "status": "validated", + "created_at": 1711471400 } ], "first_id": "extstorage_abc123", - "last_id": "extstorage_azure123", + "last_id": "extstorage_gcp123", "has_more": false } security: @@ -26744,6 +26794,40 @@ paths: "status": "pending", "created_at": 1711471533 } + - title: Google Cloud Storage + request: + curl: | + curl -X POST https://api.openai.com/v1/organization/external_storage \ + -H "Authorization: Bearer $OPENAI_ADMIN_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "project_id": "proj_gcp123", + "provider": { + "type": "gcp", + "bucket": "customer-gcp-retention", + "workload_identity_project_number": "123456789012", + "workload_identity_pool_id": "customer-pool", + "workload_identity_provider_id": "customer-provider" + } + }' + response: | + { + "object": "organization.external_storage", + "id": "extstorage_gcp123", + "project_id": "proj_gcp123", + "provider": { + "type": "gcp", + "bucket": "customer-gcp-retention", + "workload_identity_project_number": "123456789012", + "workload_identity_pool_id": "customer-pool", + "workload_identity_provider_id": "customer-provider", + "region": "us-central1", + "audience": "proj_gcp123" + }, + "geography": "US", + "status": "pending", + "created_at": 1711471400 + } security: - AdminApiKeyAuth: [] /organization/external_storage/{external_storage_id}: @@ -27955,7 +28039,12 @@ paths: group: videos path: delete examples: - response: '' + response: | + { + "id": "video_123", + "object": "video.deleted", + "deleted": true + } request: javascript: | import OpenAI from 'openai'; @@ -29668,7 +29757,12 @@ paths: x-oaiMeta: beta: true examples: - response: '' + response: | + { + "id": "cthr_123", + "object": "chatkit.thread.deleted", + "deleted": true + } request: javascript: | import OpenAI from 'openai'; @@ -32961,8 +33055,8 @@ paths: $ref: '#/components/schemas/ErrorResponse-2' post: operationId: rotateVaultCredential - summary: Rotate a vault credential - description: Rotates a vault credential's write-only secret and returns only credential metadata. See [vaults](https://developers.openai.com/api/docs/guides/agents-api/tools/vaults). + summary: Update a vault credential + description: Updates credential metadata or rotates its write-only secret. See [vaults](https://developers.openai.com/api/docs/guides/agents-api/tools/vaults). tags: - Vaults parameters: @@ -32987,9 +33081,10 @@ paths: application/json: schema: $ref: '#/components/schemas/RotateVaultCredentialParams' + required: true responses: '200': - description: The rotated vault credential without secret values. + description: The updated vault credential without secret values. content: application/json: schema: @@ -37627,7 +37722,7 @@ components: }, "metadata": { "customer_id": "user_123456789", - "batch_description": "Nightly eval job", + "batch_description": "Nightly eval job" } } BatchError: @@ -39562,10 +39657,6 @@ components: - name - created_at - status - - id - - name - - created_at - - status x-oaiMeta: name: The container object example: | @@ -42829,7 +42920,25 @@ components: maxLength: 4096 voice: description: 'The voice to use when generating the audio. Supported built-in voices are `alloy`, `ash`, `ballad`, `coral`, `echo`, `fable`, `onyx`, `nova`, `sage`, `shimmer`, `verse`, `marin`, and `cedar`. You may also provide a custom voice object with an `id`, for example `{ "id": "voice_1234" }`. Previews of the voices are available in the [Text to speech guide](https://developers.openai.com/api/docs/guides/text-to-speech#voice-options).' - $ref: '#/components/schemas/VoiceIdsOrCustomVoice' + title: Voice + anyOf: + - anyOf: + - $ref: '#/components/schemas/VoiceIdsShared' + - type: string + enum: + - fable + - onyx + - nova + - type: object + description: Custom voice reference. + additionalProperties: false + required: + - id + properties: + id: + type: string + description: The custom voice ID, e.g. `voice_1234`. + example: voice_1234 response_format: description: The format to audio in. Supported formats are `mp3`, `opus`, `aac`, `flac`, `wav`, and `pcm`. default: mp3 @@ -44426,7 +44535,7 @@ components: "item_schema": { "type": "object", "properties": { - "label": {"type": "string"}, + "label": {"type": "string"} }, "required": ["label"] }, @@ -44438,13 +44547,13 @@ components: "type": "string_check", "input": "{{sample.output_text}}", "reference": "{{item.label}}", - "operation": "eq", + "operation": "eq" } ], "name": "External Data Eval", "created_at": 1739314509, "metadata": { - "test": "synthetics", + "test": "synthetics" } } EvalApiError: @@ -44492,6 +44601,18 @@ components: The json schema for the run data source items. Learn how to build JSON schemas [here](https://json-schema.org/). additionalProperties: true + example: + type: object + properties: + item: + type: object + properties: + label: + type: string + required: + - label + required: + - item required: - type - schema @@ -44507,7 +44628,7 @@ components: "item": { "type": "object", "properties": { - "label": {"type": "string"}, + "label": {"type": "string"} }, "required": ["label"] } @@ -44538,15 +44659,7 @@ components: "type": "python", "name": "Example python grader", "image_tag": "2025-05-08", - "source": """ - def grade(sample: dict, item: dict) -> float: - \""" - Returns 1.0 if `output_text` equals `label`, otherwise 0.0. - \""" - output = sample.get("output_text") - label = item.get("label") - return 1.0 if output == label else 0.0 - """, + "source": "\ndef grade(sample: dict, item: dict) -> float:\n \"\"\"\n Returns 1.0 if `output_text` equals `label`, otherwise 0.0.\n \"\"\"\n output = sample.get(\"output_text\")\n label = item.get(\"label\")\n return 1.0 if output == label else 0.0\n", "pass_threshold": 0.8 } EvalGraderScoreModel: @@ -44825,7 +44938,7 @@ components: ], "name": "External Data Eval", "created_at": 1739314509, - "metadata": {}, + "metadata": {} } ], "first_id": "eval_67abd54d9b0081909a86353f6fb9317a", @@ -44881,7 +44994,8 @@ components: "required": [ "item", "sample" - } + ] + } } EvalResponsesSource: type: object @@ -45505,7 +45619,7 @@ components: "name": "String Check Grader", "type": "string-check-grader", "score": 1.0, - "passed": true, + "passed": true } ], "sample": { @@ -45599,7 +45713,7 @@ components: "name": "String Check Grader", "type": "string-check-grader", "score": 1.0, - "passed": true, + "passed": true } ], "sample": { @@ -45633,7 +45747,7 @@ components: "top_p": 1.0, "seed": 42 } - }, + } ], "first_id": "outputitem_67abd55eb6548190bb580745d5644a33", "last_id": "outputitem_67abd55eb6548190bb580745d5644a33", @@ -45715,7 +45829,8 @@ components: "required": [ "item", "sample" - } + ] + } } EvalStoredCompletionsSource: type: object @@ -46557,7 +46672,7 @@ components: example: | { "object": "fine_tuning.job.event", - "id": "ftevent-abc123" + "id": "ftevent-abc123", "created_at": 1677610602, "level": "info", "message": "Created fine-tuning job", @@ -46919,15 +47034,7 @@ components: "type": "python", "name": "Example python grader", "image_tag": "2025-05-08", - "source": """ - def grade(sample: dict, item: dict) -> float: - \""" - Returns 1.0 if `output_text` equals `label`, otherwise 0.0. - \""" - output = sample.get("output_text") - label = item.get("label") - return 1.0 if output == label else 0.0 - """, + "source": "\ndef grade(sample: dict, item: dict) -> float:\n \"\"\"\n Returns 1.0 if `output_text` equals `label`, otherwise 0.0.\n \"\"\"\n output = sample.get(\"output_text\")\n label = item.get(\"label\")\n return 1.0 if output == label else 0.0\n" } GraderScoreModel: type: object @@ -47011,12 +47118,7 @@ components: "content": [ { "type": "input_text", - "text": ( - "Score how close the reference answer is to the model answer. Score 1.0 if they are the same and 0.0 if they are different." - " Return just a floating point score\n\n" - " Reference answer: {{item.label}}\n\n" - " Model answer: {{sample.output_text}}" - ) + "text": "Score how close the reference answer is to the model answer. Score 1.0 if they are the same and 0.0 if they are different. Return just a floating point score\n\n Reference answer: {{item.label}}\n\n Model answer: {{sample.output_text}}" }, { "type": "input_image", @@ -47024,7 +47126,7 @@ components: "file_id": null, "detail": "auto" } - ], + ] } ], "model": "gpt-5-mini", @@ -47034,7 +47136,7 @@ components: "seed": 42, "max_completions_tokens": 32768, "reasoning_effort": "medium" - }, + } } GraderStringCheck: type: object @@ -50811,7 +50913,7 @@ components: "created_at": 1677610602, "expires_at": 1680202602, "filename": "salesOverview.pdf", - "purpose": "assistants", + "purpose": "assistants" } OrganizationCertificate: type: object @@ -61809,7 +61911,8 @@ components: "part": { "type": "output_text", "text": "", - "annotations": [] + "annotations": [], + "logprobs": [] }, "sequence_number": 1 } @@ -61863,7 +61966,8 @@ components: "part": { "type": "output_text", "text": "In a shimmering forest under a sky full of stars, a lonely unicorn named Lila discovered a hidden pond that glowed with moonlight. Every night, she would leave sparkling, magical flowers by the water's edge, hoping to share her beauty with others. One enchanting evening, she woke to find a group of friendly animals gathered around, eager to be friends and share in her magic.", - "annotations": [] + "annotations": [], + "logprobs": [] } } ResponseCreatedEvent: @@ -61967,6 +62071,7 @@ components: example: | { "type": "response.custom_tool_call_input.delta", + "sequence_number": 1, "output_index": 0, "item_id": "ctc_1234567890abcdef", "delta": "partial input text" @@ -62007,6 +62112,7 @@ components: example: | { "type": "response.custom_tool_call_input.done", + "sequence_number": 1, "output_index": 0, "item_id": "ctc_1234567890abcdef", "input": "final complete input text" @@ -62435,7 +62541,7 @@ components: "type": "response.function_call_arguments.delta", "item_id": "item-abc", "output_index": 0, - "delta": "{ \"arg\":" + "delta": "{ \"arg\":", "sequence_number": 1 } ResponseFunctionCallArgumentsDoneEvent: @@ -64510,7 +64616,8 @@ components: "output_index": 0, "content_index": 0, "delta": "In", - "sequence_number": 1 + "sequence_number": 1, + "logprobs": [] } ResponseTextDoneEvent: type: object @@ -64566,7 +64673,8 @@ components: "output_index": 0, "content_index": 0, "text": "In a shimmering forest under a sky full of stars, a lonely unicorn named Lila discovered a hidden pond that glowed with moonlight. Every night, she would leave sparkling, magical flowers by the water's edge, hoping to share her beauty with others. One enchanting evening, she woke to find a group of friendly animals gathered around, eager to be friends and share in her magic.", - "sequence_number": 1 + "sequence_number": 1, + "logprobs": [] } ResponseTextParam: type: object @@ -68196,7 +68304,7 @@ components: "bytes": 2147483648, "created_at": 1719186911, "filename": "training_examples.jsonl", - "purpose": "fine-tune", + "purpose": "fine-tune" } } UploadCertificateRequest: @@ -69976,9 +70084,7 @@ components: - type: object title: Web search approximate location description: | - The approximate location of the user. If omitted or null, defaults to the - United States. To avoid this fallback, pass `{"type": "approximate"}` without - location fields. To localize results, provide the relevant location fields. + The approximate location of the user. properties: type: type: string @@ -70076,6 +70182,10 @@ components: - type: 'null' user_location: $ref: '#/components/schemas/WebSearchApproximateLocation' + description: | + The approximate location of the user. If omitted or null, defaults to the + United States. To avoid this fallback, pass `{"type": "approximate"}` without + location fields. To localize results, provide the relevant location fields. search_context_size: type: string enum: @@ -75634,6 +75744,8 @@ components: - workload_identity_provider_id - region - audience + x-stainless-skip: + - java provider: discriminator: propertyName: type @@ -76397,6 +76509,8 @@ components: - workload_identity_project_number - workload_identity_pool_id - workload_identity_provider_id + x-stainless-skip: + - java CreateExternalStorageBody: properties: project_id: @@ -80291,6 +80405,7 @@ components: - rate_limit_exceeded - server_overloaded - cyber_policy + - misalignment_policy_violation - connection_failed - server_error - authentication_error @@ -80309,6 +80424,7 @@ components: - The request exceeds the available rate limit. - The model service is temporarily overloaded. - The request was rejected by a safety policy. + - The request was blocked by the safety systems. - The request could not connect to the model service. - The model service encountered an unexpected error. - The API credentials are invalid or lack the required access. @@ -80612,6 +80728,7 @@ components: - flex - priority - fast + - ultrafast description: The service-tier policy configured for an agent. PersistedAgentToolResourceFunction: type: object @@ -81205,12 +81322,14 @@ components: - flex - priority - fast + - ultrafast x-enumDescriptions: - Selects the service tier automatically. - Uses the default service tier. - Uses the flex service tier. - Uses the priority service tier. - Uses the fast service tier. + - Uses the ultrafast service tier. description: The service tier used for model requests. PersistedAgentToolConfigParamFunction: type: object @@ -82311,7 +82430,7 @@ components: anyOf: - $ref: '#/components/schemas/NetworkPolicyParam' - type: 'null' - description: Network access policy for the environment. Defaults to disabled for GA requests and enabled for alpha/beta requests. + description: Network access policy for the environment. Defaults to disabled for GA requests and enabled for beta requests. env: type: - object @@ -82402,7 +82521,7 @@ components: anyOf: - $ref: '#/components/schemas/NetworkPolicyParam' - type: 'null' - description: Network access available after setup completes. Omit to preserve the current policy, or pass `null` to reset to disabled for GA requests or enabled for alpha/beta requests. + description: Network access available after setup completes. Omit to preserve the current policy, or pass `null` to reset to disabled for GA requests or enabled for beta requests. env: type: - object @@ -83511,7 +83630,7 @@ components: anyOf: - $ref: '#/components/schemas/NetworkPolicyParam' - type: 'null' - description: Network access policy for the environment. Defaults to disabled for GA requests and enabled for alpha/beta requests. + description: Network access policy for the environment. Defaults to disabled for GA requests and enabled for beta requests. env: type: - object @@ -85964,6 +86083,16 @@ components: auth: $ref: '#/components/schemas/VaultCredentialAuthResource' description: The authentication method and non-secret configuration of the credential. + metadata: + type: object + additionalProperties: + type: string + minLength: 0 + propertyNames: + type: string + minLength: 0 + minProperties: 0 + description: Application-defined key-value pairs associated with this credential. created_at: type: integer format: int64 @@ -85978,6 +86107,7 @@ components: - vault_id - name - auth + - metadata - created_at - updated_at additionalProperties: false @@ -86306,6 +86436,19 @@ components: auth: $ref: '#/components/schemas/CreateVaultCredentialAuthParam' description: The authentication method and write-only secret values to store. + metadata: + type: object + additionalProperties: + type: string + minLength: 0 + maxLength: 512 + propertyNames: + type: string + minLength: 1 + maxLength: 64 + minProperties: 0 + maxProperties: 16 + description: Up to 16 string key-value pairs, with keys up to 64 and values up to 512 characters. Defaults to an empty map. required: - auth - name @@ -86485,10 +86628,24 @@ components: auth: $ref: '#/components/schemas/RotateVaultCredentialAuthParam' description: Replacement values for the credential's existing authentication method. - required: - - auth + metadata: + type: object + additionalProperties: + type: string + minLength: 0 + maxLength: 512 + propertyNames: + type: string + minLength: 1 + maxLength: 64 + minProperties: 0 + maxProperties: 16 + description: Replaces all metadata. Omit to preserve it, or pass {} to clear it. Up to 16 string key-value pairs, with keys up to 64 and values up to 512 characters. additionalProperties: false - description: Secret, expiry, and OAuth refresh scope updates for an existing vault credential. + description: Metadata, secret, expiry, and OAuth refresh scope updates for an existing vault credential. Supply at least one of `auth` or `metadata`. + minProperties: 1 + examples: + - metadata: {} DeletedVaultCredentialResource: type: object properties: @@ -90147,6 +90304,10 @@ components: - type: 'null' user_location: $ref: '#/components/schemas/BetaWebSearchApproximateLocation' + description: | + The approximate location of the user. If omitted or null, defaults to the + United States. To avoid this fallback, pass `{"type": "approximate"}` without + location fields. To localize results, provide the relevant location fields. search_context_size: type: string enum: @@ -90162,9 +90323,7 @@ components: - type: object title: Web search approximate location description: | - The approximate location of the user. If omitted or null, defaults to the - United States. To avoid this fallback, pass `{"type": "approximate"}` without - location fields. To localize results, provide the relevant location fields. + The approximate location of the user. properties: type: type: string @@ -95383,6 +95542,7 @@ components: example: | { "type": "response.custom_tool_call_input.done", + "sequence_number": 1, "output_index": 0, "item_id": "ctc_1234567890abcdef", "input": "final complete input text" @@ -95428,6 +95588,7 @@ components: example: | { "type": "response.custom_tool_call_input.delta", + "sequence_number": 1, "output_index": 0, "item_id": "ctc_1234567890abcdef", "delta": "partial input text" @@ -97183,7 +97344,8 @@ components: "output_index": 0, "content_index": 0, "text": "In a shimmering forest under a sky full of stars, a lonely unicorn named Lila discovered a hidden pond that glowed with moonlight. Every night, she would leave sparkling, magical flowers by the water's edge, hoping to share her beauty with others. One enchanting evening, she woke to find a group of friendly animals gathered around, eager to be friends and share in her magic.", - "sequence_number": 1 + "sequence_number": 1, + "logprobs": [] } BetaResponseLogProb: type: object @@ -97271,7 +97433,8 @@ components: "output_index": 0, "content_index": 0, "delta": "In", - "sequence_number": 1 + "sequence_number": 1, + "logprobs": [] } BetaResponseRefusalDoneEvent: type: object @@ -97837,7 +98000,7 @@ components: "type": "response.function_call_arguments.delta", "item_id": "item-abc", "output_index": 0, - "delta": "{ \"arg\":" + "delta": "{ \"arg\":", "sequence_number": 1 } BetaResponseFileSearchCallSearchingEvent: @@ -98143,7 +98306,8 @@ components: "part": { "type": "output_text", "text": "In a shimmering forest under a sky full of stars, a lonely unicorn named Lila discovered a hidden pond that glowed with moonlight. Every night, she would leave sparkling, magical flowers by the water's edge, hoping to share her beauty with others. One enchanting evening, she woke to find a group of friendly animals gathered around, eager to be friends and share in her magic.", - "annotations": [] + "annotations": [], + "logprobs": [] } } BetaOutputContent: @@ -98207,7 +98371,8 @@ components: "part": { "type": "output_text", "text": "", - "annotations": [] + "annotations": [], + "logprobs": [] }, "sequence_number": 1 } diff --git a/src/openai/_base_client.py b/src/openai/_base_client.py index c8830a6a9f..883bed6e19 100644 --- a/src/openai/_base_client.py +++ b/src/openai/_base_client.py @@ -475,9 +475,10 @@ def __init__( self._validate_max_retries(max_retries) def _enforce_trailing_slash(self, url: URL) -> URL: - if url.raw_path.endswith(b"/"): + path, separator, query = url.raw_path.partition(b"?") + if path.endswith(b"/"): return url - return url.copy_with(raw_path=url.raw_path + b"/") + return url.copy_with(raw_path=path + b"/" + separator + query) def _make_status_error_from_response( self, diff --git a/src/openai/lib/live/_transcript_grouping.py b/src/openai/lib/live/_transcript_grouping.py index fb38d6363f..77bed0a246 100644 --- a/src/openai/lib/live/_transcript_grouping.py +++ b/src/openai/lib/live/_transcript_grouping.py @@ -96,10 +96,11 @@ def process(self, fragments: list[TranscriptFragment]) -> list[GroupingUpdate]: return events def advance(self, time_ms: float, has_incoming_user: bool = False) -> list[GroupingUpdate]: + # Use the same sums as deadline() so fractional deadlines remain reachable. if ( self._current is not None and self._buffered is not None - and time_ms - self._current.end_ms >= self._options.min_turn_separation_ms + and time_ms >= self._current.end_ms + self._options.min_turn_separation_ms and not self._keep_backchannel(time_ms) ): self._buffered = self._maybe_drop_backchannel(time_ms) @@ -109,7 +110,7 @@ def advance(self, time_ms: float, has_incoming_user: bool = False) -> list[Group self._current is not None and self._current.speaker == "assistant" and not has_incoming_user - and time_ms - self._current.end_ms >= self._options.assistant_silence_ms + and time_ms >= self._current.end_ms + self._options.assistant_silence_ms ): return self._finish_current("inactivity") return [] @@ -274,7 +275,7 @@ def _keep_backchannel(self, time_ms: float) -> bool: and self._buffered.can_drop_as_backchannel and not self._user_continued() and not self._recent_assistant() - and time_ms - self._buffered.end_ms < self._options.backchannel_isolation_ms + and time_ms < self._buffered.end_ms + self._options.backchannel_isolation_ms ) def _maybe_drop_backchannel( @@ -292,7 +293,7 @@ def _maybe_drop_backchannel( ): return self._buffered if next_fragment is None and ( - time_ms is None or time_ms - self._buffered.end_ms < self._options.backchannel_isolation_ms + time_ms is None or time_ms < self._buffered.end_ms + self._options.backchannel_isolation_ms ): return self._buffered return None if self._buffered.can_drop_as_backchannel else self._buffered diff --git a/src/openai/resources/audio/transcriptions.py b/src/openai/resources/audio/transcriptions.py index f5ec09441d..17f4d67fa5 100644 --- a/src/openai/resources/audio/transcriptions.py +++ b/src/openai/resources/audio/transcriptions.py @@ -92,7 +92,8 @@ def create( Transcribes audio into the input language. Returns a transcription object in `json`, `diarized_json`, or `verbose_json` - format, or a stream of transcript events. + format, plain text in `text`, `srt`, or `vtt` format, or a stream of + transcript events. Supported formats depend on the model. Args: file: The audio file object (not file name) to transcribe, in one of these formats: @@ -260,7 +261,8 @@ def create( Transcribes audio into the input language. Returns a transcription object in `json`, `diarized_json`, or `verbose_json` - format, or a stream of transcript events. + format, plain text in `text`, `srt`, or `vtt` format, or a stream of + transcript events. Supported formats depend on the model. Args: file: The audio file object (not file name) to transcribe, in one of these formats: @@ -381,7 +383,8 @@ def create( Transcribes audio into the input language. Returns a transcription object in `json`, `diarized_json`, or `verbose_json` - format, or a stream of transcript events. + format, plain text in `text`, `srt`, or `vtt` format, or a stream of + transcript events. Supported formats depend on the model. Args: file: The audio file object (not file name) to transcribe, in one of these formats: @@ -595,7 +598,8 @@ async def create( Transcribes audio into the input language. Returns a transcription object in `json`, `diarized_json`, or `verbose_json` - format, or a stream of transcript events. + format, plain text in `text`, `srt`, or `vtt` format, or a stream of + transcript events. Supported formats depend on the model. Args: file: The audio file object (not file name) to transcribe, in one of these formats: @@ -757,7 +761,8 @@ async def create( Transcribes audio into the input language. Returns a transcription object in `json`, `diarized_json`, or `verbose_json` - format, or a stream of transcript events. + format, plain text in `text`, `srt`, or `vtt` format, or a stream of + transcript events. Supported formats depend on the model. Args: file: The audio file object (not file name) to transcribe, in one of these formats: @@ -878,7 +883,8 @@ async def create( Transcribes audio into the input language. Returns a transcription object in `json`, `diarized_json`, or `verbose_json` - format, or a stream of transcript events. + format, plain text in `text`, `srt`, or `vtt` format, or a stream of + transcript events. Supported formats depend on the model. Args: file: The audio file object (not file name) to transcribe, in one of these formats: diff --git a/src/openai/resources/beta/agents/agents.py b/src/openai/resources/beta/agents/agents.py index 2831847139..69c71bd6ae 100644 --- a/src/openai/resources/beta/agents/agents.py +++ b/src/openai/resources/beta/agents/agents.py @@ -91,7 +91,7 @@ def create( multi_agent: Optional[MultiAgentConfigParam] | Omit = omit, name: Optional[str] | Omit = omit, reasoning: Optional[AgentReasoningParam] | Omit = omit, - service_tier: Optional[Literal["auto", "default", "flex", "priority", "fast"]] | Omit = omit, + service_tier: Optional[Literal["auto", "default", "flex", "priority", "fast", "ultrafast"]] | Omit = omit, text: Optional[AgentTextParam] | Omit = omit, tools: Optional[Iterable[PersistedAgentToolParam]] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -129,6 +129,7 @@ def create( - `flex` - Uses the flex service tier. - `priority` - Uses the priority service tier. - `fast` - Uses the fast service tier. + - `ultrafast` - Uses the ultrafast service tier. text: Configuration for generated text. Defaults to the `text` format and medium verbosity. @@ -220,7 +221,7 @@ def update( multi_agent: Optional[MultiAgentConfigParam] | Omit = omit, name: Optional[str] | Omit = omit, reasoning: Optional[AgentReasoningParam] | Omit = omit, - service_tier: Optional[Literal["auto", "default", "flex", "priority", "fast"]] | Omit = omit, + service_tier: Optional[Literal["auto", "default", "flex", "priority", "fast", "ultrafast"]] | Omit = omit, text: Optional[AgentTextParam] | Omit = omit, tools: Optional[Iterable[PersistedAgentToolParam]] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -259,6 +260,7 @@ def update( - `flex` - Uses the flex service tier. - `priority` - Uses the priority service tier. - `fast` - Uses the fast service tier. + - `ultrafast` - Uses the ultrafast service tier. text: Configuration for text generated by the agent. @@ -441,7 +443,7 @@ async def create( multi_agent: Optional[MultiAgentConfigParam] | Omit = omit, name: Optional[str] | Omit = omit, reasoning: Optional[AgentReasoningParam] | Omit = omit, - service_tier: Optional[Literal["auto", "default", "flex", "priority", "fast"]] | Omit = omit, + service_tier: Optional[Literal["auto", "default", "flex", "priority", "fast", "ultrafast"]] | Omit = omit, text: Optional[AgentTextParam] | Omit = omit, tools: Optional[Iterable[PersistedAgentToolParam]] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -479,6 +481,7 @@ async def create( - `flex` - Uses the flex service tier. - `priority` - Uses the priority service tier. - `fast` - Uses the fast service tier. + - `ultrafast` - Uses the ultrafast service tier. text: Configuration for generated text. Defaults to the `text` format and medium verbosity. @@ -570,7 +573,7 @@ async def update( multi_agent: Optional[MultiAgentConfigParam] | Omit = omit, name: Optional[str] | Omit = omit, reasoning: Optional[AgentReasoningParam] | Omit = omit, - service_tier: Optional[Literal["auto", "default", "flex", "priority", "fast"]] | Omit = omit, + service_tier: Optional[Literal["auto", "default", "flex", "priority", "fast", "ultrafast"]] | Omit = omit, text: Optional[AgentTextParam] | Omit = omit, tools: Optional[Iterable[PersistedAgentToolParam]] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -609,6 +612,7 @@ async def update( - `flex` - Uses the flex service tier. - `priority` - Uses the priority service tier. - `fast` - Uses the fast service tier. + - `ultrafast` - Uses the ultrafast service tier. text: Configuration for text generated by the agent. diff --git a/src/openai/resources/beta/agents/environments/templates.py b/src/openai/resources/beta/agents/environments/templates.py index 067d62d21a..fa9a2f3eb6 100644 --- a/src/openai/resources/beta/agents/environments/templates.py +++ b/src/openai/resources/beta/agents/environments/templates.py @@ -81,7 +81,7 @@ def create( name: An optional human-readable display name for the template. network: Network access policy for the environment. Defaults to disabled for GA requests - and enabled for alpha/beta requests. + and enabled for beta requests. packages: Packages to install in the environment. Defaults to empty package lists. @@ -207,8 +207,8 @@ def update( name: A replacement human-readable display name, or `null` to clear the name. network: Network access available after setup completes. Omit to preserve the current - policy, or pass `null` to reset to disabled for GA requests or enabled for - alpha/beta requests. + policy, or pass `null` to reset to disabled for GA requests or enabled for beta + requests. packages: Packages installed before the runtime network policy applies. @@ -419,7 +419,7 @@ async def create( name: An optional human-readable display name for the template. network: Network access policy for the environment. Defaults to disabled for GA requests - and enabled for alpha/beta requests. + and enabled for beta requests. packages: Packages to install in the environment. Defaults to empty package lists. @@ -545,8 +545,8 @@ async def update( name: A replacement human-readable display name, or `null` to clear the name. network: Network access available after setup completes. Omit to preserve the current - policy, or pass `null` to reset to disabled for GA requests or enabled for - alpha/beta requests. + policy, or pass `null` to reset to disabled for GA requests or enabled for beta + requests. packages: Packages installed before the runtime network policy applies. diff --git a/src/openai/resources/beta/agents/vaults/credentials.py b/src/openai/resources/beta/agents/vaults/credentials.py index 44845670a2..71cabc242b 100644 --- a/src/openai/resources/beta/agents/vaults/credentials.py +++ b/src/openai/resources/beta/agents/vaults/credentials.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Optional +from typing import Dict, Optional from typing_extensions import Literal import httpx2 @@ -55,6 +55,7 @@ def create( *, auth: CredentialAuthCreateParam, name: str, + metadata: Dict[str, str] | Omit = omit, # 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, @@ -74,6 +75,9 @@ def create( name: The name is trimmed before storage. It must contain 1 to 256 UTF-8 bytes after trimming. + metadata: Up to 16 string key-value pairs, with keys up to 64 and values up to 512 + characters. Defaults to an empty map. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -91,6 +95,7 @@ def create( { "auth": auth, "name": name, + "metadata": metadata, }, credential_create_params.CredentialCreateParams, ), @@ -154,7 +159,8 @@ def update( credential_id: str, *, vault_id: str, - auth: CredentialAuthRotateParam, + auth: CredentialAuthRotateParam | Omit = omit, + metadata: Dict[str, str] | Omit = omit, # 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, @@ -162,14 +168,17 @@ def update( extra_body: Body | None = None, timeout: float | httpx2.Timeout | None | NotGiven = not_given, ) -> Credential: - """ - Rotates a vault credential's write-only secret and returns only credential - metadata. See + """Updates credential metadata or rotates its write-only secret. + + See [vaults](https://developers.openai.com/api/docs/guides/agents-api/tools/vaults). Args: auth: Replacement values for the credential's existing authentication method. + metadata: Replaces all metadata. Omit to preserve it, or pass {} to clear it. Up to 16 + string key-value pairs, with keys up to 64 and values up to 512 characters. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -187,7 +196,13 @@ def update( path_template( "/vaults/{vault_id}/credentials/{credential_id}", vault_id=vault_id, credential_id=credential_id ), - body=maybe_transform({"auth": auth}, credential_update_params.CredentialUpdateParams), + body=maybe_transform( + { + "auth": auth, + "metadata": metadata, + }, + credential_update_params.CredentialUpdateParams, + ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -338,6 +353,7 @@ async def create( *, auth: CredentialAuthCreateParam, name: str, + metadata: Dict[str, str] | Omit = omit, # 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, @@ -357,6 +373,9 @@ async def create( name: The name is trimmed before storage. It must contain 1 to 256 UTF-8 bytes after trimming. + metadata: Up to 16 string key-value pairs, with keys up to 64 and values up to 512 + characters. Defaults to an empty map. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -374,6 +393,7 @@ async def create( { "auth": auth, "name": name, + "metadata": metadata, }, credential_create_params.CredentialCreateParams, ), @@ -437,7 +457,8 @@ async def update( credential_id: str, *, vault_id: str, - auth: CredentialAuthRotateParam, + auth: CredentialAuthRotateParam | Omit = omit, + metadata: Dict[str, str] | Omit = omit, # 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, @@ -445,14 +466,17 @@ async def update( extra_body: Body | None = None, timeout: float | httpx2.Timeout | None | NotGiven = not_given, ) -> Credential: - """ - Rotates a vault credential's write-only secret and returns only credential - metadata. See + """Updates credential metadata or rotates its write-only secret. + + See [vaults](https://developers.openai.com/api/docs/guides/agents-api/tools/vaults). Args: auth: Replacement values for the credential's existing authentication method. + metadata: Replaces all metadata. Omit to preserve it, or pass {} to clear it. Up to 16 + string key-value pairs, with keys up to 64 and values up to 512 characters. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -470,7 +494,13 @@ async def update( path_template( "/vaults/{vault_id}/credentials/{credential_id}", vault_id=vault_id, credential_id=credential_id ), - body=await async_maybe_transform({"auth": auth}, credential_update_params.CredentialUpdateParams), + body=await async_maybe_transform( + { + "auth": auth, + "metadata": metadata, + }, + credential_update_params.CredentialUpdateParams, + ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, diff --git a/src/openai/resources/live/forks.py b/src/openai/resources/live/forks.py index 9c1a243238..e27846290e 100644 --- a/src/openai/resources/live/forks.py +++ b/src/openai/resources/live/forks.py @@ -594,9 +594,9 @@ def _prepare_url(self) -> httpx2.URL: ws_scheme = "ws" if scheme == "http" else "wss" base_url = self.__client._base_url.copy_with(scheme=ws_scheme) - merge_raw_path = base_url.raw_path.rstrip(b"/") + path_template( - "/live/sessions/{session_id}/fork", session_id=self.__session_id - ).encode("utf-8") + path, separator, query = base_url.raw_path.partition(b"?") + endpoint = path_template("/live/sessions/{session_id}/fork", session_id=self.__session_id).encode("utf-8") + merge_raw_path = path.rstrip(b"/") + endpoint + separator + query return base_url.copy_with(raw_path=merge_raw_path) async def __aexit__( @@ -1081,9 +1081,9 @@ def _prepare_url(self) -> httpx2.URL: ws_scheme = "ws" if scheme == "http" else "wss" base_url = self.__client._base_url.copy_with(scheme=ws_scheme) - merge_raw_path = base_url.raw_path.rstrip(b"/") + path_template( - "/live/sessions/{session_id}/fork", session_id=self.__session_id - ).encode("utf-8") + path, separator, query = base_url.raw_path.partition(b"?") + endpoint = path_template("/live/sessions/{session_id}/fork", session_id=self.__session_id).encode("utf-8") + merge_raw_path = path.rstrip(b"/") + endpoint + separator + query return base_url.copy_with(raw_path=merge_raw_path) def __exit__( diff --git a/src/openai/resources/live/live.py b/src/openai/resources/live/live.py index 36c08a6e30..1af46d4e3f 100644 --- a/src/openai/resources/live/live.py +++ b/src/openai/resources/live/live.py @@ -813,7 +813,8 @@ def _prepare_url(self) -> httpx2.URL: ws_scheme = "ws" if scheme == "http" else "wss" base_url = self.__client._base_url.copy_with(scheme=ws_scheme) - merge_raw_path = base_url.raw_path.rstrip(b"/") + b"/live/sessions" + path, separator, query = base_url.raw_path.partition(b"?") + merge_raw_path = path.rstrip(b"/") + b"/live/sessions" + separator + query return base_url.copy_with(raw_path=merge_raw_path) async def __aexit__( @@ -1295,7 +1296,8 @@ def _prepare_url(self) -> httpx2.URL: ws_scheme = "ws" if scheme == "http" else "wss" base_url = self.__client._base_url.copy_with(scheme=ws_scheme) - merge_raw_path = base_url.raw_path.rstrip(b"/") + b"/live/sessions" + path, separator, query = base_url.raw_path.partition(b"?") + merge_raw_path = path.rstrip(b"/") + b"/live/sessions" + separator + query return base_url.copy_with(raw_path=merge_raw_path) def __exit__( diff --git a/src/openai/resources/live/sideband.py b/src/openai/resources/live/sideband.py index e15a836139..043a7233e0 100644 --- a/src/openai/resources/live/sideband.py +++ b/src/openai/resources/live/sideband.py @@ -602,9 +602,9 @@ def _prepare_url(self) -> httpx2.URL: ws_scheme = "ws" if scheme == "http" else "wss" base_url = self.__client._base_url.copy_with(scheme=ws_scheme) - merge_raw_path = base_url.raw_path.rstrip(b"/") + path_template( - "/live/sessions/{session_id}/attach", session_id=self.__session_id - ).encode("utf-8") + path, separator, query = base_url.raw_path.partition(b"?") + endpoint = path_template("/live/sessions/{session_id}/attach", session_id=self.__session_id).encode("utf-8") + merge_raw_path = path.rstrip(b"/") + endpoint + separator + query return base_url.copy_with(raw_path=merge_raw_path) async def __aexit__( @@ -1094,9 +1094,9 @@ def _prepare_url(self) -> httpx2.URL: ws_scheme = "ws" if scheme == "http" else "wss" base_url = self.__client._base_url.copy_with(scheme=ws_scheme) - merge_raw_path = base_url.raw_path.rstrip(b"/") + path_template( - "/live/sessions/{session_id}/attach", session_id=self.__session_id - ).encode("utf-8") + path, separator, query = base_url.raw_path.partition(b"?") + endpoint = path_template("/live/sessions/{session_id}/attach", session_id=self.__session_id).encode("utf-8") + merge_raw_path = path.rstrip(b"/") + endpoint + separator + query return base_url.copy_with(raw_path=merge_raw_path) def __exit__( diff --git a/src/openai/resources/realtime/realtime.py b/src/openai/resources/realtime/realtime.py index fab966898e..aef7757cb0 100644 --- a/src/openai/resources/realtime/realtime.py +++ b/src/openai/resources/realtime/realtime.py @@ -772,7 +772,8 @@ def _prepare_url(self) -> httpx2.URL: ws_scheme = "ws" if scheme == "http" else "wss" base_url = self.__client._base_url.copy_with(scheme=ws_scheme) - merge_raw_path = base_url.raw_path.rstrip(b"/") + b"/realtime" + path, separator, query = base_url.raw_path.partition(b"?") + merge_raw_path = path.rstrip(b"/") + b"/realtime" + separator + query return base_url.copy_with(raw_path=merge_raw_path) async def __aexit__( @@ -1281,7 +1282,8 @@ def _prepare_url(self) -> httpx2.URL: ws_scheme = "ws" if scheme == "http" else "wss" base_url = self.__client._base_url.copy_with(scheme=ws_scheme) - merge_raw_path = base_url.raw_path.rstrip(b"/") + b"/realtime" + path, separator, query = base_url.raw_path.partition(b"?") + merge_raw_path = path.rstrip(b"/") + b"/realtime" + separator + query return base_url.copy_with(raw_path=merge_raw_path) def __exit__( diff --git a/src/openai/types/audio/speech_create_params.py b/src/openai/types/audio/speech_create_params.py index 29016840fe..0948e37675 100644 --- a/src/openai/types/audio/speech_create_params.py +++ b/src/openai/types/audio/speech_create_params.py @@ -65,5 +65,9 @@ class VoiceID(TypedDict, total=False): Voice: TypeAlias = Union[ - str, Literal["alloy", "ash", "ballad", "coral", "echo", "sage", "shimmer", "verse", "marin", "cedar"], VoiceID + str, + Literal[ + "alloy", "ash", "ballad", "coral", "echo", "sage", "shimmer", "verse", "marin", "cedar", "fable", "onyx", "nova" + ], + VoiceID, ] diff --git a/src/openai/types/beta/agent.py b/src/openai/types/beta/agent.py index ceb01d0108..2c2f5ad208 100644 --- a/src/openai/types/beta/agent.py +++ b/src/openai/types/beta/agent.py @@ -45,7 +45,7 @@ class Agent(BaseModel): effort. """ - service_tier: Literal["auto", "default", "flex", "priority", "fast"] + service_tier: Literal["auto", "default", "flex", "priority", "fast", "ultrafast"] """The resolved service-tier policy used for model requests.""" text: AgentText diff --git a/src/openai/types/beta/agent_create_params.py b/src/openai/types/beta/agent_create_params.py index 4e0f869d5f..a2d91fc5fc 100644 --- a/src/openai/types/beta/agent_create_params.py +++ b/src/openai/types/beta/agent_create_params.py @@ -41,7 +41,7 @@ class AgentCreateParams(TypedDict, total=False): reasoning: Optional[AgentReasoningParam] """Configuration for model reasoning. Omission uses the model's default effort.""" - service_tier: Optional[Literal["auto", "default", "flex", "priority", "fast"]] + service_tier: Optional[Literal["auto", "default", "flex", "priority", "fast", "ultrafast"]] """The service tier used for model requests. Defaults to `auto`. - `auto` - Selects the service tier automatically. @@ -49,6 +49,7 @@ class AgentCreateParams(TypedDict, total=False): - `flex` - Uses the flex service tier. - `priority` - Uses the priority service tier. - `fast` - Uses the fast service tier. + - `ultrafast` - Uses the ultrafast service tier. """ text: Optional[AgentTextParam] diff --git a/src/openai/types/beta/agent_session.py b/src/openai/types/beta/agent_session.py index 0c57745a56..3cdce02446 100644 --- a/src/openai/types/beta/agent_session.py +++ b/src/openai/types/beta/agent_session.py @@ -45,7 +45,7 @@ class Agent(BaseModel): reasoning: AgentReasoning """The agent's reasoning configuration.""" - service_tier: Literal["auto", "default", "flex", "priority", "fast"] + service_tier: Literal["auto", "default", "flex", "priority", "fast", "ultrafast"] """The effective service-tier policy for model requests. Defaults to `auto`.""" text: AgentText diff --git a/src/openai/types/beta/agent_update_params.py b/src/openai/types/beta/agent_update_params.py index b1f034df4a..7a888da323 100644 --- a/src/openai/types/beta/agent_update_params.py +++ b/src/openai/types/beta/agent_update_params.py @@ -43,7 +43,7 @@ class AgentUpdateParams(TypedDict, total=False): effort. """ - service_tier: Optional[Literal["auto", "default", "flex", "priority", "fast"]] + service_tier: Optional[Literal["auto", "default", "flex", "priority", "fast", "ultrafast"]] """The service tier used for model requests. - `auto` - Selects the service tier automatically. @@ -51,6 +51,7 @@ class AgentUpdateParams(TypedDict, total=False): - `flex` - Uses the flex service tier. - `priority` - Uses the priority service tier. - `fast` - Uses the fast service tier. + - `ultrafast` - Uses the ultrafast service tier. """ text: Optional[AgentTextParam] diff --git a/src/openai/types/beta/agents/environments/template_create_params.py b/src/openai/types/beta/agents/environments/template_create_params.py index e57307274c..9fc858120b 100644 --- a/src/openai/types/beta/agents/environments/template_create_params.py +++ b/src/openai/types/beta/agents/environments/template_create_params.py @@ -33,7 +33,7 @@ class TemplateCreateParams(TypedDict, total=False): network: Optional[Network] """Network access policy for the environment. - Defaults to disabled for GA requests and enabled for alpha/beta requests. + Defaults to disabled for GA requests and enabled for beta requests. """ packages: Optional[Packages] @@ -55,7 +55,7 @@ class TemplateCreateParams(TypedDict, total=False): class Network(TypedDict, total=False): """Network access policy for the environment. - Defaults to disabled for GA requests and enabled for alpha/beta requests. + Defaults to disabled for GA requests and enabled for beta requests. """ access: Required[Literal["enabled", "disabled", "restricted"]] diff --git a/src/openai/types/beta/agents/environments/template_update_params.py b/src/openai/types/beta/agents/environments/template_update_params.py index a1af3868d5..a3a0beda86 100644 --- a/src/openai/types/beta/agents/environments/template_update_params.py +++ b/src/openai/types/beta/agents/environments/template_update_params.py @@ -31,7 +31,7 @@ class TemplateUpdateParams(TypedDict, total=False): """Network access available after setup completes. Omit to preserve the current policy, or pass `null` to reset to disabled for GA - requests or enabled for alpha/beta requests. + requests or enabled for beta requests. """ packages: Optional[Packages] @@ -50,7 +50,7 @@ class TemplateUpdateParams(TypedDict, total=False): class Network(TypedDict, total=False): """Network access available after setup completes. - Omit to preserve the current policy, or pass `null` to reset to disabled for GA requests or enabled for alpha/beta requests. + Omit to preserve the current policy, or pass `null` to reset to disabled for GA requests or enabled for beta requests. """ access: Required[Literal["enabled", "disabled", "restricted"]] diff --git a/src/openai/types/beta/agents/session_create_params.py b/src/openai/types/beta/agents/session_create_params.py index c8a0857784..11d53917ed 100644 --- a/src/openai/types/beta/agents/session_create_params.py +++ b/src/openai/types/beta/agents/session_create_params.py @@ -77,7 +77,7 @@ class Agent(TypedDict, total=False): effort. """ - service_tier: Optional[Literal["auto", "default", "flex", "priority", "fast"]] + service_tier: Optional[Literal["auto", "default", "flex", "priority", "fast", "ultrafast"]] """The service tier used for model requests. - `auto` - Selects the service tier automatically. @@ -85,6 +85,7 @@ class Agent(TypedDict, total=False): - `flex` - Uses the flex service tier. - `priority` - Uses the priority service tier. - `fast` - Uses the fast service tier. + - `ultrafast` - Uses the ultrafast service tier. """ text: Optional[AgentTextParam] diff --git a/src/openai/types/beta/agents/session_update_params.py b/src/openai/types/beta/agents/session_update_params.py index e75437d93b..d10cd29622 100644 --- a/src/openai/types/beta/agents/session_update_params.py +++ b/src/openai/types/beta/agents/session_update_params.py @@ -36,7 +36,7 @@ class Agent(TypedDict, total=False): reasoning: AgentReasoning """Reasoning settings to update. Omit to keep the current effort.""" - service_tier: Optional[Literal["auto", "default", "flex", "priority", "fast"]] + service_tier: Optional[Literal["auto", "default", "flex", "priority", "fast", "ultrafast"]] """Omit to keep the current tier. Null resets it to auto. - `auto` - Selects the service tier automatically. @@ -44,4 +44,5 @@ class Agent(TypedDict, total=False): - `flex` - Uses the flex service tier. - `priority` - Uses the priority service tier. - `fast` - Uses the fast service tier. + - `ultrafast` - Uses the ultrafast service tier. """ diff --git a/src/openai/types/beta/agents/vaults/credential.py b/src/openai/types/beta/agents/vaults/credential.py index fae1e7e1fe..903072e7ee 100644 --- a/src/openai/types/beta/agents/vaults/credential.py +++ b/src/openai/types/beta/agents/vaults/credential.py @@ -1,5 +1,6 @@ # File generated from our OpenAPI spec by Castiron. See CONTRIBUTING.md for details. +from typing import Dict from typing_extensions import Literal from ....._models import BaseModel @@ -20,6 +21,9 @@ class Credential(BaseModel): created_at: int """The Unix timestamp, in seconds, when the credential was created.""" + metadata: Dict[str, str] + """Application-defined key-value pairs associated with this credential.""" + name: str """The human-readable name of the credential.""" diff --git a/src/openai/types/beta/agents/vaults/credential_create_params.py b/src/openai/types/beta/agents/vaults/credential_create_params.py index 8b411f89d0..e37ab7ab57 100644 --- a/src/openai/types/beta/agents/vaults/credential_create_params.py +++ b/src/openai/types/beta/agents/vaults/credential_create_params.py @@ -2,6 +2,7 @@ from __future__ import annotations +from typing import Dict from typing_extensions import Required, TypedDict from .credential_auth_create_param import CredentialAuthCreateParam @@ -18,3 +19,9 @@ class CredentialCreateParams(TypedDict, total=False): It must contain 1 to 256 UTF-8 bytes after trimming. """ + + metadata: Dict[str, str] + """ + Up to 16 string key-value pairs, with keys up to 64 and values up to 512 + characters. Defaults to an empty map. + """ diff --git a/src/openai/types/beta/agents/vaults/credential_update_params.py b/src/openai/types/beta/agents/vaults/credential_update_params.py index 4373177d90..31aabcfb36 100644 --- a/src/openai/types/beta/agents/vaults/credential_update_params.py +++ b/src/openai/types/beta/agents/vaults/credential_update_params.py @@ -2,6 +2,7 @@ from __future__ import annotations +from typing import Dict from typing_extensions import Required, TypedDict from .credential_auth_rotate_param import CredentialAuthRotateParam @@ -12,5 +13,12 @@ class CredentialUpdateParams(TypedDict, total=False): vault_id: Required[str] - auth: Required[CredentialAuthRotateParam] + auth: CredentialAuthRotateParam """Replacement values for the credential's existing authentication method.""" + + metadata: Dict[str, str] + """Replaces all metadata. + + Omit to preserve it, or pass {} to clear it. Up to 16 string key-value pairs, + with keys up to 64 and values up to 512 characters. + """ diff --git a/src/openai/types/beta/environment_param.py b/src/openai/types/beta/environment_param.py index c6ab930bb1..43101b23b1 100644 --- a/src/openai/types/beta/environment_param.py +++ b/src/openai/types/beta/environment_param.py @@ -31,7 +31,7 @@ class EnvironmentParamNone(TypedDict, total=False): class EnvironmentParamOpenAIHostedNetwork(TypedDict, total=False): """Network access policy for the environment. - Defaults to disabled for GA requests and enabled for alpha/beta requests. + Defaults to disabled for GA requests and enabled for beta requests. """ access: Required[Literal["enabled", "disabled", "restricted"]] @@ -89,7 +89,7 @@ class EnvironmentParamOpenAIHosted(TypedDict, total=False): network: Optional[EnvironmentParamOpenAIHostedNetwork] """Network access policy for the environment. - Defaults to disabled for GA requests and enabled for alpha/beta requests. + Defaults to disabled for GA requests and enabled for beta requests. """ packages: Optional[EnvironmentParamOpenAIHostedPackages] diff --git a/src/openai/types/beta/session_turn_error.py b/src/openai/types/beta/session_turn_error.py index 93a738a583..927d78f039 100644 --- a/src/openai/types/beta/session_turn_error.py +++ b/src/openai/types/beta/session_turn_error.py @@ -18,6 +18,7 @@ class SessionTurnError(BaseModel): "rate_limit_exceeded", "server_overloaded", "cyber_policy", + "misalignment_policy_violation", "connection_failed", "server_error", "authentication_error", @@ -39,6 +40,8 @@ class SessionTurnError(BaseModel): - `rate_limit_exceeded` - The request exceeds the available rate limit. - `server_overloaded` - The model service is temporarily overloaded. - `cyber_policy` - The request was rejected by a safety policy. + - `misalignment_policy_violation` - The request was blocked by the safety + systems. - `connection_failed` - The request could not connect to the model service. - `server_error` - The model service encountered an unexpected error. - `authentication_error` - The API credentials are invalid or lack the required diff --git a/tests/api_resources/beta/agents/vaults/test_credentials.py b/tests/api_resources/beta/agents/vaults/test_credentials.py index 16f3fcf2b8..1ac8c48095 100644 --- a/tests/api_resources/beta/agents/vaults/test_credentials.py +++ b/tests/api_resources/beta/agents/vaults/test_credentials.py @@ -53,6 +53,7 @@ def test_method_create_with_all_params(self, client: OpenAI) -> None: }, }, name="x", + metadata={"foo": "string"}, ) assert_matches_type(Credential, credential, path=["response"]) @@ -158,7 +159,7 @@ def test_method_update(self, client: OpenAI) -> None: credential = client.beta.agents.vaults.credentials.update( credential_id="credential_id", vault_id="vault_id", - auth={"type": "mcp_oauth"}, + metadata={}, ) assert_matches_type(Credential, credential, path=["response"]) @@ -180,6 +181,7 @@ def test_method_update_with_all_params(self, client: OpenAI) -> None: }, }, }, + metadata={}, ) assert_matches_type(Credential, credential, path=["response"]) @@ -188,7 +190,7 @@ def test_raw_response_update(self, client: OpenAI) -> None: response = client.beta.agents.vaults.credentials.with_raw_response.update( credential_id="credential_id", vault_id="vault_id", - auth={"type": "mcp_oauth"}, + metadata={}, ) assert response.is_closed is True @@ -201,7 +203,7 @@ def test_streaming_response_update(self, client: OpenAI) -> None: with client.beta.agents.vaults.credentials.with_streaming_response.update( credential_id="credential_id", vault_id="vault_id", - auth={"type": "mcp_oauth"}, + metadata={}, ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -217,14 +219,12 @@ def test_path_params_update(self, client: OpenAI) -> None: client.beta.agents.vaults.credentials.with_raw_response.update( credential_id="credential_id", vault_id="", - auth={"type": "mcp_oauth"}, ) with pytest.raises(ValueError, match=r"Expected a non-empty value for `credential_id` but received ''"): client.beta.agents.vaults.credentials.with_raw_response.update( credential_id="", vault_id="vault_id", - auth={"type": "mcp_oauth"}, ) @parametrize @@ -362,6 +362,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncOpenAI) -> }, }, name="x", + metadata={"foo": "string"}, ) assert_matches_type(Credential, credential, path=["response"]) @@ -467,7 +468,7 @@ async def test_method_update(self, async_client: AsyncOpenAI) -> None: credential = await async_client.beta.agents.vaults.credentials.update( credential_id="credential_id", vault_id="vault_id", - auth={"type": "mcp_oauth"}, + metadata={}, ) assert_matches_type(Credential, credential, path=["response"]) @@ -489,6 +490,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncOpenAI) -> }, }, }, + metadata={}, ) assert_matches_type(Credential, credential, path=["response"]) @@ -497,7 +499,7 @@ async def test_raw_response_update(self, async_client: AsyncOpenAI) -> None: response = await async_client.beta.agents.vaults.credentials.with_raw_response.update( credential_id="credential_id", vault_id="vault_id", - auth={"type": "mcp_oauth"}, + metadata={}, ) assert response.is_closed is True @@ -510,7 +512,7 @@ async def test_streaming_response_update(self, async_client: AsyncOpenAI) -> Non async with async_client.beta.agents.vaults.credentials.with_streaming_response.update( credential_id="credential_id", vault_id="vault_id", - auth={"type": "mcp_oauth"}, + metadata={}, ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -526,14 +528,12 @@ async def test_path_params_update(self, async_client: AsyncOpenAI) -> None: await async_client.beta.agents.vaults.credentials.with_raw_response.update( credential_id="credential_id", vault_id="", - auth={"type": "mcp_oauth"}, ) with pytest.raises(ValueError, match=r"Expected a non-empty value for `credential_id` but received ''"): await async_client.beta.agents.vaults.credentials.with_raw_response.update( credential_id="", vault_id="vault_id", - auth={"type": "mcp_oauth"}, ) @parametrize diff --git a/tests/lib/live/test_transcript_grouping.py b/tests/lib/live/test_transcript_grouping.py index aaf3789709..92c4c95ca2 100644 --- a/tests/lib/live/test_transcript_grouping.py +++ b/tests/lib/live/test_transcript_grouping.py @@ -1,7 +1,10 @@ from __future__ import annotations +import sys import itertools +import subprocess from typing import Callable, Sequence, AsyncIterator +from pathlib import Path import pytest @@ -293,6 +296,64 @@ async def test_zero_disables_suppression(make_transcript: Factory) -> None: await t.finish(("user", "Tell me more"), ("assistant", "mhm")) +@pytest.mark.parametrize("async_mode", [False, True], ids=["sync", "async"]) +@pytest.mark.parametrize( + "options,fragments,expected", + [ + pytest.param( + {"assistant_silence_ms": 0.1}, + [("assistant", "First", 0, 200), ("assistant", "Second", 201, 300)], + [("assistant", "First", "inactivity"), ("assistant", "Second", "manual")], + id="assistant-silence", + ), + pytest.param( + {"min_turn_separation_ms": 0.1}, + [("user", "Question", 0, 200), ("assistant", "Answer", 200, 400), ("user", "More", 800, 1000)], + [ + ("user", "Question", "speaker_change"), + ("assistant", "Answer", "speaker_change"), + ("user", "More", "manual"), + ], + id="speaker-separation", + ), + pytest.param( + {"min_turn_separation_ms": 0, "backchannel_isolation_ms": 0.1}, + [("user", "Question", 0, 150), ("assistant", "okay", 100, 200), ("user", " More", 800, 1000)], + [("user", "Question More", "manual")], + id="backchannel-isolation", + ), + ], +) +def test_fractional_source_deadlines( + async_mode: bool, + options: dict[str, float], + fragments: list[tuple[str, str, int, int]], + expected: list[tuple[str, str, str]], +) -> None: + # A synchronous non-progressing loop also blocks asyncio timeouts. + code = f""" +import asyncio +from tests.lib.live.test_transcript_grouping import Transcript, fragment + +async def run(): + t = Transcript({async_mode!r}, **{options!r}) + for speaker, value, start, end in {fragments!r}: + await t.feed(fragment(speaker, value, start, end)) + await t.finish(*[(speaker, value) for speaker, value, _ in {expected!r}]) + assert [(c.segment.speaker, c.segment.text, c.reason) for c in t.recording.closed] == {expected!r} + +asyncio.run(run()) +""" + result = subprocess.run( + [sys.executable, "-c", code], + cwd=Path(__file__).resolve().parents[3], + capture_output=True, + text=True, + timeout=5, + ) + assert result.returncode == 0, result.stdout + result.stderr + + async def test_fractional_timeout_rounds_up(make_transcript: Factory) -> None: t = make_transcript(assistant_silence_ms=200.5) await t.feed(fragment("assistant", "Fractional.", 0)) diff --git a/tests/lib/live/test_websocket_roles.py b/tests/lib/live/test_websocket_roles.py new file mode 100644 index 0000000000..e3b0ae5cdc --- /dev/null +++ b/tests/lib/live/test_websocket_roles.py @@ -0,0 +1,159 @@ +from __future__ import annotations + +import json +import asyncio +from urllib.parse import parse_qs, urlsplit + +import httpx2 +import pytest +from websockets.sync.server import ServerConnection + +from openai import OpenAI, AsyncOpenAI +from openai.types.live import SessionStartedEvent, SessionUpdatedEvent, OutputTranscriptDeltaEvent +from openai.resources.live.live import LiveConnection, AsyncLiveConnection +from openai.resources.live.forks import ForksConnection, AsyncForksConnection + +from ..responses.test_websocket_session import script_server + + +@pytest.fixture(autouse=True) +def bypass_loopback_proxy(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("NO_PROXY", "127.0.0.1") + monkeypatch.setenv("no_proxy", "127.0.0.1") + + +@pytest.mark.parametrize("role", ["primary", "fork", "sideband"]) +@pytest.mark.parametrize("mode", ["sync", "async"]) +@pytest.mark.parametrize("base_query", ["", "?tenant=sample"], ids=["custom-path", "custom-path-and-query"]) +async def test_live_role_startup_and_routing(role: str, mode: str, base_query: str) -> None: + session = {"id": "live_fixture", "model": "gpt-live-1", "status": "active", "expires_at": 123} + + def script(socket: ServerConnection) -> None: + assert socket.request is not None + target = urlsplit(socket.request.path) + expected_path = "/v1/customer/live/sessions" + if role != "primary": + expected_path += "/stored%20%2F%3F%23%25/" + ("fork" if role == "fork" else "attach") + assert target.path == expected_path + expected_query = {"trace": ["role-contract"]} + if base_query: + expected_query["tenant"] = ["sample"] + if role == "sideband": + expected_query["graceful_close"] = ["true"] + assert parse_qs(target.query) == expected_query + assert socket.request.headers.get_all("Authorization") == ["Bearer ek_fake_live"] + + if role == "sideband": + # Attach is already active and need not send a fresh session.started. + socket.send( + json.dumps( + { + "type": "session.output_transcript.delta", + "event_id": "first", + "delta": "Attached", + "start_ms": 0, + "end_ms": 120, + } + ) + ) + else: + # This first and only start must come from the caller, with its identifier. + expected_session = {"model": "gpt-live-1"} if role == "primary" else {} + assert json.loads(socket.recv(timeout=5)) == { + "type": "session.start", + "event_id": "caller-start", + "session": expected_session, + } + socket.send( + json.dumps( + { + "type": "session.started", + "event_id": "started", + "client_event_id": "caller-start", + "session": session, + } + ) + ) + + assert json.loads(socket.recv(timeout=5)) == { + "type": "session.update", + "event_id": "caller-update", + "session": {}, + } + socket.send( + json.dumps( + { + "type": "session.updated", + "event_id": "updated", + "client_event_id": "caller-update", + "session": session, + } + ) + ) + # script_server also checks for extra sends, one socket, and caller close. + + with script_server(script) as url: + base_url = f"{url}/customer{base_query}" + extra_query = {"trace": "role-contract"} + if mode == "sync": + with OpenAI( + api_key="ek_fake_live", + base_url=base_url, + http_client=httpx2.Client(trust_env=False), + ) as client: + if role == "primary": + manager = client.live.connect(extra_query=extra_query) + elif role == "fork": + manager = client.live.forks.connect(session_id="stored /?#%", extra_query=extra_query) + else: + manager = client.live.sideband.connect( + session_id="stored /?#%", graceful_close=True, extra_query=extra_query + ) + with manager as connection: + if isinstance(connection, LiveConnection): + connection.session.start(session={"model": "gpt-live-1"}, event_id="caller-start") + elif isinstance(connection, ForksConnection): + connection.session.start(session={}, event_id="caller-start") + first = connection.recv() + if role == "sideband": + assert isinstance(first, OutputTranscriptDeltaEvent) + assert first.delta == "Attached" + else: + assert isinstance(first, SessionStartedEvent) + assert first.client_event_id == "caller-start" + connection.session.update(session={}, event_id="caller-update") + updated = connection.recv() + assert isinstance(updated, SessionUpdatedEvent) + assert updated.client_event_id == "caller-update" + assert updated.session.id == "live_fixture" + else: + async with AsyncOpenAI( + api_key="ek_fake_live", + base_url=base_url, + http_client=httpx2.AsyncClient(trust_env=False), + ) as async_client: + if role == "primary": + async_manager = async_client.live.connect(extra_query=extra_query) + elif role == "fork": + async_manager = async_client.live.forks.connect(session_id="stored /?#%", extra_query=extra_query) + else: + async_manager = async_client.live.sideband.connect( + session_id="stored /?#%", graceful_close=True, extra_query=extra_query + ) + async with async_manager as async_connection: + if isinstance(async_connection, AsyncLiveConnection): + await async_connection.session.start(session={"model": "gpt-live-1"}, event_id="caller-start") + elif isinstance(async_connection, AsyncForksConnection): + await async_connection.session.start(session={}, event_id="caller-start") + async_first = await asyncio.wait_for(async_connection.recv(), timeout=5) + if role == "sideband": + assert isinstance(async_first, OutputTranscriptDeltaEvent) + assert async_first.delta == "Attached" + else: + assert isinstance(async_first, SessionStartedEvent) + assert async_first.client_event_id == "caller-start" + await async_connection.session.update(session={}, event_id="caller-update") + async_updated = await asyncio.wait_for(async_connection.recv(), timeout=5) + assert isinstance(async_updated, SessionUpdatedEvent) + assert async_updated.client_event_id == "caller-update" + assert async_updated.session.id == "live_fixture" diff --git a/tests/lib/test_realtime_websocket_contract.py b/tests/lib/test_realtime_websocket_contract.py new file mode 100644 index 0000000000..5dddcd2707 --- /dev/null +++ b/tests/lib/test_realtime_websocket_contract.py @@ -0,0 +1,135 @@ +from __future__ import annotations + +import json +import asyncio +from urllib.parse import parse_qs, urlsplit + +import httpx2 +import pytest +from websockets.sync.server import ServerConnection + +from openai import OpenAI, AsyncOpenAI +from openai.types.realtime.realtime_error_event import RealtimeErrorEvent +from openai.types.realtime.input_audio_buffer_cleared_event import InputAudioBufferClearedEvent + +from .responses.test_websocket_session import script_server + + +@pytest.fixture(autouse=True) +def bypass_loopback_proxy(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("NO_PROXY", "127.0.0.1") + monkeypatch.setenv("no_proxy", "127.0.0.1") + + +@pytest.mark.parametrize("mode", ["sync", "async"]) +@pytest.mark.parametrize("base_query", ["", "?tenant=sample"], ids=["custom-path", "custom-path-and-query"]) +async def test_realtime_error_keeps_connection_open(mode: str, base_query: str) -> None: + def script(socket: ServerConnection) -> None: + assert socket.request is not None + target = urlsplit(socket.request.path) + assert target.path == "/v1/customer/realtime" + expected_query = {"model": ["gpt-realtime"], "contract": ["socket"]} + if base_query: + expected_query["tenant"] = ["sample"] + assert parse_qs(target.query) == expected_query + assert socket.request.headers.get_all("Authorization") == ["Bearer ek_fake_realtime"] + assert socket.request.headers["X-Realtime-Test"] == "connection" + assert socket.request.headers["X-Client-Only"] == "preserved" + assert socket.request.headers.get("Sec-WebSocket-Extensions") is None + assert json.loads(socket.recv(timeout=5)) == { + "type": "input_audio_buffer.append", + "event_id": "append-1", + "audio": "AA==", + } + socket.send( + json.dumps( + { + "type": "error", + "event_id": "error-1", + "error": { + "type": "invalid_request_error", + "code": "invalid_audio", + "param": "audio", + "message": "Synthetic audio rejected", + "event_id": "append-1", + }, + } + ) + ) + # A documented recoverable error must not close or replace the physical socket. + assert json.loads(socket.recv(timeout=5)) == {"type": "input_audio_buffer.clear", "event_id": "clear-1"} + socket.send(json.dumps({"type": "input_audio_buffer.cleared", "event_id": "clear-1", "future": {"kept": True}})) + socket.send(' { "type": "realtime.future", "event_id": "future-1", "text": "東京🙂" }\n') + # script_server verifies one connection, a clean caller close, and no extra sends. + + with script_server(script) as url: + base_url = f"{url}/customer{base_query}" + client_headers = {"X-Realtime-Test": "client", "X-Client-Only": "preserved"} + if mode == "sync": + with OpenAI( + api_key="ek_fake_realtime", + base_url=base_url, + default_headers=client_headers, + http_client=httpx2.Client(trust_env=False), + ) as client: + with client.realtime.connect( + model="gpt-realtime", + extra_query={"contract": "socket"}, + extra_headers={"X-Realtime-Test": "connection"}, + websocket_connection_options={"compression": None}, + ) as connection: + connection.input_audio_buffer.append(audio="AA==", event_id="append-1") + error = connection.recv() + assert isinstance(error, RealtimeErrorEvent) + assert (error.error.code, error.error.param, error.error.event_id) == ( + "invalid_audio", + "audio", + "append-1", + ) + connection.input_audio_buffer.clear(event_id="clear-1") + cleared = connection.recv() + assert isinstance(cleared, InputAudioBufferClearedEvent) + assert cleared.to_dict(exclude_unset=True) == { + "type": "input_audio_buffer.cleared", + "event_id": "clear-1", + "future": {"kept": True}, + } + assert json.loads(connection.recv_bytes()) == { + "type": "realtime.future", + "event_id": "future-1", + "text": "東京🙂", + } + else: + async with AsyncOpenAI( + api_key="ek_fake_realtime", + base_url=base_url, + default_headers=client_headers, + http_client=httpx2.AsyncClient(trust_env=False), + ) as async_client: + async with async_client.realtime.connect( + model="gpt-realtime", + extra_query={"contract": "socket"}, + extra_headers={"X-Realtime-Test": "connection"}, + websocket_connection_options={"compression": None}, + ) as async_connection: + await async_connection.input_audio_buffer.append(audio="AA==", event_id="append-1") + async_error = await asyncio.wait_for(async_connection.recv(), timeout=5) + assert isinstance(async_error, RealtimeErrorEvent) + assert (async_error.error.code, async_error.error.param, async_error.error.event_id) == ( + "invalid_audio", + "audio", + "append-1", + ) + await async_connection.input_audio_buffer.clear(event_id="clear-1") + async_cleared = await asyncio.wait_for(async_connection.recv(), timeout=5) + assert isinstance(async_cleared, InputAudioBufferClearedEvent) + assert async_cleared.to_dict(exclude_unset=True) == { + "type": "input_audio_buffer.cleared", + "event_id": "clear-1", + "future": {"kept": True}, + } + assert json.loads(await asyncio.wait_for(async_connection.recv_bytes(), timeout=5)) == { + "type": "realtime.future", + "event_id": "future-1", + "text": "東京🙂", + } diff --git a/tests/test_environment_credentials.py b/tests/test_environment_credentials.py index bc41133640..596cdb4e87 100644 --- a/tests/test_environment_credentials.py +++ b/tests/test_environment_credentials.py @@ -29,6 +29,7 @@ def credential_response(auth: object) -> dict[str, object]: "vault_id": "vault_synthetic", "object": "vault.credential", "name": "Synthetic credential", + "metadata": {}, "created_at": 1, "updated_at": 1, "auth": auth,