Skip to content

feat(responses): opt-in repair for unstable SSE output item IDs #263

Closed

Description

@Eleven-is-cool

Summary

Some OpenAI Responses-compatible providers emit output items with missing, reused, or lifecycle-inconsistent IDs.

Codex Desktop correlates reasoning, tool activity, and assistant messages by item ID. Non-unique IDs can therefore produce duplicated or misplaced tool activity summary cards.

Minimal synthetic example

event: response.output_item.added
data: {"type":"response.output_item.added","output_index":0,"item":{"type":"reasoning","id":"rs_reused"}}

event: response.output_item.done
data: {"type":"response.output_item.done","output_index":0,"item":{"type":"reasoning","id":"rs_reused"}}

# A later provider response reuses the same item ID for a distinct item.
event: response.output_item.added
data: {"type":"response.output_item.added","output_index":0,"item":{"type":"reasoning","id":"rs_reused"}}

The same failure can occur when an output_item.done event omits the ID present on output_item.added.

Proposal

Add an opt-in item-ID repair middleware for openai-responses passthrough providers.

interface ResponsesItemIdRepairConfig {
  repairMissingTerminalIds?: boolean;
  placeholderIds?: {
    message?: string[];
    reasoning?: string[];
  };
}

The middleware should:

  1. Preserve all output IDs unless a provider explicitly enables repair.
  2. Generate canonical, type-prefixed IDs for configured placeholder or missing message / reasoning item IDs.
  3. Map canonical IDs by output_index.
  4. Rewrite matching item_id delta events, output_item.done items, and response output snapshots.
  5. Leave tool-call IDs and call_id pairing unchanged.

Compatibility constraints

  • Disabled by default. Native/stateful Responses providers must retain their opaque upstream IDs.
  • The initial configuration should accept exact placeholder strings only; avoid global pattern matching.
  • The feature is intended for compatibility gateways that synthesize Responses SSE and do not require message/reasoning item IDs to be replayed upstream.

Acceptance criteria

  • Default passthrough output is unchanged.
  • Missing terminal IDs are repaired when enabled.
  • Configured placeholder IDs become unique across sequential tool turns.
  • Added, delta, done, and terminal snapshot events use the same canonical ID.
  • Function-call and call_id behavior remains unchanged.
  • Unit tests cover single-stream and sequential-stream lifecycle cases.

I can prepare a PR after maintainer feedback on the public configuration name and scope.

