Skip to content

refactor(ai): resolve responses item ids once at the stream boundary - #46885

Merged
rekram1-node merged 2 commits into
v2from
responses-item-ids-cleanup
Sep 3, 2026
Merged

rekram1-node merged 2 commits into
v2from
responses-item-ids-cleanup

Conversation

@rekram1-node

@rekram1-node rekram1-node commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Why

The Responses parser had grown a second identity path for function_call items that arrive without an id: item.id ?? call_id coalescing, Object.values(state.tools).some((tool) => tool?.id === call_id) scans on dedup and terminal reconciliation, conditional itemId metadata, and per-event "message is missing id" guards. #46084 extended it further. The parser body should be able to assume item.id exists; the missing-id policy belongs in one place.

What the wire actually does

Probed every Responses API we have credentials for, with a plain-text turn and a forced tool-call turn each:

Provider output_item.added output_item.done response.completed.output
OpenAI (gpt-5-mini) id id id
Azure (gpt-5.4-nano) id id id
xAI (grok-4-1-fast*) id id id
Meta (api.meta.ai, muse-spark-1.1) id id id
Perplexity (perplexity/sonar, openai/gpt-5-mini) id id id
Bedrock Mantle (openai.gpt-oss-120b/20b) id no id — renamed to item_id, stray output_index inside the item no id — same

So the Open Responses schema is right that id is required, and five of six providers honor it, but Mantle's gateway drops it from terminal function_call items. That is the real case the old fallbacks were serving, and a strict schema would regress it.

What changes

  • One resolution point. OpenResponses.normalize(state, event) runs before step and does two things: the existing output_indexitem_id remap for delta events, and item-id resolution mirroring Codex's assign_missing_streamed_response_item_id: an item without an id adopts the id already registered in its output slot (state.outputItems[output_index]; for response.completed.output, the array position), otherwise it gets a locally minted fc_/msg_/rs_/cmp_ + UUID. step, onOutputItemAdded, onOutputItemDone, and the hosted-tool helpers take OutputItem = StreamItem & { id: string } and never look at a missing id again.
  • Removed the call_id scans, item.id ?? call_id coalescing, conditional itemId metadata, isReasoningItem narrowing, and the message is missing id guards. Pending tools, completedTools, and terminal reconciliation are keyed by item.id only.
  • Provider steps (openai-responses, xai-responses) call normalize first; outputItemID is no longer exported.
  • Tests: added a fixture captured from the real Mantle gpt-oss stream (id missing on done/completed, item_id present, stray output_index) and a done-only no-id case asserting a minted fc_ id. Removed tests whose only purpose was id-less items; fixed fixtures that omitted ids incidentally.

Live verification

Ran a real two-turn tool-call conversation through the patched client, switching provider between the tool call and the tool result:

First turn → second turn Item id on replay Result
OpenAI → OpenAI, Azure → Azure, xAI → xAI real server-issued id ok
Mantle → Mantle fc_… recovered from output_item.added (the Mantle case) ok
OpenAI → Mantle, Azure → Mantle real OpenAI-style fc_… replayed to Mantle ok
Mantle → OpenAI, Mantle → Azure, xAI → OpenAI, xAI → Azure, OpenAI → Azure dropped (provider metadata key differs) ok
minted fc_<uuid> → OpenAI / Azure / xAI / Mantle minted id on the wire (confirmed via compileRequest) all accepted
cd packages/ai
bun typecheck
bun run test   # 979 pass, 28 skip, 0 fail

@rekram1-node rekram1-node changed the title refactor(ai): require responses output item ids refactor(ai): resolve responses item ids once at the stream boundary Sep 2, 2026
@rekram1-node
rekram1-node merged commit d9c85d8 into v2 Sep 3, 2026
16 of 17 checks passed
@rekram1-node
rekram1-node deleted the responses-item-ids-cleanup branch September 3, 2026 01:31
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.

1 participant