anomalyco / anomalyco/opencode
providers: request falls back to api.anthropic.com (401 invalid x-api-key) after plugin hot-reload
@jlongster is already working on this.
Since Sep 3, 2026.
- 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_PROGRAMunset) - 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 registerssession.hook("model.request")andsession.hook("http.request")forproviderID: "anthropic"and only adds/removes request headers. It does not touchbaseURL.
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
- Configure
providers.anthropicwithsettings.apiKeybut nobaseURL. - Run any prompt:
opencode2 run --standalone -m anthropic/claude-opus-5 "say ok". - Result:
Error: invalid x-api-key. - Repeat step 2 with
ANTHROPIC_BASE_URL=http://127.0.0.1:3456set: stillinvalid 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)
- Have a session running a turn with
anthropic/claude-opus-5#mediumthrough the configured proxy. - Write to a file inside the configured local plugin directory (
~/.config/opencode/plugins/<name>/index.js), which triggers a plugin reload in the runningserve --serviceprocess. - 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.