Activity

  1. Ingwannu commented on Jul 22, 2026

    @Ingwannu
    Owner

    Thanks for the detailed proposal. I checked the current paths: OpenCodex-generated bridge events already allocate and reuse consistent item IDs, so this would specifically be a compatibility repair for raw openai-responses passthrough streams. I found no current in-repo reproduction that proves which provider needs the rewrite, and changing opaque upstream IDs without that evidence is risky for stateful/replay-capable providers.

    The proposed scope is reasonable if it stays provider-local and disabled by default. My preferred public shape is providers..responsesItemIdRepair with exact message/reasoning placeholder arrays plus repairMissingTerminalIds; do not repair function_call IDs or call_id.

    Before implementation, please add:

    1. the affected provider/gateway and adapter mode,
    2. one sanitized raw SSE sequence through response.completed,
    3. whether the collision is within one response, across responses, or both,
    4. confirmation that the rewritten message/reasoning IDs are never replayed upstream.

    With that trace, a focused PR should test added, delta, done, terminal snapshot, sequential streams, and default byte-for-byte passthrough. Marking this as enhancement/question pending that evidence.

  2. Eleven-is-cool commented on Jul 22, 2026

    @Eleven-is-cool
    ContributorAuthor

    Thanks — I collected a live, sanitized trace after temporarily disabling the local compatibility repair.

    Affected route:

    • a private enterprise OpenAI Responses-compatible gateway;
    • configured as a custom provider with the openai-responses adapter.

    I cannot disclose the gateway identity or endpoint publicly. Response IDs, call IDs, function names, model identifiers, and content are redacted below.

    The observed collision is across sequential Responses streams in one tool-loop conversation. I did not observe two same-type output_item.added items with the same ID inside a single upstream SSE response in this controlled capture.

    # Response A
    event: response.output_item.added
    data: {"type":"response.output_item.added","output_index":0,"item":{"type":"reasoning","id":"rs_0"}}
    
    event: response.output_item.done
    data: {"type":"response.output_item.done","output_index":0,"item":{"type":"reasoning","id":"rs_0"}}
    
    event: response.completed
    data: {"type":"response.completed","response":{"id":"resp_redacted","status":"completed","output":[
      {"type":"reasoning","id":"rs_0"},
      {"type":"function_call","id":"call_redacted","call_id":"call_redacted"}
    ]}}
    
    # Response B, after the function-call output from A
    event: response.output_item.added
    data: {"type":"response.output_item.added","output_index":0,"item":{"type":"reasoning","id":"rs_0"}}
    
    event: response.completed
    data: {"type":"response.completed","response":{"id":"resp_redacted","status":"completed","output":[
      {"type":"reasoning","id":"rs_0"},
      {"type":"function_call","id":"call_redacted","call_id":"call_redacted"}
    ]}}
    
    # Response C, after the function-call output from B
    event: response.output_item.added
    data: {"type":"response.output_item.added","output_index":0,"item":{"type":"reasoning","id":"rs_0"}}
    
    event: response.output_item.added
    data: {"type":"response.output_item.added","output_index":1,"item":{"type":"message","id":"msg_0","role":"assistant"}}
    
    event: response.output_item.done
    data: {"type":"response.output_item.done","output_index":1,"item":{"type":"message","role":"assistant"}}
    
    event: response.completed
    data: {"type":"response.completed","response":{"id":"resp_redacted","status":"completed","output":[
      {"type":"reasoning","id":"rs_0"},
      {"type":"message","role":"assistant"}
    ]}}
    

    The persisted client transcript from the original report also shows that one user-visible task spanning this tool loop contained rs_0 four times and msg_0 twice, while the function-call item IDs remained unique.

    The rewritten message/reasoning IDs are never replayed upstream. The repair wraps only the already-received upstream response body; upstream requests, previous_response_id, function-call IDs, and call_id values remain unchanged.

    This supports a provider-local, disabled-by-default repair for exact message/reasoning placeholders plus missing terminal IDs. It should not rewrite function-call IDs or call_id.

  3. Ingwannu commented on Jul 22, 2026

    @Ingwannu
    Owner

    Validated on current origin/dev at 85726d5709685b1fecd597f26119ddc45ff1d5b4.

    The sanitized trace matches a real gap in the current passthrough path: openai-responses SSE is relayed as-is today, so a compatibility gateway that reuses exact placeholder reasoning / message ids (for example rs_0, msg_0) across sequential tool-loop responses, or omits the terminal id after output_item.added, can hand Codex Desktop lifecycle-inconsistent ids.

    I implemented the accepted scope in draft PR #272:

    • provider-local, disabled-by-default providers.<name>.responsesItemIdRepair
    • exact message / reasoning placeholder arrays plus repairMissingTerminalIds
    • rewrites only downstream passthrough SSE by output_index
    • leaves function_call ids and call_id untouched
    • keeps the tee'd inspect/replay branch raw so repaired ids are never replayed upstream on later turns
    • documents the config and adds a Decision Log comment near the repair stream

    Validation run on the branch:

    • bun x tsc --noEmit
    • bun test --isolate ./tests/responses-item-id-repair.test.ts ./tests/config.test.ts ./tests/passthrough-abort.test.ts ./tests/openai-responses-passthrough.test.ts
    • bun run privacy:scan

    Draft PR: #272

  4. Ingwannu commented on Jul 22, 2026

    @Ingwannu
    Owner

    PR #272 security-review follow-up is now pushed at 3ba0ad51f909a0b8e9e38315b8d01b315993be08, rebased onto current dev (d4b4b05c).

    The cross-type output_index collision is closed by keying mappings by item type plus index and restricting event-level rewrites to explicit message/reasoning lifecycle events. function_call id, event item_id, and call_id are preserved in the adversarial stream and terminal snapshot.

    Verification: 82 focused tests passed; typecheck, privacy scan, and diff check passed. Draft PR: #272

  5. Ingwannu commented on Jul 22, 2026

    @Ingwannu
    Owner

    Implemented and merged into dev via PR #272, merge commit cba5b240.

    Final scope matches the accepted contract: provider-local and disabled by default, exact message/reasoning placeholders plus missing terminal IDs, type+output-index lifecycle mapping, no function-call id/item_id/call_id rewrites, raw replay tee unchanged, and byte-for-byte default passthrough. The adversarial cross-type index reuse case and full Linux/macOS/Windows CI pass. Closing as completed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions