Skip to content

fix(providers): disambiguate reused chronological tool call IDs on the Cloud Code path #15312

Description

@Kizuno18

Long coding-agent histories can reuse a tool call ID for distinct chronological operations. We needed a local normalization workaround for this on the Cloud Code path; the current translator still forwards those IDs unchanged.

The source inspection below is against release/v3.8.52 at 4399e60057aaee564ec9e31054a5f6c76cc92a2d. The earlier live incident used the published 3.8.51 Docker runtime. These are separate observations, not a claim that we reproduced the provider rejection on a fresh 3.8.52 deployment.

Reproduction from the repository root, using the existing TypeScript loader:

node --import tsx/esm --import ./tests/_setup/isolateDataDir.ts --input-type=module <<'JS'
import { openaiResponsesToOpenAIRequest } from './open-sse/translator/request/openai-responses.ts';
import { openaiToCloudCodeGeminiRequest } from './open-sse/translator/request/openai-to-gemini.ts';

const body = {
  input: [
    { type: 'message', role: 'user', content: 'First turn' },
    { type: 'function_call', call_id: 'call_repeat', name: 'first_tool', arguments: '{}' },
    { type: 'function_call_output', call_id: 'call_repeat', output: 'FIRST_RESULT' },
    { type: 'message', role: 'user', content: 'Second turn' },
    { type: 'function_call', call_id: 'call_repeat', name: 'second_tool', arguments: '{}' },
    { type: 'function_call_output', call_id: 'call_repeat', output: 'SECOND_RESULT' },
    { type: 'message', role: 'user', content: 'Continue' },
  ],
};
const chat = openaiResponsesToOpenAIRequest('gemini-3.8-flash', body, false, null);
const wire = openaiToCloudCodeGeminiRequest('gemini-3.8-flash', chat, false);
const parts = wire.contents.flatMap(content => content.parts);
console.log(JSON.stringify(parts.filter(part => part.functionCall || part.functionResponse), null, 2));
JS

Both native functionCall.id values, and their functionResponse.id values, are call_repeat. The current turn-local maps do retain the correct function names and result content; this is not the older cross-turn name/content overwrite bug.

In the live incident, full histories containing reused IDs produced an upstream HTTP 400 INVALID_ARGUMENT and a downstream stream that ended without response.completed. Short prompts with the same tool definitions worked. Removing reasoning/custom-tool entries did not help. Keeping the complete histories and renaming only subsequent repeated IDs plus their corresponding outputs did: two histories with 601 and 1,534 items subsequently completed. These were diagnostic replays, not byte-identical captures of the desktop request.

The local repair retains the first ID, reserves IDs appearing later in the input, assigns unused suffixes to subsequent chronological calls, preserves the original body, and is idempotent. Unit tests and the synthetic translator reproduction confirm the normalized Cloud Code call/result pairs use call_repeat and call_repeat_occ2.

I would not port that route-level workaround verbatim:

  • Scope any normalization to the translated non-native history, not native Responses passthrough or the client's stored history.
  • Preserve thought-signature lookup using the original identities before assigning new wire IDs.
  • Handle function and custom-tool pairs, long IDs, future-ID collisions, and repeated normalization.
  • Two overlapping calls with the same ID are ambiguous. The local "latest preceding call" heuristic attaches both following outputs to the second call; that case needs explicit validation rather than guessed pairing.

Would a translator-boundary normalization for unambiguous chronological reuse be welcome? I can send the source change and focused regressions separately from the runtime bundle patch.

Activity

  1. changed the title [-]fix(translator): disambiguate reused chronological tool call IDs on the Cloud Code path[/-] [+]fix(providers): disambiguate reused chronological tool call IDs on the Cloud Code path[/+] on Oct 2, 2026
  2. diegosouzapw commented on Oct 2, 2026

    @diegosouzapw
    Owner

    Thanks for the detailed write-up. Confirmed against release/v3.8.52: open-sse/translator/request/openai-to-gemini.ts (tool_call_id maps around lines 371-403 and the turn-local maps at ~548-570) keeps names/results correct per turn but still forwards the reused ID unchanged as functionCall.id / functionResponse.id. A translator-boundary normalization scoped to the non-native history, with thought-signature lookup done on the original IDs, is a reasonable shape, and PR #15319 already proposes that. It is open and under review, so this is tracked there. Note the provider 400 was observed on the published 3.8.51 runtime and has not been reproduced on 3.8.52, so the PR should keep the focused regressions (function and custom-tool pairs, long IDs, future-ID collisions, idempotence, and the explicit rejection of overlapping same-ID calls).

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

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions