Skip to content

[bug] Reused OpenAI tool-call IDs break multi-turn Anthropic tool workflows #502

Description

@lorenzoc25

Symptom

When an OpenAI Chat backend reuses tool-call IDs such as call_0 across responses, Switchyard preserves those IDs in the translated Anthropic conversation. Multi-turn Claude Code workflows can then repeat completed tool actions indefinitely instead of terminating.

This differs from #178: the IDs here already satisfy Anthropic's character restrictions. The problem is repeated identical IDs across turns, not lossy sanitization.

Reproduction

Environment:

  • Switchyard commit: 61ee1b31bbb29447d0ce60eae8cf327688b7cacb
  • Claude Code: 2.1.220
  • OS / architecture: Linux aarch64
  • Inbound format: Anthropic Messages
  • Backend format: OpenAI Chat
  • Backend model: grok-4.6

Configure a passthrough route to an OpenAI-compatible backend that emits call_0, call_1, etc. for each new response.

Run Claude Code with this prompt:

Use the Read tool exactly once on input/a.txt and input/b.txt. Create result.md containing the sorted unique values, followed by count=4. Use Bash only to verify the file. Do not reread unchanged files. Finish only after verification succeeds.

Input files:

# input/a.txt
alpha
beta
alpha

# input/b.txt
gamma
beta
delta

The first model response emits call_0 and call_1 for the reads. Later responses emit call_0 again for Edit, Read, and Bash operations.

Switchyard currently preserves these IDs through:

  • crates/switchyard-translation/src/codecs/openai_chat/stream.rs
  • crates/switchyard-translation/src/codecs/anthropic/stream.rs
  • crates/switchyard-translation/src/codecs/openai_chat/buffered.rs

The translated conversation consequently contains multiple unrelated tool calls and results sharing call_0.

Expected vs. actual

  • Expected: Each tool call/result pair remains unambiguous across the translated conversation. After creating and verifying result.md, Claude Code emits a terminal answer.
  • Actual: The correct file is created and verified, but the model forgets that it completed the operation. It rereads the file, attempts to create it again, rewrites it, and continues looping.

Controlled A/B evidence

Control Tool IDs Result
Grok, one tool round call_0 used once Completed normally in two turns
Grok, multi-step baseline call_0/call_1 reused Correct file, no terminal result
Grok through buffer-only proxy IDs still reused Same repeated-Edit loop
Grok with response IDs rewritten uniquely Five unique IDs Completed normally in six turns
GPT-5.5 Chat, same multi-step prompt Four unique IDs Completed normally in five turns

The buffer-only control rules out response buffering as the cause. Rewriting only the tool IDs changes the Grok run from a non-terminating loop to a normal completion.

Suggested invariant

OpenAI-to-Anthropic translation should preserve an unambiguous identity for every tool-call occurrence, even when the upstream provider reuses the same raw ID across responses.

An occurrence-aware namespace could incorporate the source message ID and tool index. The implementation should coordinate with the reversible encoding work in #397, which currently keeps already-safe IDs such as call_0 unchanged.

Tests should cover:

  1. Two sequential OpenAI responses both emitting call_0.
  2. Distinct Anthropic-facing IDs for those tool calls.
  3. Correct pairing of each replayed tool result.
  4. Buffered and streaming response translation.
  5. A multi-turn workflow terminating after the final successful tool result.

Additional context

This defect was exposed by Grok, but the translation invariant applies to any OpenAI-compatible provider that scopes tool-call IDs to a response rather than an entire conversation.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions