Summary
The Claude Code gateway-model cache writer (src/claude/gateway-cache.ts) fetches /v1/models?limit=1000&ids=cli from the local proxy without any authentication header. Since the proxy requires auth on /v1/models (401 without a key), the fetch always fails with !res.ok → the function returns null → the Claude Code model picker cache is never updated.
This silently breaks the Claude Code model list in every deployment where the proxy has API auth enabled (the default). The fetch is internal (loopback), but it still needs the admission token like every other internal call.
Reproduction
# 1. Start the proxy with API auth (default) and export OPENCODEX_API_AUTH_TOKEN
# 2. The gateway-cache refresh path runs (e.g. via ocx claude launch)
# The exact fetch the code makes:
curl -s -o /dev/null -w '%{http_code}\n' \
"http://127.0.0.1:10100/v1/models?limit=1000&ids=cli"
# → 401 {"error":{"message":"opencodex API key required",...}}
# With the token it works:
curl -s -o /dev/null -w '%{http_code}\n' \
-H "x-opencodex-api-key: $OPENCODEX_API_AUTH_TOKEN" \
"http://127.0.0.1:10100/v1/models?limit=1000&ids=cli"
# → 200
Result: refreshGatewayModelCacheFromProxy returns null (401 → !res.ok → early return), so ~/.claude/cache/gateway-models.json is never refreshed and the Claude Code picker shows a stale/empty model list.
Logs and screenshots
No error is logged — the failure is silent (if (!res.ok) return null;). The only observable symptom is the Claude Code model picker not reflecting new models until a manual cache write happens.
Area
CLI
Version
2.17.0 (present on current main — verified: src/claude/gateway-cache.ts has no auth header on origin/main)
OS
Ubuntu 24.04 (VPS, root)
Config shape
Default API auth (service token via OPENCODEX_API_AUTH_TOKEN), Claude Code launched through the proxy.
Root cause
src/claude/gateway-cache.ts refreshGatewayModelCacheFromProxy (current main):
const res = await fetch(`http://127.0.0.1:${port}/v1/models?limit=1000&ids=cli`, {
headers: { "anthropic-version": "2023-06-01" },
signal: AbortSignal.timeout(timeoutMs),
});
if (!res.ok) return null;
The /v1/models endpoint enforces API auth (401 without key), so res.ok is always false when auth is enabled. Every other internal proxy call sends the admission token via x-opencodex-api-key — this fetch is the outlier.
Proposed fix
Add the admission token to the fetch headers:
try {
// ?ids=cli pins the readable claude-ocx id family deterministically (audit 051
// #5): the cache prewrite must not depend on UA sniffing.
+ const headers: Record<string, string> = { "anthropic-version": "2023-06-01" };
+ const token = process.env.OPENCODEX_API_AUTH_TOKEN?.trim() || (loadConfig().apiKeys?.[0]?.key ?? "");
+ if (token) headers["x-opencodex-api-key"] = token;
const res = await fetch(`http://127.0.0.1:${port}/v1/models?limit=1000&ids=cli`, {
- headers: { "anthropic-version": "2023-06-01" },
+ headers,
signal: AbortSignal.timeout(timeoutMs),
});
if (!res.ok) return null;
(loadConfig is already available from src/config — needs an import if not present.)
Alternatives considered
- Make
/v1/models?ids=cli auth-exempt: weakens the API surface for a single internal caller; not desirable.
- Keep relying on the
?ids=cli query as implicit auth: no, the endpoint still 401s regardless of query params (verified).
- Do nothing: the Claude Code picker cache silently never updates on auth-enabled deployments.
Additional context
The same token pattern is used elsewhere in the codebase (e.g. x-opencodex-api-key on /v1/responses). This issue is part of a batch reported from a real deployment; siblings: #1686 (Bearer on /v1/responses), #1688 (IS_SANDBOX root bypass), #1690 (retainModels allow-list).
Test matrix:
- auth enabled + token present → fetch 200 → cache written with model list
- auth enabled + no token → fetch 401 → returns null (unchanged, graceful)
- auth disabled (loopback-only) → unchanged
- Claude Code picker shows the cached model list after a refresh
Summary
The Claude Code gateway-model cache writer (
src/claude/gateway-cache.ts) fetches/v1/models?limit=1000&ids=clifrom the local proxy without any authentication header. Since the proxy requires auth on/v1/models(401 without a key), the fetch always fails with!res.ok→ the function returnsnull→ the Claude Code model picker cache is never updated.This silently breaks the Claude Code model list in every deployment where the proxy has API auth enabled (the default). The fetch is internal (loopback), but it still needs the admission token like every other internal call.
Reproduction
Result:
refreshGatewayModelCacheFromProxyreturnsnull(401 →!res.ok→ early return), so~/.claude/cache/gateway-models.jsonis never refreshed and the Claude Code picker shows a stale/empty model list.Logs and screenshots
No error is logged — the failure is silent (
if (!res.ok) return null;). The only observable symptom is the Claude Code model picker not reflecting new models until a manual cache write happens.Area
CLI
Version
2.17.0 (present on current
main— verified:src/claude/gateway-cache.tshas no auth header onorigin/main)OS
Ubuntu 24.04 (VPS, root)
Config shape
Default API auth (service token via
OPENCODEX_API_AUTH_TOKEN), Claude Code launched through the proxy.Root cause
src/claude/gateway-cache.tsrefreshGatewayModelCacheFromProxy(currentmain):The
/v1/modelsendpoint enforces API auth (401 without key), sores.okis always false when auth is enabled. Every other internal proxy call sends the admission token viax-opencodex-api-key— this fetch is the outlier.Proposed fix
Add the admission token to the fetch headers:
try { // ?ids=cli pins the readable claude-ocx id family deterministically (audit 051 // #5): the cache prewrite must not depend on UA sniffing. + const headers: Record<string, string> = { "anthropic-version": "2023-06-01" }; + const token = process.env.OPENCODEX_API_AUTH_TOKEN?.trim() || (loadConfig().apiKeys?.[0]?.key ?? ""); + if (token) headers["x-opencodex-api-key"] = token; const res = await fetch(`http://127.0.0.1:${port}/v1/models?limit=1000&ids=cli`, { - headers: { "anthropic-version": "2023-06-01" }, + headers, signal: AbortSignal.timeout(timeoutMs), }); if (!res.ok) return null;(
loadConfigis already available fromsrc/config— needs an import if not present.)Alternatives considered
/v1/models?ids=cliauth-exempt: weakens the API surface for a single internal caller; not desirable.?ids=cliquery as implicit auth: no, the endpoint still 401s regardless of query params (verified).Additional context
The same token pattern is used elsewhere in the codebase (e.g.
x-opencodex-api-keyon/v1/responses). This issue is part of a batch reported from a real deployment; siblings: #1686 (Bearer on/v1/responses), #1688 (IS_SANDBOX root bypass), #1690 (retainModels allow-list).Test matrix: