Skip to content

[Bug]: Claude Code gateway-model cache never refreshes because the /v1/models fetch sends no auth token (401) #1713

Description

@rrmlima

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingcatalogModel catalog, slugs, visibility, routed entriescliCLI, config inject, packaging flags

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions