diff --git a/.castiron.stats.yml b/.castiron.stats.yml index 2885422fa..bd10f3211 100644 --- a/.castiron.stats.yml +++ b/.castiron.stats.yml @@ -1,7 +1,7 @@ schema_version: 1 -generation_id: c7d28dc2-cc3d-47a3-9b98-f74f327d7d06 -openapi_spec_hash: c735bf35306196d3be264afacd380ef5 -openapi_transformed_spec_hash: 26fd6dfec1625525dae954f7de5f80c1 -config_hash: 4e0613192a6ffb1791a25180a99a769c -codegen_sha: 8a8a0ff64e79040ac5f29532ff061bdfa567764a -codegen_hash: b296d6e5af2a21ef64ed15e9b4984854ca700063db8490798bd25ebbf58e3b34 +generation_id: dfd43ede-47aa-4010-85a7-902500e246d3 +openapi_spec_hash: d57efec7c76a156447c9ab739b446dc1 +openapi_transformed_spec_hash: 834e733aeb5425eae15cc1643c7e67cb +config_hash: 1d0b20f04f1b5fe5edc8b60dbad7a7d9 +codegen_sha: 95b1337ab8409a20ab2d5e3ce321bdb23ec6c18c +codegen_hash: 51771b70d1cefb3ce7562a0b0857a64d6585b338458d3b8de951b8e40c6c5391 diff --git a/api_reference/openapi.transformed.yml b/api_reference/openapi.transformed.yml index a7eb92394..e3527aadf 100644 --- a/api_reference/openapi.transformed.yml +++ b/api_reference/openapi.transformed.yml @@ -27110,10 +27110,24 @@ paths: "geography": "US", "status": "validated", "created_at": 1711471400 + }, + { + "object": "organization.external_storage", + "id": "extstorage_oci123", + "project_id": "proj_oci123", + "provider": { + "type": "oci", + "tenancy_ocid": "ocid1.tenancy.oc1..example", + "region": "us-ashburn-1", + "bucket": "customer-oci-retention" + }, + "geography": "US", + "status": "validated", + "created_at": 1711471300 } ], "first_id": "extstorage_abc123", - "last_id": "extstorage_gcp123", + "last_id": "extstorage_oci123", "has_more": false } security: @@ -27263,6 +27277,36 @@ paths: "status": "pending", "created_at": 1711471400 } + - title: OCI Object 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_oci123", + "provider": { + "type": "oci", + "tenancy_ocid": "ocid1.tenancy.oc1..example", + "region": "us-ashburn-1", + "bucket": "customer-oci-retention" + } + }' + response: | + { + "object": "organization.external_storage", + "id": "extstorage_oci123", + "project_id": "proj_oci123", + "provider": { + "type": "oci", + "tenancy_ocid": "ocid1.tenancy.oc1..example", + "region": "us-ashburn-1", + "bucket": "customer-oci-retention" + }, + "geography": "US", + "status": "pending", + "created_at": 1711471300 + } security: - AdminApiKeyAuth: [] /organization/external_storage/{external_storage_id}: @@ -33194,6 +33238,7 @@ paths: maxLength: 64 minProperties: 0 maxProperties: 16 + x-openai-go-public-type: pointer_collection description: Exact string matches supplied as `metadata[key]=value`. All supplied pairs must match. Up to 16 pairs, with keys from 1 to 64 characters and values up to 512 characters. New sessions and metadata updates may take time to appear in filtered results. style: deepObject explode: true @@ -44300,10 +44345,10 @@ components: - chatgpt-image-latest - dall-e-2 x-openai-enum-deprecations: - dall-e-2: - message: This model was retired on May 12, 2026. Use a GPT image model instead. gpt-image-1: message: GPT Image 1 was shut down on October 23, 2026. Use gpt-image-2.5-sunburst or gpt-image-2.5-flare instead. + dall-e-2: + message: This model was retired on May 12, 2026. Use a GPT image model instead. x-stainless-const: true x-oaiTypeLabel: string example: gpt-image-1.5 @@ -44455,12 +44500,12 @@ components: - dall-e-2 - dall-e-3 x-openai-enum-deprecations: + gpt-image-1: + message: GPT Image 1 was shut down on October 23, 2026. Use gpt-image-2.5-sunburst or gpt-image-2.5-flare instead. dall-e-2: message: This model was retired on May 12, 2026. Use a GPT image model instead. dall-e-3: message: This model was retired on May 12, 2026. Use a GPT image model instead. - gpt-image-1: - message: GPT Image 1 was shut down on October 23, 2026. Use gpt-image-2.5-sunburst or gpt-image-2.5-flare instead. x-stainless-nominal: false x-oaiTypeLabel: string example: gpt-image-2.5-flare @@ -45813,7 +45858,6 @@ components: - gpt-4o-mini-transcribe - gpt-4o-mini-transcribe-2025-12-15 - gpt-4o-transcribe-diarize - x-stainless-const: true x-openai-enum-deprecations: gpt-4o-mini-transcribe-2025-12-15: message: 'Announced shutdown date: 2027-02-26. See https://developers.openai.com/api/docs/deprecations for details and recommended replacements.' @@ -45825,6 +45869,7 @@ components: message: 'Announced shutdown date: 2027-02-26. See https://developers.openai.com/api/docs/deprecations for details and recommended replacements.' gpt-4o-transcribe-diarize: message: 'Announced shutdown date: 2027-02-26. See https://developers.openai.com/api/docs/deprecations for details and recommended replacements.' + x-stainless-const: true x-stainless-nominal: false x-oaiTypeLabel: string language: @@ -50636,6 +50681,9 @@ components: - gpt-image-2.5-flare-2026-09-08 - gpt-image-1.5 - chatgpt-image-latest + x-openai-enum-deprecations: + gpt-image-1: + message: GPT Image 1 was shut down on October 23, 2026. Use gpt-image-2.5-sunburst or gpt-image-2.5-flare instead. description: | The image generation model to use. One of `gpt-image-1-mini`, `gpt-image-1.5`, `gpt-image-2`, @@ -50647,9 +50695,6 @@ components: `gpt-image-1` was shut down on October 23, 2026. Use `gpt-image-2.5-sunburst` or `gpt-image-2.5-flare` instead. default: gpt-image-2 - x-openai-enum-deprecations: - gpt-image-1: - message: GPT Image 1 was shut down on October 23, 2026. Use gpt-image-2.5-sunburst or gpt-image-2.5-flare instead. quality: type: string enum: @@ -53137,11 +53182,10 @@ components: x-openai-go-public-shape: variant_names: '#/components/schemas/AssistantToolsFileSearchTypeOnly': FileSearchTool + '#/components/schemas/MessageObject/properties/attachments/anyOf/0/items/properties/tools/items/anyOf/1': FileSearchTool anyOf: - $ref: '#/components/schemas/AssistantToolsCode' - $ref: '#/components/schemas/AssistantToolsFileSearchTypeOnly' - x-openai-go-public-shape: - type_name: MessageAttachmentToolFileSearchTool description: A list of files attached to the message, and the tools they were added to. - type: 'null' metadata: @@ -58353,7 +58397,7 @@ components: example: | // Trigger a response with the default Conversation and no special parameters { - "type": "response.create", + "type": "response.create" } // Trigger an out-of-band response that does not write to the default Conversation @@ -60038,10 +60082,9 @@ components: "role": "assistant", "content": [ { - "type": "audio", + "type": "output_audio", "transcript": "Yes, I can hear you loud and clear. How can I help you today?", - "audio": "8//2//v/9//q/+//+P/s...", - "format": "pcm16" + "audio": "AAABAP//AAA=" } ] } @@ -60236,9 +60279,9 @@ components: group: realtime example: | { - "type":" input_audio_buffer.dtmf_event_received", - "event": "9", - "received_at": 1763605109, + "type": "input_audio_buffer.dtmf_event_received", + "event": "9", + "received_at": 1763605109 } RealtimeServerEventInputAudioBufferSpeechStarted: type: object @@ -60962,7 +61005,7 @@ components: }, "usage": null, "metadata": null - }, + } } RealtimeServerEventResponseDone: type: object @@ -61406,23 +61449,23 @@ components: group: realtime example: | { - "event_id": "event_3536", - "type": "response.output_item.done", - "response_id": "resp_001", - "output_index": 0, - "item": { - "id": "msg_007", - "object": "realtime.item", - "type": "message", - "status": "completed", - "role": "assistant", - "content": [ - { - "type": "text", - "text": "Sure, I can help with that." - } - ] - } + "event_id": "event_3536", + "type": "response.output_item.done", + "response_id": "resp_001", + "output_index": 0, + "item": { + "id": "msg_007", + "object": "realtime.item", + "type": "message", + "status": "completed", + "role": "assistant", + "content": [ + { + "type": "output_text", + "text": "Sure, I can help with that." + } + ] + } } RealtimeServerEventResponseTextDelta: type: object @@ -61593,7 +61636,7 @@ components: } }, "include": null - }, + } } RealtimeServerEventSessionUpdated: type: object @@ -61695,7 +61738,7 @@ components: } }, "include": null - }, + } } RealtimeServerEventTranscriptionSessionUpdated: type: object @@ -64293,6 +64336,21 @@ components: x-stainless-skip: true usage: $ref: '#/components/schemas/ResponseUsage' + tool_usage: + $ref: '#/components/schemas/ResponseToolUsage' + description: | + Usage reported by hosted tools, separate from base model `usage`. The field + can be omitted when tool usage is unavailable. When present, tool families + with no recorded usage have zero counts. + + `image_gen` sums available token counts across image generation calls. + Calls with missing or incomplete usage records may be skipped. Output + token details and cached input counts can be omitted. An omitted cache + count means cache usage is unknown. + + `web_search.num_requests` counts billable searches. Discounted searches + and actions such as opening a page are excluded. Use billing records to + reconcile final charges. prompt_cache_options: $ref: '#/components/schemas/PromptCacheOptions' prompt_cache_diagnostics: @@ -64318,21 +64376,6 @@ components: An upper bound for the number of tokens that can be generated for a response, including visible output tokens and [reasoning tokens](https://developers.openai.com/api/docs/guides/reasoning). type: integer - type: 'null' - tool_usage: - $ref: '#/components/schemas/ResponseToolUsage' - description: | - Usage reported by hosted tools, separate from base model `usage`. The field - can be omitted when tool usage is unavailable. When present, tool families - with no recorded usage have zero counts. - - `image_gen` sums available token counts across image generation calls. - Calls with missing or incomplete usage records may be skipped. Output - token details and cached input counts can be omitted. An omitted cache - count means cache usage is unknown. - - `web_search.num_requests` counts billable searches. Discounted searches - and actions such as opening a page are excluded. Use billing records to - reconcile final charges. required: - access_programs - id @@ -95466,8 +95509,9 @@ components: type: string enum: - message + description: The item type. An omitted type denotes a message. Tool call and output items have their corresponding tool type. role: - description: The message role. Always `user`. + description: The role on a message item, defaulting to user. Tool call and output items omit role. default: user x-stainless-const: true type: string @@ -95489,6 +95533,7 @@ components: x-stainless-naming: java: variant_constructor: DecisionItemCreateEventItemContentItems + description: The required content of a message item. Tool call and output items omit content. required: - role - content @@ -96500,8 +96545,9 @@ components: type: string enum: - message + description: The item type. An omitted type denotes a message. Tool call and output items have their corresponding tool type. role: - description: The message role. Always `user`. + description: The role on a message item. Tool call and output items omit role. default: user x-stainless-const: true type: string @@ -96523,6 +96569,7 @@ components: x-stainless-naming: java: variant_constructor: DecisionItemCreatedEventItemContentItems + description: The required content of a message item. Tool call and output items omit content. required: - id - role @@ -96729,7 +96776,7 @@ components: reason: close_requested session: id: live_abc123 - model: '{{models.live.default}}' + model: gpt-live-1 status: active expires_at: 1788555600 instructions: Help the caller plan a restaurant reservation. Confirm details before booking. @@ -96806,7 +96853,7 @@ components: reason: close_requested session: id: live_abc123 - model: '{{models.live.default}}' + model: gpt-live-1 status: active expires_at: 1788555600 instructions: Help the caller plan a restaurant reservation. Confirm details before booking. @@ -98362,6 +98409,9 @@ components: - gpt-image-2.5-flare-2026-09-08 - gpt-image-1.5 - chatgpt-image-latest + x-openai-enum-deprecations: + gpt-image-1: + message: GPT Image 1 was shut down on October 23, 2026. Use gpt-image-2.5-sunburst or gpt-image-2.5-flare instead. description: | The image generation model to use. One of `gpt-image-1-mini`, `gpt-image-1.5`, `gpt-image-2`, @@ -98373,9 +98423,6 @@ components: `gpt-image-1` was shut down on October 23, 2026. Use `gpt-image-2.5-sunburst` or `gpt-image-2.5-flare` instead. default: gpt-image-2 - x-openai-enum-deprecations: - gpt-image-1: - message: GPT Image 1 was shut down on October 23, 2026. Use gpt-image-2.5-sunburst or gpt-image-2.5-flare instead. quality: type: string enum: @@ -104469,6 +104516,21 @@ components: x-stainless-skip: true usage: $ref: '#/components/schemas/BetaResponseUsage' + tool_usage: + $ref: '#/components/schemas/BetaResponseToolUsage' + description: | + Usage reported by hosted tools, separate from base model `usage`. The field + can be omitted when tool usage is unavailable. When present, tool families + with no recorded usage have zero counts. + + `image_gen` sums available token counts across image generation calls. + Calls with missing or incomplete usage records may be skipped. Output + token details and cached input counts can be omitted. An omitted cache + count means cache usage is unknown. + + `web_search.num_requests` counts billable searches. Discounted searches + and actions such as opening a page are excluded. Use billing records to + reconcile final charges. prompt_cache_options: $ref: '#/components/schemas/BetaPromptCacheOptions' prompt_cache_diagnostics: @@ -104494,21 +104556,6 @@ components: An upper bound for the number of tokens that can be generated for a response, including visible output tokens and [reasoning tokens](https://developers.openai.com/api/docs/guides/reasoning). type: integer - type: 'null' - tool_usage: - $ref: '#/components/schemas/BetaResponseToolUsage' - description: | - Usage reported by hosted tools, separate from base model `usage`. The field - can be omitted when tool usage is unavailable. When present, tool families - with no recorded usage have zero counts. - - `image_gen` sums available token counts across image generation calls. - Calls with missing or incomplete usage records may be skipped. Output - token details and cached input counts can be omitted. An omitted cache - count means cache usage is unknown. - - `web_search.num_requests` counts billable searches. Discounted searches - and actions such as opening a page are excluded. Use billing records to - reconcile final charges. required: - access_programs - id @@ -109955,6 +110002,7 @@ components: anyOf: - $ref: '#/components/schemas/LiveDecisionOutputTextParam' - $ref: '#/components/schemas/LiveDecisionRefusalParam' + x-stainless-variantName: parts phase: anyOf: - description: Labels an `assistant` message as intermediate commentary (`commentary`) or the final answer (`final_answer`). For models like `gpt-5.3-codex` and beyond, when sending follow-up requests, preserve and resend phase on all assistant messages — dropping it can degrade performance. Not used for user messages. @@ -109998,6 +110046,7 @@ components: anyOf: - $ref: '#/components/schemas/LiveDecisionInputTextParam' - $ref: '#/components/schemas/LiveDecisionInputImageParam' + x-stainless-variantName: parts required: - call_id - type @@ -110076,6 +110125,7 @@ components: - type: array items: $ref: '#/components/schemas/LiveDecisionInputTextParam' + x-stainless-variantName: parts required: - role - content @@ -110115,6 +110165,7 @@ components: anyOf: - $ref: '#/components/schemas/LiveDecisionInputTextParam' - $ref: '#/components/schemas/LiveDecisionInputImageParam' + x-stainless-variantName: parts required: - call_id - type @@ -110246,6 +110297,7 @@ components: anyOf: - $ref: '#/components/schemas/LiveDecisionOutputTextParam' - $ref: '#/components/schemas/LiveDecisionRefusalParam' + x-stainless-variantName: parts phase: anyOf: - description: Labels an `assistant` message as intermediate commentary (`commentary`) or the final answer (`final_answer`). For models like `gpt-5.3-codex` and beyond, when sending follow-up requests, preserve and resend phase on all assistant messages — dropping it can degrade performance. Not used for user messages. @@ -110287,6 +110339,7 @@ components: anyOf: - $ref: '#/components/schemas/LiveDecisionInputTextParam' - $ref: '#/components/schemas/LiveDecisionInputImageParam' + x-stainless-variantName: parts required: - call_id - type @@ -110361,6 +110414,7 @@ components: - type: array items: $ref: '#/components/schemas/LiveDecisionInputTextParam' + x-stainless-variantName: parts required: - role - content @@ -110398,6 +110452,7 @@ components: anyOf: - $ref: '#/components/schemas/LiveDecisionInputTextParam' - $ref: '#/components/schemas/LiveDecisionInputImageParam' + x-stainless-variantName: parts required: - call_id - type @@ -110471,6 +110526,7 @@ components: - type: array items: $ref: '#/components/schemas/LiveDecisionInputTextParam' + x-stainless-variantName: parts required: - role - content @@ -110506,6 +110562,10 @@ components: - $ref: '#/components/schemas/LiveDecisionUserInputTextParam' - $ref: '#/components/schemas/LiveDecisionUserInputImageParam' - $ref: '#/components/schemas/LiveDecisionInputJsonParam' + x-stainless-variantName: parts + x-stainless-naming: + java: + variant_constructor: DecisionItemCreatedEventItemContentItems required: - role - content @@ -110538,6 +110598,7 @@ components: - type: array items: $ref: '#/components/schemas/LiveDecisionInputTextParam' + x-stainless-variantName: parts required: - role - content @@ -110613,6 +110674,10 @@ components: - $ref: '#/components/schemas/LiveDecisionUserInputTextParam' - $ref: '#/components/schemas/LiveDecisionUserInputImageParam' - $ref: '#/components/schemas/LiveDecisionInputJsonParam' + x-stainless-variantName: parts + x-stainless-naming: + java: + variant_constructor: DecisionItemCreateEventItemContentItems required: - role - content @@ -110792,6 +110857,7 @@ components: type: string pattern: ^arn:aws:iam::\d{12}:role/.+$ description: The Role ARN that OpenAI will assume to call your KMS + example: arn:aws:iam::123456789012:role/example-role kms_arn: type: string description: The Key Management System ARN for the master key you manage @@ -111987,6 +112053,207 @@ components: - type - server_label - name + RealtimeConnectDocumentationParams: + type: object + properties: + model: + type: string + call_id: + type: string + intent: + type: string + enum: + - realtime + - transcription + description: | + Selects the session type. Set `transcription` and omit `model` for a transcription session. If omitted, the session type from a client secret applies when present; otherwise the intent is `realtime`. + RealtimeClientEventResponseCreateWithReferencesParam: + type: object + description: "This event instructs the server to create a Response, which means triggering \nmodel inference. When in Server VAD mode, the server will create Responses \nautomatically.\n\nA Response will include at least one Item, and may have two, in which case \nthe second will be a function call. These Items will be appended to the \nconversation history by default.\n\nThe server will respond with a `response.created` event, events for Items \nand content created, and finally a `response.done` event to indicate the \nResponse is complete.\n\nThe `response.create` event includes inference configuration like \n`instructions` and `tools`. If these are set, they will override the Session's \nconfiguration for this Response only.\n\nResponses can be created out-of-band of the default Conversation, meaning that they can\nhave arbitrary input, and it's possible to disable writing the output to the Conversation.\nOnly one Response can write to the default Conversation at a time, but otherwise multiple\nResponses can be created in parallel. The `metadata` field is a good way to disambiguate\nmultiple simultaneous Responses.\n\nClients can set `conversation` to `none` to create a Response that does not write to the default\nConversation. Arbitrary input can be provided with the `input` field, which is an array accepting\nraw Items and references to existing Items.\n" + properties: + event_id: + type: string + maxLength: 512 + description: Optional client-generated ID used to identify this event. + type: + x-stainless-const: true + description: The event type, must be `response.create`. + const: response.create + response: + $ref: '#/components/schemas/RealtimeResponseCreateParams' + required: + - type + x-oaiMeta: + name: response.create + group: realtime + example: | + // Trigger a response with the default Conversation and no special parameters + { + "type": "response.create" + } + + // Trigger an out-of-band response that does not write to the default Conversation + { + "type": "response.create", + "response": { + "instructions": "Provide a concise answer.", + "tools": [], // clear any session tools + "conversation": "none", + "output_modalities": ["text"], + "metadata": { + "response_purpose": "summarization" + }, + "input": [ + { + "type": "item_reference", + "id": "item_12345" + }, + { + "type": "message", + "role": "user", + "content": [ + { + "type": "input_text", + "text": "Summarize the above message in one sentence." + } + ] + } + ] + } + } + RealtimeResponseCreateParamsWithReferencesParam: + type: object + description: Create a new Realtime response with these parameters + properties: + output_modalities: + type: array + description: | + The set of modalities the model used to respond, currently the only possible values are + `[\"audio\"]`, `[\"text\"]`. Audio output always include a text transcript. Setting the + output to mode `text` will disable audio output from the model. + items: + type: string + enum: + - text + - audio + instructions: + type: string + description: | + The default system instructions (i.e. system message) prepended to model calls. This field allows the client to guide the model on desired responses. The model can be instructed on response content and format, (e.g. "be extremely succinct", "act friendly", "here are examples of good responses") and on audio behavior (e.g. "talk quickly", "inject emotion into your voice", "laugh frequently"). The instructions are not guaranteed to be followed by the model, but they provide guidance to the model on the desired behavior. + Note that the server sets default instructions which will be used if this field is not set and are visible in the `session.created` event at the start of the session. + audio: + type: object + description: Configuration for audio input and output. + properties: + output: + type: object + properties: + format: + $ref: '#/components/schemas/RealtimeAudioFormats' + description: The format of the output audio. + voice: + $ref: '#/components/schemas/VoiceIdsOrCustomVoice' + default: alloy + description: | + The voice the model uses to respond. Supported built-in voices are + `alloy`, `ash`, `ballad`, `coral`, `echo`, `sage`, `shimmer`, `verse`, + `marin`, and `cedar`. You may also provide a custom voice object with + an `id`, for example `{ "id": "voice_1234" }`. Voice cannot be changed + during the session once the model has responded with audio at least once. + Custom voices must be created from audio samples. Voices created from text prompts are supported only in Live. + We recommend `marin` and `cedar` for best quality. + tools: + type: array + description: Tools available to the model. + items: + anyOf: + - $ref: '#/components/schemas/RealtimeFunctionTool' + - $ref: '#/components/schemas/MCPTool' + tool_choice: + description: | + How the model chooses tools. Provide one of the string modes or force a specific + function/MCP tool. + default: auto + anyOf: + - $ref: '#/components/schemas/ToolChoiceOptions' + - $ref: '#/components/schemas/ToolChoiceFunction' + - $ref: '#/components/schemas/ToolChoiceMCP' + parallel_tool_calls: + type: boolean + description: | + Whether the model may call multiple tools in parallel. Only supported by + reasoning Realtime models such as `gpt-realtime-2`. + reasoning: + $ref: '#/components/schemas/RealtimeReasoning' + max_output_tokens: + description: | + Maximum number of output tokens for a single assistant response, + inclusive of tool calls. Provide an integer between 1 and 4096 to + limit output tokens, or `inf` for the maximum available tokens for a + given model. Defaults to `inf`. + anyOf: + - type: integer + - type: string + enum: + - inf + x-stainless-const: true + conversation: + description: | + Controls which conversation the response is added to. Currently supports + `auto` and `none`, with `auto` as the default value. The `auto` value + means that the contents of the response will be added to the default + conversation. Set this to `none` to create an out-of-band response which + will not add items to default conversation. + anyOf: + - type: string + - type: string + default: auto + enum: + - auto + - none + metadata: + $ref: '#/components/schemas/Metadata' + prompt: + $ref: '#/components/schemas/Prompt' + input: + type: array + description: | + Input items to include in the prompt for the model. Using this field + creates a new context for this Response instead of using the default + conversation. An empty array `[]` will clear the context for this Response. + Note that this can include references to items that previously appeared in the session + using their id. + items: + $ref: '#/components/schemas/RealtimeConversationItem' + RealtimeResponseItemReference: + type: object + description: | + Reference an existing item in this Realtime session as response.create input. + Item references are only accepted in the response input array, not as new + conversation items. + properties: + type: + type: string + enum: + - item_reference + x-stainless-const: true + description: The type of this item. Always `item_reference`. + id: + type: string + minLength: 1 + maxLength: 32 + pattern: ^[a-zA-Z0-9_-]+$ + description: The ID of an existing item in the same Realtime session. + required: + - type + - id + RealtimeTranslationConnectParams: + type: object + properties: + model: + type: string + required: + - model responses: TooManyRequests: description: The request was rejected because a rate limit was exceeded. diff --git a/openai-java-core/src/main/kotlin/com/openai/models/live/DecisionInputItemValidation.kt b/openai-java-core/src/main/kotlin/com/openai/models/live/DecisionInputItemValidation.kt new file mode 100644 index 000000000..04e8b33d2 --- /dev/null +++ b/openai-java-core/src/main/kotlin/com/openai/models/live/DecisionInputItemValidation.kt @@ -0,0 +1,363 @@ +// File generated from our OpenAPI spec by Castiron. See CONTRIBUTING.md for details. + +package com.openai.models.live + +import com.openai.core.JsonValue +import com.openai.errors.OpenAIInvalidDataException +import kotlin.jvm.optionals.getOrNull + +/** Validates the service wire shape without changing the released Item model API. */ +internal object DecisionInputItemValidation { + @JvmSynthetic + fun validate(properties: Map) { + if (!isItem(JsonValue.from(properties))) { + throw OpenAIInvalidDataException("Invalid Live Decision item") + } + } + + private inline fun optional(value: JsonValue?, accepts: (JsonValue) -> Boolean): Boolean = + value == null || value.isMissing() || value.isNull() || accepts(value) + + private inline fun required(value: JsonValue?, accepts: (JsonValue) -> Boolean): Boolean = + value != null && !value.isMissing() && !value.isNull() && accepts(value) + + private fun isItem(value: JsonValue): Boolean { + val properties = value.asObject().getOrNull() ?: return false + return when ( + properties.getOrDefault("type", JsonValue.from("message")).asString().getOrNull() + ) { + "message" -> + when (properties["role"]?.asString()?.getOrNull()) { + "user" -> isUserMessage(value) + "developer" -> isDeveloperMessage(value) + "system" -> isSystemMessage(value) + "assistant" -> isAssistantMessage(value) + else -> false + } + "function_call" -> (isFunctionCall(value)) + "function_call_output" -> (isFunctionCallOutput(value)) + "custom_tool_call" -> (isCustomToolCall(value)) + "custom_tool_call_output" -> (isCustomToolCallOutput(value)) + else -> false + } + } + + private fun isUserMessage(value: JsonValue): Boolean { + val properties = value.asObject().getOrNull() ?: return false + return required(properties["content"]) { isUserMessageContent(it) } && + properties["role"] == JsonValue.from("user") && + optional(properties["id"]) { it.asString().isPresent } && + optional(properties["type"]) { it == JsonValue.from("message") } + } + + private fun isUserMessageContent(value: JsonValue): Boolean = + (value.asString().isPresent) || + (value.asArray().getOrNull()?.all { element -> + isUserMessageContentDecisionInputItemValidationUserMessageContentItem(element) + } == true) + + private fun isUserMessageContentDecisionInputItemValidationUserMessageContentItem( + value: JsonValue + ): Boolean { + val properties = value.asObject().getOrNull() ?: return false + return when (properties["type"]?.asString()?.getOrNull()) { + "input_text" -> + (isUserMessageContentDecisionInputItemValidationUserMessageContentItemInputText( + value + )) + "input_image" -> + (isUserMessageContentDecisionInputItemValidationUserMessageContentItemInputImage( + value + )) + "input_json" -> + (isUserMessageContentDecisionInputItemValidationUserMessageContentItemInputJson( + value + )) + else -> false + } + } + + private fun isUserMessageContentDecisionInputItemValidationUserMessageContentItemInputText( + value: JsonValue + ): Boolean { + val properties = value.asObject().getOrNull() ?: return false + return required(properties["text"]) { it.asString().isPresent } && + properties["type"] == JsonValue.from("input_text") + } + + private fun isUserMessageContentDecisionInputItemValidationUserMessageContentItemInputImage( + value: JsonValue + ): Boolean { + val properties = value.asObject().getOrNull() ?: return false + return required(properties["image_url"]) { it.asString().isPresent } && + properties["type"] == JsonValue.from("input_image") && + optional(properties["detail"]) { + it == JsonValue.from("low") || + it == JsonValue.from("high") || + it == JsonValue.from("auto") || + it == JsonValue.from("original") + } + } + + private fun isUserMessageContentDecisionInputItemValidationUserMessageContentItemInputJson( + value: JsonValue + ): Boolean { + val properties = value.asObject().getOrNull() ?: return false + return required(properties["data"]) { + isUserMessageContentDecisionInputItemValidationUserMessageContentItemInputJsonData(it) + } && properties["type"] == JsonValue.from("input_json") + } + + private fun isUserMessageContentDecisionInputItemValidationUserMessageContentItemInputJsonData( + value: JsonValue + ): Boolean = + (isUserMessageContentDecisionInputItemValidationUserMessageContentItemInputJsonDataLiveDecisionJsonObject( + value + )) || (value.asArray().getOrNull()?.all { element -> element.asObject().isPresent } == true) + + private fun isUserMessageContentDecisionInputItemValidationUserMessageContentItemInputJsonDataLiveDecisionJsonObject( + value: JsonValue + ): Boolean = value.asObject().isPresent + + private fun isDeveloperMessage(value: JsonValue): Boolean { + val properties = value.asObject().getOrNull() ?: return false + return required(properties["content"]) { isDeveloperMessageContent(it) } && + properties["role"] == JsonValue.from("developer") && + optional(properties["id"]) { it.asString().isPresent } && + optional(properties["type"]) { it == JsonValue.from("message") } + } + + private fun isDeveloperMessageContent(value: JsonValue): Boolean = + (value.asString().isPresent) || + (value.asArray().getOrNull()?.all { element -> + isDeveloperMessageContentLiveDecisionInputTextParam(element) + } == true) + + private fun isDeveloperMessageContentLiveDecisionInputTextParam(value: JsonValue): Boolean { + val properties = value.asObject().getOrNull() ?: return false + return required(properties["text"]) { it.asString().isPresent } && + properties["type"] == JsonValue.from("input_text") + } + + private fun isSystemMessage(value: JsonValue): Boolean { + val properties = value.asObject().getOrNull() ?: return false + return required(properties["content"]) { isSystemMessageContent(it) } && + properties["role"] == JsonValue.from("system") && + optional(properties["id"]) { it.asString().isPresent } && + optional(properties["type"]) { it == JsonValue.from("message") } + } + + private fun isSystemMessageContent(value: JsonValue): Boolean = + (value.asString().isPresent) || + (value.asArray().getOrNull()?.all { element -> + isSystemMessageContentLiveDecisionInputTextParam(element) + } == true) + + private fun isSystemMessageContentLiveDecisionInputTextParam(value: JsonValue): Boolean { + val properties = value.asObject().getOrNull() ?: return false + return required(properties["text"]) { it.asString().isPresent } && + properties["type"] == JsonValue.from("input_text") + } + + private fun isAssistantMessage(value: JsonValue): Boolean { + val properties = value.asObject().getOrNull() ?: return false + return required(properties["content"]) { isAssistantMessageContent(it) } && + properties["role"] == JsonValue.from("assistant") && + optional(properties["id"]) { it.asString().isPresent } && + optional(properties["phase"]) { + it == JsonValue.from("commentary") || it == JsonValue.from("final_answer") + } && + optional(properties["type"]) { it == JsonValue.from("message") } + } + + private fun isAssistantMessageContent(value: JsonValue): Boolean = + (value.asString().isPresent) || + (value.asArray().getOrNull()?.all { element -> + isAssistantMessageContentDecisionInputItemValidationAssistantMessageContentItem( + element + ) + } == true) + + private fun isAssistantMessageContentDecisionInputItemValidationAssistantMessageContentItem( + value: JsonValue + ): Boolean { + val properties = value.asObject().getOrNull() ?: return false + return when (properties["type"]?.asString()?.getOrNull()) { + "output_text" -> + (isAssistantMessageContentDecisionInputItemValidationAssistantMessageContentItemOutputText( + value + )) + "refusal" -> + (isAssistantMessageContentDecisionInputItemValidationAssistantMessageContentItemRefusal( + value + )) + else -> false + } + } + + private fun isAssistantMessageContentDecisionInputItemValidationAssistantMessageContentItemOutputText( + value: JsonValue + ): Boolean { + val properties = value.asObject().getOrNull() ?: return false + return required(properties["text"]) { it.asString().isPresent } && + properties["type"] == JsonValue.from("output_text") + } + + private fun isAssistantMessageContentDecisionInputItemValidationAssistantMessageContentItemRefusal( + value: JsonValue + ): Boolean { + val properties = value.asObject().getOrNull() ?: return false + return required(properties["refusal"]) { it.asString().isPresent } && + properties["type"] == JsonValue.from("refusal") + } + + private fun isFunctionCall(value: JsonValue): Boolean { + val properties = value.asObject().getOrNull() ?: return false + return required(properties["arguments"]) { it.asString().isPresent } && + required(properties["call_id"]) { it.asString().isPresent } && + required(properties["name"]) { it.asString().isPresent } && + properties["type"] == JsonValue.from("function_call") && + optional(properties["id"]) { it.asString().isPresent } && + optional(properties["namespace"]) { it.asString().isPresent } && + optional(properties["status"]) { + it == JsonValue.from("in_progress") || + it == JsonValue.from("completed") || + it == JsonValue.from("incomplete") + } + } + + private fun isFunctionCallOutput(value: JsonValue): Boolean { + val properties = value.asObject().getOrNull() ?: return false + return required(properties["call_id"]) { it.asString().isPresent } && + required(properties["output"]) { isFunctionCallOutputOutput(it) } && + properties["type"] == JsonValue.from("function_call_output") && + optional(properties["id"]) { it.asString().isPresent } && + optional(properties["status"]) { + it == JsonValue.from("in_progress") || + it == JsonValue.from("completed") || + it == JsonValue.from("incomplete") + } + } + + private fun isFunctionCallOutputOutput(value: JsonValue): Boolean = + (value.asString().isPresent) || + (value.asArray().getOrNull()?.all { element -> + isFunctionCallOutputOutputDecisionInputItemValidationFunctionCallOutputOutputItem( + element + ) + } == true) + + private fun isFunctionCallOutputOutputDecisionInputItemValidationFunctionCallOutputOutputItem( + value: JsonValue + ): Boolean { + val properties = value.asObject().getOrNull() ?: return false + return when (properties["type"]?.asString()?.getOrNull()) { + "input_text" -> + (isFunctionCallOutputOutputDecisionInputItemValidationFunctionCallOutputOutputItemInputText( + value + )) + "input_image" -> + (isFunctionCallOutputOutputDecisionInputItemValidationFunctionCallOutputOutputItemInputImage( + value + )) + else -> false + } + } + + private fun isFunctionCallOutputOutputDecisionInputItemValidationFunctionCallOutputOutputItemInputText( + value: JsonValue + ): Boolean { + val properties = value.asObject().getOrNull() ?: return false + return required(properties["text"]) { it.asString().isPresent } && + properties["type"] == JsonValue.from("input_text") + } + + private fun isFunctionCallOutputOutputDecisionInputItemValidationFunctionCallOutputOutputItemInputImage( + value: JsonValue + ): Boolean { + val properties = value.asObject().getOrNull() ?: return false + return required(properties["image_url"]) { it.asString().isPresent } && + properties["type"] == JsonValue.from("input_image") && + optional(properties["detail"]) { + it == JsonValue.from("low") || + it == JsonValue.from("high") || + it == JsonValue.from("auto") || + it == JsonValue.from("original") + } + } + + private fun isCustomToolCall(value: JsonValue): Boolean { + val properties = value.asObject().getOrNull() ?: return false + return required(properties["call_id"]) { it.asString().isPresent } && + required(properties["input"]) { it.asString().isPresent } && + required(properties["name"]) { it.asString().isPresent } && + properties["type"] == JsonValue.from("custom_tool_call") && + optional(properties["id"]) { it.asString().isPresent } && + optional(properties["namespace"]) { it.asString().isPresent } && + optional(properties["status"]) { + it == JsonValue.from("in_progress") || + it == JsonValue.from("completed") || + it == JsonValue.from("incomplete") + } + } + + private fun isCustomToolCallOutput(value: JsonValue): Boolean { + val properties = value.asObject().getOrNull() ?: return false + return required(properties["call_id"]) { it.asString().isPresent } && + required(properties["output"]) { isCustomToolCallOutputOutput(it) } && + properties["type"] == JsonValue.from("custom_tool_call_output") && + optional(properties["id"]) { it.asString().isPresent } && + optional(properties["status"]) { + it == JsonValue.from("in_progress") || + it == JsonValue.from("completed") || + it == JsonValue.from("incomplete") + } + } + + private fun isCustomToolCallOutputOutput(value: JsonValue): Boolean = + (value.asString().isPresent) || + (value.asArray().getOrNull()?.all { element -> + isCustomToolCallOutputOutputDecisionInputItemValidationCustomToolCallOutputOutputItem( + element + ) + } == true) + + private fun isCustomToolCallOutputOutputDecisionInputItemValidationCustomToolCallOutputOutputItem( + value: JsonValue + ): Boolean { + val properties = value.asObject().getOrNull() ?: return false + return when (properties["type"]?.asString()?.getOrNull()) { + "input_text" -> + (isCustomToolCallOutputOutputDecisionInputItemValidationCustomToolCallOutputOutputItemInputText( + value + )) + "input_image" -> + (isCustomToolCallOutputOutputDecisionInputItemValidationCustomToolCallOutputOutputItemInputImage( + value + )) + else -> false + } + } + + private fun isCustomToolCallOutputOutputDecisionInputItemValidationCustomToolCallOutputOutputItemInputText( + value: JsonValue + ): Boolean { + val properties = value.asObject().getOrNull() ?: return false + return required(properties["text"]) { it.asString().isPresent } && + properties["type"] == JsonValue.from("input_text") + } + + private fun isCustomToolCallOutputOutputDecisionInputItemValidationCustomToolCallOutputOutputItemInputImage( + value: JsonValue + ): Boolean { + val properties = value.asObject().getOrNull() ?: return false + return required(properties["image_url"]) { it.asString().isPresent } && + properties["type"] == JsonValue.from("input_image") && + optional(properties["detail"]) { + it == JsonValue.from("low") || + it == JsonValue.from("high") || + it == JsonValue.from("auto") || + it == JsonValue.from("original") + } + } +} diff --git a/openai-java-core/src/main/kotlin/com/openai/models/live/DecisionItemCreateEvent.kt b/openai-java-core/src/main/kotlin/com/openai/models/live/DecisionItemCreateEvent.kt index c38433508..287cfda26 100644 --- a/openai-java-core/src/main/kotlin/com/openai/models/live/DecisionItemCreateEvent.kt +++ b/openai-java-core/src/main/kotlin/com/openai/models/live/DecisionItemCreateEvent.kt @@ -299,23 +299,20 @@ private constructor( ) : this(content, role, id, type, mutableMapOf()) /** - * The text input to the model. + * The required content of a message item. Tool call and output items omit content. * - * @throws OpenAIInvalidDataException if the JSON field has an unexpected type or is - * unexpectedly missing or null (e.g. if the server responded with an unexpected value). + * Use `_content().isMissing()` to check for content before reading it. + * + * @throws OpenAIInvalidDataException if content is missing, null, or has an unexpected + * type. Tool call and output items normally omit content. */ fun content(): Content = content.getRequired("content") /** - * The message role. Always `user`. - * - * Expected to always return the following: - * ```java - * JsonValue.from("user") - * ``` + * The role on a message item, defaulting to user. Tool call and output items omit role. * - * However, this method can be useful for debugging and logging (e.g. if the server - * responded with an unexpected value). + * Returns the raw role. Use `_role().isMissing()` to check its presence; tool call and + * output items omit this field. */ @JsonProperty("role") @ExcludeMissing fun _role(): JsonValue = role @@ -328,6 +325,9 @@ private constructor( fun id(): Optional = id.getOptional("id") /** + * The item type. An omitted type denotes a message. Tool call and output items have their + * corresponding tool type. + * * @throws OpenAIInvalidDataException if the JSON field has an unexpected type (e.g. if the * server responded with an unexpected value). */ @@ -421,7 +421,7 @@ private constructor( additionalProperties = item.additionalProperties.toMutableMap() } - /** The text input to the model. */ + /** The required content of a message item. Tool call and output items omit content. */ fun content(content: Content) = content(JsonField.of(content)) /** @@ -479,6 +479,10 @@ private constructor( */ fun id(id: JsonField) = apply { this.id = id } + /** + * The item type. An omitted type denotes a message. Tool call and output items have + * their corresponding tool type. + */ fun type(type: Type) = type(JsonField.of(type)) /** @@ -547,14 +551,21 @@ private constructor( return@apply } - content().validate() - _role().let { - if (it != JsonValue.from("user")) { - throw OpenAIInvalidDataException("'role' is invalid, received $it") + if (_type().isNull()) { + // The released Item validated null like an omitted type. Preserve its + // field checks without changing the wire value or newer item variants. + content().validate() + _role().let { + if (it != JsonValue.from("user")) { + throw OpenAIInvalidDataException("'role' is invalid, received $it") + } } + id() + type().ifPresent { it.validate() } + } else { + val properties = _allProperties() + com.openai.models.live.DecisionInputItemValidation.validate(properties) } - id() - type().ifPresent { it.validate() } validated = true } @@ -579,7 +590,7 @@ private constructor( (if (id.asKnown().isPresent) 1 else 0) + (type.asKnown().getOrNull()?.validity() ?: 0) - /** The text input to the model. */ + /** The required content of a message item. Tool call and output items omit content. */ @JsonDeserialize(using = Content.Deserializer::class) @JsonSerialize(using = Content.Serializer::class) class Content @@ -1790,6 +1801,10 @@ private constructor( } } + /** + * The item type. An omitted type denotes a message. Tool call and output items have their + * corresponding tool type. + */ class Type @JsonCreator private constructor(private val value: JsonField) : Enum { /** diff --git a/openai-java-core/src/main/kotlin/com/openai/models/live/DecisionItemCreatedEvent.kt b/openai-java-core/src/main/kotlin/com/openai/models/live/DecisionItemCreatedEvent.kt index e5e8f36d4..e6ce575d3 100644 --- a/openai-java-core/src/main/kotlin/com/openai/models/live/DecisionItemCreatedEvent.kt +++ b/openai-java-core/src/main/kotlin/com/openai/models/live/DecisionItemCreatedEvent.kt @@ -340,27 +340,27 @@ private constructor( fun id(): String = id.getRequired("id") /** - * The text input to the model. + * The required content of a message item. Tool call and output items omit content. * - * @throws OpenAIInvalidDataException if the JSON field has an unexpected type or is - * unexpectedly missing or null (e.g. if the server responded with an unexpected value). + * Use `_content().isMissing()` to check for content before reading it. + * + * @throws OpenAIInvalidDataException if content is missing, null, or has an unexpected + * type. Tool call and output items normally omit content. */ fun content(): Content = content.getRequired("content") /** - * The message role. Always `user`. + * The role on a message item. Tool call and output items omit role. * - * Expected to always return the following: - * ```java - * JsonValue.from("user") - * ``` - * - * However, this method can be useful for debugging and logging (e.g. if the server - * responded with an unexpected value). + * Returns the raw role. Use `_role().isMissing()` to check its presence; tool call and + * output items omit this field. */ @JsonProperty("role") @ExcludeMissing fun _role(): JsonValue = role /** + * The item type. An omitted type denotes a message. Tool call and output items have their + * corresponding tool type. + * * @throws OpenAIInvalidDataException if the JSON field has an unexpected type (e.g. if the * server responded with an unexpected value). */ @@ -467,7 +467,7 @@ private constructor( */ fun id(id: JsonField) = apply { this.id = id } - /** The text input to the model. */ + /** The required content of a message item. Tool call and output items omit content. */ fun content(content: Content) = content(JsonField.of(content)) /** @@ -510,6 +510,10 @@ private constructor( */ fun role(role: JsonValue) = apply { this.role = role } + /** + * The item type. An omitted type denotes a message. Tool call and output items have + * their corresponding tool type. + */ fun type(type: Type) = type(JsonField.of(type)) /** @@ -579,14 +583,25 @@ private constructor( return@apply } - id() - content().validate() - _role().let { - if (it != JsonValue.from("user")) { - throw OpenAIInvalidDataException("'role' is invalid, received $it") + if (_type().isNull()) { + // The released Item validated null like an omitted type. Preserve its + // field checks without changing the wire value or newer item variants. + id() + content().validate() + _role().let { + if (it != JsonValue.from("user")) { + throw OpenAIInvalidDataException("'role' is invalid, received $it") + } + } + type().ifPresent { it.validate() } + } else { + val properties = _allProperties() + com.openai.models.live.DecisionInputItemValidation.validate(properties) + // Stored items require an ID in the same wire properties we validated. + if (properties["id"]?.isNull() != false) { + throw OpenAIInvalidDataException("Missing required 'id'") } } - type().ifPresent { it.validate() } validated = true } @@ -611,7 +626,7 @@ private constructor( role.let { if (it == JsonValue.from("user")) 1 else 0 } + (type.asKnown().getOrNull()?.validity() ?: 0) - /** The text input to the model. */ + /** The required content of a message item. Tool call and output items omit content. */ @JsonDeserialize(using = Content.Deserializer::class) @JsonSerialize(using = Content.Serializer::class) class Content @@ -1822,6 +1837,10 @@ private constructor( } } + /** + * The item type. An omitted type denotes a message. Tool call and output items have their + * corresponding tool type. + */ class Type @JsonCreator private constructor(private val value: JsonField) : Enum { /** diff --git a/openai-java-core/src/main/kotlin/com/openai/models/realtime/ConversationItem.kt b/openai-java-core/src/main/kotlin/com/openai/models/realtime/ConversationItem.kt index 8732c17d3..08ca27510 100644 --- a/openai-java-core/src/main/kotlin/com/openai/models/realtime/ConversationItem.kt +++ b/openai-java-core/src/main/kotlin/com/openai/models/realtime/ConversationItem.kt @@ -475,37 +475,41 @@ private constructor( */ fun visitRealtimeConversationItemSystemMessage( realtimeConversationItemSystemMessage: RealtimeConversationItemSystemMessage - ): T + ): T = unknown(JsonValue.from(realtimeConversationItemSystemMessage)) /** A user message item in a Realtime conversation. */ fun visitRealtimeConversationItemUserMessage( realtimeConversationItemUserMessage: RealtimeConversationItemUserMessage - ): T + ): T = unknown(JsonValue.from(realtimeConversationItemUserMessage)) /** An assistant message item in a Realtime conversation. */ fun visitRealtimeConversationItemAssistantMessage( realtimeConversationItemAssistantMessage: RealtimeConversationItemAssistantMessage - ): T + ): T = unknown(JsonValue.from(realtimeConversationItemAssistantMessage)) /** A function call item in a Realtime conversation. */ - fun visitFunctionCall(functionCall: RealtimeConversationItemFunctionCall): T + fun visitFunctionCall(functionCall: RealtimeConversationItemFunctionCall): T = + unknown(JsonValue.from(functionCall)) /** A function call output item in a Realtime conversation. */ fun visitFunctionCallOutput( functionCallOutput: RealtimeConversationItemFunctionCallOutput - ): T + ): T = unknown(JsonValue.from(functionCallOutput)) /** A Realtime item responding to an MCP approval request. */ - fun visitMcpApprovalResponse(mcpApprovalResponse: RealtimeMcpApprovalResponse): T + fun visitMcpApprovalResponse(mcpApprovalResponse: RealtimeMcpApprovalResponse): T = + unknown(JsonValue.from(mcpApprovalResponse)) /** A Realtime item listing tools available on an MCP server. */ - fun visitMcpListTools(mcpListTools: RealtimeMcpListTools): T + fun visitMcpListTools(mcpListTools: RealtimeMcpListTools): T = + unknown(JsonValue.from(mcpListTools)) /** A Realtime item representing an invocation of a tool on an MCP server. */ - fun visitMcpCall(mcpCall: RealtimeMcpToolCall): T + fun visitMcpCall(mcpCall: RealtimeMcpToolCall): T = unknown(JsonValue.from(mcpCall)) /** A Realtime item requesting human approval of a tool invocation. */ - fun visitMcpApprovalRequest(mcpApprovalRequest: RealtimeMcpApprovalRequest): T + fun visitMcpApprovalRequest(mcpApprovalRequest: RealtimeMcpApprovalRequest): T = + unknown(JsonValue.from(mcpApprovalRequest)) /** * Maps an unknown variant of [ConversationItem] to a value of type [T]. @@ -515,6 +519,9 @@ private constructor( * version than the API, then the API may respond with new variants that the SDK is unaware * of. * + * Recognized variants also reach this method when their visit method is not overridden. + * This allows existing visitors to handle variants added by newer SDK versions. + * * @throws OpenAIInvalidDataException in the default implementation. */ fun unknown(json: JsonValue?): T { diff --git a/openai-java-core/src/main/kotlin/com/openai/services/async/admin/OrganizationServiceAsync.kt b/openai-java-core/src/main/kotlin/com/openai/services/async/admin/OrganizationServiceAsync.kt index 6a6ba2114..c877a44ec 100644 --- a/openai-java-core/src/main/kotlin/com/openai/services/async/admin/OrganizationServiceAsync.kt +++ b/openai-java-core/src/main/kotlin/com/openai/services/async/admin/OrganizationServiceAsync.kt @@ -58,6 +58,9 @@ interface OrganizationServiceAsync { fun dataRetention(): DataRetentionServiceAsync = throw UnsupportedOperationException("Subservice is not supported by this implementation") + fun externalKeys(): ExternalKeyServiceAsync = + throw UnsupportedOperationException("Subservice is not supported by this implementation") + fun externalStorage(): ExternalStorageServiceAsync = throw UnsupportedOperationException("Subservice is not supported by this implementation") @@ -73,9 +76,6 @@ interface OrganizationServiceAsync { fun projects(): ProjectServiceAsync = throw UnsupportedOperationException("Subservice is not supported by this implementation") - fun externalKeys(): ExternalKeyServiceAsync = - throw UnsupportedOperationException("Subservice is not supported by this implementation") - /** * A view of [OrganizationServiceAsync] that provides access to raw HTTP responses for each * method. @@ -132,6 +132,11 @@ interface OrganizationServiceAsync { "Subservice is not supported by this implementation" ) + fun externalKeys(): ExternalKeyServiceAsync.WithRawResponse = + throw UnsupportedOperationException( + "Subservice is not supported by this implementation" + ) + fun externalStorage(): ExternalStorageServiceAsync.WithRawResponse = throw UnsupportedOperationException( "Subservice is not supported by this implementation" @@ -156,10 +161,5 @@ interface OrganizationServiceAsync { throw UnsupportedOperationException( "Subservice is not supported by this implementation" ) - - fun externalKeys(): ExternalKeyServiceAsync.WithRawResponse = - throw UnsupportedOperationException( - "Subservice is not supported by this implementation" - ) } } diff --git a/openai-java-core/src/main/kotlin/com/openai/services/async/admin/OrganizationServiceAsyncImpl.kt b/openai-java-core/src/main/kotlin/com/openai/services/async/admin/OrganizationServiceAsyncImpl.kt index 4cc2844db..e8c40fad6 100644 --- a/openai-java-core/src/main/kotlin/com/openai/services/async/admin/OrganizationServiceAsyncImpl.kt +++ b/openai-java-core/src/main/kotlin/com/openai/services/async/admin/OrganizationServiceAsyncImpl.kt @@ -60,6 +60,10 @@ class OrganizationServiceAsyncImpl internal constructor(private val clientOption DataRetentionServiceAsyncImpl(clientOptions) } + private val externalKeys: ExternalKeyServiceAsync by lazy { + ExternalKeyServiceAsyncImpl(clientOptions) + } + private val externalStorage: ExternalStorageServiceAsync by lazy { ExternalStorageServiceAsyncImpl(clientOptions) } @@ -78,10 +82,6 @@ class OrganizationServiceAsyncImpl internal constructor(private val clientOption private val projects: ProjectServiceAsync by lazy { ProjectServiceAsyncImpl(clientOptions) } - private val externalKeys: ExternalKeyServiceAsync by lazy { - ExternalKeyServiceAsyncImpl(clientOptions) - } - override fun withRawResponse(): OrganizationServiceAsync.WithRawResponse = withRawResponse override fun withOptions(modifier: Consumer): OrganizationServiceAsync = @@ -104,6 +104,8 @@ class OrganizationServiceAsyncImpl internal constructor(private val clientOption override fun dataRetention(): DataRetentionServiceAsync = dataRetention + override fun externalKeys(): ExternalKeyServiceAsync = externalKeys + override fun externalStorage(): ExternalStorageServiceAsync = externalStorage override fun spendLimit(): SpendLimitServiceAsync = spendLimit @@ -114,8 +116,6 @@ class OrganizationServiceAsyncImpl internal constructor(private val clientOption override fun projects(): ProjectServiceAsync = projects - override fun externalKeys(): ExternalKeyServiceAsync = externalKeys - class WithRawResponseImpl internal constructor(private val clientOptions: ClientOptions) : OrganizationServiceAsync.WithRawResponse { @@ -151,6 +151,10 @@ class OrganizationServiceAsyncImpl internal constructor(private val clientOption DataRetentionServiceAsyncImpl.WithRawResponseImpl(clientOptions) } + private val externalKeys: ExternalKeyServiceAsync.WithRawResponse by lazy { + ExternalKeyServiceAsyncImpl.WithRawResponseImpl(clientOptions) + } + private val externalStorage: ExternalStorageServiceAsync.WithRawResponse by lazy { ExternalStorageServiceAsyncImpl.WithRawResponseImpl(clientOptions) } @@ -171,10 +175,6 @@ class OrganizationServiceAsyncImpl internal constructor(private val clientOption ProjectServiceAsyncImpl.WithRawResponseImpl(clientOptions) } - private val externalKeys: ExternalKeyServiceAsync.WithRawResponse by lazy { - ExternalKeyServiceAsyncImpl.WithRawResponseImpl(clientOptions) - } - override fun withOptions( modifier: Consumer ): OrganizationServiceAsync.WithRawResponse = @@ -199,6 +199,8 @@ class OrganizationServiceAsyncImpl internal constructor(private val clientOption override fun dataRetention(): DataRetentionServiceAsync.WithRawResponse = dataRetention + override fun externalKeys(): ExternalKeyServiceAsync.WithRawResponse = externalKeys + override fun externalStorage(): ExternalStorageServiceAsync.WithRawResponse = externalStorage @@ -209,7 +211,5 @@ class OrganizationServiceAsyncImpl internal constructor(private val clientOption override fun certificates(): CertificateServiceAsync.WithRawResponse = certificates override fun projects(): ProjectServiceAsync.WithRawResponse = projects - - override fun externalKeys(): ExternalKeyServiceAsync.WithRawResponse = externalKeys } } diff --git a/openai-java-core/src/main/kotlin/com/openai/services/blocking/admin/OrganizationService.kt b/openai-java-core/src/main/kotlin/com/openai/services/blocking/admin/OrganizationService.kt index c766afdd2..38ff39eb8 100644 --- a/openai-java-core/src/main/kotlin/com/openai/services/blocking/admin/OrganizationService.kt +++ b/openai-java-core/src/main/kotlin/com/openai/services/blocking/admin/OrganizationService.kt @@ -58,6 +58,9 @@ interface OrganizationService { fun dataRetention(): DataRetentionService = throw UnsupportedOperationException("Subservice is not supported by this implementation") + fun externalKeys(): ExternalKeyService = + throw UnsupportedOperationException("Subservice is not supported by this implementation") + fun externalStorage(): ExternalStorageService = throw UnsupportedOperationException("Subservice is not supported by this implementation") @@ -73,9 +76,6 @@ interface OrganizationService { fun projects(): ProjectService = throw UnsupportedOperationException("Subservice is not supported by this implementation") - fun externalKeys(): ExternalKeyService = - throw UnsupportedOperationException("Subservice is not supported by this implementation") - /** * A view of [OrganizationService] that provides access to raw HTTP responses for each method. */ @@ -131,6 +131,11 @@ interface OrganizationService { "Subservice is not supported by this implementation" ) + fun externalKeys(): ExternalKeyService.WithRawResponse = + throw UnsupportedOperationException( + "Subservice is not supported by this implementation" + ) + fun externalStorage(): ExternalStorageService.WithRawResponse = throw UnsupportedOperationException( "Subservice is not supported by this implementation" @@ -155,10 +160,5 @@ interface OrganizationService { throw UnsupportedOperationException( "Subservice is not supported by this implementation" ) - - fun externalKeys(): ExternalKeyService.WithRawResponse = - throw UnsupportedOperationException( - "Subservice is not supported by this implementation" - ) } } diff --git a/openai-java-core/src/main/kotlin/com/openai/services/blocking/admin/OrganizationServiceImpl.kt b/openai-java-core/src/main/kotlin/com/openai/services/blocking/admin/OrganizationServiceImpl.kt index fe89c8e81..a6adb7f8f 100644 --- a/openai-java-core/src/main/kotlin/com/openai/services/blocking/admin/OrganizationServiceImpl.kt +++ b/openai-java-core/src/main/kotlin/com/openai/services/blocking/admin/OrganizationServiceImpl.kt @@ -58,6 +58,8 @@ class OrganizationServiceImpl internal constructor(private val clientOptions: Cl DataRetentionServiceImpl(clientOptions) } + private val externalKeys: ExternalKeyService by lazy { ExternalKeyServiceImpl(clientOptions) } + private val externalStorage: ExternalStorageService by lazy { ExternalStorageServiceImpl(clientOptions) } @@ -70,8 +72,6 @@ class OrganizationServiceImpl internal constructor(private val clientOptions: Cl private val projects: ProjectService by lazy { ProjectServiceImpl(clientOptions) } - private val externalKeys: ExternalKeyService by lazy { ExternalKeyServiceImpl(clientOptions) } - override fun withRawResponse(): OrganizationService.WithRawResponse = withRawResponse override fun withOptions(modifier: Consumer): OrganizationService = @@ -94,6 +94,8 @@ class OrganizationServiceImpl internal constructor(private val clientOptions: Cl override fun dataRetention(): DataRetentionService = dataRetention + override fun externalKeys(): ExternalKeyService = externalKeys + override fun externalStorage(): ExternalStorageService = externalStorage override fun spendLimit(): SpendLimitService = spendLimit @@ -104,8 +106,6 @@ class OrganizationServiceImpl internal constructor(private val clientOptions: Cl override fun projects(): ProjectService = projects - override fun externalKeys(): ExternalKeyService = externalKeys - class WithRawResponseImpl internal constructor(private val clientOptions: ClientOptions) : OrganizationService.WithRawResponse { @@ -141,6 +141,10 @@ class OrganizationServiceImpl internal constructor(private val clientOptions: Cl DataRetentionServiceImpl.WithRawResponseImpl(clientOptions) } + private val externalKeys: ExternalKeyService.WithRawResponse by lazy { + ExternalKeyServiceImpl.WithRawResponseImpl(clientOptions) + } + private val externalStorage: ExternalStorageService.WithRawResponse by lazy { ExternalStorageServiceImpl.WithRawResponseImpl(clientOptions) } @@ -161,10 +165,6 @@ class OrganizationServiceImpl internal constructor(private val clientOptions: Cl ProjectServiceImpl.WithRawResponseImpl(clientOptions) } - private val externalKeys: ExternalKeyService.WithRawResponse by lazy { - ExternalKeyServiceImpl.WithRawResponseImpl(clientOptions) - } - override fun withOptions( modifier: Consumer ): OrganizationService.WithRawResponse = @@ -189,6 +189,8 @@ class OrganizationServiceImpl internal constructor(private val clientOptions: Cl override fun dataRetention(): DataRetentionService.WithRawResponse = dataRetention + override fun externalKeys(): ExternalKeyService.WithRawResponse = externalKeys + override fun externalStorage(): ExternalStorageService.WithRawResponse = externalStorage override fun spendLimit(): SpendLimitService.WithRawResponse = spendLimit @@ -198,7 +200,5 @@ class OrganizationServiceImpl internal constructor(private val clientOptions: Cl override fun certificates(): CertificateService.WithRawResponse = certificates override fun projects(): ProjectService.WithRawResponse = projects - - override fun externalKeys(): ExternalKeyService.WithRawResponse = externalKeys } } diff --git a/openai-java-core/src/test/kotlin/com/openai/core/LiveDecisionInputValidationTest.kt b/openai-java-core/src/test/kotlin/com/openai/core/LiveDecisionInputValidationTest.kt new file mode 100644 index 000000000..a951ca59e --- /dev/null +++ b/openai-java-core/src/test/kotlin/com/openai/core/LiveDecisionInputValidationTest.kt @@ -0,0 +1,211 @@ +package com.openai.core + +import com.openai.core.http.HttpClient +import com.openai.core.http.HttpRequest +import com.openai.core.http.HttpResponse +import com.openai.core.http.WebSocketClient +import com.openai.errors.OpenAIInvalidDataException +import com.openai.helpers.LiveConnection +import com.openai.helpers.LiveForkConnection +import com.openai.helpers.LiveSidebandConnection +import com.openai.models.live.ClientEvent +import com.openai.models.live.DecisionItemCreateEvent +import com.openai.models.live.forks.ForkClientEvent +import java.util.concurrent.CompletableFuture +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit.SECONDS +import java.util.stream.Stream +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import org.junit.jupiter.params.provider.ValueSource + +/** Public input validation and typed sends, separate from stored-item validation. */ +internal class LiveDecisionInputValidationTest { + private val mapper = jsonMapper() + + @ParameterizedTest(name = "{0}") + @MethodSource("validInputs") + fun validatesCurrentInputItemsWithAllInputIdStates(label: String, item: String) { + val event = decode(item) + assertThat(event.item().isValid()).describedAs(label).isTrue() + assertThat(event.item().validate()).isSameAs(event.item()) + assertThat(event.isValid()).describedAs(label).isTrue() + assertThat(event.validate()).isSameAs(event) + assertThat(mapper.readTree(mapper.writeValueAsString(event.item()))) + .isEqualTo(mapper.readTree(item)) + } + + @ParameterizedTest + @ValueSource( + strings = + [ + """{"type":"message","role":"administrator","content":"fixture"}""", + """{"type":"unknown_type","role":"user","content":"fixture"}""", + """{"type":"message","role":"user","content":false}""", + """{"type":"message","role":"user","content":[{"type":"input_text","text":false}]}""", + """{"type":"message","role":"user","content":[{"type":"unknown_part","text":"fixture"}]}""", + """{"type":"message","role":"user","content":"fixture","id":37}""", + """{"type":"function_call","name":"fixture","arguments":"{}"}""", + """{"type":"function_call","call_id":"fake-call","arguments":"{}"}""", + """{"type":"function_call","call_id":"fake-call","name":"fixture","arguments":false}""", + """{"type":"function_call_output","call_id":"fake-call"}""", + """{"type":"custom_tool_call","call_id":"fake-call","name":"fixture"}""", + """{"type":"custom_tool_call_output","output":"fixture"}""", + ] + ) + fun rejectsMalformedKnownInputFieldsAndDiscriminators(item: String) { + val event = decode(item) + assertThat(event.item().isValid()).isFalse() + assertThrows { event.item().validate() } + assertThat(event.isValid()).isFalse() + assertThrows { event.validate() } + } + + @Test + fun releasedMessageBuilderRequiresContentAndRevalidatesCurrentFields() { + assertThrows { DecisionItemCreateEvent.Item.builder().build() } + val original = + DecisionItemCreateEvent.Item.builder().content("fixture").id("first").build().validate() + assertThat(original.content().asString()).isEqualTo("fixture") + assertThat(original.id()).contains("first") + assertThat(original._role()).isEqualTo(JsonValue.from("user")) + + val updated = + original.toBuilder().id(null as String?).role(JsonValue.from("developer")).build() + assertThat(updated.isValid()).isTrue() + assertThat(updated.validate().id()).isEmpty() + assertThat(original.id()).contains("first") + for (bad in + listOf( + updated.toBuilder().id(JsonValue.from(42)).build(), + updated.toBuilder().content(JsonValue.from(false)).build(), + updated.toBuilder().role(JsonValue.from("administrator")).build(), + updated + .toBuilder() + .type(DecisionItemCreateEvent.Item.Type.of("unknown_type")) + .build(), + )) { + val event = DecisionItemCreateEvent.builder().item(bad).build() + assertThat(bad.isValid()).isFalse() + assertThrows { bad.validate() } + assertThat(event.isValid()).isFalse() + assertThrows { event.validate() } + } + } + + @ParameterizedTest + @ValueSource(strings = ["live", "sideband", "fork"]) + fun decodedAndValidatedTypedInputsReachEachSupportedSocket(family: String) { + val transport = InputTransport() + val options = + ClientOptions.builder().httpClient(transport).apiKey("fake-live-input-key").build() + fun sendInputs(send: (DecisionItemCreateEvent) -> Unit) { + for ((label, item) in samples()) { + val event = decode(item).validate() + send(event) + assertThat(mapper.readTree(transport.sent.last()).get("item")) + .describedAs("$family $label") + .isEqualTo(mapper.readTree(item)) + } + assertThat(transport.sent).hasSize(samples().size) + assertThat(transport.closed.count).isEqualTo(1L) + } + when (family) { + "live" -> + LiveConnection.connect(options).use { live -> + sendInputs { event -> live.send(ClientEvent.ofDecisionItemCreate(event)) } + } + "sideband" -> + LiveSidebandConnection.connect(options, "fake-session").use { sideband -> + sendInputs { event -> sideband.send(event) } + } + "fork" -> + LiveForkConnection.connect(options, "fake-session").use { fork -> + sendInputs { event -> fork.send(ForkClientEvent.ofDecisionItemCreate(event)) } + } + else -> error("Unexpected Live family: $family") + } + assertThat(transport.closed.await(5, SECONDS)).isTrue() + } + + private fun decode(item: String): DecisionItemCreateEvent = + mapper.readValue( + """{"type":"decision.item.create","event_id":"fake-event","item":$item}""", + DecisionItemCreateEvent::class.java, + ) + + private class InputTransport : HttpClient, WebSocketClient { + val sent = mutableListOf() + val closed = CountDownLatch(1) + + override fun execute(request: HttpRequest, requestOptions: RequestOptions): HttpResponse = + error("Unexpected HTTP request") + + override fun executeAsync( + request: HttpRequest, + requestOptions: RequestOptions, + ): CompletableFuture = error("Unexpected async HTTP request") + + override fun connectWebSocket( + request: HttpRequest, + options: RequestOptions, + maxMessageBytes: Int, + listener: WebSocketClient.Listener, + ): CompletableFuture = + CompletableFuture.completedFuture( + object : WebSocketClient.Connection { + override fun send(text: String) { + sent.add(text) + } + + override fun close() { + closed.countDown() + } + } + ) + + override fun close() {} + } + + companion object { + private fun samples(): List> { + val items = + listOf( + "user" to """{"type":"message","role":"user","content":"fixture"}""", + "developer" to """{"type":"message","role":"developer","content":"fixture"}""", + "system" to """{"type":"message","role":"system","content":"fixture"}""", + "assistant" to """{"type":"message","role":"assistant","content":"fixture"}""", + "function_call" to + """{"type":"function_call","call_id":"fake-call","name":"fixture","arguments":"{}"}""", + "function_call_output" to + """{"type":"function_call_output","call_id":"fake-call","output":"fixture"}""", + "custom_tool_call" to + """{"type":"custom_tool_call","call_id":"fake-call","name":"fixture","input":"fixture"}""", + "custom_tool_call_output" to + """{"type":"custom_tool_call_output","call_id":"fake-call","output":"fixture"}""", + "user_json_omitted_type" to + """{"role":"user","content":[{"type":"input_json","data":{"fixture":[1,true]}}]}""", + "user_text_omitted_type" to """{"role":"user","content":"fixture"}""", + ) + + listOf("user", "developer", "system", "assistant").map { role -> + "$role with tool-shaped extensions" to + """{"type":"message","role":"$role","content":"fixture","call_id":"fake-call","name":"fixture","arguments":"{}","namespace":"fixture","status":"completed"}""" + } + return items.flatMap { (label, item) -> + listOf( + "$label missing ID" to item, + "$label null ID" to """${item.dropLast(1)},"id":null}""", + "$label present ID" to """${item.dropLast(1)},"id":"fake-item"}""", + ) + } + } + + @JvmStatic + fun validInputs(): Stream = + samples().map { (label, item) -> Arguments.of(label, item) }.stream() + } +} diff --git a/openai-java-core/src/test/kotlin/com/openai/core/LiveDecisionLegacyNullTypeTest.kt b/openai-java-core/src/test/kotlin/com/openai/core/LiveDecisionLegacyNullTypeTest.kt new file mode 100644 index 000000000..dbed4df21 --- /dev/null +++ b/openai-java-core/src/test/kotlin/com/openai/core/LiveDecisionLegacyNullTypeTest.kt @@ -0,0 +1,119 @@ +package com.openai.core + +import com.openai.errors.OpenAIInvalidDataException +import com.openai.models.live.DecisionItemCreateEvent +import com.openai.models.live.DecisionItemCreatedEvent +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource + +internal class LiveDecisionLegacyNullTypeTest { + private val mapper = jsonMapper() + + @ParameterizedTest + @ValueSource( + strings = + [ + "{\"role\":\"user\",\"content\":\"fixture\",\"type\":null}", + "{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"fixture\"}],\"type\":null}", + "{\"role\":\"user\",\"content\":[{\"type\":\"input_image\",\"image_url\":\"https://invalid.example/image.png\"}],\"type\":null}", + "{\"role\":\"user\",\"content\":[],\"type\":null}", + "{\"role\":\"user\",\"content\":\"fixture\",\"type\":null,\"call_id\":false,\"name\":false,\"status\":false}", + ] + ) + fun acceptsReleasedNullTypeMessagesAndPreservesWireType(wire: String) { + for (id in listOf("", ",\"id\":null", ",\"id\":\"i_demo\"")) { + val inputJson = wire.dropLast(1) + id + "}" + val input = mapper.readValue(inputJson, DecisionItemCreateEvent.Item::class.java) + assertThat(input._type().isNull()).isTrue() + assertThat(input.isValid()).isTrue() + assertThat(input.validate()).isSameAs(input) + assertThat(mapper.readTree(mapper.writeValueAsString(input))) + .isEqualTo(mapper.readTree(inputJson)) + } + val storedJson = wire.dropLast(1) + ",\"id\":\"i_demo\"}" + val stored = mapper.readValue(storedJson, DecisionItemCreatedEvent.Item::class.java) + assertThat(stored._type().isNull()).isTrue() + assertThat(stored.isValid()).isTrue() + assertThat(stored.validate()).isSameAs(stored) + assertThat(mapper.readTree(mapper.writeValueAsString(stored))) + .isEqualTo(mapper.readTree(storedJson)) + } + + @ParameterizedTest + @ValueSource( + strings = + [ + "{\"role\":\"user\",\"content\":null,\"type\":null}", + "{\"role\":\"user\",\"type\":null}", + "{\"role\":\"user\",\"content\":false,\"type\":null}", + "{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":false}],\"type\":null}", + "{\"role\":\"user\",\"content\":[{\"type\":\"input_image\",\"image_url\":null}],\"type\":null}", + "{\"role\":\"user\",\"content\":[{\"type\":\"input_json\",\"data\":{\"n\":1}}],\"type\":null}", + "{\"role\":\"system\",\"content\":\"fixture\",\"type\":null}", + "{\"content\":\"fixture\",\"type\":null}", + "{\"role\":null,\"content\":\"fixture\",\"type\":null}", + "{\"role\":\"admin\",\"content\":\"fixture\",\"type\":null}", + "{\"role\":\"user\",\"content\":\"fixture\",\"type\":null,\"id\":37}", + ] + ) + fun rejectsNullTypeValuesThatFailedReleasedValidation(wire: String) { + val input = mapper.readValue(wire, DecisionItemCreateEvent.Item::class.java) + assertThat(input.isValid()).isFalse() + assertThrows { input.validate() } + val withStoredId = + mapper.readTree(wire).deepCopy() + if (!withStoredId.has("id")) withStoredId.put("id", "i_demo") + val stored = mapper.treeToValue(withStoredId, DecisionItemCreatedEvent.Item::class.java) + assertThat(stored.isValid()).isFalse() + assertThrows { stored.validate() } + } + + @Test + fun retainsReleasedBuilderAndRequiredStoredIdBehavior() { + val input = + DecisionItemCreateEvent.Item.builder() + .content("fixture") + .type(JsonValue.from(null)) + .build() + assertThat(input.validate()).isSameAs(input) + assertThat(input._type().isNull()).isTrue() + val stored = + DecisionItemCreatedEvent.Item.builder() + .id("i_demo") + .content("fixture") + .type(JsonValue.from(null)) + .build() + assertThat(stored.validate()).isSameAs(stored) + assertThat(stored._type().isNull()).isTrue() + for (id in listOf("", ",\"id\":null")) { + val wire = "{\"role\":\"user\",\"content\":\"fixture\",\"type\":null" + id + "}" + val missingStoredId = mapper.readValue(wire, DecisionItemCreatedEvent.Item::class.java) + assertThat(missingStoredId.isValid()).isFalse() + assertThrows { missingStoredId.validate() } + } + } + + @ParameterizedTest + @ValueSource( + strings = + [ + """[{"type":"input_json","data":[null]}]""", + """[{"type":"input_json","data":[{"ok":[null,3,true]},false]}]""", + ] + ) + fun rejectsMalformedNewContentWithConcreteOrOmittedMessageType(content: String) { + for (type in listOf("", ""","type":"message"""")) { + val wire = """{"role":"user","content":$content$type}""" + val input = mapper.readValue(wire, DecisionItemCreateEvent.Item::class.java) + assertThat(input.isValid()).isFalse() + assertThrows { input.validate() } + val withId = wire.dropLast(1) + ""","id":"i_demo"}""" + val stored = mapper.readValue(withId, DecisionItemCreatedEvent.Item::class.java) + assertThat(stored.isValid()).isFalse() + assertThrows { stored.validate() } + } + } +} diff --git a/openai-java-core/src/test/kotlin/com/openai/core/LiveDecisionValidationTest.kt b/openai-java-core/src/test/kotlin/com/openai/core/LiveDecisionValidationTest.kt new file mode 100644 index 000000000..a1cb78362 --- /dev/null +++ b/openai-java-core/src/test/kotlin/com/openai/core/LiveDecisionValidationTest.kt @@ -0,0 +1,238 @@ +package com.openai.core + +import com.openai.core.http.HttpClient +import com.openai.core.http.HttpRequest +import com.openai.core.http.HttpResponse +import com.openai.core.http.WebSocketClient +import com.openai.errors.OpenAIInvalidDataException +import com.openai.helpers.LiveConnection +import com.openai.helpers.LiveForkConnection +import com.openai.helpers.LiveSidebandConnection +import com.openai.models.live.DecisionItemCreateEvent +import com.openai.models.live.DecisionItemCreatedEvent +import java.util.concurrent.CompletableFuture +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource + +/** Checks the released Item API at the actual strict WebSocket receive boundary. */ +internal class LiveDecisionValidationTest { + @ParameterizedTest + @ValueSource(strings = ["live", "sideband", "fork"]) + fun strictReceiveAcceptsServiceItemsAndKeepsSocketUsable(family: String) { + val transport = DecisionTransport() + val options = + ClientOptions.builder() + .httpClient(transport) + .apiKey("fake-live-key") + .responseValidation(true) + .build() + withConnection(family, options) { receive -> + val inputs = + listOf( + """{"id":"user","type":"message","role":"user","content":"fixture"}""", + """{"id":"bad","type":null,"role":"user","content":"fixture"}""", + """{"id":"developer","type":"message","role":"developer","content":"fixture"}""", + """{"id":"system","type":"message","role":"system","content":"fixture"}""", + """{"id":"assistant","type":"message","role":"assistant","content":"fixture"}""", + """{"id":"fc","type":"function_call","call_id":"fake-call","name":"fixture","arguments":"{}"}""", + """{"id":"fo","type":"function_call_output","call_id":"fake-call","output":"fixture"}""", + """{"id":"cc","type":"custom_tool_call","call_id":"fake-call","name":"fixture","input":"fixture"}""", + """{"id":"co","type":"custom_tool_call_output","call_id":"fake-call","output":"fixture"}""", + """{"id":"json","role":"user","content":[{"type":"input_json","data":{"fixture":[1,true]}}]}""", + """{"id":"tail","role":"user","content":"still open"}""", + ) + + listOf("user", "developer", "system", "assistant").map { role -> + """{"id":"$role-extra","type":"message","role":"$role","content":"fixture","call_id":"fake-call","name":"fixture","arguments":"{}","namespace":"fixture","status":"completed"}""" + } + for (input in inputs) { + transport.deliver(input) + val event = receive() + assertThat(event.eventId()).isEqualTo("fake-event") + // A typed, strict receive must preserve every declared and tool-specific field. + assertThat( + options.jsonMapper.readTree( + options.jsonMapper.writeValueAsString(event.item()) + ) + ) + .isEqualTo(options.jsonMapper.readTree(input)) + } + assertThat(transport.closed).isFalse() + } + } + + @ParameterizedTest + @ValueSource( + strings = + [ + """{"type":"message","role":"user","content":"fixture"}""", + """{"id":19,"type":"message","role":"user","content":"fixture"}""", + """{"id":"bad","type":"message","role":"administrator","content":"fixture"}""", + """{"id":"bad","type":"unknown_type","role":"user","content":"fixture"}""", + """{"id":"bad","type":"message","role":"user","content":29}""", + """{"id":"bad","type":"message","role":"user","content":[{"type":"input_json","data":9}]}""", + """{"id":"bad","type":"function_call","call_id":"fake-call","name":"fixture","arguments":42}""", + """{"id":"bad","type":"function_call","name":"fixture","arguments":"{}"}""", + """{"id":"bad","type":"custom_tool_call","call_id":"fake-call","name":"fixture","input":false}""", + """{"id":"bad","type":"function_call_output","call_id":"fake-call","output":false}""", + """{"id":"bad","type":"custom_tool_call_output","call_id":"fake-call"}""", + ] + ) + fun strictReceiveRejectsKnownInvalidFields(input: String) { + val transport = DecisionTransport() + val options = + ClientOptions.builder() + .httpClient(transport) + .apiKey("fake-live-key") + .responseValidation(true) + .build() + LiveConnection.connect(options).use { live -> + transport.deliver(input) + assertThrows { live.receive() } + } + } + + @ParameterizedTest + @ValueSource(strings = ["developer", "system"]) + fun textContentListValidatesButSingletonObjectDoesNot(role: String) { + val mapper = jsonMapper() + val textPart = """{"type":"input_text","text":"fixture"}""" + for ((content, expectedValid) in + listOf("[]" to true, "[$textPart]" to true, textPart to false)) { + val node = mapper.createObjectNode().put("role", role) + node.set("content", mapper.readTree(content)) + val input = mapper.treeToValue(node, DecisionItemCreateEvent.Item::class.java) + val stored = + mapper.treeToValue( + node.put("id", "i_demo"), + DecisionItemCreatedEvent.Item::class.java, + ) + assertThat(input.isValid()).isEqualTo(expectedValid) + assertThat(stored.isValid()).isEqualTo(expectedValid) + if (expectedValid) { + assertThat(input.validate()).isSameAs(input) + assertThat(stored.validate()).isSameAs(stored) + } else { + assertThrows { input.validate() } + assertThrows { stored.validate() } + } + } + } + + @Test + fun releasedBuilderAndGettersStillWorkAndValidationUsesRebuiltValues() { + val legacy = + DecisionItemCreatedEvent.Item.builder() + .id("fake-original") + .content("fixture") + .build() + .validate() + assertThat(legacy.id()).isEqualTo("fake-original") + assertThat(legacy.content().asString()).isEqualTo("fixture") + assertThat(legacy._role()).isEqualTo(JsonValue.from("user")) + assertThat(legacy.type()).contains(DecisionItemCreatedEvent.Item.Type.MESSAGE) + + val rebuilt = + legacy.toBuilder().id("fake-updated").role(JsonValue.from("developer")).build() + assertThat(rebuilt.validate().id()).isEqualTo("fake-updated") + assertThat(rebuilt._role()).isEqualTo(JsonValue.from("developer")) + assertThat(legacy._role()).isEqualTo(JsonValue.from("user")) + + val invalidCopies = + listOf( + legacy.toBuilder().id(JsonValue.from(42)).build(), + rebuilt.toBuilder().content(JsonValue.from(false)).build(), + rebuilt.toBuilder().role(JsonValue.from("administrator")).build(), + ) + for (invalid in invalidCopies) { + assertThat(invalid.isValid()).isFalse() + assertThrows { invalid.validate() } + } + } + + @Test + fun validatesStoredStringIdSuppliedThroughAdditionalProperties() { + val mapper = jsonMapper() + val withoutId = + mapper.readValue( + """{"type":"message","role":"user","content":"fixture"}""", + DecisionItemCreatedEvent.Item::class.java, + ) + assertThat(withoutId.isValid()).isFalse() + assertThrows { withoutId.validate() } + + val withId = + withoutId.toBuilder().putAdditionalProperty("id", JsonValue.from("i_extra")).build() + assertThat(withId.isValid()).isTrue() + assertThat(withId.validate()).isSameAs(withId) + assertThat(mapper.readTree(mapper.writeValueAsString(withId))) + .isEqualTo( + mapper.readTree( + """{"type":"message","role":"user","content":"fixture","id":"i_extra"}""" + ) + ) + } + + private fun withConnection( + family: String, + options: ClientOptions, + action: (() -> DecisionItemCreatedEvent) -> Unit, + ) { + when (family) { + "live" -> + LiveConnection.connect(options).use { live -> + action { live.receive().asDecisionItemCreated() } + } + "sideband" -> + LiveSidebandConnection.connect(options, "fake-session").use { sideband -> + action { sideband.receive().asDecisionItemCreated() } + } + "fork" -> + LiveForkConnection.connect(options, "fake-session").use { fork -> + action { fork.receive().asDecisionItemCreated() } + } + else -> error("Unknown family: $family") + } + } + + private class DecisionTransport : HttpClient, WebSocketClient { + lateinit var listener: WebSocketClient.Listener + var closed = false + + fun deliver(item: String) { + listener.onMessage( + """{"type":"decision.item.created","event_id":"fake-event","item":$item}""" + ) + } + + override fun execute(request: HttpRequest, requestOptions: RequestOptions): HttpResponse = + error("Unexpected HTTP call") + + override fun executeAsync( + request: HttpRequest, + requestOptions: RequestOptions, + ): CompletableFuture = error("Unexpected async HTTP call") + + override fun connectWebSocket( + request: HttpRequest, + options: RequestOptions, + maxMessageBytes: Int, + listener: WebSocketClient.Listener, + ): CompletableFuture { + this.listener = listener + return CompletableFuture.completedFuture( + object : WebSocketClient.Connection { + override fun send(text: String) {} + + override fun close() { + closed = true + } + } + ) + } + + override fun close() {} + } +} diff --git a/openai-java-core/src/test/kotlin/com/openai/models/admin/organization/externalkeys/ExternalKeyCreateParamsTest.kt b/openai-java-core/src/test/kotlin/com/openai/models/admin/organization/externalkeys/ExternalKeyCreateParamsTest.kt index 9ab808009..3e002b2ef 100644 --- a/openai-java-core/src/test/kotlin/com/openai/models/admin/organization/externalkeys/ExternalKeyCreateParamsTest.kt +++ b/openai-java-core/src/test/kotlin/com/openai/models/admin/organization/externalkeys/ExternalKeyCreateParamsTest.kt @@ -15,7 +15,7 @@ internal class ExternalKeyCreateParamsTest { .externalId("external_id") .kmsArn("kms_arn") .name("name") - .roleArn("arn:aws:iam::732166991022:role/0*bWz\"J5bo7") + .roleArn("arn:aws:iam::123456789012:role/example-role") .purpose(ExternalKeyCreateParams.Body.Aws.Purpose.EKM) .build() ) @@ -31,7 +31,7 @@ internal class ExternalKeyCreateParamsTest { .externalId("external_id") .kmsArn("kms_arn") .name("name") - .roleArn("arn:aws:iam::732166991022:role/0*bWz\"J5bo7") + .roleArn("arn:aws:iam::123456789012:role/example-role") .purpose(ExternalKeyCreateParams.Body.Aws.Purpose.EKM) .build() ) @@ -46,7 +46,7 @@ internal class ExternalKeyCreateParamsTest { .externalId("external_id") .kmsArn("kms_arn") .name("name") - .roleArn("arn:aws:iam::732166991022:role/0*bWz\"J5bo7") + .roleArn("arn:aws:iam::123456789012:role/example-role") .purpose(ExternalKeyCreateParams.Body.Aws.Purpose.EKM) .build() ) @@ -62,7 +62,7 @@ internal class ExternalKeyCreateParamsTest { .externalId("external_id") .kmsArn("kms_arn") .name("name") - .roleArn("arn:aws:iam::732166991022:role/0*bWz\"J5bo7") + .roleArn("arn:aws:iam::123456789012:role/example-role") .build() ) .build() @@ -76,7 +76,7 @@ internal class ExternalKeyCreateParamsTest { .externalId("external_id") .kmsArn("kms_arn") .name("name") - .roleArn("arn:aws:iam::732166991022:role/0*bWz\"J5bo7") + .roleArn("arn:aws:iam::123456789012:role/example-role") .build() ) ) diff --git a/openai-java-core/src/test/kotlin/com/openai/services/async/admin/organization/ExternalKeyServiceAsyncTest.kt b/openai-java-core/src/test/kotlin/com/openai/services/async/admin/organization/ExternalKeyServiceAsyncTest.kt index 84edb6765..bd9831fbc 100644 --- a/openai-java-core/src/test/kotlin/com/openai/services/async/admin/organization/ExternalKeyServiceAsyncTest.kt +++ b/openai-java-core/src/test/kotlin/com/openai/services/async/admin/organization/ExternalKeyServiceAsyncTest.kt @@ -28,7 +28,7 @@ internal class ExternalKeyServiceAsyncTest { .externalId("external_id") .kmsArn("kms_arn") .name("name") - .roleArn("arn:aws:iam::732166991022:role/0*bWz\"J5bo7") + .roleArn("arn:aws:iam::123456789012:role/example-role") .purpose(ExternalKeyCreateParams.Body.Aws.Purpose.EKM) .build() ) diff --git a/openai-java-core/src/test/kotlin/com/openai/services/blocking/admin/organization/ExternalKeyServiceTest.kt b/openai-java-core/src/test/kotlin/com/openai/services/blocking/admin/organization/ExternalKeyServiceTest.kt index 18c0b23cc..41d3f7d9c 100644 --- a/openai-java-core/src/test/kotlin/com/openai/services/blocking/admin/organization/ExternalKeyServiceTest.kt +++ b/openai-java-core/src/test/kotlin/com/openai/services/blocking/admin/organization/ExternalKeyServiceTest.kt @@ -28,7 +28,7 @@ internal class ExternalKeyServiceTest { .externalId("external_id") .kmsArn("kms_arn") .name("name") - .roleArn("arn:aws:iam::732166991022:role/0*bWz\"J5bo7") + .roleArn("arn:aws:iam::123456789012:role/example-role") .purpose(ExternalKeyCreateParams.Body.Aws.Purpose.EKM) .build() ) diff --git a/scripts/castiron/generated-files.json b/scripts/castiron/generated-files.json index 6f6c1a215..05330e7f0 100644 --- a/scripts/castiron/generated-files.json +++ b/scripts/castiron/generated-files.json @@ -4565,13 +4565,17 @@ "mode": "100644", "sha256": "5b07a9cd5f7579a160ec9174a5eee17444ea593f6fc4949ba903d84f5a74a8d5" }, + "openai-java-core/src/main/kotlin/com/openai/models/live/DecisionInputItemValidation.kt": { + "mode": "100644", + "sha256": "117a87ceaaa94d75053d3b345b36f311f306bb11030904da61d2f12e7b2c5c14" + }, "openai-java-core/src/main/kotlin/com/openai/models/live/DecisionItemCreateEvent.kt": { "mode": "100644", - "sha256": "8dc4d8e5e54e48e1ff6da534a5f309e352e76a0f6085df76273c5cd4b692c5a4" + "sha256": "16204c7f0d68b9be676b2042aab9df5d3054d7660ccc8c478c8abcf0f141cbb4" }, "openai-java-core/src/main/kotlin/com/openai/models/live/DecisionItemCreatedEvent.kt": { "mode": "100644", - "sha256": "0c69fe9c2b1a3d5e718566346efb3aae82219053faf7bdd5f76d3a04fd27cc96" + "sha256": "6ff8f0c2a93ee83511d0b855dc65a9a356b249c84532706e74505582a9adf868" }, "openai-java-core/src/main/kotlin/com/openai/models/live/DecisionItemDeleteEvent.kt": { "mode": "100644", @@ -4899,7 +4903,7 @@ }, "openai-java-core/src/main/kotlin/com/openai/models/realtime/ConversationItem.kt": { "mode": "100644", - "sha256": "d81d2c7a67ef5da5001f36309bd95f555214f400538a3659ca27702f45f7408b" + "sha256": "6e59779cabb7364a5bc90ec08ce84fd4ef5978067354b08833dea19ae588d8e8" }, "openai-java-core/src/main/kotlin/com/openai/models/realtime/ConversationItemAdded.kt": { "mode": "100644", @@ -6907,11 +6911,11 @@ }, "openai-java-core/src/main/kotlin/com/openai/services/async/admin/OrganizationServiceAsync.kt": { "mode": "100644", - "sha256": "00c05610df1a68f74d0e0c14aaf6fe362f144de77cc568af365972988d70c404" + "sha256": "f4f67f1e67cfcf95125bd60607657351368e814ed877b0805f49b09ac6d92317" }, "openai-java-core/src/main/kotlin/com/openai/services/async/admin/OrganizationServiceAsyncImpl.kt": { "mode": "100644", - "sha256": "6c8797a53866d210d762552fbcc9eecc3837f5d22136e403ec7a0e295885a998" + "sha256": "d406d93c6818c4f82023202c0970f87aea038edc543ab6184ad2b1897b4a1251" }, "openai-java-core/src/main/kotlin/com/openai/services/async/admin/organization/AdminApiKeyServiceAsync.kt": { "mode": "100644", @@ -7915,11 +7919,11 @@ }, "openai-java-core/src/main/kotlin/com/openai/services/blocking/admin/OrganizationService.kt": { "mode": "100644", - "sha256": "22e3c0415ace659bb5a370878113dc26f73aea8983cfca97cf8a7e1f35b2df62" + "sha256": "43f29b3288f34dfb6996df8258d1be51ba9692fd74aa41bc80b567b62f2afce3" }, "openai-java-core/src/main/kotlin/com/openai/services/blocking/admin/OrganizationServiceImpl.kt": { "mode": "100644", - "sha256": "4540521f82af7bbc2c9e41800fb7687248ae7c932b3517b14b83934938093530" + "sha256": "8380ebcb143814e2162f7455acf02deb2367866b25b0592b26360e94f47a61aa" }, "openai-java-core/src/main/kotlin/com/openai/services/blocking/admin/organization/AdminApiKeyService.kt": { "mode": "100644", @@ -8907,7 +8911,7 @@ }, "openai-java-core/src/test/kotlin/com/openai/models/admin/organization/externalkeys/ExternalKeyCreateParamsTest.kt": { "mode": "100644", - "sha256": "f432d69ed80a0bb3b7f1e8d9cd4af1a4e3e04c09aa46b98511a1d92584fbd32d" + "sha256": "a174afac5567da231904e7efa2290c2974d026a37b0533a3b97459b4461f99ba" }, "openai-java-core/src/test/kotlin/com/openai/models/admin/organization/externalkeys/ExternalKeyDeleteParamsTest.kt": { "mode": "100644", @@ -14727,7 +14731,7 @@ }, "openai-java-core/src/test/kotlin/com/openai/services/async/admin/organization/ExternalKeyServiceAsyncTest.kt": { "mode": "100644", - "sha256": "771d71d1a04655433c6e4eb2303c905f43d2b90d58f969db14ac5de5fcbf65cf" + "sha256": "fdfe90ada604ad575854bd46ed7a6872e2a0fdee228ef3c9f92f5a642d8230ce" }, "openai-java-core/src/test/kotlin/com/openai/services/async/admin/organization/ExternalStorageServiceAsyncPaginationContractTest.kt": { "mode": "100644", @@ -15467,7 +15471,7 @@ }, "openai-java-core/src/test/kotlin/com/openai/services/blocking/admin/organization/ExternalKeyServiceTest.kt": { "mode": "100644", - "sha256": "f974a66dd28c0dcda051c50445b97641fe0514bb6f84d3e5f725b7c61c7c5689" + "sha256": "7e9adb3b2260d9cca1367ba936e95fc598b358d9c3d0c7c12b2bd68705396d03" }, "openai-java-core/src/test/kotlin/com/openai/services/blocking/admin/organization/ExternalStorageServicePaginationContractTest.kt": { "mode": "100644",