NVIDIA-NeMo / NVIDIA-NeMo/Switchyard
[bug] Reused OpenAI tool-call IDs break multi-turn Anthropic tool workflows
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 3.2k
- Forks
- 291
- Avg merge
- 1d 8h
- Merged PRs (30d)
- 182
Description
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.txtandinput/b.txt. Createresult.mdcontaining the sorted unique values, followed bycount=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.rscrates/switchyard-translation/src/codecs/anthropic/stream.rscrates/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:
- Two sequential OpenAI responses both emitting
call_0. - Distinct Anthropic-facing IDs for those tool calls.
- Correct pairing of each replayed tool result.
- Buffered and streaming response translation.
- 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.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the OpenAI streaming and buffered codecs in crates/switchyard-translation/src/codecs/openai_chat/stream.rs and buffered.rs, then compare the Anthropic translation in crates/switchyard-translation/src/codecs/anthropic/stream.rs and the reversible encoding work in #397. Add coverage for repeated call_0 IDs across sequential responses, including streaming and buffered translation, and verify distinct IDs pair results correctly and allow the multi-turn workflow to terminate.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- api, backend-api-design
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 55/100