Repository navigation
[Bug]: Routed providers emit 'undeclared client tool Write' on Claude Code passthrough due to missing inboundWire exemption in passthrough-dispatch #6648
Description
Client or integration
Claude Code
Area
Proxy and routing
Summary
When proxying Claude Code (or VS Code Claude Agent) through OpenCodex using the Anthropic inbound wire (inboundWire: "anthropic"), routed third-party providers (such as DeepSeek V3/V4.1, Qwen, etc. routed via openai-responses or passthrough) frequently fail during multi-turn coding sessions when the model calls standard Claude tools (e.g. Write, Edit, Bash, Read).
The proxy aborts the streaming session and returns:
API Error: routed provider emitted undeclared client tool "Write"; only request-declared tools may be called
Root Cause Analysis
There is an inconsistency in how the undeclared tool guard is enforced between the bridged adapter path and the passthrough dispatch path:
-
In
src/server/responses/adapter-delivery.ts(lines 186 and 270),enforceDeclaredToolNamesexplicitly excludes Anthropic and Chat inbound wires:enforceDeclaredToolNames: options.inboundWire !== "chat" && options.inboundWire !== "anthropic",
This is intentional, because Anthropic clients declare and execute their own client-side tool catalog (
Write,Edit,Bash, etc.) under the Anthropic tool wire schema rather than Codex's internal declared wire tool representations. -
However, in
src/server/responses/passthrough-dispatch.ts(line 513),undeclaredToolGuardActiveomitted the inbound wire check:undeclaredToolGuardActive = ( declaredWireToolNames.size > 0 || clientDeclaredNamelessCallTypes.size > 0 || clientExplicitWireToolCatalog ) && route.provider.authMode !== "forward";
Notice that
options.inboundWire !== "chat"andoptions.inboundWire !== "anthropic"are missing. -
Consequently, when a routed provider returns a tool call under
passthrough-dispatch,undeclaredToolGuardActiveevaluates totrue. Because Claude's client tools (such asWrite) are not indeclaredWireToolNames,createUndeclaredToolCallGuardBlockRewritetrips and abruptly terminates the session with the error above.
Proposed Fix
1. Synchronize undeclaredToolGuardActive in src/server/responses/passthrough-dispatch.ts
Align the guard condition with adapter-delivery.ts:
--- a/src/server/responses/passthrough-dispatch.ts
+++ b/src/server/responses/passthrough-dispatch.ts
@@ -515,7 +515,10 @@ export async function dispatchPassthroughTurn(
declaredWireToolNames.size > 0
|| clientDeclaredNamelessCallTypes.size > 0
|| clientExplicitWireToolCatalog
- ) && route.provider.authMode !== "forward";
+ ) && route.provider.authMode !== "forward"
+ && options.inboundWire !== "chat"
+ && options.inboundWire !== "anthropic"
+ && inboundWire !== "anthropic";
options.nativeControl?.configureToolAuthorization?.(
undeclaredToolGuardActive,
declaredWireToolNames,2. (Optional Defense-in-Depth) Recognize Claude Code built-in tools in src/server/responses-undeclared-tool-guard.ts
When Codex code-mode shell execution is not active (!declared.has(CODE_MODE_EXEC_TOOL_NAME)), allow standard Claude Code client tools (Write, Edit, Read, Bash, etc.) through undeclaredNameInItem:
--- a/src/server/responses-undeclared-tool-guard.ts
+++ b/src/server/responses-undeclared-tool-guard.ts
@@ -93,6 +93,9 @@ function undeclaredNameInItem(
const effectiveName = normalizeDeclaredToolName(name, declared, declaredBare, declaredCustom);
if (declared.has(effectiveName)) return undefined;
+ if (CLAUDE_CODE_CLIENT_TOOLS.has(name) && !declared.has(CODE_MODE_EXEC_TOOL_NAME)) {
+ return undefined;
+ }
return name;3. (Optional) Case-insensitive normalization fallback in src/types/tools.ts
In normalizeDeclaredToolName, add a case-insensitive lookup fallback if exact case match fails.
Reproduction
- Start OpenCodex proxy (
ocx start --port 10100). - Route Anthropic wire requests to a non-forward routed provider (e.g., DeepSeek-V3 or DeepSeek-V4.1 via
openai-responsesor passthrough). - Connect Claude Code or VS Code Claude Agent to
http://127.0.0.1:10100. - Prompt the model with a multi-step task that causes it to invoke the
WriteorEdittool. - Observed: The turn fails immediately with
API Error: routed provider emitted undeclared client tool "Write"; only request-declared tools may be called. - Expected: The tool call passes through to the client for local execution, just as it does in
adapter-delivery.ts.
Version
@bitkyc08/opencodex 2.78.0
Operating system
Windows 11 (Build 26200)
Provider and model
Routed providers (e.g. DeepSeek-V3 / DeepSeek-V4.1 Flash) via Anthropic inbound wire
Logs or error output
API Error: routed provider emitted undeclared client tool "Write"; only request-declared tools may be called
Redacted configuration
{
"routing": {
"providers": {
"deepseek": {
"authMode": "api-key",
"wire": "openai-responses"
}
}
}
}Checks
- I searched existing issues and documentation.
- I removed secrets, tokens, account details, request credentials, and personal data.
Activity
Ingwannu commentedon Oct 6, 2026
At current dev
f924820652, the passthrough guard condition does differ from the adapter-delivery condition as reported. However, removing the entire guard for Anthropic/Chat ingress does not by itself explain why a genuinely declaredWriteis absent from its normalized declared-name set.Please attach a minimal synthetic failing fixture showing the inbound
toolsdeclaration, the translated tool name sent upstream, and the returned tool-call name/event. The fix should preserve an actually declared Claude tool while retaining refusal of a genuinely undeclared call on Responses ingress, and cover streaming plus terminal/non-stream output. The optional blanket builtin allowlist and case-insensitive fallback should not be bundled in without demonstrating the corresponding mismatch; they can authorize a name the caller did not declare.This is separate from the Cursor wire-alias work in #6640 and ToolSearch result-content loss in #6647. No guard bypass or upstream request was executed during triage; the local failure mechanism still needs a focused regression before changing authorization.
lidge-jun commentedon Oct 7, 2026
Held: the passthrough guard asymmetry is real in code, but translation already preserves a declared Write name, so the reported abort is not yet reproduced. A minimal reproduction (declared tool list in, routed tool call out) would let us land the parity change with confidence.
lidge-jun commentedon Oct 8, 2026
Fixed on
devby #6732 (mergeb1e1735cdf).Cause. The documented contract (#4799,
structure/adapters/compatibility-contracts.md) is that only theresponsesinbound wire refuses a routed provider's call to an undeclared tool (#1700);/v1/messagesand/v1/chat/completionsrelay it to the client's own tool runner. The adapter and runTurn paths already did that, but the Responses passthrough path (src/server/responses/passthrough-delivery.ts) refused on every wire. A routedopenai-responsesprovider calling a tool the Claude Code request had not declared (for example a deferred or runtime-discovered tool) therefore ended the turn withrouted provider emitted undeclared client tool "Write". A new endpoint test reproduced this on unmodifieddev: 8 failures across Anthropic and Chat, stream and buffered.Fix. Only the two passthrough refusal points (SSE rewrite and buffered JSON) are now scoped by inbound wire. Name normalization (
default.Read→Read) and continuation suppression still run on every wire. A relayed undeclared call is still never written toprevious_response_idreplay state./v1/responsesrefusal is unchanged; that endpoint sets its wire server-side, so a caller cannot opt out of it.The optional builtin-name allowlist and case-insensitive fallback from the report were not adopted, because they would authorize undeclared names on the
responseswire.Thanks for the detailed report.