Skip to content

fix(core): reuse recent Console config checks for rejected inference - #57465

Merged
thdxr merged 1 commit into
devfrom
config-refetch-storm
Oct 20, 2026
Merged

thdxr merged 1 commit into
devfrom
config-refetch-storm

Conversation

@thdxr

@thdxr thdxr commented Oct 20, 2026

Copy link
Copy Markdown
Member

Problem

The OpenCode Console provider plugin (packages/core/src/plugin/provider/opencode.ts) registers an http.response session hook (explain) for the opencode provider and every Console-configured provider. On every inference response with status 401 or 403 it called check(), which re-fetched GET <server>/api/v2/config to learn why the request was rejected. A client whose inference keeps getting rejected (out of credits, blocked account, SSO required, revoked key) and keeps retrying therefore sent one config request per failed inference request. Concurrent failures also each queued their own load behind the semaphore.

Production evidence (Cloudflare analytics, 2026-10-19):

  • single IPs sent 1,400–1,700 GET /console/api/v2/config per minute
  • one IP sent ~81k in 6 hours, all with user agent opencode/latest/2.0.32/cli
  • fleet-wide /console/api/v2/config is ~408k requests per 12 hours, the Console API's largest endpoint by far

Fix

check now takes an optional maxAge. load() records when it started (via Clock), and explain calls check(Duration.seconds(30)). If any config load (startup, the 1-minute poll, a credential-switch refresh, or an earlier explanation) started within the last 30 seconds, the hook reuses the last reported connection status instead of fetching again. The age check runs inside the existing loading semaphore, so concurrent failing responses wait for the load already in flight and then reuse its result.

Rejected responses are still replaced with the same actionable message (reconnect / sign in with SSO again), based on the Console's last reported status. The trade-off: a status change in the Console shows up in explanations up to 30 seconds late. The 1-minute poll is unchanged.

I also checked the rest of the file for other ways to loop: apply re-registering hooks, Credential.Event.Switched → refresh, and snapshot equality. None of them fetch config. Token refresh publishes Credential.Event.Updated, not Switched. Connection status updates publish Integration.Event.Updated, which the plugin does not subscribe to. The poll uses onlyIfMissing, so it never stacks timers.

Tests

The existing explanation test in packages/core/test/plugin/provider-opencode.test.ts is now an it.effect test on TestClock, and it counts config requests. After startup it sends 1 rejected response, then 10 concurrent and 20 sequential SSO-rejected responses within the 30-second window, plus responses for the opencode provider and an unrelated provider. It checks that every Console-provider response is still explained and that only one config request is made after startup.

  • With the fix, the test passes, and passes 5/5 with --rerun-each 5.
  • Without the fix, the test fails (Expected: 1, Received: 2 on the first rejected response). Instrumented, the same sequence made 33 config requests for 32 rejected responses; with the fix it makes 2 (startup + one check).
  • bun typecheck in packages/core and the root bun run check both pass.

— from 𝕺𝖕𝖊𝖓𝕮𝖔𝖉𝖊

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change looks sound. I ran the updated explanation test against the base, where it fails (Expected: 1, Received: 2) because every rejected response triggers its own config fetch. With this PR the full provider-opencode suite passes (40/40), and typecheck in packages/core passes. The age check runs inside the loading semaphore, so concurrent rejections share the load already in progress. Because checked is set when a load starts, failed loads also hold back retries for the window.

The public API is unchanged: check is internal to the plugin.

No blocking concerns.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant