Skip to content

fix(responses): keep translated output indexes unique - #2747

Open
ryanngit wants to merge 2 commits into
decolua:masterfrom
ryanngit:fix/responses-output-index
Open

ryanngit wants to merge 2 commits into
decolua:masterfrom
ryanngit:fix/responses-output-index

Conversation

@ryanngit

@ryanngit ryanngit commented Jul 21, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Allocate one monotonically increasing Responses output_index for every reasoning, assistant-message, and function-call item, then reuse that index across added, delta, done, and terminal events.

The fix covers both callable conversion paths:

  • active translator pipeline in open-sse/translator
  • legacy createResponsesApiTransformStream implementation

OpenAI Chat choice indexes and tool-call indexes are separate source namespaces. Reusing them directly can assign index 0 to reasoning, text, and the first function call in one response, producing an invalid item stream.

Scope

This recut contains output-index allocation only. Custom tools, deferred tool search, terminal payloads, cache details, Claude schema normalization, refusal-history repair, and model fallback are separate review surfaces documented in the split comment.

Verification

Tested on pristine v0.5.45 (6fcd2733):

  • index parity suite: 3/3 passed
  • adjacent terminal regression suite: 4/4 passed
  • changed-file ESLint and git diff --check passed
  • Gitleaks found no leaks

Fixes #2611.

@ryanngit ryanngit changed the title fix(responses): keep translated output indexes unique fix(responses): preserve output indexes and custom tools Jul 21, 2026
@zierocode

Copy link
Copy Markdown
Contributor

Fresh cross-check on 2026-07-23 at 30082c65c84c76a96f7212656710ee6990345765: both focused files pass, 12/12 tests total. This is the implementation I plan to rely on rather than opening a parallel custom-tool PR.

One provider-neutral gap remains outside the current suite: route-wide restoration of a synthetic _ide suffix to the unique client-declared tool name. This matters when a translated Responses, Chat, or Messages path returns emit_probe_ide even though the request declared only emit_probe.

The fail-closed contract is small:

// Unique declaration: restore the known synthetic alias.
declared = ["emit_probe"];
providerName = "emit_probe_ide";
expect(clientName).toBe("emit_probe");

// Ambiguous declaration: preserve the provider name.
declared = ["emit_probe", "emit_probe_ide"];
providerName = "emit_probe_ide";
expect(clientName).toBe("emit_probe_ide");

The same mapping should apply to Anthropic tool_use, Responses output items, and Chat tool_calls, while leaving explicitly declared _ide names untouched. This is separate from custom/freeform input preservation and should not block the current PR if you prefer it as a follow-up.

@ryanngit ryanngit changed the title fix(responses): preserve output indexes and custom tools fix(responses): preserve indexes, custom tools, and deferred tools Jul 29, 2026
@ryanngit ryanngit changed the title fix(responses): preserve indexes, custom tools, and deferred tools fix(responses): preserve translated tools, output, and terminal semantics Jul 30, 2026
@ryanngit

Copy link
Copy Markdown
Contributor Author

Added d1494630 to handle empty OpenAI function-tool schemas in the Claude bridge.

GitHub Copilot /v1/messages requires input_schema.type; Responses clients can legally send parameters: {}. The bridge now normalizes missing, empty, non-object, and array schemas to { type: "object", properties: {} }, while preserving schemas that already declare a root type.

Tests:

  • responses-custom-tool-roundtrip.test.js: 11/11 passed
  • Adjacent Claude/Responses matrix: 30 passed, 1 pre-existing unrelated failure in openai-to-claude.test.js
  • The same unrelated failure reproduces at prior PR head e7d8350c without this commit.

@ryanngit

Copy link
Copy Markdown
Contributor Author

Updated in 25bd48bb with centralized Claude tool-schema compatibility.

  • Normalizes missing/invalid input schemas to object schemas.
  • Removes unsupported root oneOf, anyOf, and allOf while preserving branch object properties.
  • Merges required only for allOf; nested unions remain unchanged.
  • Applies same helper during OpenAI-to-Claude translation and final Claude request formatting.
  • Adds request-translation, final-format, allOf, nested-union, and invalid-root-metadata regressions.

Public compatibility references:

Verification:

  • responses-custom-tool-roundtrip.test.js: 14/14 passed
  • final Claude-format union guard: 1/1 passed
  • provider canary: 25 tools with root oneOf at index 20 returned HTTP 200, a function call, one completion event, and one [DONE]

@ryanngit

ryanngit commented Aug 1, 2026

Copy link
Copy Markdown
Contributor Author

Updated with Claude refusal history closure (8780011a). Claude stop_reason=refusal now maps to content_filter, then emits visible assistant refusal text with one response.completed instead of leaving an unanswered failed turn that poisons every continuation. Other reasoning-only/protocol-only/whitespace terminals remain upstream_empty_response. Verification: Responses matrix 43/43, changed-file ESLint clean, git diff --check clean.

@ryanngit

ryanngit commented Aug 1, 2026

Copy link
Copy Markdown
Contributor Author

Updated with post-refusal continuation repair (f94a01bd).

A completed synthetic Claude refusal is still shown to the client, but its exact generated _refusal assistant item and immediately paired direct-user message are omitted from later Claude provider history. This prevents unrelated later turns from inheriting the refusal while preserving ordinary assistant text, current input, tool-bearing history, model/account selection, and the original refusal policy decision.

Verification:

  • focused semantic terminal suite: 11/11
  • broader Responses/Claude matrix: 45/45
  • changed-file ESLint: clean
  • git diff --check: clean

@ryanngit
ryanngit force-pushed the fix/responses-output-index branch from 8f0a4de to f705201 Compare August 3, 2026 07:03
@ryanngit ryanngit changed the title fix(responses): preserve translated tools, output, and terminal semantics fix(responses): keep translated output indexes unique Aug 3, 2026
@ryanngit

ryanngit commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

v0.5.45 split and recut

The prior 17-commit head mixed nine independent behaviors. It has been replaced by one reviewable output-index commit.

  • Base: v0.5.45 (6fcd2733)
  • New head: f705201d
  • Merge state: CLEAN
  • Active and legacy transformer parity: covered
  • Focused tests: 7/7 passed
  • ESLint, git diff --check, and Gitleaks: clean

Displaced commit map:

  • custom/function tool round-trip: 7486afb6, 19947790, a163dfa8, 30082c65
  • complete terminal payload: 8894c2bf
  • cache details: f761272a
  • deferred tool search: 69bf5fbd
  • semantic-empty rejection: e7d8350c
  • Claude schema normalization: d1494630, 25bd48bb
  • Claude refusal closure/history pruning: 8780011a, f94a01bd, 1af56075, 04902276, 682e1cac
  • GitHub Fable-to-Opus fallback: 8f0a4deb

Each retained behavior needs its own PR and focused tests. Refusal-history deletion and silent model fallback remain blocked on maintainer policy decisions; they are not part of this recut.

Keep unique funcOutputIndexes and custom-tool Responses events.
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.

Responses API: output_index collision between reasoning and tool_call causes Codex to drop tool execution (apply_patch loop)

2 participants