Claude Code ToolSearch loads re-miss the prompt cache on translated Chat routes; `ocx claude` now hits this by default
I maintainer di solito rispondono entro 1 giorno
Nessuno ha ancora preso questa issue.
Valutazione
- Difficoltà
- 5/5
- Tempo stimato
- Più di una settimana
- Idoneità per principianti
- 25/100
- Tipo di issue
- Bug
- Chiarezza
- Specificata chiaramente
- Stato di attività
- Attiva
- Stack tecnologico
- typescript
- Ambito
- api, backend, performance
Direzione di ricerca
Start with src/adapters/tool-catalog-nudge.ts and the Chat route adapter paths described in the issue; compare them with the existing Responses handling in #7452 and #7453. Reproduce with Claude Code 2.1.304 or later using the provided ocx claude commands and inspect usage.jsonl cache counters. Done means a ToolSearch load no longer causes a large cache miss on translated Chat routes, or the limitation and supported mitigation are documented; the proposed prototype and tests are available locally.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
Client or integration
Claude Code
Area
Tools / MCP / web search
Summary
On a translated Chat Completions route (DeepSeek, GLM, …), each deferred-tool load through Claude Code's ToolSearch makes the next upstream request re-read almost the whole conversation instead of reusing the prompt cache. This applies to built-ins such as WebFetch and NotebookEdit, and to MCP tools. The cache recovers on the following request and breaks again at the next load.
Two parts of the outbound request change on every load:
src/adapters/tool-catalog-nudge.tslists every current tool name in the system message ("Valid tool names for this turn are exactly …"). This sentence is the first differing byte, about 6 kB into the system message, so only the first ~1.1k tokens stay cached.- The loaded definition is added to the top-level
toolsarray (toolsToResponsesignoresdefer_loading). Chat providers render that array ahead of the messages.
#7452 / #7453 fixed the same mechanism for Responses destinations that accept additional_tools. Chat routes have no equivalent. The Responses opt-in does not cover DeepSeek either, because DeepSeek's Responses API ignores unsupported input item types.
Affected sessions:
-
Claude Desktop Code-tab sessions in first-party/picker mode. Claude Code keeps tool search on there.
-
ocx claudesessions on Claude Code v2.1.304 or later. Claude Code now enables tool search behind a gateway by default (Tool search through an LLM gateway).ocx claudeinjects nothing whenclaudeCode.toolSearchis unset.The #4838 comment in
src/cli/claude.tsand the troubleshooting entry MCP tool schemas fill the context on turn one still describe the old behaviour: tool search off behind a gateway. The same comment says running a translated route with tool search on is "worse than the problem", and that is now the default state.
Expected
- A tool load on a Chat route does not invalidate the conversation prefix, or the limitation is documented with a supported mitigation.
ocx claudemakes an explicit choice for translated routes now that the client default has changed. One option is injectingENABLE_TOOL_SEARCH=falsewhentoolSearchis unset. The other is documenting the new behaviour and its cache cost.
Proposed fix (Chat routes)
An opt-in for direct Chat routes, modelled on #7453: the same selector rules, applied at the same physical-adapter boundary, with byte-identical requests when off.
- Keep one stable function in the catalog from the first request, e.g.
use_loaded_tool {name, arguments}. Keep deferred tools out of it, so the catalog and the nudge never change. - Append a loaded tool's definition as text to the ToolSearch result that loaded it. At that point this is the end of the transcript.
- Rewrite
use_loaded_toolcalls in the response stream back to the real tool before Claude Code sees them. Re-encode history the same way on every request, so earlier bytes stay identical.
This is the pattern the MCP client guide recommends for prompt caching with dynamic tools: one stable call_tool({name, args}) tool, so the tools array never changes. Unlike Codex code mode (#2106), Claude inbound does not enforce the undeclared-tool guard. A model that calls a loaded tool directly instead of through the wrapper still gets the call executed by Claude Code.
A working prototype with tests exists locally; the measurements are below. I can open a PR if this direction is acceptable.
Reproduction
Via the CLI. Use Claude Code 2.1.304 or later, with claudeCode.toolSearch and ENABLE_TOOL_SEARCH unset.
ocx claude -p "Turn 1: run echo hello with Bash" --model deepseek/deepseek-flash --output-format json- Resume the session with: "use ToolSearch to load the WebFetch tool, then call WebFetch on https://example.com".
- Compare
usage.cachedInputTokensinusage.jsonlfor the requests before and after the load.
Via Desktop. Pick a routed Chat model in the Code-tab picker, warm the session with a few tool turns, request a ToolSearch load, and compare the same counters. OPENCODEX_CACHE_DEBUG=1 shows the tool count growing by one.
Version
opencodex 2.94.0 (npm). The code paths above are unchanged on dev at 3ea89653f. Claude Code 2.1.306–2.1.312.
Operating system
Windows 11 (two machines)
Provider and model
Official DeepSeek deepseek-flash (Chat), and a third-party Chat gateway serving deepseek-v4.1-flash and glm-5.3-flash.
Logs or error output
Raw upstream counters from usage.jsonl (cachedInputTokens / inputTokens).
ocx claude, Claude Code 2.1.312, consecutive requests:
in=22,167 cached=22,016
in=22,541 cached= 1,536 <- first request after ToolSearch loaded WebFetch
in=22,623 cached=22,400
Desktop picker, DeepSeek official, consecutive requests:
in=58,086 cached=57,728 tools=51
in=59,551 cached= 7,680 tools=52 <- after ToolSearch select:WebFetch
in=59,846 cached=59,520 tools=52
Controlled 8-turn script with two tool loads (WebFetch, NotebookEdit) in a Desktop-identified session. Hit rate is for the main stream, excluding the first request:
| Build | DeepSeek official | DeepSeek via gateway | GLM via gateway | Breaks per run |
|---|---|---|---|---|
| released 2.94.0 | 86.7% | – | – | 2, both right after the loads |
| prototype, off | 85.0% / 85.1% | 85.5% | 79.5% | 2–3 |
| prototype, on | 97.2% / 96.7% | 97.6% | 93.4% | 0 from loads (1 early vendor miss on GLM) |
All task turns were correct in every run, including tool calls made through the wrapper. Latency stayed within run-to-run noise.
Two long Desktop sessions (23 turns, about 2.5 h, 2–3 tool loads each):
- Each load dropped the cache to about 6–8k tokens, while the conversation was 88k–124k tokens at the time.
- Removing only those misses would have cut total cost by about 17% at the gateway's list prices. Sessions without tool loads are unaffected.
- Idle gaps of up to 65 minutes caused no misses, so this is not TTL-related.
Screenshots and supporting files
Related:
- #7452, #7453: same mechanism, fixed for Responses.
- #4838, #5029: the launcher tool-search setting.
- #7919: eager degradation of
defer_loadingon unsupported wires. - anthropics/claude-code#83756: large ToolSearch batches also rewrite the cache on Anthropic's own API.
- router-for-me/CLIProxyAPI#6560: an anchor-tool fix for the Anthropic-upstream variant.
- MCP Client Best Practices, section Interaction with Prompt Caching.
Measurement scripts and raw counters are available on request.
Redacted configuration
{
"providers": {
"deepseek": { "adapter": "openai-chat", "baseUrl": "https://api.deepseek.com" },
"<third-party-gateway>": { "adapter": "openai-chat" }
},
"providerContextCaps": { "deepseek": 1000000, "<third-party-gateway>": 1000000 },
"claudeCode": { "desktopMode": "first-party", "intercept": { "picker": true } }
}
claudeCode.toolSearch, appendOnlyToolLoadingModels and ENABLE_TOOL_SEARCH are unset.
Checks
- I searched existing issues and documentation.
- I removed secrets, tokens, account details, request credentials, and personal data.
- Lingua principale
- TypeScript
- Stelle
- 16.9k
- Fork
- 1.3k
- Merge medio
- 16h 42m
- PR unite (30g)
- 695
Preparare l'ambiente
Questo progetto non fornisce container di sviluppo, Dockerfile né guida per i contributori, quindi l'ambiente è a tuo carico: parti dal suo README e consulta la nostra guida al primo contributo per i passaggi generali.
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di lidge-jun/opencodex
-
bug provider tools
Difficoltà 4/5 3-5 giorni Idoneità per principianti 52/100
I maintainer di solito rispondono entro 1 giorno
-
[Bug]: Routed Claude sessions emit call_ tool_use ids, and Remote Control drops their approval cardsApertabug tools
Difficoltà 3/5 1-2 giorni Idoneità per principianti 67/100
lidge-jun/opencodex#8051 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
bug
Difficoltà 2/5 1-3 ore Idoneità per principianti 35/100
lidge-jun/opencodex#8050 · 1 commento · 1 reazione ·
I maintainer di solito rispondono entro 1 giorno
-
account-pool bug
Difficoltà 4/5 3-5 giorni Idoneità per principianti 45/100
lidge-jun/opencodex#8042 · 4 commenti · 1 reazione ·
I maintainer di solito rispondono entro 1 giorno
-
bug gui proxy service
Difficoltà 5/5 Più di una settimana Idoneità per principianti 28/100
lidge-jun/opencodex#7967 · 4 commenti ·
I maintainer di solito rispondono entro 1 giorno
Tutte le issue di lidge-jun/opencodex
Issue simili
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 76/100
Northeastern-Electric-Racing/FinishLine#4660 ·
I maintainer di solito rispondono entro 2 giorni
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 86/100
excaliburjs/Excalibur#3895 ·
I maintainer di solito rispondono entro 3 giorni
-
infer_audio_type only recognises one MPEG Layer III frame header, so most MP3 files are rejectedForse già presa @Steve-20-ai l’ha presa oggi. Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
I maintainer di solito rispondono entro 1 giorno
-
Earn report crashes with "RangeError: Invalid time value" when a report row has an unparsable timestampForse già presa @Gurkaran18 l’ha presa oggi. Apertabug
Difficoltà 2/5 1-3 ore Idoneità per principianti 75/100
joinmarket-webui/jam#1588 ·
I maintainer di solito rispondono entro 3 giorni
-
BeOS .json missingAperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
mihaip/infinite-mac#475 ·