anomalyco / anomalyco/opencode

providers: request falls back to api.anthropic.com (401 invalid x-api-key) after plugin hot-reload

Open
#47,114 0 comments 0 reactions 1 assignee View on GitHub

@jlongster is already working on this.

Since Sep 3, 2026.

2.0
Dominant language
TypeScript
Stars
209k
Forks
27.5k
PR merge metrics
PR metrics pending

Description

Summary

With a custom providers.anthropic.settings.baseURL pointing at a local proxy (and a dummy apiKey), two requests bypassed the configured baseURL and went to api.anthropic.com, failing with 401 invalid x-api-key. Both happened less than a second after a plugin file change triggered a plugin reload and catalog rebuild.

Beyond the failed turn, this is a credential-exposure concern: the API key configured for a private relay is sent to a third-party endpoint the user never configured. ANTHROPIC_BASE_URL is not honored, so there is no environment-level safety net.

I could not reproduce it on demand (~40 controlled turns across five hypotheses, all listed below), so this report focuses on hard evidence rather than a recipe.

Environment

  • opencode version: 0.0.0-beta-18999
  • OS: Darwin 27.0.0, arm64 (macOS)
  • Terminal: TERM=xterm-256color (TERM_PROGRAM unset)
  • Shell: /bin/zsh
  • Install/channel: beta, npm global (@opencode-ai/cli)
  • Active plugins: one local plugin directory (plugins: ["/Users/<user>/.config/opencode/plugins/<name>"]). It registers session.hook("model.request") and session.hook("http.request") for providerID: "anthropic" and only adds/removes request headers. It does not touch baseURL.

Relevant config (redacted):

{
  "providers": {
    "anthropic": {
      "package": "@opencode-ai/ai/providers/anthropic",
      "settings": { "baseURL": "http://127.0.0.1:3456", "apiKey": "x" },
      "models": { "claude-opus-5": { "limit": { "context": 1000000, "output": 32000 } } }
    }
  }
}

The proxy at 127.0.0.1:3456 accepts any API key, so a request that reaches it can never produce invalid x-api-key. Verified directly:

$ curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:3456/v1/messages \
    -H 'x-api-key: totally-bogus' -H 'anthropic-version: 2023-06-01' \
    -H 'content-type: application/json' -d '{"model":"claude-haiku-4-5","max_tokens":8,"messages":[{"role":"user","content":"hi"}]}'
200

$ curl -s https://api.anthropic.com/v1/messages -H 'x-api-key: x' ... 
{"type":"error","error":{"type":"authentication_error","message":"invalid x-api-key"},...}

The error string is verbatim the upstream Anthropic response, and the proxy logged no request at those moments. The request therefore left the machine.

Reproduction

A. The failure mode itself is deterministic
  1. Configure providers.anthropic with settings.apiKey but no baseURL.
  2. Run any prompt: opencode2 run --standalone -m anthropic/claude-opus-5 "say ok".
  3. Result: Error: invalid x-api-key.
  4. Repeat step 2 with ANTHROPIC_BASE_URL=http://127.0.0.1:3456 set: still invalid x-api-key.

Step 4 is the part worth fixing regardless of the race below: there is no way to redirect the built-in Anthropic provider outside of settings.baseURL, so any code path that loses that value fails open toward api.anthropic.com.

B. The observed occurrences (not reproducible on demand)
  1. Have a session running a turn with anthropic/claude-opus-5#medium through the configured proxy.
  2. Write to a file inside the configured local plugin directory (~/.config/opencode/plugins/<name>/index.js), which triggers a plugin reload in the running serve --service process.
  3. Occasionally the in-flight turn dies with provider.auth / invalid x-api-key / status 401.

Observed 2 times out of many reloads. Both times the log shows the same ordering within ~0.5 s:

15:54:07.386Z INFO  msg="loading plugin" id=/Users/<user>/.config/opencode/plugins/<name>
15:54:07.496Z INFO  event.type=catalog.updated event.location.directory=/…/worktree-1
15:54:07.510Z INFO  event.type=catalog.updated event.location.directory=/…/worktree-3
15:54:07.855Z ERROR message="Failed to drain Session" cause="AI.Error: invalid x-api-key
    at SessionStep.attempt … at SessionRunner.drain …"

The service had several sessions across multiple project directories (git worktrees) attached at the time.

Expected Behavior

When providers.<id>.settings.baseURL is configured, a request must never be sent to the provider's default endpoint. If the override cannot be resolved for a request — during a reload, a catalog rebuild, or anything else — the request should fail closed with a clear error instead of silently contacting api.anthropic.com with the configured credential.

Actual Behavior

The request is sent to api.anthropic.com with the locally configured key, and the turn dies with:

{ "type": "provider.auth", "message": "invalid x-api-key", "status": 401 }

Additional Context

Exactly two occurrences, confirmed structurally against the session store rather than by text search:

sqlite3 "file:$HOME/.local/share/opencode/opencode.db?mode=ro" \
  "SELECT time_created, json_extract(data,'\$.error')
     FROM session_message
    WHERE json_extract(data,'\$.error.type')='provider.auth' ORDER BY time_created;"

Both are in the same session, both agent=build, model=claude-opus-5 (variant medium), and each lands within one second of a plugin-directory write. None before, none in the ~2 hours since, across a service restart.

Hypotheses I tested and ruled out (isolated XDG_CONFIG_HOME/XDG_DATA_HOME/XDG_CACHE_HOME, ~40 turns total, zero reproductions):

Hypothesis Turns Result
Writing opencode.json / AGENTS.md / a plugin file mid-turn (3 s and 6 s in) 8 clean
Legacy provider + native providers conflict, and legacy-only config 12 clean
Model ref carrying a variant (claude-opus-5#medium) plus reload 4 clean
Shared background service, 3 concurrent turns from 3 project directories, plugin edit mid-turn 12 clean
Model not listed in config (resolved from built-in catalog), incl. full dated ID 3 clean

So the trigger is narrower than any of those alone — my guess is a request that resolves its model while the per-location catalog is being rebuilt, but I could not force that window.

Prior art for the same class of bug: openclaw/openclaw#23332 describes an Anthropic-relay setup where a dropped provider override silently fell back to api.anthropic.com, producing both a 401 and a credential leak. Their fix was to default the missing field and fail loudly instead of discarding overrides.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.