[BUG] Gateway model discovery (CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY) never fires under OAuth-passthrough gateway setups — verified root cause
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet (closely related: #77247, same underlying
On()/gatewayAuthmisresolution class, but that issue is about the 1M-context budget; this one is aboutCLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERYnever firing) - [x] This is a single bug report
- [x] I am using the latest version of Claude Code
What's Wrong?
CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1 has no effect — and fails completely silently, with no warning or log message visible to a normal user — when Claude Code is configured with a custom ANTHROPIC_BASE_URL pointing at a self-hosted LLM gateway (e.g. Bifrost, LiteLLM) using OAuth-passthrough auth (a real claude login session, no static ANTHROPIC_AUTH_TOKEN/API key). The gateway's /v1/models-equivalent endpoint is never queried, so custom models the gateway serves never appear in the /model picker, regardless of the discovery flag.
Root cause (verified against the actual CLI source, not just observed behavior — see below): the discovery fetch is gated behind the CLI's internal provider-mode resolver returning "gateway", and that mode can only be reached in one of two ways, neither of which matches this (very common) deployment shape:
CLAUDE_CODE_USE_GATEWAY=1+ANTHROPIC_BASE_URL+ a staticANTHROPIC_AUTH_TOKEN, all three set together, or- Anthropic's own first-party enterprise "Cloud gateway"
/loginflow (forceLoginMethod: "gateway"), which persists its own credential and is a different product entirely.
A team using OAuth passthrough specifically because they don't want a static/shared Anthropic credential (to preserve per-seat subscription billing across multiple developers, rather than shifting to per-token billing) has no way to satisfy path 1 without reintroducing the exact problem they're avoiding, and path 2 isn't the product they're using at all.
What Should Happen?
Gateway model discovery should fire whenever a custom ANTHROPIC_BASE_URL is configured and CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1 is set, independent of whether the CLI resolved its provider mode to "gateway" internally, and independent of whether a static auth token is present. Discovery should work equally well over OAuth-passthrough sessions as it does over statically-authenticated ones — the discovery request only needs some valid credential to attach to the outbound GET /v1/models call (the OAuth access token already in use for inference would do), not specifically the "gateway" mode's stored JWT.
Root cause (from the actual bundled CLI source — not decompiled, extracted directly)
Claude Code's CLI is a Bun-compiled standalone executable. Bun standalone binaries embed the full, unmodified minified JS source (not native machine code) in an appended data section — this is documented Bun behavior (StandaloneModuleGraph, described in Bun's own source, src/standalone_graph/StandaloneModuleGraph.rs), not an obfuscation layer, and even --bytecode builds keep the source alongside the bytecode since JSC requires it at runtime. Extracting and reading it (via strings/manual offset parsing, and separately via the open-source tool bun-demincer, github.com/vicnaum/bun-demincer, adjusted for the current Bun binary layout) is how both this report and #77247 were produced — this is the same category of investigation as reading a stack trace or a --debug log, just applied to the shipped bundle when no other way to see the actual gating logic exists.
Minified names below are from claude --version 2.1.222 and will very likely differ (but resolve to the same logic) in other builds — see #77247's own note that the same functions were renamed between 2.1.205 and 2.1.209.
// Provider-mode resolver — this is the ONLY thing that decides "gateway" mode:
function On(){
if (L_()) return "gateway";
return te.CLAUDE_CODE_USE_BEDROCK ? "bedrock"
: te.CLAUDE_CODE_USE_FOUNDRY ? "foundry"
: /* ...other 3P providers... */
: "firstParty"; // ← plain ANTHROPIC_BASE_URL + OAuth login lands here
}
function L_(){ return Ut.gatewayAuth } // trivial getter over in-memory state
// The gateway-discovery fetch itself: gated on On()==="gateway", not on ANTHROPIC_BASE_URL
async function zH_(e){
if (On() === "gateway") {
if (!te.CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY)
return T("[Bootstrap] Skipped gateway /v1/models (CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY not set)"),
{response:{additional_model_options:[]}, viaScopelessOAuth:!1};
let a = await qH_();
return a && {response:a, viaScopelessOAuth:!1};
}
// ...firstParty/3P-provider bootstrap path, unrelated to gateway discovery...
}
// The only place gatewayAuth is populated with a non-null value from env vars:
async function Zga(){
if (te.CLAUDE_CODE_USE_GATEWAY) {
let e = te.ANTHROPIC_BASE_URL, t = te.ANTHROPIC_AUTH_TOKEN;
if (e && t) {
let r = RFo(e); // parse/validate URL
let n = cut(t); // decode JWT exp claim, if any
_Ve({ url: r, jwt: t, expiresAt: n!==null ? n*1000 : Number.MAX_SAFE_INTEGER, unpinned: !0 });
return;
}
T("CLAUDE_CODE_USE_GATEWAY is set but ANTHROPIC_BASE_URL or ANTHROPIC_AUTH_TOKEN is missing; ignoring", {level:"warn"});
}
// ...falls through to restoring a persisted enterprise-gateway credential from disk, if any...
}
I searched every call site of _Ve(...) (the sole setter for gatewayAuth, confirmed via function _Ve(e){Ut.gatewayAuth=e}) across the full extracted CLI bundle (~22MB, one file). There are exactly three:
Zga()— the env-var path shown above (CLAUDE_CODE_USE_GATEWAY+ANTHROPIC_BASE_URL+ANTHROPIC_AUTH_TOKEN, all required together).- A function that persists an
enterpriseGatewaycredential to disk, called from the/loginflow whenforceLoginMethod === "gateway"— Anthropic's own first-party Cloud-gateway OIDC device-flow product. - Session restore at startup, which just re-hydrates whichever of the above two was previously persisted — not an independent third path.
There is no path that reaches "gateway" mode (and therefore no path that reaches the discovery fetch) from a plain ANTHROPIC_BASE_URL + normal OAuth-passthrough claude login, however the gateway itself authenticates outbound Anthropic requests.
Steps to Reproduce
- Stand up (or use) any self-hosted LLM gateway that: (a) forwards Claude Code's real OAuth
Authorizationheader through toapi.anthropic.comunchanged (no static credential stored on the gateway), and (b) also serves one or more custom/local models under its own model ids, exposed via an Anthropic-shapedGET /v1/modelsendpoint ({"data":[{"id":"...","type":"model",...}],"has_more":false}), reachable at<ANTHROPIC_BASE_URL>/v1/models. claude loginnormally (noANTHROPIC_AUTH_TOKEN, no API key).- Set:
````
ANTHROPIC_BASE_URL=https://your-gateway.example.com/anthropic
CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1
- Confirm the gateway's discovery endpoint is reachable and correct independently, e.g.:
````
curl $ANTHROPIC_BASE_URL/v1/models
# returns valid Anthropic-shaped JSON, custom model ids already "claude"/"anthropic"-prefixed
- Run
claude, then/model.
Actual: only the built-in Claude models appear. No error, warning, or log line about discovery is shown by default.
Expected: the gateway's custom models should appear in the picker, same as they would if CLAUDE_CODE_USE_GATEWAY=1 + a static ANTHROPIC_AUTH_TOKEN were also set (confirmed via code reading that this combination does reach the discovery fetch — not independently re-verified at runtime for this specific report, since the code-level proof already fully accounts for the behavior, but this matches the exact mechanism #77247 verified at runtime for the related 1M-context bug using the same CLAUDE_CODE_USE_GATEWAY workaround).
Error Messages/Logs
None shown by default. With claude --debug, the only trace is a silent skip, logged but easy to miss and not surfaced anywhere in normal UI:
[Bootstrap] Skipped gateway /v1/models (CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY not set)
This message is misleading in this scenario: the env var is set — the actual skip condition is On() !== "gateway", which is checked first and isn't mentioned in the log line at all.
Suggested Fix (any of)
- Decouple the discovery fetch from
On()==="gateway"specifically. Fire it wheneverCLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1and a non-defaultANTHROPIC_BASE_URLare both set, using whatever credential the CLI already has on hand for inference (the OAuth access token, in the passthrough case) rather than requiring the separategatewayAuth/static-token state. - At minimum, correct the debug log message to state the real reason for skipping (
provider mode is "firstParty", not "gateway") rather than implying the discovery flag itself wasn't recognized — this alone would have saved significant debugging time and made the actual gate discoverable without needing to read the bundled source. - Document, next to
CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERYin the LLM gateway docs, that it has a hard (and currently undocumented) dependency onCLAUDE_CODE_USE_GATEWAY+ a staticANTHROPIC_AUTH_TOKEN, and that it is not currently reachable under OAuth-passthrough-only gateway setups — so at least this becomes a known limitation rather than an apparent silent bug.
Option 1 is the actual fix; options 2–3 are the minimum acceptable mitigation if 1 isn't feasible soon.
Use case / why this matters
Model discovery exists specifically so gateway-served custom models are addressable without hardcoding IDs everywhere. That value is real independent of any single fixed "session model" — teams running a self-hosted inference tier alongside Claude typically want an orchestrator model (e.g. Opus/Fable) delegating specific tasks to different custom models, and reviewing their output, all within one session — not one model for the whole session. ANTHROPIC_MODEL/--model/a subagent's model: frontmatter field/the Agent SDK's options.model do all already accept arbitrary gateway-routed model ids without needing discovery (confirmed against the model-config docs — this isn't blocked entirely), which is a workable path today. But it means every model id has to be typed/hardcoded by hand everywhere it's used, with no interactive picker, no autocomplete, and no visibility into what a given gateway actually serves — exactly the discoverability gap CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY was built to close, and which currently only works for a deployment shape (static shared token) that OAuth-passthrough gateways exist specifically to avoid.
Claude Model
Not sure / Multiple models
Is this a regression?
I don't know
Claude Code Version
2.1.222 (Claude Code)
Platform
Other (self-hosted LLM gateway with OAuth passthrough — Anthropic API is the real upstream, reached via the gateway)
Operating System
Other Linux
Terminal/Shell
Other
Additional Information
Related: #77247 reports the same underlying class of bug (the On()-equivalent provider-mode resolver failing to reach "gateway" mode behind a gateway) causing a different symptom — natively-1M-context models getting incorrectly budgeted at 200K. That report's workaround (CLAUDE_CODE_USE_GATEWAY=1 + static ANTHROPIC_AUTH_TOKEN) is exactly the "gateway mode" env combination this report also traces to — but that workaround isn't viable for OAuth-passthrough-only deployments (the whole point of passthrough is not needing a static token), so a fix should ideally address the underlying On()/gatewayAuth resolution gap itself (which would fix both reports) rather than just this discovery-specific symptom.
Happy to share the exact extraction method (Bun StandaloneModuleGraph layout + offset fix for current Bun versions) if useful for anyone else trying to verify this independently.