Skip to content

Claude Code ToolSearch loads re-miss the prompt cache on translated Chat routes; ocx claude now hits this by default #7963

Closed

Description

@Gudals0320

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:

  1. src/adapters/tool-catalog-nudge.ts lists 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.
  2. The loaded definition is added to the top-level tools array (toolsToResponses ignores defer_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:

Expected

  • A tool load on a Chat route does not invalidate the conversation prefix, or the limitation is documented with a supported mitigation.
  • ocx claude makes an explicit choice for translated routes now that the client default has changed. One option is injecting ENABLE_TOOL_SEARCH=false when toolSearch is 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_tool calls 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.

  1. ocx claude -p "Turn 1: run echo hello with Bash" --model deepseek/deepseek-flash --output-format json
  2. Resume the session with: "use ToolSearch to load the WebFetch tool, then call WebFetch on https://example.com".
  3. Compare usage.cachedInputTokens in usage.jsonl for 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:

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.

Activity

  1. added
    bugSomething isn't working
    cliCLI, config inject, packaging flags
    toolstool_calls, MCP, web-search / sidecar tools
    on Oct 29, 2026
  2. lidge-jun commented on Oct 30, 2026

    @lidge-jun
    Owner

    Maintainer triage (2026-10-30, against dev 3a70dd4788):

    Confirmed on current dev. On translated Chat routes every tool Claude Code has loaded is serialized into the Chat tools array (src/adapters/openai-chat/tool-schema.ts), and the catalog nudge in the system message lists the valid tool names for the turn (src/adapters/tool-catalog-nudge.ts, injected from src/adapters/openai-chat/messages.ts). When ToolSearch loads WebFetch, NotebookEdit or MCP tools, both the tool array and the system text change, so the provider's prefix cache misses on the next request, which matches the 22,016 → 1,536 cached-token drop in the report. The existing mitigation (src/claude/inbound-tool-loading.ts, projecting deferred tools through additional_tools) is selected only when the physical adapter is openai-responses (src/server/responses/request-transport.ts), so Chat routes never get it.

    A real fix is feature-sized rather than a bounded patch: Chat routes would need an opt-in that represents deferred tools through one stable wrapper function (for example use_loaded_tool(name, arguments)), appends loaded definitions at the ToolSearch result position instead of the system prompt and top-level tool list, replays earlier calls in the same wrapper shape, and maps wrapper calls in responses back to the real tool. It also has to handle forced tool_choice, streaming fragments, duplicate call ids and fallback to non-Chat targets, and keep requests byte-identical when the option is off. That needs a design decision and its own PR, so I am not landing a partial version in this maintenance pass.

    Keeping this open as a confirmed enhancement. Until then, routing these models through a Responses-capable adapter (where available) keeps the deferred-tool projection and its cache stability.

  3. Gudals0320 commented on Oct 31, 2026

    @Gudals0320
    ContributorAuthor

    Opened #8057 with the opt-in Chat dispatcher described in the triage above (claudeCode.chatToolDispatchModels). The PR description maps each of the five conditions (forced tool_choice, streaming fragments, duplicate call ids, fallback to non-Chat targets, byte-identical when off) to its test.

    Before writing it I measured the suggested interim workaround for DeepSeek: routing it through a Responses adapter with appendOnlyToolLoadingModels. Official DeepSeek API, deepseek-flash, Claude Code 2.1.314, isolated server on dev.

    The cache holds, but DeepSeek drops additional_tools, so loaded tools never reach the model.

    • Direct request to https://api.deepseek.com/responses: with and without an additional_tools item, input_tokens is identical (332 / 332), and the model replies that the tool does not exist. The item is discarded without an error.
    • 8-turn Claude Code scenario (ToolSearch → WebFetch, ToolSearch → NotebookEdit): main-stream hit rate 97.2% / 96.9% with 0 breaks (Responses with the opt-in off: 89.8% / 90.8%; Chat: 87.9%). Every turn completed, but only because the model already knows those built-in schemas and DeepSeek accepts calls to names absent from tools.
    • Deferred MCP tool with non-guessable parameter names: with the opt-in on, the first call used invented arguments in 2 of 2 runs and was rejected. The Chat dispatcher calls it correctly on the first attempt (97.4% hit rate, 0 breaks).

    So for DeepSeek the Responses route is not a usable interim workaround. The PR also notes this next to the Responses opt-in in the Claude Code guide.

  4. lidge-jun commented on Nov 10, 2026

    @lidge-jun
    Owner

    Rechecked on dev df0e6ccc4: the Chat path is unchanged. #8057 (the opt-in claudeCode.chatToolDispatchModels dispatcher, +944/-22 across 16 files) was closed by its author on 2026-11-05 without merging, so nothing currently addresses this. The earlier triage put it behind a design decision; the open choices are:

    1. Carry feat(claude): keep the Chat tool catalog stable across ToolSearch loads (opt-in dispatcher) #8057 as a maintainer carry with Co-authored-by: Gudals0320, after a design review of the new config key and the wrapper-call rewrite.
    2. Have ocx claude inject ENABLE_TOOL_SEARCH=false when claudeCode.toolSearch is unset and the route is a translated Chat route. This restores the old cache behavior at the cost of first-turn context size.
    3. Docs only. Correct the ocx claude should set ENABLE_TOOL_SEARCH=true: Claude Code disables MCP tool deferral behind a non-Anthropic base URL, so every tool schema ships in full #4838 comment in src/cli/claude.ts and the "MCP tool schemas fill the context on turn one" troubleshooting entry, which still describe tool search as off behind a gateway.

    Recommendation: 3 immediately, because those two texts are now wrong whatever else is chosen, plus 1 if the owner accepts the config key. 2 trades one cost for another and is best left to the operator.

  5. added
    needs-design설계 필요. Implementation waits on a written design.
    on Nov 10, 2026
  6. lidge-jun commented on Nov 10, 2026

    @lidge-jun
    Owner

    Decision: option 3 (correct the src/cli/claude.ts #4838 comment and the troubleshooting entry) lands now, and option 1 proceeds as a maintainer carry of #8057 (Co-authored-by: Gudals0320), opt-in only, after a design review, in a separate PR.

  7. removed
    needs-design설계 필요. Implementation waits on a written design.
    on Nov 10, 2026
  8. added a commit that references this issue on Nov 10, 2026
    4bf2ec8
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingcliCLI, config inject, packaging flagsenhancementNew feature or requesttoolstool_calls, MCP, web-search / sidecar tools

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions