Skip to content

fix: OpenAI Responses reasoning deltas carry the item id - #323

Closed
cunninghamcard-bit wants to merge 7 commits into
masterfrom
fix/openai-responses-reasoning-deltas-carry-the-item-id
Closed

cunninghamcard-bit wants to merge 7 commits into
masterfrom
fix/openai-responses-reasoning-deltas-carry-the-item-id

Conversation

@cunninghamcard-bit

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

Copy link
Copy Markdown
Contributor

Summary

Fixes the 3 behaviour differences in the OpenAI code that Azure reuses, found while porting the upstream Azure tests. Each difference is its own commit (listed below) and adds a test that fails on master and a CHANGELOG Fixed entry. They were separate PRs (#321, #322 and this one). To keep one fix PR per vendor, the others are closed and this PR carries all of them. The Azure test PR (#324) stacks on this branch. Part of the #276 list.

# Commit Difference Was
1 cc72496c OpenAI file search sends max_num_results as an integer #321
2 595eedc2 OpenAI Responses stream reports a null response id #322
3 5d54de30 OpenAI Responses reasoning deltas carry the item id #323

1. OpenAI file search sends max_num_results as an integer

Before: the typed OpenAI file search factory (openai::tools::file_search, and provider.tools.file_search on OpenAI and Azure) took max_num_results as an f64, so maxNumResults: 5 went out on the wire as "max_num_results": 5.0.

After: as in @ai-sdk/openai 4.0.83, it goes out as 5. FileSearchArgs::max_num_results is an Option<u32>; partial_images and output_compression in the same file are integers already.

Found while porting the upstream Azure tests (cases responses › doGenerate › file search tool › … › should send request body with tool and should send include provider option for file search results).

How it works today

openai::tools::file_search(FileSearchArgs { max_num_results: Option<f64>, .. })   aimux-providers/src/openai/tools.rs:199
└─ provider_tool("file_search", args) → serde_json::to_value → { "maxNumResults": 5.0 }   tools.rs:557
OpenAIResponsesLanguageModel::do_generate / do_stream
└─ prepare_responses_tools → provider tool "file_search"
   └─ copies maxNumResults → max_num_results as is                                   responses/convert.rs:1254
      → body.tools[0].max_num_results = 5.0      ← upstream: z.number() (tool/file-search.ts:29), sent as is
                                                   (openai-responses-prepare-tools.ts:219) → 5

What this PR changes

pub max_num_results: Option<u32>,      tools.rs:199   (was Option<f64>)

Tests (tests/openai_upstream_e2e_test.rs): file_search_sends_max_num_results_as_an_integer deserializes {"vectorStoreIds":["vs_1"],"maxNumResults":5} into FileSearchArgs and checks the request body; on master it fails with left: Number(5.0), right: Number(5). The upstream cases are ported in the Azure test PR.

Docs: CHANGELOG Fixed.

Note: The field type change is source-breaking for Rust code that wrote Some(5.0); nothing in the workspace or the bindings constructs FileSearchArgs, and JSON callers that send 5 are unchanged.

2. OpenAI Responses stream reports a null response id

Before: an OpenAI or Azure Responses stream that never received response.created finished with providerMetadata.<key>.responseId: "".

After: as in @ai-sdk/openai 4.0.83, it finishes with responseId: null. The generate path already wrote null for a missing id.

Found while porting the upstream Azure tests (case responses › doStream › should handle file_citation annotations without optional fields in streaming).

How it works today

OpenAIResponsesLanguageModel::do_stream
└─ ResponsesStreamTransformer
   ├─ "response.created" → response_id = response.id                 aimux-providers/src/openai/responses/responses_convert.rs:740
   └─ flush → Finish { provider_metadata: { <key>: { responseId: response_id.unwrap_or_default() } } }   :1517
                                                ← upstream: let responseId: string | null = null   (openai-responses-language-model.ts:1587)
                                                   flush → providerMetadata.responseId: responseId → null   (:2932)
convert_responses_generate → { responseId: data.id or null }        responses_convert.rs:328   (already null)

What this PR changes

let mut pm = json!({ "responseId": self.response_id });   responses_convert.rs:1517   (None → null)

Tests (tests/openai_upstream_e2e_test.rs): a_stream_without_response_created_finishes_with_a_null_response_id streams only response.completed; on master it fails with left: String(""), right: Null. The upstream case is ported in the Azure test PR.

Docs: CHANGELOG Fixed.

Note: Nothing in the workspace reads responseId back from the finish metadata.

3. OpenAI Responses reasoning deltas carry the item id

Before: OpenAI and Azure Responses reasoning deltas had no provider metadata, while their reasoning start and end parts carried { <key>: { itemId, reasoningEncryptedContent } }.

After: as in @ai-sdk/openai 4.0.83, every reasoning delta carries { <key>: { itemId } }.

Found while porting the upstream Azure tests (case responses › doStream › reasoning › should stream with reasoning encrypted content include reasoning-delta part, whose snapshot has the item id on all 89 deltas).

How it works today

OpenAIResponsesLanguageModel::do_stream
└─ ResponsesStreamTransformer
   ├─ "response.reasoning_summary_part.added" → ReasoningStart { provider_metadata: reasoning_stream_metadata(..) }   responses_convert.rs:999
   ├─ "response.reasoning_summary_text.delta"
   │    └─ controller.reasoning_delta(id, delta)          → provider_metadata: None           responses_convert.rs:1059
   │                                                       ← upstream: providerMetadata: { [key]: { itemId } }
   │                                                          (openai-responses-language-model.ts:2735-2749)
   └─ "response.reasoning_summary_part.done" → ReasoningEnd { provider_metadata: .. }

What this PR changes

"response.reasoning_summary_text.delta"                                                  responses_convert.rs:1059
└─ controller.enqueue(StreamPart::ReasoningDelta {
       id, delta,
       provider_metadata: Some(provider_namespace(provider_key, { "itemId": item_id })),
   })

Tests (tests/openai_upstream_e2e_test.rs): reasoning_deltas_carry_the_item_id streams one reasoning summary delta; on master it fails with left: Null, right: {"openai":{"itemId":"rs_1"}}. The upstream case is ported in the Azure test PR.

Docs: CHANGELOG Fixed.

Note: Upstream resolves the item id through resolveOutputItemId(item_id, output_index); aimux's reasoning start and end parts use item_id directly, and this PR does the same for deltas.

Type of change

  • Bug fix (non-breaking)
  • New feature (non-breaking)
  • Breaking change
  • New provider
  • New / updated binding
  • Docs / RFC

Checklist

  • cargo +1.99 fmt -p aimux-providers -- --check passes
  • cargo +1.99 clippy -p aimux-providers --lib --tests -- -D warnings passes
  • cargo +1.99 test -p aimux-providers: 129 test binaries, all pass
  • scripts/check_provider_boundaries.sh passes
  • Updated relevant docs (CHANGELOG)
  • Commit messages follow Conventional Commits

Notes for reviewers

🤖 Generated with Claude Code

https://claude.ai/code/session_01RoxcKU4zBD64Lri3e4Lrq5

cunninghamcard-bit and others added 3 commits October 20, 2026 09:38
@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
… 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
…m-id

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

@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.

reasoning delta 现在带 item id,但用的是原始 item_id;上游在 output_item.added 按 output_index 记录并在三个 summary 分支用 resolveOutputItemId。Responses-compatible 旋转 opaque id 时,新的 providerMetadata/openai/itemId 与 reasoning-start 的 {id}:0 不一致,可能开出第二个 reasoning part。建议按 output_index 解析,并让测试使用 rotated id + output_index:0。另:FileSearchArgs.max_num_results 的 f64→u32 是公开类型破坏性变化,请移到 CHANGELOG 的 Breaking。

cunninghamcard-bit and others added 3 commits October 25, 2026 08:07
… 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
…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-reasoning-deltas-carry-the-item-id

# Conflicts:
#	CHANGELOG.md

Copy link
Copy Markdown
Contributor Author
eric 的意见(原话) 处理
「reasoning delta 现在带 item id,但用的是原始 item_id;上游在 output_item.added 按 output_index 记录并在三个 summary 分支用 resolveOutputItemId…建议按 output_index 解析」 b185778:新增 resolve_output_item_id,reasoning_summary_part.added、reasoning_summary_text.delta、reasoning_summary_part.done 三处都按 output_index 取 output_item.added 记下的 id;reasoning 的 output_item.done 结束时移除该记录,与上游一致
「并让测试使用 rotated id + output_index:0」 同一提交:reasoning_deltas_carry_the_item_id 的 summary 事件改用 rs_2 / rs_3 + output_index: 0,仍期望 itemId: rs_1;另加 rotated_reasoning_item_ids_are_correlated_by_output_index
「FileSearchArgs.max_num_results 的 f64→u32 是公开类型破坏性变化,请移到 CHANGELOG 的 Breaking」 5d4c318:条目移到 Breaking,并写明调用方现在要传整数

另:d6f40825 合入了最新 master(只有 CHANGELOG 冲突,两边都保留)。本地全量检查通过(fmt、边界脚本、clippy、2825 个测试)。


Generated by Claude Code


Generated by Claude Code

cunninghamcard-bit added a commit that referenced this pull request Oct 25, 2026
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

@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.

inference 现在按 output_index 的 resolve_output_item_id 解析 rotated id,测试覆盖 rs_1..rs_5;FileSearchArgs 的 u32 变更已移入 Breaking。Approve。

eric8810 pushed a commit that referenced this pull request Oct 25, 2026
…the AI SDK (#368)

* fix(openai): finish a Responses stream with a null responseId when it 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

* fix(openai): give Responses reasoning deltas the item id

@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

* fix(openai): send the file search max_num_results as an integer

@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

* fix(openai): leave null keys and unknown fields out of computer actions

@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

* fix(openai): map only the AI SDK's provider tools back to caller tool 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

* fix(openai): keep only the schema fields of Responses text annotations

@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

* fix(openai): list apply patch operation fields in the AI SDK's schema 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

* fix(openai): list local shell action fields in the AI SDK's order

@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

* fix(openai): reject a Responses message part that is not output_text

@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

* fix(openai): drop bytes from Responses logprobs metadata

@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

* fix(openai): put the namespace on a streamed function call's tool-input-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

* fix(openai): end early Responses reasoning summary parts with the item 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

* fix(openai): correlate rotated Responses reasoning item ids by output 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

* fix(openai): report a Chat Completions chunk on a Responses stream

@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

* fix(openai): report schema-invalid known Responses stream events as errors

@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

* fix(openai): reject a Responses stream that errors before any output

@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

* fix(openai): warn about unsupported async tools with the quoted tool 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

* fix(openai): quote allowedTools entries in warnings and give the upstream 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

* fix(openai): name the dropped entries when no allowedTools entry can 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

* fix(openai): quote the namespace name in the conflicting namespace descriptions 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

* fix(openai): name the available providers when a Responses file or skill 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

* fix(openai): list the supported efforts when a reasoningEffortUpdate 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

* fix(openai): replay provider-executed tool search and program calls in 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

* fix(openai): send the caller of a tool result on its function_call_output

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

* fix(provider-utils): unparsable stream events report the AI SDK parse 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

* fix(openai): name GPT-6 models in the chat promptCacheRetention warning

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

* fix(openai): send embedding dimensions as the number given

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

* fix(openai): name the evaluation structured output "evaluation"

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

* fix(openai): report image file and mask support by model id

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

* fix(openai): unwrap singleton allOf references in JSON schemas

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

* fix(openai): reject an empty base URL as "baseURL must be a non-empty 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

* fix(openai): send transcription responseFormat and chunkingStrategy

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

* fix(openai): return diarized transcription speakers in provider metadata

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

* fix(openai): send the realtime transcription key as a WebSocket subprotocol

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

* fix(openai): take realtime transcription session options from streaming

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

* fix(openai): warn about REST-only options on realtime transcription streams

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

* test(python): answer the realtime subprotocol in the fake WebSocket server

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

* test(openai): check the reasoning delta item id under rotated item ids

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

* docs(changelog): list the file search max_num_results type change under 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

* docs(changelog): keep the merged SSE parse error entry where master has 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

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Copy link
Copy Markdown
Contributor Author
情况 处理
#368 先合入了 master(4d8ae891),它带着本 PR 的全部改动:提交 1-3(null responseId、reasoning delta 的 item id、file search 整数)、按 output_index 解析轮换 id(resolve_output_item_id)、对应的 4 个测试和 CHANGELOG 条目(max_num_results 在 Breaking) 核对过:tools.rs 的 max_num_results: Option<u32>、responses_convert.rs 的 resolve_output_item_id、file_search_sends_max_num_results_as_an_integer / a_stream_without_response_created_finishes_with_a_null_response_id / reasoning_deltas_carry_the_item_id / rotated_reasoning_item_ids_are_correlated_by_output_index 四个测试和四条 CHANGELOG 在 master 上都已存在。本 PR 合 master 后没有剩余改动,所以关闭
叠在本分支上的 Azure 测试 PR #324 改基到 master 并合入 master

Generated by Claude Code


Generated by Claude Code

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