Skip to content

Sanitize tool-call IDs on the Anthropic Messages wire - #52

Open
jcheng5 wants to merge 1 commit into
mainfrom
fix/anthropic-tool-call-id-sanitization
Open

Sanitize tool-call IDs on the Anthropic Messages wire#52
jcheng5 wants to merge 1 commit into
mainfrom
fix/anthropic-tool-call-id-sanitization

Conversation

@jcheng5

@jcheng5 jcheng5 commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

Problem

The Anthropic Messages API validates tool_use.id / tool_result.tool_use_id against ^[a-zA-Z0-9_-]+$. Other wires accept arbitrary IDs — e.g. Kimi K3 emits <toolName>:<counter> like ls:0, with the counter resetting per turn so duplicates across history are the norm. When a conversation containing such IDs is replayed to an Anthropic-wire model (model switch mid-conversation, /compact summarizing with Claude Haiku), the API rejects the request:

Error making request (HTTP 400): messages.1.content.1.tool_use.id: String should match pattern '^[a-zA-Z0-9_-]+$'

Fixes posit-dev/assistant#2016.

Fix

The constraint is a property of the target wire, so Anthropic-wire clients now sanitize outbound IDs unconditionally via a new sanitizeToolCallIdsForAnthropic helper (tool-call-ids.ts):

  • Base: deterministic character replacement ([^a-zA-Z0-9_-]-); empty IDs fall back to call.
  • Uniqueness: rewritten ID = <base>-<i> where i is the message index in the outbound array. Pairing is positional: a tool-result in a tool message at j uses j-1; a provider-executed result in an assistant message uses its own index. Deterministic and prompt-cache-stable because history is append-only.
  • Conforming duplicates (same ID used by a call in an earlier message) are index-suffixed via first-occurrence tracking on tool-calls — results mirror their paired call. (A naive seen-set over all parts would break pairing, since results legitimately repeat their call's ID.)
  • No-op fast path: conforming first-use IDs pass through untouched; pure-Claude histories get the input array back by reference.
  • Observability: warns on non-conforming IDs, same-message base collisions, and orphan tool-result parts.

Wiring goes through a shared streamTextAnthropicWire() choke point so future Anthropic-wire clients get sanitization by default: AnthropicClient, PositAiClient (anthropic-messages branch), and SnowflakeClient (chatAnthropic) use the wrapper; BedrockClient and GoogleVertexClient sanitize conditionally on their Anthropic routes since those streamText call sites are shared across protocols.

Tests

  • 11 sanitizer unit tests: deterministic rewrite, positional pairing (both result kinds), cross-turn duplicates (ls:0 in two turns), same-turn parallel calls, conforming-duplicate suffixing, empty-ID fallback, no-op passthrough, collision/orphan warnings.
  • Wire-level regression test through PositAiClient with the observed Kimi ls:0 format repeated across two turns, asserting the serialized request body contains only pattern-conforming, unique, paired IDs. Verified to fail on the unfixed code.

Full bridge suite (286 tests) and monorepo check-types pass.

The Anthropic Messages API validates tool_use.id against
^[a-zA-Z0-9_-]+$. Other wires accept arbitrary IDs (e.g. Kimi K3 emits
<toolName>:<counter> like ls:0), so replaying a cross-provider history
to an Anthropic-wire model — model switch mid-conversation, /compact
summarizing with Claude — fails with a 400.

Add sanitizeToolCallIdsForAnthropic: deterministic character replacement
plus a message-index suffix for per-request uniqueness, with positional
call/result pairing (tool-message results use j-1, provider-executed
assistant-message results use their own index). Conforming, first-use
IDs pass through untouched, so pure-Claude histories are a zero-copy
no-op. Conforming duplicates are index-suffixed via first-occurrence
tracking. Warns on non-conforming IDs, same-message base collisions,
and orphan tool-results.

Wire in via a shared streamTextAnthropicWire choke point used by
AnthropicClient, PositAiClient (anthropic-messages branch), and
SnowflakeClient (chatAnthropic); BedrockClient and GoogleVertexClient
sanitize conditionally on their Anthropic routes.

Fixes posit-dev/assistant#2016
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant