[BUG] Gateway model discovery (CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY) never fires under OAuth-passthrough gateway setups — verified root cause

Status Open
Reported on v2.1.222
Maintainer reply None cached
Activity 0 comments · opened Aug 6, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet (closely related: #77247, same underlying On()/gatewayAuth misresolution class, but that issue is about the 1M-context budget; this one is about CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY never 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:

  1. CLAUDE_CODE_USE_GATEWAY=1 + ANTHROPIC_BASE_URL + a static ANTHROPIC_AUTH_TOKEN, all three set together, or
  2. Anthropic's own first-party enterprise "Cloud gateway" /login flow (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:

  1. Zga() — the env-var path shown above (CLAUDE_CODE_USE_GATEWAY + ANTHROPIC_BASE_URL + ANTHROPIC_AUTH_TOKEN, all required together).
  2. A function that persists an enterpriseGateway credential to disk, called from the /login flow when forceLoginMethod === "gateway" — Anthropic's own first-party Cloud-gateway OIDC device-flow product.
  3. 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

  1. Stand up (or use) any self-hosted LLM gateway that: (a) forwards Claude Code's real OAuth Authorization header through to api.anthropic.com unchanged (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-shaped GET /v1/models endpoint ({"data":[{"id":"...","type":"model",...}],"has_more":false}), reachable at <ANTHROPIC_BASE_URL>/v1/models.
  2. claude login normally (no ANTHROPIC_AUTH_TOKEN, no API key).
  3. Set:

``
ANTHROPIC_BASE_URL=https://your-gateway.example.com/anthropic
CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1
``

  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
``

  1. 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)

  1. Decouple the discovery fetch from On()==="gateway" specifically. Fire it whenever CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1 and a non-default ANTHROPIC_BASE_URL are 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 separate gatewayAuth/static-token state.
  2. 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.
  3. Document, next to CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY in the LLM gateway docs, that it has a hard (and currently undocumented) dependency on CLAUDE_CODE_USE_GATEWAY + a static ANTHROPIC_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.

View original on GitHub ↗