fix(claude): restore canonical tool names so Claude Code accepts tool calls - #11085
diegosouzapw merged 35 commits into
Conversation
… calls Claude Code rejects lowercase tool_use names with 'No such tool available: bash', killing Bash/Read/CronCreate for the whole session. Two gaps let the lowercase form through: - translateNonStreamingResponse emitted tool_use.name verbatim when an upstream answered a stream:true /v1/messages request with a JSON body (openai -> claude non-streaming conversion never consulted restoreClaudeToolName). - restoreClaudeToolName honoured identity echoes in _toolNameMap (map entry croncreate -> croncreate) over canonical casing, and lacked cron-era tools (TaskCreate/TaskUpdate/TaskList/TaskGet) in TOOL_RENAME_MAP. restoreClaudeToolName now upgrades known Claude Code tools to canonical PascalCase even through identity-echo maps, while real request-side aliases (mcp__fs__read etc.) still win; the diegosouzapw#7926 no-map TitleCase->lowercase fallback is untouched.
…uild diegosouzapw#10939 added `import { getConnection } from "@/lib/db/connections"` to the utilization route, but no such module exists in the repo — the Next build fails with "Module not found: Can't resolve '@/lib/db/connections'". Add the module following the existing db-helper pattern (getDbInstance + prepared SELECT on provider_connections), returning the email/name metadata the route maps into ConnectionMetaEntry. The route's `conn?.displayName` read is replaced with a literal null: the provider_connections table has no display-name column, and the Account Split card already falls back through getAccountDisplayName (displayName -> name -> email).
2c78a4f to
ae96cae
Compare
…e with no alias map Live-tested against glm-5.2 via opencode-go on /v1/messages: the request declared CronCreate/Bash, the gateway echoed them TitleCase verbatim, and the claude-to-openai route ships no _toolNameMap — so restoreClaudeToolName fell through to the diegosouzapw#7926 REVERSE_MAP fallback and downcased the echo to croncreate/bash. Claude Code rejects those with 'No such tool available', killing Bash/Read/CronCreate for the whole session. Every restoreClaudeToolName caller converts toward a Claude-format client, so blind TitleCase→lowercase downcasing has no legitimate consumer left: legacy OpenCode/XML-style lowercase clients are protected by explicit alias maps (request-side cloak), which this keeps honoring first. - canonical echo is now terminal in restoreClaudeToolName (after the alias-map block, before the diegosouzapw#7926 fallbacks) - tests updated from downcase expectations to canonical-preserve, each annotated with the live repro
|
Live-environment validation (Hard Rule #18) — extended fix in 52af7f5 The original commits fixed lowercase echoes through alias maps, but a live test against a real upstream exposed a remaining gap on the most common route: Claude Code → OpenAI-style upstream ships no Live repro (before 52af7f5) — real glm-5.2-high via opencode-go, isolated DATA_DIR, port 20129: After 52af7f5 (canonical echo terminal after the alias-map block): TDD: failing tests added first ( Legacy OpenCode/XML-style lowercase clients remain protected via explicit alias maps ({Read→read}), which restore honors before the canonical-echo terminal. |
…iegosouzapw#11060) Inherited base-red at merge time (discriminated against the pure base tip, both reproduce WITHOUT this diff): getTokenLimit test + Vietnamese i18n key parity (new UI strings merged untranslated) + No new ESLint warnings gate. Merge integrity, Docs Gates, Vitest, Fast Production Build: green.
…iegosouzapw#11095) Same inherited base-red set as diegosouzapw#11148 (getTokenLimit + vi parity + ESLint gate — all reproduce on the pure base tip). Merge integrity, Docs Gates, Vitest, Build: green.
…uzapw#11109) Validated on the combined batch board over release/v3.8.50 tip d91238b: static gates clean (changelog-integrity, file-size, complexity 2619<=2774, cognitive 1178<=1223, dead-code 411<=416), typecheck:core clean, focused tests green (262 batch-touched tests pass; the only red is the pre-existing vi.json harImport key drift from diegosouzapw#11069, reproduced on the pure tip — not this batch). Follow-up fix pushed to the branch pre-merge: the changelog fragment was prose, now a bullet (fragment gate). Thank you @maxmad64bis — the root-cause writeup (per-call finish_reason vs general terminal marker making the in-flight guard a no-op) is exactly right.
…Arguments (diegosouzapw#11043 followup) (diegosouzapw#11135) Validated on the combined batch board over release/v3.8.50 tip d91238b: static gates clean, typecheck:core clean, focused tests green. Test-only followup to diegosouzapw#11043 — 3 direct tests for splitConcatenatedToolCallArguments plus the index-normalization comment, exactly the two review nits. Thank you @maxmad64bis!
…items (diegosouzapw#11129) Validated on the combined batch board over release/v3.8.50 tip d91238b: static gates clean, typecheck:core clean, focused tests green. Live-confirmed 400 (missing summary on freshly-built Chat->Responses reasoning items); the two legacy assertions updated are contract propagation, each commented. Thank you @maxmad64bis!
…souzapw#11050) (diegosouzapw#11130) Validated on the combined batch board over release/v3.8.50 tip d91238b: static gates clean, typecheck:core clean, focused tests green. Closes the two diegosouzapw#11050 blockers: dispatcher tests now derive from WEBHOOK_EVENT_VALUES (no more TypeError on removed events) and vi.json carries a real translation. Thank you @maxmad64bis!
…erlay (diegosouzapw#11051/diegosouzapw#11049) (diegosouzapw#11133) Validated on the combined batch board over release/v3.8.50 tip d91238b: static gates clean, typecheck:core clean, focused tests green. Restores the models[0] dashboard default silently changed by diegosouzapw#11051, with narrow guards instead of a brittle full snapshot. Thank you @maxmad64bis!
…ouzapw#11102) Validated on the combined batch board over release/v3.8.50 tip d91238b: static gates clean, typecheck:core clean, focused tests green. Real suggestionCount replaces the conflated link count (deprecated alias kept), dashboard deep-link fixed. Thank you @maxmad64bis!
…t items (diegosouzapw#11110) Validated on the combined batch board over release/v3.8.50 tip d91238b: static gates clean, typecheck:core clean, focused tests green. Defaults summary and strips malformed ids on kept Responses input items — both 400s observed against live muse-spark traffic; the flipped legacy assertion is documented contract propagation. Thank you @maxmad64bis!
…ected keys (diegosouzapw#11101) Validated on the combined batch board over release/v3.8.50 tip d91238b: static gates clean, typecheck:core clean, focused tests green. Strict schema + {sanitized, rejected} DB boundary — silent validation degradation now answers 400 with the offending keys. Caller audit done: only the providers write path consumes the sanitizers. Thank you @maxmad64bis!
…iegosouzapw#11103) Validated on the combined batch board over release/v3.8.50 tip d91238b: static gates clean, typecheck:core clean, focused tests green. Config-audit survives restarts with bounded growth (migration 161 + OR IGNORE seed, retention wired into runAutoCleanup); route cabling deliberately out of scope. Thank you @maxmad64bis!
…ngs (diegosouzapw#11104) Validated on the combined batch board over release/v3.8.50 tip d91238b: static gates clean, typecheck:core clean, focused tests green. Operator-declared per-provider error rules via settings, consulted before the built-ins; the allowlist bypass is correct — declaring a rule is itself the opt-in, and no provider-specific rule is hardcoded. Thank you @maxmad64bis!
… Responses->Chat translation (diegosouzapw#11144) Validated on the combined batch board over release/v3.8.50 tip d91238b: static gates clean, typecheck:core clean, focused tests green. TDD red->green: parallel function_call items now get distinct stable index/id at .added time via a per-call Map, interleaved argument deltas no longer glue, dual item_id/output_index correlation. 120-test translator suite green. Thank you @maxmad64bis!
…nstead of a hardcoded/opt-out default (diegosouzapw#11116) Validated on the combined batch board over release/v3.8.50 tip d91238b: static gates clean, typecheck:core clean, focused tests green. Learned reasoning_effort caps mirror the merged learnedThinkingCaps mechanism: parse the upstream 4xx enum, clamp, retry once, consult proactively — covers custom openai-compatible connections the static registry can't. 22 new test cases + full regression list green. Fixes diegosouzapw#11111. Thank you @maxmad64bis!
diegosouzapw#11099) (diegosouzapw#11132) Cherry-picked the value commit (ac09b6b) onto the current release tip, dropping the stale base-red sync commits that no longer apply. Focused test cline-model-format-11099 2/2 green; check:provider-consistency OK (267/348/0). Fixes diegosouzapw#11099 — the zai→z-ai namespace typo. Thank you @rqzbeh!
… i18n labels (diegosouzapw#11096) (diegosouzapw#11117) Cherry-picked the value commit (0e91881) onto the current tip, dropping the stale base-red sync commits. Focused tests: pollinations-api-key-required 1/1 plus the whole optional-key suite 142/142 (two legacy assertions in provider-route-schemas flipped to the new key-required contract, commented with the PR). Fixes diegosouzapw#11096 — Pollinations answers 401 anonymously now. Thank you @rqzbeh!
…ouzapw#11070) (diegosouzapw#11079) Cherry-picked the value commit (55da60f) onto the current tip, dropping the stale base-red sync commits. Focused tests: opencode-v2-config-11070 + merge-provider-guard + config-dir-single-source 12/12; mutation coverage gate no-drift (new test registered in tap.testFiles by the PR itself). Fixes diegosouzapw#11070 — setup-opencode now emits both V1 and V2 blocks. Thank you @rqzbeh!
…ction model lockout (diegosouzapw#11071) (diegosouzapw#11078) Cherry-picked the value commit (2c9202e) onto the current tip, dropping the stale base-red sync commits. Focused tests: ollama-404-model-lockout 2/2 + the five sibling lockout suites (combo-provider-cooldown-sibling, 8247-model-unhealthy, vertex-passthrough, nvidia-410, account-fallback-service) 112/112; mutation coverage no-drift. Fixes diegosouzapw#11071 — local/self-hosted 404s now scope to model lockout per the resilience doctrine. Thank you @rqzbeh!
…vider (diegosouzapw#11091) (diegosouzapw#11122) Cherry-picked both value commits (a1fa49a + a91f6e9, incl. the fail-open guard for unparseable hostnames) onto the current tip, dropping the stale base-red sync commits. Focused tests: is-local-provider-11091 + the isLocalProvider consumer suites (provider-validation-specialty, ollama-local-provider) 127/127; mutation coverage no-drift. Fixes diegosouzapw#11091 — RFC1918/CGNAT/link-local/mDNS hosts now classify as local via the existing isPrivateHost. Thank you @rqzbeh!
…gosouzapw#11100) (diegosouzapw#11125) Cherry-picked the three value commits onto the current tip, dropping the stale base-red sync commits. Focused tests: search-blocked-providers-11100 + search-registry/searxng-loopback/chat-guard/x-search suites 65/65. Fixes diegosouzapw#11100 (endpoint half) — GET /v1/search now honors blockedProviders via getAllSearchProviders. Thank you @rqzbeh!
…8n parity, eslint gate (diegosouzapw#9985) Discriminated against the pure base tip (all three reproduced without any PR diff): (1) getTokenLimit test aligned to the contract changes of diegosouzapw#8228/diegosouzapw#11034 with the bluesminds 200k pin kept as the original guard; (2) Vietnamese translations completed for harImport*/omni-webhooks (upstream already carried equivalent translations — conflict resolved to base); (3) eslint gate fixed by typing the dynamic core imports in capture-critical-db-state.test.ts (no-explicit-any). 41/41 tests green, typecheck clean, eslint exit 0.
…egosouzapw#11123) Cherry-picked the author's cleaned single-commit head onto the current tip. One fix applied pre-merge: the removal migration collided at number 161 with today's config_audit_log migration (diegosouzapw#11103) — renumbered to 162_remove_hackclub_provider.sql. Focused tests: remove-hackclub-11118 + provider-metrics-deleted-provider green; check:provider-consistency OK (266/348/0). Provider removal requested by Hack Club maintainers (diegosouzapw#11118). Thank you @rqzbeh!
…roviders (diegosouzapw#11081) Validated on a combined board over tip aa12873: focused tests local-rerank-logging + call-logs-row-filter green (7/7 across runs), typecheck:core clean. One pre-merge fix: widened waitForCallLogSaves 5s→15s — the call-log artifact writer's fsync latency exceeds 5s under a loaded host (pre-existing call-log-save-drain flakes identically on the pure tip), so the budget was load-flaky, not the code. Local rerank now logs call entries on success and error paths like embeddings/cloud rerank, captures full request/response payloads, attaches x-omniroute-* headers, and falls back /v1/rerank→/rerank on 404. Thank you @AndrianBalanescu!
…iegosouzapw#11082) Validated on a combined board over tip aa12873 (incl. sibling diegosouzapw#11081): call-logs-row-filter 4/4 green, typecheck:core clean. The merged-row predicate fix closes a real gap — in-memory (in-flight/recently-completed) rows bypassed every filter except correlationId; rowMatchesFilter() now applies search/model/provider/account/apiKey/status/combo uniformly while DB rows stay idempotent. Thank you @AndrianBalanescu!
…11045) Validated on the combined board over tip 80d931a: kimi suites green (executor-kimi-web, kimi-partner-aff-links, token-health-check-kimi 33 assertions), vitest providerPageHeaderKimiPartnerLink green, typecheck:core clean. Audited the diff: only kimi-web switches to the international www.kimi.ai (Connect-RPC base, website, auth hints); kimi-coding / kimi-coding-apikey affiliate links stay on kimi.com as intended — asserted by the updated tests. Owner approved merge without the VPS smoke. Thank you @MeRezaRezaei!
Validated on the combined board over tip 80d931a: quota-redis-store (incl. the KEY_PREFIX derivation test), local-redis-status and rate-limiter-redis-optional green, typecheck:core clean. One pre-merge fix pushed to the branch: docs/reference/ENVIRONMENT.md gained the REDIS_KEY_PREFIX row (env-doc-sync gate requires every .env.example var documented). Board note: the redis tests leave an ioredis retry handle open and hang the runner exit locally — assertions all pass; pre-existing pattern, not from this PR. Thank you @MeRezaRezaei!
Merged with the ENVIRONMENT.md hunk dropped: the tip already documents unset=unlimited for DEFAULT_RATE_LIMIT_PER_DAY via diegosouzapw#11022 (eba58cc), so the docs conflict resolved to the tip text. What lands is the .env.example comment correction, verified against src/shared/utils/apiKeyPolicy.ts::buildDefaultRateLimits — unset/empty → [] (unlimited), malformed → legacy 1000/day windows, explicit 0 → unlimited. Conflict resolution validated on the combined board (env-doc-sync gate green). Thank you @Prajeeth-12!
c19c73f
into
diegosouzapw:release/v3.8.50
… calls (diegosouzapw#11085) Merged after conflict resolution validated on the combined board (50/50 casing tests green, typecheck:core clean). Two pre-merge adjustments on the branch: (1) the utilization route conflict resolved to the tip shape — its asNullableString/displayName version is newer than the branch's; (2) dropped the newly-added src/lib/db/connections.ts, orphaned once the route kept the tip shape (tip already uses getProviderConnectionById) — nothing imported it. The casing fix itself lands intact: non-streaming OpenAI→Claude conversion now restores canonical tool names, identity echoes no longer pin lowercase, and TOOL_RENAME_MAP gained the Task* tools. Fixes the live-reproduced Claude Code 'No such tool available: bash' failures. Thank you @linhdmn — outstanding repro and root-cause writeup!
Problem
Claude Code clients routed through OmniRoute to OpenAI-compatible providers lose native tool calls entirely. Live reproduction (2026-08-22,
opencode-go/ox-alpha-freeupstream,/v1/messageswith 143 declared tools):Bashtool, but the name reaches the client lowercased asbash.Every retry fails the same way — Bash/Read/Write/CronCreate stay dead for the whole session while MCP tools (which happen to be lowercase already) keep working.
Call logs show two distinct leaks:
stream:truerequest with anapplication/jsonbody, OmniRoute converts viatranslateNonStreamingResponse, whoseconvertOpenAINonStreamingToClaudecopiedfunction.nameverbatim into the Claudetool_useblock. The streaming path already calledrestoreClaudeToolName; the non-streaming path did not._toolNameMapalias channel (croncreate → croncreate).restoreClaudeToolNamereturned that identity match before considering canonical casing, andTOOL_RENAME_MAPwas missing the cron-era tools (TaskCreate,TaskUpdate,TaskList,TaskGet), so even the canonical upgrade had nothing to upgrade to.Fix
open-sse/handlers/responseTranslator.ts: pass the request'stoolNameMapintoconvertOpenAINonStreamingToClaudeand restore each emitted tool name viarestoreClaudeToolName, matching the streaming translator.open-sse/services/claudeCodeToolRemapper.ts:bash → Bash,croncreate → CronCreate, …).TOOL_RENAME_MAP:TaskCreate,TaskUpdate,TaskList,TaskGet.read → mcp__fs__readis preserved), and the#7926no-map TitleCase→lowercase fallback is untouched.Tests
New suite
tests/unit/claude-code-tool-casing-identity-echo.test.ts(12 cases):bash → Bashand cron-era tools) in bothrestoreClaudeToolNameand the streamingopenaiToClaudeResponsetranslateNonStreamingResponseupgradesbash → Bashwith no map (live repro) and honours aliases/canonical order#7926fallback unchangedAll green together with the existing suites:
gemini-to-claude-tool-name-case-9008,claude-tool-name-casing-fix,claude-code-tool-remapper-flag-leak,provider-request-capture-toolname-map-4091,claude-server-tool-name-preserve,translator-resp-openai-to-claude,translator-openai-to-claude,claude-code-parity,xai-translators,9568-gemini-tool-casing-mismatch(172 tests).