Repository navigation
[Provider compatibility] Claude Code Auto Mode classifier loses provider affinity and routes bare claude-opus-5 to an incompatible default provider #1697
Description
Client or integration
Claude Code
Provider or upstream service
OpenCodex multi-provider routing for Claude Code internal Auto Mode permission-classifier requests. Reproduced with:
- an explicitly selected Claude-compatible relay for the main model; and
- DeepSeek configured as the global
defaultProvider.
This is not an upstream DeepSeek defect: DeepSeek correctly rejects the Claude model slug it should never have received.
OpenCodex version
2.11.1
Environment:
- Claude Code
2.1.228 - macOS
26.5.2 - OpenCodex Claude proxy mode
- Claude Code Auto permission mode
- inbound endpoint:
POST /v1/messages
Endpoint or capability
POST /v1/messages — Claude Code Auto Mode background permission classifier used before Bash, Agent/subagent, Workflow, and other tool actions.
Current behaviour
A Claude Code session can explicitly run its main model through a routed provider, for example:
RelayA/claude-fable-5
The main-model requests resolve to RelayA and return HTTP 200.
Before Claude Code executes a tool in Auto Mode, it sends a separate internal classifier request whose model is the bare slug:
claude-opus-5
That side request does not preserve the main session's provider namespace. With no explicit claudeCode.modelMap entry, OpenCodex passes the bare slug through and routeModel() sends it to the global defaultProvider.
If the global default is DeepSeek, the observed route is:
requestedModel: claude-opus-5
routeKind: default-provider
selected.provider: deepseek
selected.model: claude-opus-5
selected.reason: default-provider
DeepSeek then correctly rejects the unsupported model with HTTP 400. Claude Code converts that failure into the misleading user-facing message:
claude-opus-5 is temporarily unavailable, so auto mode cannot
determine the safety of Bash/Agent/Workflow right now.
Consequences:
- the explicitly routed main model continues answering normally;
- Bash, Agent/subagent, Workflow, and other classified actions are blocked;
- retries repeat the same incorrect route;
- users can easily misdiagnose this as an Anthropic/Claude outage, a broken script, or a subagent configuration problem;
- changing the featured subagent roster does not fix it because the permission classifier is a separate internal request path.
On this machine, the same deterministic mismatch produced 71 HTTP 400 requests between 2026-08-07 and 2026-08-14. It affected multiple Claude Code conversations and multiple tool types.
The current static workaround is:
claudeCode.modelMap["claude-opus-5"] = "RelayA/claude-opus-5"
However, this is not sufficient as the default integration behaviour:
- It is not created automatically when the user selects
RelayA/claude-fable-5. - It becomes stale when the user changes the main provider in Claude Code's model picker.
- It has no health-aware or configured fallback.
- The GUI does not make the hidden classifier dependency and its resolved provider obvious.
- A static cross-provider map can send command/context metadata to a provider different from the main provider without making that boundary visible.
The relevant current flow appears to be:
src/claude/inbound.ts::resolveInboundModel(): alias → Desktop alias → staticmodelMap→ bare passthrough.src/router.ts::routeModel(): an otherwise unresolved bare Claude slug falls through toconfig.defaultProvider.
Expected behaviour
Claude Code internal classifier side requests should not silently lose provider affinity and fall through to an incompatible global default.
Recommended resolution order:
- Same-provider affinity: when the current Claude Code conversation/session is explicitly routed through
RelayA/claude-fable-5, resolve a bare classifier request forclaude-opus-5toRelayA/claude-opus-5if that model is available and enabled there. - Explicit classifier configuration: support a clear setting such as
claudeCode.classifierModeland/or orderedclaudeCode.classifierFallbacks. - Opt-in compatible fallback: if the current provider lacks Opus 5, try only user-approved fallback providers in deterministic order. Cross-provider fallback should be explicit because it affects privacy, billing, and data boundaries.
- Fail closed with a truthful error: if no compatible classifier route exists, return an actionable message such as:
No enabled provider can serve the Claude Code Auto Mode classifier model claude-opus-5. Configure claudeCode.classifierModel or disable Auto Mode. - Never silently send a Claude-family classifier slug to a provider whose catalog/model family is incompatible.
- Expose classifier routing in diagnostics/UI: requested model, resolved provider/model, affinity source, fallback reason, and availability.
A static modelMap should remain a valid explicit override, but users should not need to discover and hand-maintain it merely to make Claude Code's built-in safety path work after selecting a routed model.
Minimal redacted request or reproduction
# 1. Configure two enabled providers (all URLs and credentials omitted):
# - defaultProvider = deepseek
# - RelayA exposes claude-fable-5 and claude-opus-5
#
# 2. Enable Claude Code proxy integration and leave claudeCode.modelMap empty.
ocx claude
# 3. In Claude Code, select the routed main model:
# RelayA/claude-fable-5
#
# 4. Switch to Auto permission mode.
#
# 5. Ask Claude Code to perform either action:
# - run Bash: pwd
# - launch an Agent/subagent that replies WORKER_OK
#
# 6. Observe:
# - RelayA/claude-fable-5 main requests return 200
# - the internal bare claude-opus-5 request routes to deepseek
# - the classifier request returns 400
# - Bash/Agent is blocked as "classifier temporarily unavailable"Actual response or error
# Redacted OpenCodex usage/route evidence
{
"provider": "deepseek",
"model": "claude-opus-5",
"requestedModel": "claude-opus-5",
"status": 400,
"errorCode": "invalid_request_error",
"routeDecision": {
"requestedModel": "claude-opus-5",
"routeKind": "default-provider",
"selected": {
"provider": "deepseek",
"model": "claude-opus-5",
"reason": "default-provider"
}
},
"upstreamError": "The supported API model names are deepseek-v4-pro or deepseek-v4-flash, but you passed claude-opus-5."
}
# Claude Code tool result
claude-opus-5 is temporarily unavailable, so auto mode cannot
determine the safety of Agent right now. Wait briefly and try again.The same error is reproducible for Bash and Workflow. No API keys, provider URLs, prompts, source code, or account identifiers are included above.
Upstream documentation
Claude Code permission modes document that Auto Mode performs background safety checks before tool actions:
https://code.claude.com/docs/en/permissions#permission-modes
There is no public specification for the exact internal classifier model slug or its side-request routing contract. The concrete client requirement is observable from Claude Code 2.1.228: it sends a separate /v1/messages request with the bare claude-opus-5 slug and blocks the tool when that request fails.
Suggested mapping or implementation notes
Potential implementation directions:
- Track the most recent explicit main-model provider for the Claude Code conversation/session and use it as classifier affinity.
- Recognize the classifier request using stable Claude Code request metadata/shape rather than treating every user-selected bare Claude model as an internal classifier.
- Add validated config:
{ "claudeCode": { "classifierModel": "RelayA/claude-opus-5", "classifierFallbacks": [ "RelayB/claude-opus-5" ] } } - Validate candidates against enabled providers and configured/live catalogs before selection.
- Preserve
modelMapas the highest-priority explicit override. - Add a route-decision kind such as
claude-classifier-affinityorclaude-classifier-fallback. - Include a regression test where:
- main model =
RelayA/claude-fable-5; - bare classifier model =
claude-opus-5; - global default = DeepSeek;
- expected classifier route =
RelayA/claude-opus-5, never DeepSeek.
- main model =
- Add a negative test where no approved provider exposes Opus 5 and assert a clear, actionable fail-closed error.
Additional context and attachments
Related but distinct:
- Claude Desktop 3P Auto Mode classifier retries after 64-token Anthropic OAuth outputs #545 concerns classifier responses being truncated at a 64-token output limit through an Anthropic translation path.
- This report concerns provider selection before the request reaches any compatible Claude upstream.
Impact assessment: high. A single hidden routing mismatch disables the core agentic capabilities of Claude Code in Auto Mode while leaving ordinary main-model chat apparently healthy, producing a confusing split-brain failure that can persist across conversations.
Checks
- I searched existing provider and compatibility issues.
- The request and response were redacted.
- The expected behaviour is based on an upstream specification or a concrete client requirement.
Activity
[-][Provider compatibility][High impact] Claude Code Auto Mode classifier loses provider affinity and routes bare claude-opus-5 to an incompatible default provider[/-][+][Provider compatibility] Claude Code Auto Mode classifier loses provider affinity and routes bare claude-opus-5 to an incompatible default provider[/+]fix(claude): route Auto Mode classifiers only to operator-declared ta…
fix(claude): preserve Auto Mode classifier provider affinity and supp…
fix(claude): route Auto Mode classifiers only to operator-declared ta…
Merge pull request #1703 from ardakrt/fix/claude-classifier-provider-…
lidge-jun commentedon Aug 16, 2026
Fixed on
devby #1703, though not with the mechanism the original draft proposed.Auto Mode classifier turns arrive as bare
claude-opus-5with no provider, so they fell through todefaultProvidereven when it does not speak Anthropic. They now route toclaudeCode.classifierModel, then the orderedclaudeCode.classifierFallbacks, with an explicitmodelMapentry still outranking both:{ "claudeCode": { "classifierModel": "RelayA/claude-opus-5", "classifierFallbacks": ["RelayB/claude-opus-5"] } }Both are settable through
GET/PUT /api/claude-codeand documented in the configuration reference.Two mechanisms from the draft were deliberately dropped rather than shipped, because each was a silent provider crossing of exactly the kind this issue warns about:
Affinity inferred from
claudeCode.model. That value is the injected/default config slot, not the provider the live session actually selected — it goes stale the moment the model picker changes, and acting on it moves a classifier turn onto a provider with its own privacy and billing consequences. Real live-session affinity needs request and session state thatresolveInboundModeldoes not have; approximating it from static config is worse than not doing it, so classifier routing now uses only operator-declared targets."First enabled provider with an Anthropic adapter" in the router. It picked by object insertion order and checked neither
models,selectedModels,disabledModelsnor discovery state. That is gone. The other half of that change is kept: a disabled provider matching a known-model pattern is no longer selected, which was a real defect.One activation bug surfaced while adding load-time validation:
normalizePersistedClaudeCodewas only reachable through asubagentEffortshort-circuit, so a config whose only defect was elsewhere inclaudeCodewas never normalized. It now runs unconditionally, and a malformedclassifierFallbacksis coerced instead of reaching the resolver.What is not done: live per-session affinity — remembering the provider a session actually resolved to and reusing it for that session's classifier turns — is the stronger fix and needs a session-scoped store plus a request-shape predicate to tell an Auto Mode classifier turn from an ordinary bare-Opus request. That work is not in this change. If you need it before configuring
classifierModelexplicitly, please open a follow-up and I will scope it separately.Verified: full suite on Linux at the merge head, 12651 pass / 16 fail, with every failure also failing on
devat the same commit (GUI lint spawners, process-lock contention, and onebun-not-on-PATH harness case). No classifier, router, or inbound test fails.Closing as completed. The change is on
devand ships with the next release; it is not in a published version yet.fix(claude): preserve Auto Mode classifier provider affinity and supp…
fix(claude): route Auto Mode classifiers only to operator-declared ta…
Merge pull request lidge-jun#1703 from ardakrt/fix/claude-classifier-…