Claude Code ToolSearch loads re-miss the prompt cache on translated Chat routes; `ocx claude` now hits this by default
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 5/5
- Estimated time
- Over a week
- Newbie friendliness
- 25/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- typescript
- Domain
- api, backend, performance
Research direction
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.
Written by the indexing model from the issue text.
Description
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.
- Dominant language
- TypeScript
- Stars
- 16.9k
- Forks
- 1.3k
- Avg merge
- 16h 42m
- Merged PRs (30d)
- 695
Getting set up
This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from lidge-jun/opencodex
-
bug provider tools
Difficulty 4/5 3-5 days Newbie friendliness 52/100
Maintainers usually reply within 1 day
-
[Bug]: Routed Claude sessions emit call_ tool_use ids, and Remote Control drops their approval cardsOpenbug tools
Difficulty 3/5 1-2 days Newbie friendliness 67/100
lidge-jun/opencodex#8051 · 1 comment ·
Maintainers usually reply within 1 day
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 35/100
lidge-jun/opencodex#8050 · 1 comment · 1 reaction ·
Maintainers usually reply within 1 day
-
account-pool bug
Difficulty 4/5 3-5 days Newbie friendliness 45/100
lidge-jun/opencodex#8042 · 4 comments · 1 reaction ·
Maintainers usually reply within 1 day
-
bug gui proxy service
Difficulty 5/5 Over a week Newbie friendliness 28/100
lidge-jun/opencodex#7967 · 4 comments ·
Maintainers usually reply within 1 day
All issues in lidge-jun/opencodex
Similar issues
-
backend bug documentation
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
geolens-io/geolens#3568 ·
Maintainers usually reply within 1 day
-
area:docs documentation good first issue priority:low
Difficulty 1/5 Under an hour Newbie friendliness 80/100
Maintainers usually reply within 1 day
-
API: `tsc.exe` opens a visible console window on Windows when the parent process has no consoleOpen
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
microsoft/TypeScript#65083 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 67/100
Maintainers usually reply within 1 day
-
bug p2 platform:web triaged
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Maintainers usually reply within 1 day