Skip to content

fix: OpenAI Responses, chat, transcription and image behaviour match the AI SDK - #368

Merged
eric8810 merged 41 commits into
masterfrom
fix/openai-responses-chat-transcription-and-image-behaviour-match-the-ai-sdk
Oct 25, 2026
Merged

eric8810 merged 41 commits into
masterfrom
fix/openai-responses-chat-transcription-and-image-behaviour-match-the-ai-sdk

Conversation

@cunninghamcard-bit

@cunninghamcard-bit cunninghamcard-bit commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Fixes the 36 behaviour differences found while porting the upstream OpenAI tests (@ai-sdk/openai 4.0.83). Each difference is its own commit with a test that fails on master and a CHANGELOG entry (Fixed, except #3 under Breaking). Commits 1-3 are the same changes as the open Azure fix PR #323 and commit 25 is the same change as #354; they are carried here so the OpenAI test PR passes on this branch alone. #354 has merged, so commit 25 adds nothing on top of master now; commits 1-3 become no-ops once #323 merges. The OpenAI test PR #369 stacks on this branch. Part of the #276 list.

# Commit Difference
1 b877402a A Responses stream that never sees response.created finishes with responseId: null
2 98224707 Responses reasoning deltas carry the reasoning item id
3 f1525842 The file search tool sends max_num_results as an integer
4 aa74417d Computer actions leave out null keys and unknown fields
5 5307efe8 Only the AI SDK's provider tools are mapped back to the caller's tool names
6 f4c76534 Text annotations keep only the fields of their type
7 18b47819 Apply patch operation fields follow the AI SDK's schema order
8 017c5f8f Local shell action fields follow the AI SDK's order
9 023ee862 A Responses message part that is not output_text rejects the response
10 2c19e2b8 Responses logprobs metadata has no bytes
11 5616957d A streamed function call's tool-input-end carries the namespace
12 9a809298 Early reasoning summary part ends carry only the item id
13 dc5919f3 Rotated reasoning item ids are matched by output index
14 3f75546d A Chat Completions chunk on a Responses stream gives a helpful error
15 fe9790d7 Schema-invalid known Responses stream events are error parts
16 4f35f428 A Responses stream that errors before any output rejects the call
17 1cf10d1c An async tool the model cannot run warns with the quoted tool name and the GPT-6 requirement
18 1be94f43 allowedTools warnings quote the entry and give the upstream reason
19 0b52c20c The error for an allowedTools list with nothing expressible names the dropped entries
20 019b1e8e The conflicting namespace descriptions error quotes the namespace name
21 03be7cec A missing openai file or skill reference names the available providers
22 61411ca1 An unsupported reasoningEffortUpdate lists the efforts the model supports
23 ac39bf74 Hosted tool search and program calls are replayed in full when responses are not stored
24 0716f8f0 A tool result sends its caller on the function_call_output
25 2bcf0983 Unparsable stream events report the AI SDK parse error
26 14c8db99 The chat promptCacheRetention warning says "GPT-6 and later models"
27 781c201d Embedding dimensions is sent as the number it was given
28 1f110cab The evaluation model names its structured output "evaluation"
29 de606beb Image models report file and mask support by model id
30 692b6020 A singleton allOf: [{ $ref }] becomes a direct $ref
31 d337a6ed An empty or blank base URL fails with "baseURL must be a non-empty string."
32 a4979583 Transcription sends responseFormat and chunkingStrategy
33 505a6123 Diarized transcription speakers are returned in provider metadata
34 520caeba The realtime transcription key goes in a WebSocket subprotocol
35 e33ad9d7 Realtime session options come from streaming
36 68ce701f Realtime transcription warns about REST-only options

Where the changes sit

create_openai                                         aimux-providers/src/openai/mod.rs:253
├ responses → OpenAIResponsesModel                     responses/mod.rs   do_generate :260, do_stream :296
│   ├ request: convert_to_responses_input, prepare_responses_tools_for   responses/convert.rs     #17-24
│   ├ response: parse output, annotations, tool inputs                   responses/responses_convert.rs   #4-10
│   └ stream: read_until_output, event transform                         responses_convert.rs, provider_events.rs   #1, 2, 11-16
├ chat → OpenAIModel, normalize_json_schema            model.rs, convert.rs   #26, 30
├ embedding / image                                    embedding.rs, image.rs (Azure: azure/mod.rs)   #27, 29
├ transcription (REST and realtime WebSocket)          transcription.rs   #32-36
├ evaluation → DecisionLanguageModel                   aimux-provider-utils/src/decision_language_model.rs   #28
└ base URL check, SSE event parsing                    aimux-provider-utils/src/url.rs, response_handler.rs   #31, 25
tools: file_search max_num_results                     openai/tools.rs   #3

Size: 825 lines added / 126 removed in non-test source over 13 files; about 1,300 lines of regression tests in the old aimux-providers/tests/ files; 36 CHANGELOG bullets.

1. A Responses stream that never sees response.created finishes with responseId: null

Same change as #323, which this PR carries so the OpenAI tests pass on its own.

Before: the finish part's provider metadata said responseId: "" when no response.created event arrived. The generate path already wrote null.

After: responseId stays null until response.created sets it (responses/openai-responses-language-model.ts:1587, set at :2663).

How it works today

OpenAIResponsesModel::do_stream                  openai/responses/mod.rs:297
└─ build_responses_event_stream_with_tools       responses_convert.rs:593
   └─ OpenAIResponsesStream::transform           :686
      ├─ "response.created" sets response_id     :740
      └─ flush                                   :1512
         └─ json!({"responseId": self.response_id.unwrap_or_default()})   :1517
            ← upstream: starts as null and is written as is (:1587, :2932)

What this PR changes

OpenAIResponsesStream::flush                     openai/responses/responses_convert.rs:1847
└─ json!({"responseId": self.response_id})       :1852   a missing id serialises as null

Test: openai_upstream_e2e_test.rs::a_stream_without_response_created_finishes_with_a_null_response_id, on master it fails with responseId equal to "", not null.

2. Responses reasoning deltas carry the reasoning item id

Same change as #323, which this PR carries so the OpenAI tests pass on its own.

Before: reasoning-delta parts had no provider metadata, while the start and end parts already had itemId.

After: every reasoning-delta has providerMetadata.<key>.itemId (responses/openai-responses-language-model.ts:2735-2749).

How it works today

OpenAIResponsesStream::transform                 openai/responses/responses_convert.rs:686
└─ "response.reasoning_summary_text.delta"       :1044
   └─ controller.reasoning_delta(id, delta)      :1059
      └─ StreamPart::ReasoningDelta { provider_metadata: None }   stream/mod.rs:152-157
         ← upstream: puts { itemId } on every reasoning-delta (:2735-2749)

What this PR changes

OpenAIResponsesStream::transform                 openai/responses/responses_convert.rs:979
└─ "response.reasoning_summary_text.delta"       :1367
   └─ controller.enqueue(StreamPart::ReasoningDelta { … })   :1378
      └─ provider_metadata: Some({ <key>: { itemId } })      :1381-1384

Test: openai_upstream_e2e_test.rs::reasoning_deltas_carry_the_item_id, on master it fails with null metadata instead of {"openai":{"itemId":"rs_1"}}.

3. The file search tool sends max_num_results as an integer

Same change as #323, which this PR carries so the OpenAI tests pass on its own.

Before: FileSearchArgs::max_num_results was an Option<f64>, so maxNumResults: 5 went out as 5.0.

After: it is an Option<u32> and goes out as 5, as the AI SDK sends the number as is (responses/openai-responses-prepare-tools.ts:219).

How it works today

file_search(args: FileSearchArgs)                openai/tools.rs:560
└─ provider_tool("file_search", args)            :524
   └─ serde_json::to_value(args)                 :525   max_num_results: Option<f64> (:200), 5 becomes 5.0
OpenAIResponsesModel::request_body               openai/responses/mod.rs:83
└─ provider_tool_to_json                         convert.rs:1223
   └─ ("maxNumResults", "max_num_results")       :1261   copied as is
      ← upstream: a JS number, sent as 5

What this PR changes

FileSearchArgs                                   openai/tools.rs:196
└─ max_num_results: Option<u32>                  :200   serialises as 5
provider_tool_to_json                            openai/responses/convert.rs:1253
└─ ("maxNumResults", "max_num_results")          :1291

Test: openai_upstream_e2e_test.rs::file_search_sends_max_num_results_as_an_integer, on master it fails with 5.0 instead of 5.

4. Computer actions leave out null keys and unknown fields

Before: aimux copied the wire action and only renamed the scroll offsets, so "keys": null and any extra field reached the tool input, and the renamed keys moved to the end.

After: each action keeps only the fields of its type, renames scroll_x and scroll_y, and adds keys only when it is not null (responses/openai-responses-language-model.ts:128, mapComputerAction).

How it works today

OpenAIResponsesModel::do_generate                openai/responses/mod.rs:261
└─ build_responses_generate_result_with_tools    responses_convert.rs:101
   └─ provider_tool_content                      :299 → provider_events.rs:11
      └─ "computer_call"                         :92
         └─ computer_input                       :275
            └─ actions.map(rename scroll_x/y)    :283-296   everything else copied
               ← upstream: mapComputerAction builds each action from its type's fields (:128)

What this PR changes

provider_tool_content                            openai/responses/provider_events.rs:11
└─ "computer_call"                               :92
   └─ computer_input                             :352
      └─ actions.iter().map(computer_action)     :360 → :321
         └─ per-type field list, null values skipped   :322-349

Test: openai_upstream_e2e_test.rs::computer_actions_leave_out_null_keys, on master it fails with "keys":null in the input and the scroll keys in the wrong order.

5. Only the AI SDK's provider tools are mapped back to the caller's tool names

Before: any openai.<name> provider tool put its caller-chosen name in the tool name map. A computer call without a call_id then came out as computerUse.

After: only the twelve names in the AI SDK's providerToolNames are mapped, so computer_use keeps its canonical name (responses/openai-responses-language-model.ts:378, :1358).

How it works today

OpenAIResponsesModel::do_generate                openai/responses/mod.rs:261
├─ tool_name_mapping(options)                    :292 → :356
│  └─ any "openai.<x>" tool: names[x] = caller name   :359-362
└─ build_responses_generate_result_with_tools    responses_convert.rs:101
   └─ provider_tool_content                      :299 → provider_events.rs:11
      └─ "computer_call" without call_id         :87
         └─ pair("computer_use", …) → name(…)    :27   returns "computerUse"
            ← upstream: a fixed list of twelve names (:378)

What this PR changes

OpenAIResponsesModel::do_generate                openai/responses/mod.rs:260
└─ tool_name_mapping(options)                    :291 → :375
   ├─ PROVIDER_TOOL_NAMES (the twelve names)     :360
   └─ strip_prefix("openai.") && PROVIDER_TOOL_NAMES.contains(&name)   :379-380

Test: openai_upstream_e2e_test.rs::computer_use_results_keep_the_canonical_tool_name, on master it fails with ["computerUse", "computerUse"] instead of ["computer_use", "computer_use"].

6. Text annotations keep only the fields of their type

Before: the text part's provider metadata copied the wire annotation, so a container_file_citation kept an extra index field.

After: each annotation keeps the fields its type has in the AI SDK's schema, in both the generate and the streamed text end (responses/openai-responses-api.ts:1500-1535, responses/openai-responses-language-model.ts:1072).

How it works today

OpenAIResponsesModel::do_generate                openai/responses/mod.rs:261
└─ build_responses_generate_result_with_tools    responses_convert.rs:101
   └─ message → output_text part                 :165
      └─ text_metadata(part, annotations)        :188 → :426
         └─ metadata["annotations"] = json!(annotations)   :432
            ← upstream: one zod object per annotation type drops other keys (:1500-1535)
stream: text_annotations → text_metadata         :1126   same call

What this PR changes

build_responses_generate_result_with_tools       openai/responses/responses_convert.rs:101
└─ text_metadata(part, annotations)              :209 → :672
   └─ annotations.iter().map(annotation_fields)  :678 → :707
      └─ url_citation / file_citation / container_file_citation / file_path field lists   :708-722
stream: text_metadata(&metadata_item, …)         :1447   same function

Test: openai_upstream_e2e_test.rs::text_annotations_keep_only_the_fields_of_their_type, on master it fails with "index": 2 still in the annotation.

7. Apply patch operation fields follow the AI SDK's schema order

Before: the tool input was the wire operation as the API sent it, so diff could come before type.

After: the operation is rebuilt as type, path, diff, and the delete-file delta in the stream uses the same value (responses/openai-responses-language-model.ts:1443, :1857).

How it works today

build_responses_generate_result_with_tools      openai/responses/responses_convert.rs:101
└─ provider_tool_content                         :299 → provider_events.rs:11
   └─ "apply_patch_call"                         :129
      └─ json!({"callId", "operation": part["operation"]})   :132   wire key order
         ← upstream: operation parsed as {type, path, diff}, then stringified (openai-responses-api.ts:1698)
stream: provider_tool_start                      responses_convert.rs:1564
└─ let operation = &item["operation"]            :1631

What this PR changes

provider_tool_content                            openai/responses/provider_events.rs:11
└─ "apply_patch_call"                            :129
   └─ "operation": apply_patch_operation(&part["operation"])   :132 → :304
stream: provider_tool_start                      responses_convert.rs:1899
└─ let operation = &apply_patch_operation(&item["operation"])  :1966

Test: openai_upstream_e2e_test.rs::apply_patch_input_lists_operation_fields_in_schema_order, on master it fails with diff listed before type in the input.

8. Local shell action fields follow the AI SDK's order

Before: the generated input stringified the wire action, and the stream moved the renamed timeoutMs and workingDirectory keys to the end.

After: the action is rebuilt as type, command, timeout_ms, user, working_directory, env, with the camel-case names in the stream (responses/openai-responses-language-model.ts:998, :2435).

How it works today

build_responses_generate_result_with_tools      openai/responses/responses_convert.rs:101
└─ provider_tool_content                         :299 → provider_events.rs:11
   └─ "local_shell_call"                         :100
      └─ json!({"action": part["action"]})       :103   wire key order
stream: output_item.done, `_` arm                responses_convert.rs:1233
└─ remove + insert of timeout_ms / working_directory   :1235-1246   renamed keys go last
   ← upstream: action parsed as {type, command, timeout_ms, user, working_directory, env} (openai-responses-api.ts:147)

What this PR changes

provider_tool_content                            openai/responses/provider_events.rs:11
└─ "local_shell_call"                            :100
   └─ "action": local_shell_action(&part["action"])   :103 → :280   fixed field order
stream: output_item.done, `_` arm                responses_convert.rs:1570-1581 → provider_tool_content :1617

Test: openai_upstream_e2e_test.rs::local_shell_input_lists_action_fields_in_schema_order, on master it fails with env listed before working_directory.

9. A Responses message part that is not output_text rejects the response

Before: do_generate skipped any message content part that was not output_text, so a refusal part gave an Ok result with empty content.

After: it fails with an ApiCall error "Invalid JSON response", as the AI SDK's response schema allows only output_text parts (responses/openai-responses-api.ts:1486). The stream is unchanged, because its schema does not check message content (:1098).

How it works today

OpenAIResponsesModel::do_generate                openai/responses/mod.rs:261
└─ build_responses_generate_result_with_tools    responses_convert.rs:101
   └─ Some("message")                            :165
      └─ for cp in content_parts                 :167
         └─ if type == "output_text" { … }       :168   any other part is skipped
            ← upstream: the schema rejects the whole response (openai-responses-api.ts:1486)

What this PR changes

build_responses_generate_result_with_tools       openai/responses/responses_convert.rs:101
└─ Some("message")                               :165
   ├─ content_parts.iter().find(type != "output_text")   :170-172
   └─ return Err(AiMuxError::ApiCall("Invalid JSON response: …"))   :174-186

Test: openai_upstream_e2e_test.rs::a_refusal_message_part_rejects_the_response, on master it fails because do_generate returns Ok with empty content.

10. Responses logprobs metadata has no bytes

Before: the wire logprobs went into providerMetadata.openai.logprobs unchanged, bytes included.

After: each entry keeps token, logprob and top_logprobs (with token and logprob), for generate and stream (responses/openai-responses-api.ts:875, :1490; responses/openai-responses-language-model.ts:1065, :2685).

How it works today

build_responses_generate_result_with_tools      openai/responses/responses_convert.rs:101
└─ logprobs.push(value.clone())                  :180 → pm["logprobs"] :339
OpenAIResponsesStream::transform                 :686
└─ "response.output_text.delta"                  :955
   └─ logprobs.push(value.clone())               :962 → flush :1525
      ← upstream: pushes the parsed value, which has no `bytes`

What this PR changes

build_responses_generate_result_with_tools      openai/responses/responses_convert.rs:101
└─ logprobs.push(logprobs_fields(value))         :201 → :686
OpenAIResponsesStream::transform                 :979
└─ "response.output_text.delta"                  :1286   logprobs.push(logprobs_fields(value))

Test: openai_upstream_e2e_test.rs::responses_logprobs_metadata_has_no_bytes, on master it fails with bytes still in the metadata, in both generate and stream.

11. A streamed function call's tool-input-end carries the namespace

Before: tool-input-end had no provider metadata, and the namespace was only on the tool-call.

After: when the item has a namespace, tool-input-end has providerMetadata.<key>.namespace (responses/openai-responses-language-model.ts:1986).

How it works today

OpenAIResponsesStream::transform                 openai/responses/responses_convert.rs:686
└─ "response.output_item.done"                   :1095
   └─ "function_call"                            :1133
      ├─ controller.tool_input_end(call_id)      :1150 → stream/mod.rs:186   provider_metadata: None
      └─ StreamPart::ToolCall (has the namespace)   :1152
         ← upstream: tool-input-end gets { namespace } too (:1986)

What this PR changes

OpenAIResponsesStream::transform                 openai/responses/responses_convert.rs:979
└─ "response.output_item.done"                   :1416
   └─ "function_call"                            :1453
      └─ StreamPart::ToolInputEnd { provider_metadata: item.namespace → Some({ <key>: { namespace } }) }   :1471-1483

Test: openai_upstream_e2e_test.rs::a_streamed_namespaced_function_call_ends_its_input_with_the_namespace, on master it fails with null metadata instead of {"openai":{"namespace":"weather_ns"}}.

12. Early reasoning summary part ends carry only the item id

Before: a summary part ended early (when the next part is added, or on reasoning_summary_part.done with store) carried reasoningEncryptedContent.

After: those ends carry { itemId } only. The ends sent on output_item.done still carry the encrypted content (responses/openai-responses-language-model.ts:2709, :2762, :2523).

How it works today

OpenAIResponsesStream::transform                 openai/responses/responses_convert.rs:686
├─ "response.reasoning_summary_part.added"       :1000
│  └─ meta = reasoning_stream_metadata(…, encrypted_content)   :1014 → :400
│     └─ ReasoningEnd { provider_metadata: Some(meta) }        :1028-1031
└─ "response.reasoning_summary_part.done" (store)              :1063
   └─ ReasoningEnd { reasoning_stream_metadata(…, encrypted_content) }   :1078-1084
      ← upstream: both ends are { itemId } only; only output_item.done adds the encrypted content

What this PR changes

OpenAIResponsesStream::transform                 openai/responses/responses_convert.rs:979
├─ "response.reasoning_summary_part.added"       :1325
│  └─ ReasoningEnd { reasoning_item_metadata(provider_key, &item_id) }   :1350 → :650
└─ "response.reasoning_summary_part.done" (store)                       :1390
   └─ ReasoningEnd { reasoning_item_metadata(provider_key, &item_id) }   :1402

Test: openai_upstream_e2e_test.rs::early_reasoning_summary_ends_carry_only_the_item_id, on master it fails because the rs_1:0 end has reasoningEncryptedContent.

13. Rotated reasoning item ids are matched by output index

Before: reasoning events were matched by their own item_id. When an OpenAI-compatible provider changed the id between events, one reasoning part split into several.

After: the id the item started with is kept per output_index and used for the summary events and the item end, as for messages (responses/openai-responses-language-model.ts:1633-1644, set at :1905; used at :2688, :2736, :2751, :2503).

How it works today

OpenAIResponsesStream::transform                 openai/responses/responses_convert.rs:686
├─ "response.output_item.added", reasoning arm   :828   output_ids is set for messages only (:777)
├─ "response.reasoning_summary_part.added"       :1000   item_id = parsed["item_id"] (:1001)
├─ "response.reasoning_summary_text.delta"       :1044   same (:1045)
├─ "response.reasoning_summary_part.done"        :1063   same (:1064)
└─ "response.output_item.done", reasoning arm    :1195   id = item["id"] (:1196)
   ← upstream: resolveOutputItemId looks the id up by output_index

What this PR changes

OpenAIResponsesStream::transform                 openai/responses/responses_convert.rs:979
├─ reasoning arm: output_ids.insert(output_index, id)     :1170
├─ part.added / text.delta / part.done                    :1325, :1368, :1390
│  └─ resolve_output_item_id(&parsed, output_ids)         → :639
└─ output_item.done reasoning arm: output_ids.remove(&output_index)   :1530

Test: openai_upstream_e2e_test.rs::rotated_reasoning_item_ids_are_correlated_by_output_index, on master it fails with ids such as rs_3:0 instead of one rs_1:0 part.

14. A Chat Completions chunk on a Responses stream gives a helpful error

Before: a chunk with a choices array and no string type was ignored as an unknown event, so a wrong baseURL gave an empty stream.

After: it gives an error part with an ApiCall error that says the endpoint speaks Chat Completions, and the stream finishes with error (responses/openai-responses-language-model.ts:1664-1680, :2962, :2972).

How it works today

OpenAIResponsesStream::transform                 openai/responses/responses_convert.rs:686
└─ Ok(parsed)                                    :724
   └─ etype = parsed["type"] or ""               :730
      └─ `_ =>` unknown chunk, ignored           :1496
         ← upstream: a chunk that fails the schema and looks like Chat Completions is an error (:2962)

What this PR changes

OpenAIResponsesStream::transform                 openai/responses/responses_convert.rs:979
└─ is_chat_completion_chunk(&parsed)             :1023 → :440
   └─ StreamPart::Error { stream_error_api_call(CHAT_COMPLETIONS_MISMATCH, …) }   :1027-1036 → :432
      └─ stream_errored, finish reason error, return
read_until_output also stops at such a chunk     :511

Test: openai_upstream_e2e_test.rs::a_chat_completions_chunk_on_a_responses_stream_gives_a_helpful_error, on master it fails with no error part in the stream.

15. Schema-invalid known Responses stream events are error parts

Before: events were read as loose JSON. A missing output_index defaulted to 0 and a missing item was skipped, so no error was reported.

After: a modeled event that misses an envelope field (type, output_index, item and its type, item_id, the delta or arguments, summary_index, response) is an error part, and the stream finishes with error (responses/openai-responses-api.ts:1433-1441, responses/openai-responses-language-model.ts:1664-1680). Item internals stay unchecked.

How it works today

OpenAIResponsesStream::transform                 openai/responses/responses_convert.rs:686
├─ "response.function_call_arguments.done"       no arm → `_ =>` :1496, ignored
└─ "response.output_item.done"                   :1095
   └─ output_index = parsed.get(…).unwrap_or(0)  :1096-1100
      └─ "function_call" → ToolCall emitted      :1133
         ← upstream: a modeled chunk that fails its schema is not skipped (:1433-1441)

What this PR changes

OpenAIResponsesStream::transform                 openai/responses/responses_convert.rs:979
└─ chunk_schema_error(&parsed)                   :1039 → :574
   └─ StreamPart::Error { AiMuxError::TypeValidation }   :1042-1049, then return

Test: openai_upstream_e2e_test.rs::schema_invalid_known_responses_events_are_errors, on master it fails with 0 error parts instead of 2.

16. A Responses stream that errors before any output rejects the call

Before: only the first event was checked, so an error frame after response.created became an error part of a stream that had already resolved.

After: do_stream reads events until the first output event and rejects with an ApiCall error for an error frame or a response.failed with an error. After response.in_progress each read waits at most 50 ms (openai-stream-error.ts:39, called at responses/openai-responses-language-model.ts:1552).

How it works today

OpenAIResponsesModel::do_stream                  openai/responses/mod.rs:297
├─ first_event = sse_stream.next()               :328   only an ApiCall error is returned
└─ build_responses_event_stream_with_tools       responses_convert.rs:593
   ├─ peek at first_event: "error" / "response.failed"   :610-620 → Err
   └─ transform, "error" arm                     :1480 → StreamPart::Error
      ← upstream: reads until the first output event (isResponseOutputChunk) before returning

What this PR changes

OpenAIResponsesModel::do_stream                  openai/responses/mod.rs:296
├─ read_until_output(&mut sse_stream, …)         :327 → responses_convert.rs:482
│  ├─ 50 ms timeout after response.in_progress   :495
│  ├─ "error" / response.failed with error → Err(openai_stream_error)   :516-517
│  └─ first output event → return the events read   :538
└─ events replayed ahead of sse_stream           :334-336

Test: openai_upstream_e2e_test.rs::a_responses_error_before_output_after_response_created_rejects_the_call, on master it fails because do_stream resolved instead of rejecting.

17. An async tool the model cannot run warns with the quoted tool name and the GPT-6 requirement

Before: a function tool with async: true on a model without async tool calling lost the flag, with the warning async tool calling for weather and the details "Async tool calling is not supported by this model."

After: the warning reads async tool calling for "weather" with "Async tool calling is only supported by GPT-6 and later models." (responses/openai-responses-prepare-tools.ts:684-688).

How it works today

build_responses_request_body_for                     openai/responses/convert.rs:1894
└─ prepare_responses_tools_for                       :2092 → :971
   └─ function tool: resolve_async_tool_option       :1014 → :1105
      └─ Warning::Unsupported                        :1115-1118
         ← upstream: quotes the name and says "only supported by GPT-6 and later models"

What this PR changes

build_responses_request_body_for                     openai/responses/convert.rs:1925
└─ prepare_responses_tools_for                       :2123 → :984
   └─ function tool: resolve_async_tool_option       :1027 → :1118
      └─ Warning::Unsupported                        :1128-1131   feature with \"{name}\", GPT-6 details

Test: aimux-providers/tests/openai_upstream_e2e_test.rs::responses_async_tool_warning_names_the_gpt_6_requirement, on master it fails with feature async tool calling for weather and the old details text.

18. allowedTools warnings quote the entry and give the upstream reason

Before: allowedTools warnings used the bare name, and namespaced and deferred tools shared one reason.

After: the feature is allowedTools entry "<name>", and namespaced, deferred, tool_search and ambiguous entries each get upstream's reason (responses/openai-responses-prepare-tools.ts:465-499).

How it works today

build_responses_request_body_for                     openai/responses/convert.rs:1894
└─ prepare_responses_tools_for                       :2092 → :971
   └─ prepare_allowed_tools                          :1096 → :1122
      ├─ namespace or deferLoading tool              :1173-1176   one merged reason, name unquoted
      ├─ tool_search entry                           :1190
      └─ ambiguous provider tool name                :1162
         ← upstream: quoted name, one reason per case (:465-499)

What this PR changes

prepare_allowed_tools                                openai/responses/convert.rs:1135
├─ reason: namespace, else deferred                  :1188-1196
├─ Warning::Unsupported { allowedTools entry "…" }   :1198-1203
├─ tool_search entry: upstream wording               :1218
└─ ambiguous provider tool name: upstream wording    :1176

Test: aimux-providers/tests/openai_upstream_e2e_test.rs::responses_allowed_tools_warnings_quote_the_entry_and_give_the_reason, on master it fails with feature allowedTools entry deferred and details "namespace and deferred tools are not visible …".

19. The error for an allowedTools list with nothing expressible names the dropped entries

Before: when every allowedTools entry was dropped, the error was "allowedTools with only tools that cannot be allow-listed" with no names.

After: it ends with the dropped names joined by , , for example (deferred) (responses/openai-responses-prepare-tools.ts:509-515).

How it works today

build_responses_request_body_for                     openai/responses/convert.rs:1894
└─ prepare_responses_tools_for                       :2092 → :971
   └─ prepare_allowed_tools                          :1096 → :1122
      ├─ each unusable entry: warn, continue         :1162, :1176, :1190   name not kept
      └─ entries.is_empty() → UnsupportedFunctionality   :1205-1207
         ← upstream: appends (droppedToolNames.join(", ")) (:511)

What this PR changes

prepare_allowed_tools                                openai/responses/convert.rs:1135
├─ let mut dropped = Vec::new()                      :1156
├─ each unusable entry: dropped.push(name)           :1177, :1204, :1219
└─ entries.is_empty() → UnsupportedFunctionality("… ({})", dropped.join(", "))   :1233-1237

Test: aimux-providers/tests/openai_upstream_e2e_test.rs::responses_allowed_tools_error_names_the_dropped_entries, on master it fails because the message has no (deferred) suffix.

20. The conflicting namespace descriptions error quotes the namespace name

Before: two tools in the same OpenAI namespace with different descriptions failed with conflicting descriptions for OpenAI tool namespace crm.

After: the name is quoted, … namespace "crm" (responses/openai-responses-prepare-tools.ts:174-178).

How it works today

build_responses_request_body_for                     openai/responses/convert.rs:1894
└─ prepare_responses_tools_for                       :2092 → :971
   └─ function tool with a namespace option
      └─ group["description"] != description         :1035
         └─ UnsupportedFunctionality(… namespace {name})   :1036-1037
            ← upstream: the name is in double quotes (:177)

What this PR changes

build_responses_request_body_for                     openai/responses/convert.rs:1925
└─ prepare_responses_tools_for                       :2123 → :984
   └─ group["description"] != description            :1048
      └─ UnsupportedFunctionality(… namespace \"{name}\")   :1049-1050

Test: aimux-providers/tests/openai_upstream_e2e_test.rs::responses_conflicting_namespace_descriptions_quote_the_namespace, on master it fails with the unquoted crm.

21. A missing openai file or skill reference names the available providers

Before: a Responses file part or shell skill reference with no openai entry failed with "No file reference for openai" or "No skill reference for openai".

After: both fail with "No provider reference found for provider 'openai'. Available providers: anthropic", as chat already did (responses/convert-to-openai-responses-input.ts:142, responses/openai-responses-prepare-tools.ts:780).

How it works today

OpenAIResponsesModel::request_body                   openai/responses/mod.rs:83
├─ file part check: reference.contains_key(…)        :190-198   "No file reference for …"
└─ build_responses_request_body_for                  :216 → convert.rs:1894
   ├─ convert_user_part                              convert.rs:723 → :749-754   same message
   └─ map_shell_environment                          :1402 → :1434   "No skill reference for openai"
      ← upstream: resolveProviderReference lists the available providers

What this PR changes

OpenAIResponsesModel::request_body                   openai/responses/mod.rs:83
├─ file part check: resolve_provider_reference       :190-196   (openai/convert.rs:297)
└─ build_responses_request_body_for                  :215 → convert.rs:1925
   ├─ convert_user_part                              convert.rs:739 → :765-767
   └─ map_shell_environment                          :1432 → :1465

Test: aimux-providers/tests/openai_upstream_e2e_test.rs::responses_missing_provider_reference_names_the_available_providers, on master it fails with "No file reference for openai" and "No skill reference for openai".

22. An unsupported reasoningEffortUpdate lists the efforts the model supports

Before: a request-level reasoningEffortUpdate outside the model's supported efforts warned "gpt-6-astra does not support reasoning effort none".

After: it warns "gpt-6-astra only supports the following reasoning efforts: low, medium, high, xhigh, max" and leaves the update out (responses/openai-responses-language-model.ts:455-492).

How it works today

build_responses_request_body_for                     openai/responses/convert.rs:1894
└─ reasoningEffortUpdate option                      :1948
   └─ let reason = …                                 :1951
      └─ else if supported_reasoning_efforts lacks the effort   :1959-1965
         └─ "{model_id} does not support reasoning effort {effort}"   :1963
            ← upstream: "{modelId} only supports the following reasoning efforts: …" (:459)

What this PR changes

build_responses_request_body_for                     openai/responses/convert.rs:1925
└─ reasoningEffortUpdate option                      :1979
   └─ let reason = …                                 :1982
      └─ supported_reasoning_efforts.filter(…).map(…)   :1990-1997
         └─ "{model_id} only supports the following reasoning efforts: {}" (values.join(", "))   :1994

Test: aimux-providers/tests/openai_upstream_e2e_test.rs::responses_unsupported_reasoning_effort_update_lists_the_supported_efforts, on master it fails with "gpt-6-astra does not support reasoning effort none".

23. Hosted tool search and program calls are replayed in full when responses are not stored

Before: with store: false, a provider-executed tool_search or program call in the history was dropped, so the request had an empty input for it. Only shell calls were rebuilt.

After: they are rebuilt as tool_search_call and program items, because upstream checks them before the provider-executed rule (responses/convert-to-openai-responses-input.ts:780-842).

How it works today

convert_responses_input_with_conversation            openai/responses/convert.rs:150
└─ assistant tool call                               :259
   ├─ provider_executed == Some(true)                :260
   │  └─ store || kind != "shell" → continue         :264-266   call dropped
   └─ if let Some(kind) = kind → provider_tool_call_input   :268, :281   never reached
      ← upstream: tool search and program are handled first (:780-842)

What this PR changes

convert_responses_input_with_conversation            openai/responses/convert.rs:150
└─ assistant tool call                               :259
   ├─ provider_executed and kind is not tool_search / programmatic_tool_calling   :262-267
   └─ if let Some(kind) = kind → provider_tool_call_input   :275, :288
      └─ tool_search_call and program items          with store false

Test: aimux-providers/tests/openai_upstream_e2e_test.rs::responses_replays_hosted_tool_search_and_program_calls_without_storage, on master it fails with an empty input instead of the two items.

24. A tool result sends its caller on the function_call_output

Before: the caller provider option was mapped only on function_call items. A tool result for a call made by a program lost it.

After: the result's function_call_output carries caller: {type: "program", caller_id} (responses/convert-to-openai-responses-input.ts:1646-1683, mapToolCaller at :58).

How it works today

convert_responses_input_with_conversation            openai/responses/convert.rs:150
├─ assistant tool call: inline caller mapping        :312-323   function_call only
└─ tool result                                       :478
   └─ input.push(function_call_output { call_id, output })   :521-525
      ← upstream: adds mapToolCaller(resultCaller) to the output item (:1676)

What this PR changes

convert_responses_input_with_conversation            openai/responses/convert.rs:150
├─ assistant tool call: tool_caller                  :319
└─ tool result                                       :475
   ├─ item = function_call_output { … }              :518-522
   ├─ !custom → tool_caller                          :523
   └─ input.push(item)                               :526
tool_caller (shared mapping, drops caller_id unless type is program)   :554

Test: aimux-providers/tests/openai_upstream_e2e_test.rs::responses_function_call_output_keeps_the_program_caller, on master it fails with a function_call_output that has no caller.

25. Unparsable stream events report the AI SDK parse error

Same change as #354, which has merged into master; this branch carried it so the chat and completion stream cases passed on their own, and after the master merge it adds nothing.

Before: an SSE event whose data is not JSON yielded JsonParse with only the parser's message.

After: the message is JSON parsing failed: Text: <text>.\nError message: <message>, as safeParseJSON reports it (@ai-sdk/provider src/errors/json-parse-error.ts:16-19). Valid JSON of the wrong shape stays InvalidResponseData.

How it works today

OpenAICompatibleChatModel::do_stream                 openai_compatible/chat.rs:316
└─ post_json_to_api(… create_event_source_response_handler)   :330, :333
   └─ create_event_source_response_handler            aimux-provider-utils/src/response_handler.rs:362
      └─ yield serde_json::from_str::<T>(&event.data).map_err(AiMuxError::from)   :413
         ← upstream: parseJsonEventStream wraps the text in JSONParseError

What this PR changes

create_event_source_response_handler                 aimux-provider-utils/src/response_handler.rs:376
└─ yield parse_event::<T>(&event.data)               :427
   └─ parse_event                                    :361
      └─ JsonParse message = "JSON parsing failed: Text: {data}.\nError message: {message}"

Test: aimux-provider-utils/tests/api_helpers_test.rs::event_source_handler_reports_unparsable_events_like_the_ai_sdk, on master it fails with the bare parser message key must be a string at line 1 column 2. The completion handler (openai_compatible/completion.rs:464-467) takes the same path.

26. The chat promptCacheRetention warning says "GPT-6 and later models"

Before: for models that take promptCacheOptions, the chat model dropped prompt_cache_retention but warned "not supported by sixth-generation and later models".

After: the warning text is "promptCacheRetention is not supported by GPT-6 and later models; use promptCacheOptions instead", as in chat/openai-chat-language-model.ts:235-245.

How it works today

OpenAIChatLanguageModel::do_generate                          openai/model.rs:166
└─ execute_generate                                           :221
   └─ build_request_body_with_chat_options                    :229 → convert.rs:1049
      ├─ supported_reasoning_efforts set and prompt_cache_retention in body   :1131
      ├─ body.remove("prompt_cache_retention")                  :1134
      └─ Warning::Unsupported { details: "…sixth-generation and later models…" }   :1135
         ← upstream: the same drop, but the text says "GPT-6 and later models"

What this PR changes

build_request_body_with_chat_options                          openai/convert.rs:1091
├─ supported_reasoning_efforts set and prompt_cache_retention in body   :1173
├─ body.remove("prompt_cache_retention")                      :1176
└─ Warning::Unsupported { details: "…GPT-6 and later models; use promptCacheOptions instead" }   :1177

Test: aimux-providers/tests/openai_upstream_e2e_test.rs::chat_prompt_cache_retention_warning_names_gpt_6_models, on master it fails because the details say "sixth-generation and later models".

27. Embedding dimensions is sent as the number it was given

Before: providerOptions.openai.dimensions was parsed into an f64, so 64 went out as 64.0.

After: it is kept as a JSON number and sent unchanged, so 64 stays 64 (embedding/openai-embedding-model.ts:94).

How it works today

OpenAIEmbeddingModel::do_embed                                openai/embedding.rs:53
├─ parse_openai_provider_options                              :63 → :146
│  └─ serde_json::from_value into OpenAIEmbeddingProviderOptions   :160 (field `dimensions: Option<f64>` :142)
└─ body.insert("dimensions", json!(dimensions))               :69-70   serialized as 64.0
   ← upstream: passes openaiOptions.dimensions through, so 64 is serialized as 64

What this PR changes

OpenAIEmbeddingModel::do_embed                                openai/embedding.rs:53
├─ parse_openai_provider_options                              :63 → :146
│  └─ field `dimensions: Option<serde_json::Number>`          :142
└─ body.insert("dimensions", json!(dimensions))               :69-70   serialized as 64

Test: aimux-providers/tests/openai_embedding_test.rs::should_pass_integer_dimensions_as_an_integer, on master it fails because the body has 64.0 where 64 is expected.

28. The evaluation model names its structured output "evaluation"

Before: DecisionLanguageModel asked for a JSON response named "decision", so the Responses request had text.format.name "decision".

After: the name is "evaluation" (openai-evaluation.test.ts:72-89).

Shared code: DecisionLanguageModel is in provider-utils, so this also reaches the Anthropic and Google evaluation models.

How it works today

DecisionLanguageModel::do_decide                              aimux-provider-utils/src/decision_language_model.rs:72
├─ call.response_format = Json { name: Some("decision") }     :187-189
└─ self.model.do_generate(&call)                              :198
   └─ OpenAIResponsesModel::do_generate → request_body        openai/responses/mod.rs:261 → :263
      └─ build_responses_request_body_for → apply_responses_text_format   responses/convert.rs:2015 → :1642
         └─ text.format.name = name                           :1666   "decision"
            ← upstream: EvaluationLanguageModel sends the name "evaluation"

What this PR changes

DecisionLanguageModel::do_decide                              aimux-provider-utils/src/decision_language_model.rs:72
├─ call.response_format = Json { name: Some("evaluation") }   :188-190
└─ self.model.do_generate(&call)                              :199
   └─ OpenAIResponsesModel::do_generate                       openai/responses/mod.rs:260
      └─ apply_responses_text_format                          responses/convert.rs:2046 → :1673
         └─ text.format.name                                  :1697   "evaluation"

Test: aimux-providers/tests/openai_decision_test.rs::evaluation_model_decides_through_the_responses_api, on master it fails with "decision" where "evaluation" is expected. The provider-utils test decision_language_model_test.rs::uses_flat_schema_with_internal_option_codes now expects the same name.

29. Image models report file and mask support by model id

Before: supports_file_inputs and supports_mask_inputs were not implemented, so every OpenAI and Azure image model reported unknown (None).

After: known editing ids (dall-e-2 and the listed gpt-image and chatgpt-image ids) report true, dall-e-3 reports false, other ids stay unknown, and masks follow files (image/openai-image-model.ts:61-92). Azure deployments stay unknown (azure-openai-provider.ts:372-376).

How it works today

image model info for the registry                             aimux-ops/src/ops/registry.rs:460
├─ m.supports_file_inputs() / m.supports_mask_inputs()
└─ impl ImageModel for OpenAIImageModel                       openai/image.rs:77   no override
   └─ trait default → None                                    aimux-core/src/image_model.rs:249, :254
      ← upstream: true for the listed ids, false for dall-e-3, undefined otherwise

What this PR changes

impl ImageModel for OpenAIImageModel                          openai/image.rs:109
├─ supports_file_inputs                                       :122
│  ├─ infer_input_capabilities set → supports_file_inputs(model_id)   :123 → :91   true / false / None
│  └─ otherwise None
└─ supports_mask_inputs → supports_file_inputs                :131
AzureOpenAIProvider::image                                    azure/mod.rs:498
└─ .with_unknown_input_capabilities()                         :505 → image.rs:84

Test: aimux-providers/tests/openai_image_test.rs::image_input_capabilities_follow_the_model_id, on master it fails because the capabilities are None where Some(true) is expected. azure_upstream_e2e_test.rs::image_input_capabilities_are_unknown_for_deployments pins the Azure side.

30. A singleton allOf: [{ $ref }] becomes a direct $ref

Before: the schema normalizer walked into allOf but left it in place, so OpenAI got an allOf it does not accept.

After: a schema whose allOf is exactly [{ $ref }] gets a direct $ref. At the root, a local #/definitions/ or #/$defs/ reference is expanded into the root object and the root's other keys are kept (normalize-openai-json-schema.ts:176-246).

How it works today

build_request_body_with_chat_options                          openai/convert.rs:1049
└─ apply_response_format                                      :1128 → :867
   └─ normalize_json_schema                                   :884 → :27
      ├─ walk(&mut result, &mut removed)                      :92 → :31
      └─ "allOf" is only a key whose children are walked      :71, :81   the array stays
         ← upstream: rewrites allOf [{ $ref }] to { $ref }, and expands it at the root

What this PR changes

apply_response_format                                         openai/convert.rs:909
└─ normalize_json_schema                                      :926 → :27
   ├─ walk(&mut result, &mut removed, true)                   :134 → :31   `is_root` flag
   └─ after the children are walked                           :91
      ├─ allOf is one item with only `$ref`                   :91-99
      ├─ not root: remove allOf, insert "$ref"                :101-103
      └─ root: local_reference(…) → merge the definition      :104-108 → :114

Test: aimux-providers/tests/openai_upstream_e2e_test.rs::chat_json_schema_unwraps_singleton_reference_all_of, on master it fails because the sent schema still has the allOf.

31. An empty or blank base URL fails with "baseURL must be a non-empty string."

Before: "" failed with "base URL cannot be empty", and a blank value such as " " got through that check and failed later as an unparsable URL.

After: both fail with "baseURL must be a non-empty string." (validateBaseURL as used in openai-provider.ts:207-215).

Shared code: validate_base_url is in provider-utils, so this reaches every provider's base URL check.

How it works today

create_openai                                                 openai/mod.rs:253
└─ shared::base_url_setting(explicit, …, validate_base_url)   :254 → shared/mod.rs:55
   └─ normalize(url)                                          :61-62
      └─ validate_base_url                                    aimux-provider-utils/src/url.rs:33
         ├─ url.is_empty() → "base URL cannot be empty"       :34-36   only for ""
         └─ Url::parse("   ") fails                           :39   "invalid base URL: …"
            ← upstream: "" and blank values both give "baseURL must be a non-empty string."

What this PR changes

validate_base_url                                             aimux-provider-utils/src/url.rs:34
├─ url.trim().is_empty()                                      :36
└─ "baseURL must be a non-empty string."                      :38

Test: aimux-providers/tests/openai_factory_test.rs::an_empty_base_url_fails_with_the_sdk_message, on master it fails with "base URL cannot be empty".

32. Transcription sends responseFormat and chunkingStrategy

Before: both options were ignored, and the diarize model got verbose_json, or no format at all without provider options.

After: responseFormat is sent. The diarize model defaults to diarized_json (also without provider options) and chunking_strategy defaults to auto. A server_vad object is sent with snake_case keys (transcription/openai-transcription-model.ts:196-259, options at openai-transcription-model-options.ts:51-70).

How it works today

OpenAITranscriptionModel::do_generate                         openai/transcription.rs:258
├─ parse_openai_options                                       :269 → :121   no responseFormat, no chunkingStrategy
├─ MultipartForm::new()                                       :277
├─ response_format: "json" or "verbose_json" when openai options exist   :296-302
└─ form.finish()                                              :334   no chunking_strategy field
   ← upstream: sends responseFormat and chunking_strategy

What this PR changes

OpenAITranscriptionModel::do_generate                         openai/transcription.rs:291
├─ parse_openai_options                                       :302 → :147
│  ├─ responseFormat                                          :184-185
│  └─ chunkingStrategy → chunking_strategy()                  :187 → :127
├─ response_format: option, else diarized_json for the diarize model   :330-342
├─ chunking_strategy: option, else "auto" for the diarize model   :373-378
└─ form.finish()                                              :381

Test: aimux-providers/tests/openai_transcription_test.rs::diarization_response_format_and_chunking_strategy, on master it fails because the form has no response_format field.

33. Diarized transcription speakers are returned in provider metadata

Before: segments were parsed as text, start and end only, and the result had no provider metadata.

After: segments that carry a speaker are returned in providerMetadata.openai.segments with text, startSecond, endSecond and speaker, when there is at least one (transcription/openai-transcription-model.ts:315-353).

How it works today

OpenAITranscriptionModel::do_generate                         openai/transcription.rs:258
├─ post_to_api → OpenAITranscriptionResponse                  :341
│  └─ OpenAISegment { text, start, end }                      :174   no speaker field
├─ segments → Vec<TranscriptionSegment>                       :363
└─ TranscriptionResult { provider_metadata: None }            :386, :399
   ← upstream: maps segments with a speaker to providerMetadata.openai.segments

What this PR changes

OpenAITranscriptionModel::do_generate                         openai/transcription.rs:291
├─ post_to_api → OpenAITranscriptionResponse                  :388
│  └─ OpenAISegment { …, speaker: Option<String> }            :204
├─ diarized_segments (only segments with a speaker)           :411
├─ provider_metadata = openai { segments } if not empty       :425-432
└─ TranscriptionResult { provider_metadata }                  :458, :471

Test: aimux-providers/tests/openai_transcription_test.rs::diarized_segments_carry_the_speaker_in_provider_metadata, on master it fails because the provider metadata is Null.

34. The realtime transcription key goes in a WebSocket subprotocol

Before: the stream connected with the Authorization header and no subprotocol.

After: the bearer token of the last authorization header (name and scheme case-insensitive) is offered as ["realtime", "openai-insecure-api-key.<token>"] and only that header is removed, because OpenAI rejects a handshake that sends both. Without a bearer header only realtime is offered and the headers are kept (transcription/openai-transcription-model.ts:620-652).

How it works today

OpenAITranscriptionModel::do_stream                           openai/transcription.rs:405
└─ stream_transcription                                       :414 → :425
   ├─ header_list = config.request_headers(…)                 :479
   ├─ WebSocketRequest { headers: header_list,
   │                     subprotocols: Vec::new() }           :492-495
   └─ ws_connect(&req)                                        :500
      ← upstream: getOpenAIRealtimeConnection moves the key into the subprotocols

What this PR changes

OpenAITranscriptionModel::do_stream                           openai/transcription.rs:477
└─ stream_transcription                                       :486 → :529
   ├─ header_list = config.request_headers(…)                 :621
   ├─ realtime_connection(header_list) → (subprotocols, headers)   :634 → :497
   ├─ WebSocketRequest { headers, subprotocols }              :635-638
   └─ ws_connect(&req)                                        :643

Test: aimux-providers/tests/openai_realtime_ws_replay_test.rs::realtime_transcription_replays_through_injected_web_socket, on master it fails because the connect request has no subprotocols ([]). The local test servers now select realtime, as the endpoint does.

35. Realtime session options come from streaming

Before: providerOptions.openai.streaming was ignored, and the REST include option was sent as session.include.

After: streaming.delay goes to session.audio.input.transcription.delay and streaming.include to session.include (transcription/openai-transcription-model.ts:580-617, options at openai-transcription-model-options.ts:72-88). The REST include no longer reaches the session.

How it works today

OpenAITranscriptionModel::stream_transcription                openai/transcription.rs:425
├─ parse_openai_options                                       :442 → :121   REST options only
├─ session_update = json!({ … })                              :451
└─ if let Some(include) = openai_options.include              :475-476
   └─ session.include = include                               the REST `include`
      ← upstream: reads `streaming.delay` and `streaming.include`

What this PR changes

OpenAITranscriptionModel::stream_transcription                openai/transcription.rs:529
├─ session_update = json!({ … })                              :576
├─ streaming = provider_options["openai"]["streaming"]        :603-607
├─ streaming.delay → session.audio.input.transcription.delay  :608-613
└─ streaming.include → session.include                        :614-618

Test: aimux-providers/tests/openai_realtime_ws_replay_test.rs::realtime_session_takes_the_streaming_options, on master it fails because the session update has no delay.

36. Realtime transcription warns about REST-only options

Before: stream-start always had an empty warning list, even when include, prompt, temperature or timestampGranularities were set.

After: stream-start carries one unsupported warning per set option: "OpenAI streaming transcription does not support ." The options are read from the raw providerOptions.openai (transcription/openai-transcription-model.ts:395-410).

How it works today

OpenAITranscriptionModel::stream_transcription                openai/transcription.rs:425
├─ parse_openai_options                                       :442
├─ session_update, WebSocketRequest, ws_connect               :451, :492, :500
└─ async_stream::stream!                                      :518
   └─ yield StreamStart { warnings: vec[] }                  :519
      ← upstream: puts an unsupported warning for each REST-only option set

What this PR changes

OpenAITranscriptionModel::stream_transcription                openai/transcription.rs:529
├─ raw_openai = provider_options["openai"]                    :550
├─ warnings: include, prompt, temperature, timestampGranularities
│  that are set → Warning::Unsupported "…does not support {option}."   :554-567
└─ yield StreamStart { warnings }                             :662

Test: aimux-providers/tests/openai_realtime_ws_replay_test.rs::realtime_stream_start_warns_about_rest_only_options, on master it fails because stream-start has no warnings ([]).

Notes for reviewers

Type of change

Checklist

  • cargo +1.99 fmt --all -- --check passes
  • cargo +1.99 clippy -p aimux-providers -p aimux-provider-utils -p aimux-ops --lib --tests --all-features -- -D warnings passes
  • cargo +1.99 test -p aimux-providers -p aimux-provider-utils -p aimux-ops --all-features: 166 test binaries, 2855 passed (after merging master)
  • scripts/check_provider_boundaries.sh passes
  • Commit messages follow Conventional Commits

Generated by Claude Code

cunninghamcard-bit and others added 30 commits October 24, 2026 11:34
… never started

@ai-sdk/openai 4.0.83 starts the stream's `responseId` at `null`
(openai-responses-language-model.ts:1587) and only `response.created` sets
it, so a stream without that event finishes with `responseId: null`. aimux
wrote an empty string. The generate path already writes `null`.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoxcKU4zBD64Lri3e4Lrq5
@ai-sdk/openai 4.0.83 puts the reasoning item's id on every
`reasoning-delta` as `providerMetadata.<key>.itemId`
(openai-responses-language-model.ts:2735-2749). aimux sent the deltas
without provider metadata, while its reasoning start and end parts
already carried it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoxcKU4zBD64Lri3e4Lrq5
@ai-sdk/openai 4.0.83 takes `maxNumResults` as a JS number and sends it as
is (openai-responses-prepare-tools.ts:219), so `5` reaches the API's integer
field as `5`. aimux typed the field `Option<f64>`, so the typed factory sent
`5.0`. It is an `Option<u32>` now; the other number fields of the tool
arguments that count things (`partial_images`, `output_compression`) are integers already.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoxcKU4zBD64Lri3e4Lrq5
@ai-sdk/openai 4.0.83 maps each computer action with mapComputerAction
(openai-responses-language-model.ts:128): it keeps the fields of the
action's type, renames the scroll offsets, and adds `keys` only when it
is not null. aimux copied the wire action and only renamed the scroll
offsets, so `"keys":null` (and any extra field) reached the tool input.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoxcKU4zBD64Lri3e4Lrq5
… names

@ai-sdk/openai 4.0.83 builds its tool name mapping from a fixed
providerToolNames table (openai-responses-language-model.ts:378): code
interpreter, computer, file search, image generation, local shell, shell,
web search (and preview), MCP, apply patch, tool search and programmatic
tool calling. aimux mapped the name of any `openai.*` provider tool, so
`toCustomToolName('computer_use')` (line 1358) returned the caller's name
for an `openai.computer_use` tool instead of `computer_use`.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoxcKU4zBD64Lri3e4Lrq5
@ai-sdk/openai 4.0.83 parses annotations with one zod object per type
(openai-responses-api.ts:1500-1535 for responses, 1366-1395 for the
annotation.added chunk), which drops unknown keys, and puts the parsed
annotations in the text part's provider metadata
(openai-responses-language-model.ts:1072). aimux copied the wire
annotation, so a container_file_citation kept an `index` field.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoxcKU4zBD64Lri3e4Lrq5
… order

@ai-sdk/openai 4.0.83 parses an apply_patch_call operation as {type, path,
diff} (openai-responses-api.ts:1698) and JSON.stringifies the parsed
operation into the tool input (openai-responses-language-model.ts:1443,
and the delete_file delta in the stream at line 1857), so the keys follow
the schema order. aimux stringified the wire object, keeping the order the
API sent.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoxcKU4zBD64Lri3e4Lrq5
@ai-sdk/openai 4.0.83 parses a local_shell_call action as {type, command,
timeout_ms, user, working_directory, env} (openai-responses-api.ts:147) and
stringifies the parsed action into the generated tool input
(openai-responses-language-model.ts:998); the stream writes {type, command,
timeoutMs, user, workingDirectory, env} (line 2435). aimux stringified the
wire action, and in the stream moved the renamed keys to the end.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoxcKU4zBD64Lri3e4Lrq5
@ai-sdk/openai 4.0.83 lets a message's content hold only output_text parts
(openai-responses-api.ts:1486), so a refusal part fails the response schema
and createJsonResponseHandler rejects the call with an APICallError
("Invalid JSON response"); openai-evaluation.test.ts expects doEvaluate to
reject with APICallError for it. aimux skipped non-output_text parts and
returned a result without them. The stream schema does not validate message
content (openai-responses-api.ts:1098), so streaming is unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoxcKU4zBD64Lri3e4Lrq5
@ai-sdk/openai 4.0.83 parses Responses logprobs as {token, logprob,
top_logprobs: [{token, logprob}]} (openai-responses-api.ts:875 chunk,
:1490 response), dropping other keys, and pushes the parsed value into
providerMetadata.openai.logprobs (openai-responses-language-model.ts:1065
generate, :2685 stream). aimux pushed the wire value, `bytes` included.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoxcKU4zBD64Lri3e4Lrq5
…ut-end

@ai-sdk/openai 4.0.83 enqueues the tool-input-end of a streamed
function_call with providerMetadata { namespace } when the item has one
(openai-responses-language-model.ts:1986). aimux sent the tool-input-end
without provider metadata and put the namespace only on the tool-call.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoxcKU4zBD64Lri3e4Lrq5
…m id only

@ai-sdk/openai 4.0.83 ends a summary part concluded when the next part is
added (openai-responses-language-model.ts:2709) or on
reasoning_summary_part.done with store (:2762) with providerMetadata
{ itemId }; only the ends sent on output_item.done (:2523) carry
reasoningEncryptedContent. aimux added reasoningEncryptedContent to the
early ends as well.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoxcKU4zBD64Lri3e4Lrq5
… index

@ai-sdk/openai 4.0.83 records the id each output item started with by
output_index (openai-responses-language-model.ts:1633-1644, set at :1905
for reasoning) and resolves reasoning_summary_part.added (:2688),
reasoning_summary_text.delta (:2736), reasoning_summary_part.done (:2751)
and the reasoning output_item.done (:2503) through it. aimux did this only
for messages, so rotated reasoning item ids split one reasoning part into
several.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoxcKU4zBD64Lri3e4Lrq5
@ai-sdk/openai 4.0.83 sends an error part and finishes with error for a
chunk that fails the Responses schema
(openai-responses-language-model.ts:1664-1680); when the chunk looks like a
Chat Completions chunk (a choices array and no string type, :2962) the
error is an APICallError explaining that the endpoint speaks Chat
Completions (:2972). aimux ignored the chunk as an unknown event.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoxcKU4zBD64Lri3e4Lrq5
…rrors

@ai-sdk/openai 4.0.83 parses each stream chunk with openaiResponsesChunkSchema
(openai-responses-api.ts:869-1443); a modeled event that fails its schema
is not matched by the unknown-chunk fallback (:1433-1441), so doStream sends
the TypeValidationError as an error part and finishes with error
(openai-responses-language-model.ts:1664-1680). aimux read events as loose
JSON: a missing output_index defaulted to 0, a missing item was skipped.

This checks the envelope fields of each modeled event (type, output_index,
item and its type, item_id, the delta/arguments/code/diff string,
summary_index, the response object); item internals stay unchecked.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoxcKU4zBD64Lri3e4Lrq5
@ai-sdk/openai 4.0.83 passes the Responses event stream through
throwIfOpenAIStreamErrorBeforeOutput (openai-stream-error.ts:39, called at
openai-responses-language-model.ts:1552): it reads events until the first
output event (isResponseOutputChunk, :3112), throws an APICallError for an
error frame or a response.failed with an error, stops at a chunk that fails
parsing, and after response.in_progress races each read against 50 ms.
aimux only checked the first event, so an error after response.created
became an error part of a resolved stream.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoxcKU4zBD64Lri3e4Lrq5
…name and the GPT-6 requirement

Upstream resolveAsyncToolOption (src/responses/openai-responses-prepare-tools.ts:684-688)
warns with feature `async tool calling for "<name>"` and details
"Async tool calling is only supported by GPT-6 and later models.". aimux
left the name unquoted and said "Async tool calling is not supported by
this model."

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoxcKU4zBD64Lri3e4Lrq5
…ream reasons

Upstream prepareResponsesTools (src/responses/openai-responses-prepare-tools.ts:183-202,
:454-504, :654-660) warns with feature `allowedTools entry "<name>"` and
separate reasons for a namespaced tool ("tools inside an OpenAI tool
namespace are not visible ..."), a deferred tool ("deferred tools are not
visible ..."), an unlistable provider tool ("OpenAI does not support
<type> tools in tool_choice.allowed_tools") and an ambiguous provider tool
name. aimux left the name unquoted, merged the namespace and deferred
reasons, and worded the tool_search and ambiguity details differently.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoxcKU4zBD64Lri3e4Lrq5
…be expressed

Upstream prepareResponsesTools (src/responses/openai-responses-prepare-tools.ts:509-515)
throws UnsupportedFunctionalityError "allowedTools with only tools that
cannot be allow-listed (<dropped names joined by ", ">)". aimux threw the
message without the list.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoxcKU4zBD64Lri3e4Lrq5
…scriptions error

Upstream prepareResponsesTools (src/responses/openai-responses-prepare-tools.ts:174-178)
throws UnsupportedFunctionalityError `conflicting descriptions for OpenAI
tool namespace "<name>"`. aimux left the name unquoted.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoxcKU4zBD64Lri3e4Lrq5
…ill reference has no openai entry

Upstream resolves file references (src/responses/convert-to-openai-responses-input.ts:142-145,
:548-551) and shell skill references (src/responses/openai-responses-prepare-tools.ts:780-783)
with resolveProviderReference, whose NoSuchProviderReferenceError says
"No provider reference found for provider '<provider>'. Available
providers: <keys>", as the chat conversion in aimux already does. The
Responses code said "No file reference for openai" and "No skill
reference for openai".

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoxcKU4zBD64Lri3e4Lrq5
…is not supported by the model

Upstream (src/responses/openai-responses-language-model.ts:455-492) warns
about a request-level reasoningEffortUpdate outside the model's supported
efforts with "<model> only supports the following reasoning efforts:
<efforts joined by ", ">". aimux said "<model> does not support reasoning
effort <effort>".

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoxcKU4zBD64Lri3e4Lrq5
…n full without response storage

Upstream convertToOpenAIResponsesInput (src/responses/convert-to-openai-responses-input.ts:780-842)
handles tool search and programmatic tool calls before the
providerExecuted check: with store false they are rebuilt as
tool_search_call / program items. aimux applied the provider-executed rule
first and dropped them (only shell calls were rebuilt).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoxcKU4zBD64Lri3e4Lrq5
…tput

Upstream convertToOpenAIResponsesInput (src/responses/convert-to-openai-responses-input.ts:1646-1683)
maps the caller provider option of a tool result with mapToolCaller and
adds it to the function_call_output item. aimux mapped the caller only on
function_call items.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoxcKU4zBD64Lri3e4Lrq5
… error

A streamed SSE event whose data is not JSON yielded `JsonParse` with only
the parser's message. The AI SDK's `parseJsonEventStream` reads each event
with `safeParseJSON`, whose `JSONParseError` reads
`JSON parsing failed: Text: <text>.\nError message: <message>`
(`@ai-sdk/provider` src/errors/json-parse-error.ts:16-19). Found while
porting the `@ai-sdk/openai-compatible` 3.0.62 tests (`doStream › should
handle unparsable stream parts`, chat and completion); every stream read
through `create_event_source_response_handler` now reports it the same way.
Well-formed JSON of the wrong shape stays `InvalidResponseData`.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoxcKU4zBD64Lri3e4Lrq5
Upstream chat/openai-chat-language-model.ts:235-245 drops
prompt_cache_retention for models with supportedReasoningEfforts and warns
"promptCacheRetention is not supported by GPT-6 and later models; use
promptCacheOptions instead". aimux dropped the field but said
"sixth-generation and later models".

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoxcKU4zBD64Lri3e4Lrq5
Upstream embedding/openai-embedding-model.ts:94 passes
openaiOptions.dimensions through unchanged, so 64 is serialized as 64.
aimux parsed it into an f64 and sent 64.0.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoxcKU4zBD64Lri3e4Lrq5
Upstream src/openai-evaluation.test.ts:72-89 expects the Responses request
of provider.evaluationModel() to carry text.format.name "evaluation"
(EvaluationLanguageModel from provider-utils; its own test,
evaluation-language-model.test.ts:87, expects responseFormat.name
"evaluation"). aimux's shared DecisionLanguageModel sent "decision", for
OpenAI, Anthropic and Google alike.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoxcKU4zBD64Lri3e4Lrq5
Upstream image/openai-image-model.ts:61-92 returns supportsFileInputs true
for dall-e-2 and its listed gpt-image/chatgpt-image ids, false for
dall-e-3, undefined otherwise, and supportsMaskInputs follows it; Azure
passes imageInputCapabilities with undefined values
(azure-openai-provider.ts:372-376). aimux used the trait default (unknown)
for every model.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoxcKU4zBD64Lri3e4Lrq5
Upstream normalize-openai-json-schema.ts:176-246 rewrites a schema whose
allOf is exactly [{ $ref }] to a direct $ref, and at the root expands a
local #/definitions or #/$defs reference into the root object (keeping the
root's other keys). aimux's normalize_json_schema left the allOf in place.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoxcKU4zBD64Lri3e4Lrq5
cunninghamcard-bit and others added 6 commits October 24, 2026 11:52
… string."

Upstream src/openai-provider.ts:207-215 passes the base URL through
provider-utils validateBaseURL, which throws InvalidArgumentError
"baseURL must be a non-empty string." for "" and blank values
(validate-base-url.test.ts). aimux's validate_base_url said "base URL
cannot be empty" and only for "", so a blank value surfaced as an
unparsable URL.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoxcKU4zBD64Lri3e4Lrq5
Upstream transcription/openai-transcription-model.ts:196-259 sends
responseFormat (default diarized_json for gpt-4o-transcribe-diarize, also
without provider options) and chunking_strategy ("auto", the diarize
model's default, or a server_vad object serialized with snake_case keys);
the options are declared in openai-transcription-model-options.ts:51-70.
aimux ignored both options and sent verbose_json or nothing for the
diarize model.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoxcKU4zBD64Lri3e4Lrq5
Upstream transcription/openai-transcription-model.ts:315-353 maps response
segments that carry a speaker (openai-transcription-api.ts:34-41) to
providerMetadata.openai.segments with text, startSecond, endSecond and
speaker, when there is at least one. aimux parsed only text/start/end and
returned no provider metadata.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoxcKU4zBD64Lri3e4Lrq5
…otocol

Upstream transcription/openai-transcription-model.ts:620-652
(getOpenAIRealtimeConnection) takes the bearer token of the last
authorization header (case-insensitive name and scheme), offers the
subprotocols ["realtime", "openai-insecure-api-key.<token>"] and strips
only the authorization header, since OpenAI rejects handshakes that send
both auth channels; without a bearer header it offers ["realtime"] and
keeps the headers. aimux sent the Authorization header and no subprotocol.
The local test servers now select "realtime", as the endpoint does.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoxcKU4zBD64Lri3e4Lrq5
Upstream transcription/openai-transcription-model.ts:580-617
(buildOpenAIRealtimeTranscriptionSession) adds
providerOptions.openai.streaming.delay to
session.audio.input.transcription.delay and streaming.include to
session.include (options declared in openai-transcription-model-options.ts:72-88).
aimux ignored streaming and sent the REST include option as session.include.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoxcKU4zBD64Lri3e4Lrq5
…treams

Upstream transcription/openai-transcription-model.ts:395-410 checks the raw
providerOptions.openai for include, prompt, temperature and
timestampGranularities and puts an unsupported warning
"OpenAI streaming transcription does not support <option>." for each one
set into stream-start. aimux sent stream-start with no warnings.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoxcKU4zBD64Lri3e4Lrq5
…erver

OpenAI realtime transcription now offers the `realtime` subprotocol (with the key as `openai-insecure-api-key.<token>`), and a WebSocket client fails when its offer gets no answer, as the realtime API's own server answers it. The fixture server in ops_fixtures.py sent no Sec-WebSocket-Protocol, so test_gil_release's operation_next case failed with "Server sent no subprotocol".

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoxcKU4zBD64Lri3e4Lrq5
@cunninghamcard-bit
cunninghamcard-bit marked this pull request as ready for review October 24, 2026 12:24
cunninghamcard-bit and others added 3 commits October 25, 2026 08:08
The reasoning delta now resolves its item id through output_index (commit "correlate rotated Responses reasoning item ids by output index"), so its test sends rotated ids on the summary events and expects the id the item started with, as asked on #323.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoxcKU4zBD64Lri3e4Lrq5
…er Breaking

FileSearchArgs::max_num_results changed from Option<f64> to Option<u32>, a change to a public type, so its entry belongs under Breaking rather than Fixed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoxcKU4zBD64Lri3e4Lrq5
…s-chat-transcription-and-image-behaviour-match-the-ai-sdk

# Conflicts:
#	CHANGELOG.md

@eric8810 eric8810 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OpenAI 行为修复与 pinned AI SDK 一致,但 PR 当前 DIRTY;且包含已合入 #354 的 response_handler commit,需要 drop duplicate,并 rebase 到 master。另 FileSearchArgs.max_num_results 的 f64→u32 是公开破坏性变化,应移到 CHANGELOG Breaking 或改回兼容 serializer。

…as it

#354 merged the same entry that commit 25 carried. The master merge kept
this branch's copy at its old place, so the diff against master moved it;
it now sits where master has it and the diff no longer touches it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoxcKU4zBD64Lri3e4Lrq5

Copy link
Copy Markdown
Contributor Author
eric 的意见(原话) 处理
「PR 当前 DIRTY…并 rebase 到 master」 9b19caa 合入了最新 master(5ea86ed7),冲突只在 CHANGELOG。按项目约定不改写已推送的历史(不 rebase、不 force-push),所以用的是 merge
「包含已合入 #354 的 response_handler commit,需要 drop duplicate」 提交 25(2bcf0983)留在历史里,但合入 master 后它对 master 已是空改动:aimux-provider-utils/src/response_handler.rs 与 master 完全相同。1efda6ef 再把那条 CHANGELOG 放回 master 的位置,现在对 master 的 diff 里不再有 #354 的任何内容。PR 描述第 25 节和开头已注明
「FileSearchArgs.max_num_results 的 f64→u32 是公开破坏性变化,应移到 CHANGELOG Breaking 或改回兼容 serializer」 选了移到 Breaking:b6cf2de7,与 #323 的 5d4c318 同一条,写明调用方现在要传整数。没改回兼容 serializer,因为上游发的就是整数 5,Option<u32> 在类型上就保证了这一点

另:37136f34 让 reasoning delta 的测试也用轮换的 item id 加 output_index: 0,与 #323 按你的意见改的一致。本地全量检查通过(166 个测试二进制,2855 个测试)。#369 已合入本分支的最新提交。


Generated by Claude Code


Generated by Claude Code

@eric8810 eric8810 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dedup/rebase 后无 #354 diff;FileSearchArgs 变更已列为 Breaking;schema 行为与 pinned OpenAI 一致,CI 16/16 通过。Approve。

@eric8810
eric8810 merged commit 4d8ae89 into master Oct 25, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants