Skip to content

fix(claude): sanitize tool_use ids on native Anthropic passthrough - #5628

Closed
Haven2026 wants to merge 1 commit into
lidge-jun:devfrom
Haven2026:fix/passthrough-tooluse-id
Closed

Haven2026 wants to merge 1 commit into
lidge-jun:devfrom
Haven2026:fix/passthrough-tooluse-id

Conversation

@Haven2026

@Haven2026 Haven2026 commented Sep 23, 2026

Copy link
Copy Markdown

Summary

  • fix(anthropic): normalize tool call ids so cross-provider history replays #1780 added createToolCallIdAllocator and wired it into adapters/anthropic.ts and adapters/google.ts, so translated Anthropic requests replay cross-provider tool history safely.
  • anthropicNativePassthrough in src/server/claude-messages.ts bypasses that adapter by design and serialized the request body verbatim — sessions carrying non-Anthropic tool call ids (e.g. Bash:0#<hex> minted by third-party models) still 400 with tool_use.id: String should match pattern '^[a-zA-Z0-9_-]+$' when switched to a native Claude model on a real sk-ant-* credential.
  • This runs the same allocator over *tool_use / *tool_result blocks before the body is serialized. Conforming ids pass through byte-identical (prompt-cache keys untouched); non-conforming ids get the deterministic sanitize+hash rewrite with stable call/result pairing. Covers /v1/messages and /v1/messages/count_tokens, which share the branch.

Test plan

  • New case in tests/claude-integration/claude-native-passthrough.test.ts: polluted tool_use/server_tool_use/web_search_tool_result/tool_result ids rewritten to conforming wire ids, pairing preserved, conforming toolu_* ids byte-identical, deterministic across requests (messages vs count_tokens)
  • bun test tests/claude-integration/claude-native-passthrough.test.ts — 17/17 pass
  • bun run typecheck — clean
  • bun run structure:check — pass (no contract change: docs describe the passthrough as "forwards without translation"; id sanitization is the same class as the image normalization already applied on this branch)
  • bun run privacy:scan — pass
  • bun run test:changed — 2626 pass / 20 fail; all 20 are management-provider-validation cases that fail identically on clean dev (api.example.test provider POST → 400; unrelated environment issue)
  • Live verification against real Anthropic: polluted Bash:0#<hex> history → 400 before this change, 200 after (claude-haiku-4-5 via passthrough)

Refs #1767, #1780.

Review readiness checklist

This PR stays in draft until every box below is ticked. Tick all four boxes once the requirements are met:

  • Required local validation passed; commands, results, and any full-suite exception are documented.

  • I pushed my PR to the latest dev commit.

  • I resolved all correct Codex and CodeRabbit findings.

  • My PR is ready for review.

lidge-jun#1780 normalizes tool call ids in the Anthropic and Google adapters, but
anthropicNativePassthrough bypasses the adapter by design and forwarded
message bodies verbatim. Sessions that carry third-party tool call ids —
e.g. `Bash:0#<hex>` minted by non-Anthropic models — therefore still 400
(`tool_use.id: String should match pattern '^[a-zA-Z0-9_-]+$'`) the moment
the model is switched to a real Anthropic model with native credentials.

Run the same createToolCallIdAllocator over *tool_use / *tool_result blocks
before the body is serialized: conforming ids pass through byte-identical,
non-conforming ids are rewritten deterministically with a hash suffix, and
call/result pairing stays stable. Covers /v1/messages and
/v1/messages/count_tokens, which share this branch.
@github-actions

Copy link
Copy Markdown
Contributor

Deterministic PR hygiene checks passed.

@github-actions github-actions Bot added the bug Something isn't working label Sep 23, 2026
@github-actions

Copy link
Copy Markdown
Contributor

⏳ DRAFT

  • review readiness checklist open (0/4 boxes ticked).

What to do

  • Tick all four boxes in the PR description once you're done (currently 0/4).

Review readiness checklist

  • ⬜ Required local validation passed; commands, results, and any full-suite exception are documented.
  • ⬜ I pushed my PR to the latest dev commit.
  • ⬜ I resolved all correct Codex and CodeRabbit findings.
  • ⬜ My PR is ready for review.

0/4 boxes ticked.

This PR stays in draft until every box above is ticked.

@github-actions
github-actions Bot marked this pull request as draft September 23, 2026 02:41
@lidge-jun

Copy link
Copy Markdown
Owner

리뷰 · 우선순위 64 / 80

클로드 본사로 요청을 거의 그대로 넘기는 길에서는 도구 호출 번호(tool_use id)를 고치지 않았습니다. 번호 고치기는 번역 어댑터에만 있었습니다. 다른 모델이 만든 번호가 대화에 남아 있으면, 예를 들어 Bash:0#긴16진수처럼 콜론이나 #이 섞인 번호면, 진짜 클로드 키로 바꾸는 순간 본사가 400을 냅니다. 본사 규칙은 영문, 숫자, _, -만 허용하고 길이는 64자 이하입니다.

이 PR은 보내기 직전에 같은 고치기를 이 길에도 넣습니다. 규칙에 맞는 번호는 한 글자도 안 바꿉니다. 안 맞는 번호는 같은 입력이면 항상 같은 값으로 바뀌고, 호출과 결과의 짝은 유지됩니다. /v1/messages/v1/messages/count_tokens가 이 함수를 같이 씁니다. base는 dev이고, 브랜치는 지금 dev 맨 위에 있습니다. types.tsconfig.ts를 나누는 변경은 아닙니다.

라인 - src/server/claude-messages.tssanitizePassthroughToolCallIds — 빈 문자열 번호는 그대로 넘어갑니다. allocate("")undefined를 돌려주고, 코드는 그 값이 문자열일 때만 칸을 바꿉니다. 어댑터는 이런 번호를 요청에 실지 않습니다 (#1767). 이 길에서는 빈 번호가 남고, 본사는 그 요청도 400으로 거절할 수 있습니다.

라인 - tests/claude-integration/claude-native-passthrough.test.ts — 콜론이 섞인 번호를 고치는지, 짝이 유지되는지, 정상 번호 toolu_는 그대로인지, count_tokens와 결과가 같은지는 확인합니다. 65자를 넘는 번호, 빈 번호, 고친 값이 이미 있는 정상 번호와 겹치는 경우는 없습니다. 겹침을 피하려고 정상 번호를 먼저 잡아 두는 reserve가 빠지거나 순서가 바뀌어도 이 테스트는 통과합니다.

라인 - 상태 · draft — 준비 체크리스트 네 칸이 비어 있습니다. 최신 dev 위에 있는 것은 맞습니다. Ready로 올리기 전에는 머지 대상이 아닙니다.

메인테이너의 판단이 필요한 지점

structure/overview.md는 이 길을 번역 없이 그대로 전달한다고 적습니다. 이미지 정리만 이미 본문을 바꾸고 있고, 이번에도 번호를 바꿉니다. 그 문장을 고칠지, 예외로 남겨 둘지 정하면 됩니다.

빈 번호를 이번 PR에서 막을지, Bash:0#... 같은 글자 문제만 고치고 빈 번호는 다음으로 미룰지도 같이 보면 됩니다.

너의 추천

닫지 않는 편이 맞습니다. 같은 수정을 담은 다른 열린 PR은 없습니다. 빈 번호는 요청에 남기지 말고, 번호가 겹치는 테스트를 하나 넣은 다음 체크리스트를 채우고 Ready로 올리면 됩니다.

이 댓글은 grok-bot이 작성했습니다

lidge-jun added a commit that referenced this pull request Sep 23, 2026
…riptions, launchd levers, passthrough tool ids (#5678)

* test(claude): cover bounded skill marker paths and document pass-through

Carries #5606. Adds the exact 4,096/4,097 UTF-16 code unit boundary cases for
POSIX and Windows skill directory markers and a long newline-free carrier, and
documents that a longer directory line is sent unchanged. The seven translated
Claude Code guides now state the same pass-through rule as the English guide.

Supersedes #5606.

Co-authored-by: Epinephrine <27862058+luvs01@users.noreply.github.com>

* fix(system-env): refresh and drop the launchd levers opencodex owns

Carries #5622. A lever opencodex already tracks as injected is refreshed instead
of skipped, a tracked lever the current config no longer produces is unset, and
PUT /api/claude-code reconciles on every model slot and lever field, not only
systemEnv and authMode. A launchd value the user set before injection is never
touched. The management API structure note records the PUT reconciliation.

Supersedes #5622.

Co-authored-by: terin <100397903+sh940701@users.noreply.github.com>

* feat(claude): describe routed models instead of "From gateway"

Carries #5621. Readable Claude Code /v1/models rows carry a description naming
the native model or provider/model they route to, which Claude Code 2.1.257 and
later shows in the picker; the 1M copy keeps it and a Fast sibling appends
" · Fast". The gateway-model cache preserves string descriptions. The Claude
Desktop structure note records the picker description contract.

Supersedes #5621.

Co-authored-by: terin <100397903+sh940701@users.noreply.github.com>

* fix(claude): keep routed model windows without disabling compact

Carries #5665. Claude Code aliases are minted as ocx-claude-/ocx-claude2- so the
picker keeps them while Claude Code applies CLAUDE_CODE_MAX_CONTEXT_TOKENS
without DISABLE_COMPACT; saved claude-ocx-/claude-ocx2- ids keep decoding.
maxContextTokens now injects only the window. The gateway cache mirrors the
picker's contains-claude rule and keeps #5621's descriptions.

Folded review fixes: native fallback recognizes the current prefixes; the
context-window map keeps registering the legacy spellings at the same window;
no compact lever is ever derived from maxContextTokens (pinned for values
outside the 100k-1M compact range); the tracked DISABLE_COMPACT from older
releases is removed by the system-env produced-key sweep instead of a one-off
cleanup; the Claude Desktop structure note records the alias and env contract.

Supersedes #5665.

Co-authored-by: terin <100397903+sh940701@users.noreply.github.com>

* fix(claude): sanitize tool_use ids on native Anthropic passthrough

Carries #5628. The native passthrough bypasses the Anthropic adapter, so tool
call ids minted by routed models (Devin's Bash:0#<hex>) reached
api.anthropic.com verbatim and 400ed. The request-scoped allocator now rewrites
non-conforming and overlength ids, keeps call/result pairing, and leaves
conforming ids byte-identical.

Folded review fixes: an empty id fails locally with a 400 before the upstream
fetch instead of being forwarded; regressions cover the empty id, an overlength
id and a collision with an existing valid id; the Claude Desktop structure note
and the Claude Code guide in all eight languages describe the id rewrite.

Supersedes #5628.

Co-authored-by: wuwei <27188611+Haven2026@users.noreply.github.com>

* fix(claude): keep legacy slot selectors and hand-edited DISABLE_COMPACT safe on upgrade

Folds the bundle's adversarial review into the #5665 and #5622 carries.

A legacy claude-ocx-/claude-ocx2- selector configured in an OpenCodex model slot
is emitted in its current ocx-claude spelling on every env path. The route is
identical, and Claude Code then applies the configured window instead of
falling back to 200k accounting now that DISABLE_COMPACT is no longer paired
with maxContextTokens. A selection saved by Claude Code's own picker stays a
documented re-pick.

The system-env sweep removes a tracked DISABLE_COMPACT only while it still holds
the 1 older releases injected; a value the user changed by hand is released
from tracking without being deleted.

Co-authored-by: terin <100397903+sh940701@users.noreply.github.com>

* fix(claude): keep legacy picker selectors on connected clients and Fable passthrough

Folds the Codex and CodeRabbit review of the bundle. A legacy
claude-ocx-native--claude-fable-* picker value compared only against the new
ocx-claude spelling and fell off the native Anthropic passthrough; both
spellings are accepted again, with the legacy value back in the endpoint test.
The connected-client context-window map registers the legacy route and native
spellings like the local map does, so a saved legacy selector keeps its [1m]
subagent marker on a connected hub.

Co-authored-by: terin <100397903+sh940701@users.noreply.github.com>

---------

Co-authored-by: Epinephrine <27862058+luvs01@users.noreply.github.com>
Co-authored-by: terin <100397903+sh940701@users.noreply.github.com>
Co-authored-by: wuwei <27188611+Haven2026@users.noreply.github.com>
@lidge-jun

Copy link
Copy Markdown
Owner

Carried onto dev in bundle PR #5678 (squash-merged as 9d1fa87), rebuilt on current dev as commit c9c1e3b on the lane branch with a Co-authored-by trailer for you, so the credit stays on the merged commit. Closing this one as superseded. Thank you for the fix.

@lidge-jun lidge-jun closed this Sep 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants