[BUG] 3P LLM gateway: natively-1M models (Sonnet 5) are budgeted at 200K because the embedded/standalone CLI never resolves provider "gateway" — verified root cause + working env workaround

Open 💬 0 comments Opened Jul 13, 2026 by jonathanglasmeyer

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet (closest neighbors: #73414, #69444, #74562, #76155 — see "Related issues" below)
  • [x] This is a single bug report
  • [x] I am using the latest version of Claude Code

Summary

Behind a 3P LLM gateway (Claude Desktop inferenceProvider: "gateway", or standalone CLI with ANTHROPIC_BASE_URL pointing at the gateway), claude-sonnet-5 (base, no [1m] picker variant) is budgeted at 200K, even though the built-in model catalog declares it natively 1M including for 3P (native_1m_3p: {bedrock, vertex, foundry} = true), and even though the gateway's /v1/models serves the exact catalog id.

We reverse-engineered the bundled CLI (v2.1.205, macOS) and pinned the runtime root cause: the native-1M path is gated on the resolved inference provider, and the CLI only ever resolves provider "gateway" when an internal gatewayAuth state is set — which neither the Desktop app's gateway config nor a plain ANTHROPIC_BASE_URL setup ever sets. The request path works fine; only the context-window resolution mis-classifies the provider.

Setting the (apparently undocumented) env trio CLAUDE_CODE_USE_GATEWAY=1 + ANTHROPIC_BASE_URL + ANTHROPIC_AUTH_TOKEN fixes it completely for the standalone CLI — verified end-to-end with a 662K-token request through the gateway (LiteLLM → Bedrock). The Desktop app cannot be fixed from the outside (details below).

Environment

  • Claude Desktop 1.20186.1 (bundled CLI 2.1.205), macOS 15 (Darwin 25.5)
  • 3P gateway: LiteLLM proxy in front of Amazon Bedrock; /v1/models returns catalog ids (claude-sonnet-5, claude-opus-4-8, …)
  • Desktop config: {"inferenceProvider": "gateway", "inferenceGatewayBaseUrl": "https://llm-gateway.internal.example", "inferenceCredentialKind": "helper-script", "inferenceCredentialHelper": "/opt/…/token-helper"}
  • Standalone repro uses the bundled CLI binary headless (-p "/context"), same binary byte-identical across Desktop profile slots

Test matrix (all live-verified, same binary 2.1.205)

| # | Setup | /context budget | Inference |
|---|-------|------------------|-----------|
| 1 | First-party (api.anthropic.com, OAuth), claude-sonnet-5 base | 967k / 1M ✅ | ok |
| 2 | Gateway, ANTHROPIC_BASE_URL + ANTHROPIC_API_KEY, claude-sonnet-5 base (status quo for gateway users) | 200k ❌ | ok |
| 3 | Gateway, CLAUDE_CODE_USE_GATEWAY=1 + ANTHROPIC_BASE_URL + ANTHROPIC_AUTH_TOKEN, claude-sonnet-5 base | 967k / 1M ✅ | ok |
| 4 | Same as 3, claude-opus-4-8 base | 200k (catalog-correct: no native_1m_3p) | — |
| 5 | Same as 3 plus CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST=1 (Desktop-spawn emulation) | 967k / 1M ✅ | — |
| 6 | Gateway, _CLAUDE_CODE_ASSUME_FIRST_PARTY_BASE_URL=1 + ANTHROPIC_API_KEY | 967k / 1M ✅ (works, but lies to the CLI about being 1P) | ok |
| 7 | E2E long-context: setup 3, 662K input tokens piped via stdin | request accepted and fully processed by Bedrock through the gateway (651K cache_creation_input_tokens) | ok |
| 8 | Gateway, claude-sonnet-5[1m] / claude-opus-4-8[1m] picker variants | 1M (suffix path is a pure string test, unaffected) | ok |

Row 2 vs 3 is the bug. Row 7 proves the 200K budget is purely a client-side mis-classification — the backend happily serves 1M through the same gateway.

Root cause (from the bundled CLI, names are minified and build-specific)

Context-window resolution (usc) reaches the native-1M branch (sO), which requires catalog native_1m and an eligible provider:

function sO(e){ …
  let n = Z_(e);                       // resolved provider
  if (n==="firstParty" && kd() || n==="anthropicAws" || n==="mantle") return true;
  return Kih(n, r);                    // "gateway": needs native_1m_3p.{bedrock,vertex,foundry} — ALL true for claude-sonnet-5
}
function En(){                          // global provider resolution (Z_ falls through to this)
  if (By()) return "gateway";           // By() = internal gatewayAuth state — NOT config/env derived
  return USE_BEDROCK ? "bedrock" : … : "firstParty";   // ← gateway setups land here
}
function kd(){                          // "is first-party base URL"
  if (_CLAUDE_CODE_ASSUME_FIRST_PARTY_BASE_URL) return true;
  let e = process.env.ANTHROPIC_BASE_URL;
  return !e || new URL(e).host === "api.anthropic.com";   // ← false behind a gateway
}

So behind a gateway the provider resolves to "firstParty" with kd() === falsesO() falls through every branch → 200K. The Kih("gateway") gate itself would pass for claude-sonnet-5; it is simply never reached.

gatewayAuth (making En() return "gateway") is only set by restoreGatewayAuth:

  1. Env path: CLAUDE_CODE_USE_GATEWAY=1 + ANTHROPIC_BASE_URL + ANTHROPIC_AUTH_TOKEN (this is test row 3 — works, including non-JWT bearer tokens: no exp claim → no client-side refresh attempts).
  2. secureStorage path: an enterpriseGateway credential + gatewayTrust entry (the first-class "Cloud gateway" flow with TLS pinning and JWT refresh).

Why the Desktop app can't be fixed from the outside

The Desktop shell knows it's a gateway (inferenceProvider: "gateway"), but its CLI spawn-env builder:

  • sets CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST=1 and feeds credentials via CLAUDE_CODE_HOST_CREDS_FILE (fd-based — never lands in process.env.ANTHROPIC_AUTH_TOKEN),
  • explicitly sets ANTHROPIC_AUTH_TOKEN: "",
  • never sets CLAUDE_CODE_USE_GATEWAY,
  • and sanitizes provider-related vars (including CLAUDE_CODE_USE_GATEWAY) out of settings-/remote-provided env in host-managed mode.

So the embedded CLI has no way to learn it is talking to a gateway, and every gateway-configured Desktop install silently budgets natively-1M models at 200K.

Expected

inferenceProvider: "gateway" (Desktop) — and ideally a documented equivalent for the standalone CLI — should make the CLI resolve provider "gateway", so that natively-1M models get their catalog window (Kih("gateway") already implements the intended per-model gating).

Suggested fix (any of)

  1. Desktop: when inferenceProvider === "gateway", propagate gateway mode to the embedded CLI (set CLAUDE_CODE_USE_GATEWAY alongside the host-managed credential plumbing, or pass an explicit host-managed provider hint the CLI honors).
  2. CLI: in host-managed mode, accept a provider hint from the host instead of falling back to "firstParty".
  3. At minimum: document CLAUDE_CODE_USE_GATEWAY for standalone-CLI gateway users — it works today (rows 3, 5, 7) and removes the need for per-model [1m] picker workarounds.

Follow-up findings (2026-07-14, bundled CLI 2.1.209)

Re-verified on the current bundled CLI (2.1.209; minified names differ from 2.1.205 — usc/sO/ST are now cvc/eL/q_ — logic identical). Two additional effects of the same provider mis-resolution:

1. The mis-resolution also skews default-model selection.
The tier-default logic appends [1m] to the Opus default iff an internal gate passes, and that gate is essentially En() === "firstParty":

function ZVl(){ … if (sm()) return { setting: z1() ? mq(aT()) : aT(), envFamily: "opus" } … }  // mq() appends "[1m]"
function z1(){ if (VUe() || ZUe() || En() !== "firstParty") return false; … return true }

Because a gateway setup mis-resolves to "firstParty", gateway users get first-party subscriber defaults: the default/Opus model rows resolve to claude-opus-4-8[1m], and Desktop spawns workers with --model claude-opus-4-8[1m] (verified via ps on running Desktop worker processes). With a correctly resolved "gateway" provider, z1() would be false and the Opus default would be the catalog-correct bare model at 200K.

2. Desktop injects the context-1m SDK beta for Opus 4.8 sessions — but not for Sonnet 5.
The CLI whitelists exactly one host-providable beta (context-1m-2025-08-07); when present, the window resolution grants 1M to any supports_1m_beta model regardless of provider. Live repro (fresh CLAUDE_CONFIG_DIR, same gateway, -p "/context"):

| probe | /context budget |
|---|---|
| bare claude-opus-4-8, no betas | 200k |
| bare claude-opus-4-8, --betas context-1m-2025-08-07 | 1m |
| bare claude-sonnet-4-6, with the beta | 1m |

In real Desktop sessions this beta is evidently injected for bare-Opus-4.8 sessions but not for Sonnet: sessions spawned with --model claude-opus-4-8 (no suffix, no --betas arg — the beta arrives via the SDK options) grew to 276K and 932K tokens with zero auto-compaction, while a Sonnet 4.6 session in the same install auto-compacted at ~188K (a real 200K budget).

Net effect on a 3P gateway — inverted from the catalog's intent: the natively-1M model (Sonnet 5, full native_1m_3p) is budgeted 200K, while the beta-gated model (Opus 4.8, no native_1m_3p) silently runs with a 1M window — cost-relevant and invisible to the user, since the UI displays the bare id with a 200K window (display side belongs to #76155 / #73414). Fixing the provider resolution would make both models behave as the catalog declares.

Related issues

  • #73414 — same 200K symptom behind a 3P gateway for Fable 5, framed as a picker-persistence regression; its root-cause note "the bare row is not special-cased as natively-1M" is explained by the provider mis-resolution above.
  • #69444 — Desktop lost supports1m picker variants for 3P inference (the opt-in path this bug forces everyone onto).
  • #74562 — [1m] aliases not applying (different path: alias handling; the raw [1m] id suffix works).
  • #76155 — Desktop shows 200K for Sonnet 5 on first-party; that one is display-only (session ran past 200K), whereas here the 200K budget is real (auto-compact at 200K).

Related observation (not this bug)

claude-opus-4-8[1m] over the gateway gets a 1M client window (suffix path) and Bedrock accepts >200K requests for it — while the built-in catalog omits native_1m_3p for Opus 4.8, keeping the base model at 200K even with the provider fix. If Bedrock 1M for Opus 4.8 is GA, the catalog entry may be lagging. See also follow-up finding 2 above — Desktop already injects the context-1m beta for Opus 4.8 over 3P, so >200K Opus sessions are live in practice today.

View original on GitHub ↗