Skip to content

fix(translation): preserve tool IDs across Anthropic - #397

Open
ting-hong-shieh wants to merge 1 commit into
NVIDIA-NeMo:mainfrom
ting-hong-shieh:fix/reversible-tool-use-ids
Open

fix(translation): preserve tool IDs across Anthropic#397
ting-hong-shieh wants to merge 1 commit into
NVIDIA-NeMo:mainfrom
ting-hong-shieh:fix/reversible-tool-use-ids

Conversation

@ting-hong-shieh

@ting-hong-shieh ting-hong-shieh commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

What

  • Replace lossy Anthropic tool-use ID sanitization with sy64_-prefixed, unpadded Base64URL encoding.
  • Restore encoded IDs when emitting OpenAI Chat tool calls and tool results, and OpenAI Responses function calls and outputs.
  • Keep ordinary [A-Za-z0-9_-] IDs unchanged, escape the reserved prefix, and pass malformed encoded values through unchanged.
  • Add buffered request round-trip and streaming response regression tests.

Why

Some OpenAI-compatible models emit tool-call IDs containing characters that Anthropic does not accept. Kimi K2, for example, can emit IDs such as functions.list_skills:0 and expects the same ID on the next turn.

The previous sanitizer replaced unsupported characters with underscores. That mapping could not be reversed, so Switchyard replayed a different ID to the upstream model and could break multi-turn tool calling. The new encoding keeps Anthropic-facing IDs within the accepted character set while allowing the OpenAI-bound request path to recover the original bytes.

Closes #178

How tested

  • cargo fmt --all --check
  • PYO3_NO_PYTHON=1 cargo clippy --workspace --all-targets -- -D warnings
  • cargo test -p switchyard-translation
  • PYO3_NO_PYTHON=1 cargo test --workspace --exclude switchyard-py
  • PYO3_NO_PYTHON=1 cargo check -p switchyard-py

Checklist

  • No new classes or downstream public symbols.
  • Unit and integration tests cover the bug fix.
  • README and --help are unchanged because this does not add a configuration or command surface.
  • Commit signed off per the DCO.

Notes for reviewers

sy64_ is reserved for encoded IDs. An upstream ID that already starts with this prefix is encoded again before it reaches an Anthropic client, which keeps decoding unambiguous. Invalid prefixed input is left unchanged.

Summary by CodeRabbit

  • Bug Fixes
    • Preserved tool-call and tool-result IDs accurately when translating between Anthropic, OpenAI Chat, and Responses formats.
    • Added reversible handling for IDs containing special characters, Unicode, empty values, or reserved prefixes.
    • Prevented malformed encoded IDs from being altered unexpectedly.
  • Tests
    • Added coverage for request and streaming translations, including regression cases for unsafe tool IDs.

Signed-off-by: Ting-Hong Shieh <32212900+ting-hong-shieh@users.noreply.github.com>
@ting-hong-shieh
ting-hong-shieh marked this pull request as ready for review August 13, 2026 14:45
@ting-hong-shieh
ting-hong-shieh requested a review from a team as a code owner August 13, 2026 14:45
@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 8e49488f-35af-4a56-aa0a-d99295c3dade

📥 Commits

Reviewing files that changed from the base of the PR and between b256d93 and 18ef28b.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock, !Cargo.lock
📒 Files selected for processing (7)
  • Cargo.toml
  • crates/switchyard-translation/Cargo.toml
  • crates/switchyard-translation/src/codecs/openai_chat/buffered.rs
  • crates/switchyard-translation/src/codecs/responses/buffered.rs
  • crates/switchyard-translation/src/util.rs
  • crates/switchyard-translation/tests/request_translation.rs
  • crates/switchyard-translation/tests/stream_translation.rs

Walkthrough

The change replaces lossy tool ID sanitization with reversible URL-safe Base64 encoding. OpenAI Chat and Responses encoders restore original IDs before serialization. Request and streaming tests cover round trips, reserved prefixes, malformed values, Unicode, and empty IDs.

Changes

Tool ID round-trip

Layer / File(s) Summary
Reversible ID encoding
Cargo.toml, crates/switchyard-translation/Cargo.toml, crates/switchyard-translation/src/util.rs
The workspace adds base64. Tool IDs now use reversible URL-safe encoding with reserved-prefix handling. Utility tests cover safe, empty, Unicode, reserved-prefix, and malformed values.
Upstream ID restoration
crates/switchyard-translation/src/codecs/openai_chat/buffered.rs, crates/switchyard-translation/src/codecs/responses/buffered.rs
OpenAI Chat tool IDs and Responses call_id values are desanitized before serialization.
Translation regression coverage
crates/switchyard-translation/tests/request_translation.rs, crates/switchyard-translation/tests/stream_translation.rs
Request and streaming tests verify sanitized IDs are restored and unsafe IDs use the reversible encoding.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Mergeability Score: ⚪ Minimal · up to 18ef2

This change preserves tool-call IDs across translation boundaries and adds regression coverage; no actionable merge-blocking risk remains beyond normal checks and review.

Poem

I’m a rabbit with IDs in a neat little row,
Safe through the tunnel wherever they go.
Base64 wraps them, then sends them back true,
Chat and Responses know what to do.
Hop, hop—tool loops continue anew! 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: preserving tool IDs across Anthropic translation.
Linked Issues check ✅ Passed The changes implement reversible tool ID encoding, upstream restoration for Chat and Responses, and buffered and streaming regression tests required by issue #178.
Out of Scope Changes check ✅ Passed All reported changes support reversible tool ID translation, dependency setup, or focused regression coverage for issue #178.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

Comment @coderabbitai help to get the list of available commands.

@ayushag-nv

Copy link
Copy Markdown
Contributor

@ting-hong-shieh Thanks for putting this up. Can you always do the before and after the change snapshot of the results. It helps me to do better and fast reviews.

@ting-hong-shieh

Copy link
Copy Markdown
Contributor Author

Thanks, @ayushag-nv. I ran the same in-process buffered translation at the PR base (b256d936) and current head (18ef28bd): OpenAI Chat response → Anthropic response → Anthropic replay → OpenAI Chat request. No provider call was involved.

Input upstream tool-call ID: functions.list_skills:0

Before (b256d936):

{
  "anthropic_tool_use_id": "functions_list_skills_0",
  "replayed_openai_tool_call_id": "functions_list_skills_0",
  "replayed_openai_tool_result_id": "functions_list_skills_0",
  "upstream_emitted_id": "functions.list_skills:0"
}

The unsupported characters are replaced with underscores, and both replayed OpenAI IDs differ from the ID emitted by the upstream model.

After (18ef28bd):

{
  "anthropic_tool_use_id": "sy64_ZnVuY3Rpb25zLmxpc3Rfc2tpbGxzOjA",
  "replayed_openai_tool_call_id": "functions.list_skills:0",
  "replayed_openai_tool_result_id": "functions.list_skills:0",
  "upstream_emitted_id": "functions.list_skills:0"
}

The Anthropic-facing ID stays within the accepted character set, while the OpenAI tool call and tool result both recover the exact upstream ID on replay.

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

Labels

None yet

Projects

None yet

2 participants