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:
- 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.
Symptom
When an OpenAI Chat backend reuses tool-call IDs such as
call_0across 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:
61ee1b31bbb29447d0ce60eae8cf327688b7cacb2.1.220grok-4.6Configure 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:
Input files:
The first model response emits
call_0andcall_1for the reads. Later responses emitcall_0again 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.rsThe translated conversation consequently contains multiple unrelated tool calls and results sharing
call_0.Expected vs. actual
result.md, Claude Code emits a terminal answer.Controlled A/B evidence
call_0used oncecall_0/call_1reusedThe 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_0unchanged.Tests should cover:
call_0.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.