Repository navigation
fix: OpenAI Responses, chat, transcription and image behaviour match the AI SDK - #368
Merged
eric8810 merged 41 commits intoOct 25, 2026
Conversation
… 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
… 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
This was referenced Oct 24, 2026
…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
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
…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
Contributor
Author
另:37136f34 让 reasoning delta 的测试也用轮换的 item id 加 Generated by Claude Code Generated by Claude Code |
This was referenced Oct 25, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes the 36 behaviour differences found while porting the upstream OpenAI tests (
@ai-sdk/openai4.0.83). Each difference is its own commit with a test that fails on master and a CHANGELOG entry (Fixed, except #3 underBreaking). 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.b877402aresponse.createdfinishes withresponseId: null98224707f1525842max_num_resultsas an integeraa74417d5307efe8f4c7653418b47819017c5f8f023ee862output_textrejects the response2c19e2b8bytes5616957dtool-input-endcarries the namespace9a809298dc5919f33f75546dfe9790d74f35f4281cf10d1c1be94f430b52c20c019b1e8e03be7cec61411ca1ac39bf740716f8f02bcf098314c8db99promptCacheRetentionwarning says "GPT-6 and later models"781c201ddimensionsis sent as the number it was given1f110cabde606beb692b6020allOf: [{ $ref }]becomes a direct$refd337a6eda4979583responseFormatandchunkingStrategy505a6123520caebae33ad9d7streaming68ce701fWhere the changes sit
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.createdfinishes withresponseId: nullSame 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 noresponse.createdevent arrived. The generate path already wrotenull.After:
responseIdstaysnulluntilresponse.createdsets it (responses/openai-responses-language-model.ts:1587, set at:2663).How it works today
What this PR changes
Test:
openai_upstream_e2e_test.rs::a_stream_without_response_created_finishes_with_a_null_response_id, on master it fails withresponseIdequal 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-deltaparts had no provider metadata, while the start and end parts already haditemId.After: every
reasoning-deltahasproviderMetadata.<key>.itemId(responses/openai-responses-language-model.ts:2735-2749).How it works today
What this PR changes
Test:
openai_upstream_e2e_test.rs::reasoning_deltas_carry_the_item_id, on master it fails withnullmetadata instead of{"openai":{"itemId":"rs_1"}}.3. The file search tool sends
max_num_resultsas an integerSame change as #323, which this PR carries so the OpenAI tests pass on its own.
Before:
FileSearchArgs::max_num_resultswas anOption<f64>, somaxNumResults: 5went out as5.0.After: it is an
Option<u32>and goes out as5, as the AI SDK sends the number as is (responses/openai-responses-prepare-tools.ts:219).How it works today
What this PR changes
Test:
openai_upstream_e2e_test.rs::file_search_sends_max_num_results_as_an_integer, on master it fails with5.0instead of5.4. Computer actions leave out null keys and unknown fields
Before: aimux copied the wire action and only renamed the scroll offsets, so
"keys": nulland 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_xandscroll_y, and addskeysonly when it is not null (responses/openai-responses-language-model.ts:128,mapComputerAction).How it works today
What this PR changes
Test:
openai_upstream_e2e_test.rs::computer_actions_leave_out_null_keys, on master it fails with"keys":nullin 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 acall_idthen came out ascomputerUse.After: only the twelve names in the AI SDK's
providerToolNamesare mapped, socomputer_usekeeps its canonical name (responses/openai-responses-language-model.ts:378,:1358).How it works today
What this PR changes
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_citationkept an extraindexfield.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
What this PR changes
Test:
openai_upstream_e2e_test.rs::text_annotations_keep_only_the_fields_of_their_type, on master it fails with"index": 2still 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
diffcould come beforetype.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
What this PR changes
Test:
openai_upstream_e2e_test.rs::apply_patch_input_lists_operation_fields_in_schema_order, on master it fails withdifflisted beforetypein 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
timeoutMsandworkingDirectorykeys 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
What this PR changes
Test:
openai_upstream_e2e_test.rs::local_shell_input_lists_action_fields_in_schema_order, on master it fails withenvlisted beforeworking_directory.9. A Responses message part that is not
output_textrejects the responseBefore:
do_generateskipped any message content part that was notoutput_text, so arefusalpart gave anOkresult with empty content.After: it fails with an
ApiCallerror "Invalid JSON response", as the AI SDK's response schema allows onlyoutput_textparts (responses/openai-responses-api.ts:1486). The stream is unchanged, because its schema does not check message content (:1098).How it works today
What this PR changes
Test:
openai_upstream_e2e_test.rs::a_refusal_message_part_rejects_the_response, on master it fails becausedo_generatereturnsOkwith empty content.10. Responses logprobs metadata has no
bytesBefore: the wire logprobs went into
providerMetadata.openai.logprobsunchanged,bytesincluded.After: each entry keeps
token,logprobandtop_logprobs(withtokenandlogprob), for generate and stream (responses/openai-responses-api.ts:875,:1490;responses/openai-responses-language-model.ts:1065,:2685).How it works today
What this PR changes
Test:
openai_upstream_e2e_test.rs::responses_logprobs_metadata_has_no_bytes, on master it fails withbytesstill in the metadata, in both generate and stream.11. A streamed function call's
tool-input-endcarries the namespaceBefore:
tool-input-endhad no provider metadata, and the namespace was only on thetool-call.After: when the item has a
namespace,tool-input-endhasproviderMetadata.<key>.namespace(responses/openai-responses-language-model.ts:1986).How it works today
What this PR changes
Test:
openai_upstream_e2e_test.rs::a_streamed_namespaced_function_call_ends_its_input_with_the_namespace, on master it fails withnullmetadata 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.donewithstore) carriedreasoningEncryptedContent.After: those ends carry
{ itemId }only. The ends sent onoutput_item.donestill carry the encrypted content (responses/openai-responses-language-model.ts:2709,:2762,:2523).How it works today
What this PR changes
Test:
openai_upstream_e2e_test.rs::early_reasoning_summary_ends_carry_only_the_item_id, on master it fails because thers_1:0end hasreasoningEncryptedContent.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_indexand 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
What this PR changes
Test:
openai_upstream_e2e_test.rs::rotated_reasoning_item_ids_are_correlated_by_output_index, on master it fails with ids such asrs_3:0instead of oners_1:0part.14. A Chat Completions chunk on a Responses stream gives a helpful error
Before: a chunk with a
choicesarray and no stringtypewas ignored as an unknown event, so a wrongbaseURLgave an empty stream.After: it gives an error part with an
ApiCallerror that says the endpoint speaks Chat Completions, and the stream finishes witherror(responses/openai-responses-language-model.ts:1664-1680,:2962,:2972).How it works today
What this PR changes
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_indexdefaulted to 0 and a missingitemwas skipped, so no error was reported.After: a modeled event that misses an envelope field (
type,output_index,itemand itstype,item_id, the delta orarguments,summary_index,response) is an error part, and the stream finishes witherror(responses/openai-responses-api.ts:1433-1441,responses/openai-responses-language-model.ts:1664-1680). Item internals stay unchecked.How it works today
What this PR changes
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
errorframe afterresponse.createdbecame an error part of a stream that had already resolved.After:
do_streamreads events until the first output event and rejects with anApiCallerror for anerrorframe or aresponse.failedwith an error. Afterresponse.in_progresseach read waits at most 50 ms (openai-stream-error.ts:39, called atresponses/openai-responses-language-model.ts:1552).How it works today
What this PR changes
Test:
openai_upstream_e2e_test.rs::a_responses_error_before_output_after_response_created_rejects_the_call, on master it fails becausedo_streamresolved 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: trueon a model without async tool calling lost the flag, with the warningasync tool calling for weatherand 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
What this PR changes
Test:
aimux-providers/tests/openai_upstream_e2e_test.rs::responses_async_tool_warning_names_the_gpt_6_requirement, on master it fails with featureasync tool calling for weatherand the old details text.18. allowedTools warnings quote the entry and give the upstream reason
Before:
allowedToolswarnings used the bare name, and namespaced and deferred tools shared one reason.After: the feature is
allowedTools entry "<name>", and namespaced, deferred,tool_searchand ambiguous entries each get upstream's reason (responses/openai-responses-prepare-tools.ts:465-499).How it works today
What this PR changes
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 featureallowedTools entry deferredand 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
allowedToolsentry 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
What this PR changes
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
What this PR changes
Test:
aimux-providers/tests/openai_upstream_e2e_test.rs::responses_conflicting_namespace_descriptions_quote_the_namespace, on master it fails with the unquotedcrm.21. A missing openai file or skill reference names the available providers
Before: a Responses file part or shell skill reference with no
openaientry 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
What this PR changes
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
reasoningEffortUpdateoutside 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
What this PR changes
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-executedtool_searchorprogramcall 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_callandprogramitems, because upstream checks them before the provider-executed rule (responses/convert-to-openai-responses-input.ts:780-842).How it works today
What this PR changes
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 emptyinputinstead of the two items.24. A tool result sends its caller on the function_call_output
Before: the
callerprovider option was mapped only onfunction_callitems. A tool result for a call made by a program lost it.After: the result's
function_call_outputcarriescaller: {type: "program", caller_id}(responses/convert-to-openai-responses-input.ts:1646-1683,mapToolCallerat:58).How it works today
What this PR changes
Test:
aimux-providers/tests/openai_upstream_e2e_test.rs::responses_function_call_output_keeps_the_program_caller, on master it fails with afunction_call_outputthat has nocaller.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
JsonParsewith only the parser's message.After: the message is
JSON parsing failed: Text: <text>.\nError message: <message>, assafeParseJSONreports it (@ai-sdk/providersrc/errors/json-parse-error.ts:16-19). Valid JSON of the wrong shape staysInvalidResponseData.How it works today
What this PR changes
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 messagekey 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
promptCacheRetentionwarning says "GPT-6 and later models"Before: for models that take
promptCacheOptions, the chat model droppedprompt_cache_retentionbut 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
What this PR changes
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
dimensionsis sent as the number it was givenBefore:
providerOptions.openai.dimensionswas parsed into anf64, so64went out as64.0.After: it is kept as a JSON number and sent unchanged, so
64stays64(embedding/openai-embedding-model.ts:94).How it works today
What this PR changes
Test:
aimux-providers/tests/openai_embedding_test.rs::should_pass_integer_dimensions_as_an_integer, on master it fails because the body has64.0where64is expected.28. The evaluation model names its structured output "evaluation"
Before:
DecisionLanguageModelasked for a JSON response named "decision", so the Responses request hadtext.format.name"decision".After: the name is "evaluation" (
openai-evaluation.test.ts:72-89).Shared code:
DecisionLanguageModelis in provider-utils, so this also reaches the Anthropic and Google evaluation models.How it works today
What this PR changes
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 testdecision_language_model_test.rs::uses_flat_schema_with_internal_option_codesnow expects the same name.29. Image models report file and mask support by model id
Before:
supports_file_inputsandsupports_mask_inputswere not implemented, so every OpenAI and Azure image model reported unknown (None).After: known editing ids (
dall-e-2and the listedgpt-imageandchatgpt-imageids) reporttrue,dall-e-3reportsfalse, 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
What this PR changes
Test:
aimux-providers/tests/openai_image_test.rs::image_input_capabilities_follow_the_model_id, on master it fails because the capabilities areNonewhereSome(true)is expected.azure_upstream_e2e_test.rs::image_input_capabilities_are_unknown_for_deploymentspins the Azure side.30. A singleton
allOf: [{ $ref }]becomes a direct$refBefore: the schema normalizer walked into
allOfbut left it in place, so OpenAI got anallOfit does not accept.After: a schema whose
allOfis 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
What this PR changes
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 theallOf.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." (
validateBaseURLas used inopenai-provider.ts:207-215).Shared code:
validate_base_urlis in provider-utils, so this reaches every provider's base URL check.How it works today
What this PR changes
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
responseFormatandchunkingStrategyBefore: both options were ignored, and the diarize model got
verbose_json, or no format at all without provider options.After:
responseFormatis sent. The diarize model defaults todiarized_json(also without provider options) andchunking_strategydefaults toauto. Aserver_vadobject is sent with snake_case keys (transcription/openai-transcription-model.ts:196-259, options atopenai-transcription-model-options.ts:51-70).How it works today
What this PR changes
Test:
aimux-providers/tests/openai_transcription_test.rs::diarization_response_format_and_chunking_strategy, on master it fails because the form has noresponse_formatfield.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.segmentswithtext,startSecond,endSecondandspeaker, when there is at least one (transcription/openai-transcription-model.ts:315-353).How it works today
What this PR changes
Test:
aimux-providers/tests/openai_transcription_test.rs::diarized_segments_carry_the_speaker_in_provider_metadata, on master it fails because the provider metadata isNull.34. The realtime transcription key goes in a WebSocket subprotocol
Before: the stream connected with the
Authorizationheader and no subprotocol.After: the bearer token of the last
authorizationheader (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 onlyrealtimeis offered and the headers are kept (transcription/openai-transcription-model.ts:620-652).How it works today
What this PR changes
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 selectrealtime, as the endpoint does.35. Realtime session options come from
streamingBefore:
providerOptions.openai.streamingwas ignored, and the RESTincludeoption was sent assession.include.After:
streaming.delaygoes tosession.audio.input.transcription.delayandstreaming.includetosession.include(transcription/openai-transcription-model.ts:580-617, options atopenai-transcription-model-options.ts:72-88). The RESTincludeno longer reaches the session.How it works today
What this PR changes
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 nodelay.36. Realtime transcription warns about REST-only options
Before:
stream-startalways had an empty warning list, even wheninclude,prompt,temperatureortimestampGranularitieswere set.After:
stream-startcarries one unsupported warning per set option: "OpenAI streaming transcription does not support ." The options are read from the rawproviderOptions.openai(transcription/openai-transcription-model.ts:395-410).How it works today
What this PR changes
Test:
aimux-providers/tests/openai_realtime_ws_replay_test.rs::realtime_stream_start_warns_about_rest_only_options, on master it fails becausestream-starthas no warnings ([]).Notes for reviewers
37136f34makes the reasoning delta test use rotated item ids withoutput_index: 0(the code already resolves the id by output index, as upstreamresolveOutputItemId), andb6cf2de7moves theFileSearchArgs::max_num_resultsentry (RFC-0014: 统一日志体系(tracing 接通) #3,Option<f64>toOption<u32>) fromFixedtoBreakingin the CHANGELOG.41a66c7atest(python): answer the realtime subprotocol in the fake WebSocket server, goes with chore: post-#21 ffi cleanup + ProviderName generator drift fix #34: the Python binding's fake realtime server inbindings/python/tests/ops_fixtures.pynow answers therealtimesubprotocol the client offers, as the realtime API does; without ittest_gil_release[operation_next]failed with "Server sent no subprotocol".DecisionLanguageModel(OpenAI, Anthropic, Google); fix(jvm): route SNAPSHOT vs release to correct portal repo #31 changes the message of every provider's empty base URL check; iOS: CocoaPods fails to link vendored aimux_ffi.xcframework ('Framework aimux_ffi not found') #25 is the provider-utils SSE parser used by every vendor. Their tests and the fullaimux-providers/aimux-provider-utils/aimux-opssuites pass.layout.toml(Provider 构造与配置:迁移后遗留缺口 #275): Files get/download/delete and stream uploads,paralleltool call wrapper expansion,provider_metadataon prompt tool parts, a message field onUnsupportedFunctionality, embedding input byte limit, transcriptionusage, and the live, realtime, batch, skills and speech translation modules.Usage.rawstays verbatim (RFC-0016), an invalidOPENAI_BASE_URLfails the call rather than the factory, WebSocket transcription connects beforedo_streamreturns (RFC-0028), and a lateresponse.failedis compared throughApiCallError.Type of change
FileSearchArgs::max_num_resultsis anOption<u32>)Checklist
cargo +1.99 fmt --all -- --checkpassescargo +1.99 clippy -p aimux-providers -p aimux-provider-utils -p aimux-ops --lib --tests --all-features -- -D warningspassescargo +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.shpassesGenerated by Claude Code