feat(opencode): killswitch — block requests when quota drops below threshold#35
Open
iceteaSA wants to merge 1 commit into
Open
feat(opencode): killswitch — block requests when quota drops below threshold#35iceteaSA wants to merge 1 commit into
iceteaSA wants to merge 1 commit into
Conversation
This was referenced May 21, 2026
There was a problem hiding this comment.
2 issues found across 10 files
Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.
Fix all with cubic | Re-trigger cubic
d89dd9d to
f667649
Compare
0054553 to
d125adb
Compare
9092526 to
37961f1
Compare
|
Want your agent to iterate on Greptile's feedback? Try greploops. |
20f6330 to
ecf1511
Compare
Contributor
Author
…reshold Self-review fixes folded in: - Token-aware fail-closed read: const mainQuota = quotaManager.getMain(auth.access) so a previous main account's cached quota can't satisfy the fail-closed check or feed the killswitch eval after a main-account switch. - Removed a stray inner 'let sessionRequestCount = 0' + unconditional increment that shadowed the process-scoped counter, which had left the active-route fallback every-N refresh reading a never-incremented counter.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Per-account request blocking when remaining quota drops below configurable thresholds. Returns a synthetic 429 only when the request genuinely cannot be routed anywhere.
Features:
killswitchblock.null).killswitchPassesPolicyevaluates all present quota windows — a present below-threshold window still blocks even when the other window is missing (failClosed=false).Retry-Aftercomputation read the fresh, token-aware QuotaManager cache, never the stale request-start storage snapshot.Retry-Afterheader set to the earliest quota reset across the unroutable accounts./claude-killswitchslash command (on/off/set/status) for runtime management.Files:
packages/core/src/killswitch.ts— newpackages/core/src/accounts.ts— killswitch types and policy functions (sharedDEFAULT_KILLSWITCH_THRESHOLDS)packages/opencode/src/index.ts— killswitch fetch-gate + command registrationpackages/opencode/src/tests/killswitch.test.ts+index.test.tsfetch-gate testsREADME.md+packages/opencode/README.md— killswitch documentationSummary by cubic
Adds a per‑account killswitch and a unified quota cache in
@cortexkit/anthropic-auth-coreso OpenCode blocks low‑quota requests and returns 429 with Retry‑After when unroutable or when the quota API is backed off. Adds/claude-killswitch, enforces the killswitch across all routing paths, and keeps the sidebar correct after reroutes and background updates.New Features
getRoutableFallbackAccounts; respectsfailClosedOnUnknownQuota; optionalquota.refreshEveryNRequests./claude-killswitch on|off|set|statuswith per‑account thresholds, persisted to the sidecarkillswitchblock; READMEs document config and usage.Bug Fixes
Written for commit dc6650b. Summary will update on new commits.
Greptile Summary
This PR adds a per-account killswitch to the OpenCode plugin that hard-blocks requests when remaining quota drops below configurable thresholds, returning a synthetic 429 with
Retry-Afterwhen no routable accounts remain. It also wires the killswitch consistently across all routing paths (fallback-first, soft-quota skip-main, reactive retries) via the sharedgetRoutableFallbackAccountshelper, and adds a/claude-killswitchslash command with disk persistence.packages/core/src/accounts.ts: AddskillswitchPassesPolicy(correctly continues past missing quota windows instead of short-circuiting),killswitchRetryAfterSeconds, threshold normalisation with5h/1waliases, andsetKillswitchPersistent.packages/opencode/src/index.ts: Inserts the killswitch fetch-gate after main-token refresh; performs an eager quota refresh whenneedsRefresh, re-readsmainQuotafrom the quota-manager cache post-refresh, then either routes to surviving killswitch-filtered fallbacks or emits a hard-block 429. All fallback-selection paths now go throughgetRoutableFallbackAccountsso the killswitch is enforced uniformly.killswitchPassesPolicy,killswitchRetryAfterSeconds, and command parsing.Confidence Score: 4/5
Safe to merge with the two P2 notes considered; the killswitch logic is sound and the core routing invariants are maintained.
The killswitch gate,
killswitchPassesPolicyfix, and unifiedgetRoutableFallbackAccountshelper are all well-implemented and tested. Two non-blocking concerns remain: theRetry-Afterheader for non-replayable 429s can be shorter than expected when healthy-fallback reset times are included in the minimum, and a transient quota-API failure on the first request produces a misleading 'no routable accounts' message before backoff is armed. Neither causes incorrect routing or data loss, but both affect operator diagnostics and client retry behaviour.packages/opencode/src/index.ts — the 429 response construction at the killswitch hard-block path (Retry-After computation and error message when quota is unknown due to API failure rather than threshold breach).
Important Files Changed
/claude-killswitchcommand parsing, status rendering, andexecuteKillswitchCommand. Theset allhandler assigns the samethresholdsobject toupdated.mainand every per-account entry (already flagged in a previous review comment).KillswitchThresholds/KillswitchConfigtypes,killswitchPassesPolicy(correctly usescontinuefor missing windows),killswitchRetryAfterSeconds, and persistence helpers. Policy logic looks correct after thesawUnknownWindowfix.getRoutableFallbackAccounts, and reactive-fallback killswitch filtering. The gate is placed correctly beforesendWithAccessToken. One UX concern: when the quota API transiently fails (not yet backed off) the 429 message reads 'Killswitch: no routable accounts' rather than indicating a quota-API problem.killswitchPassesPolicy,killswitchRetryAfterSeconds, config helpers, persistence, andexecuteKillswitchCommand. Good coverage of edge cases including missing windows with fail-closed/open behaviour.Sequence Diagram
sequenceDiagram participant Client participant Plugin as OpenCode Plugin participant QM as QuotaManager participant Main as Main Account participant FB as Fallback Accounts Client->>Plugin: fetch(request) Plugin->>Plugin: loadAccounts() / getAuth() alt fallback-first routing mode Plugin->>QM: getRoutableFallbackAccounts() QM-->>Plugin: killswitch-filtered fallbacks Plugin->>FB: tryUsableFallbackAccounts() FB-->>Plugin: response (if any) Plugin-->>Client: response (early return) end Plugin->>Plugin: refresh main access token (if expired) alt "failClosedOnUnknownQuota && !mainQuota && isBackedOff()" Plugin-->>Client: 429 Quota API unavailable end alt killswitch enabled Plugin->>QM: needsRefresh? opt needs refresh Plugin->>QM: refreshMain() + refreshAllFallbacks() end Plugin->>QM: "getMain(token).quota -> mainQuota (re-read)" end alt "killswitch enabled && !killswitchPassesPolicy(mainQuota)" Plugin->>QM: getRoutableFallbackAccounts() QM-->>Plugin: surviving fallbacks alt "replayable body && surviving fallbacks exist" Plugin->>FB: tryUsableFallbackAccounts(survivingFallbacks) FB-->>Plugin: fallbackResponse Plugin-->>Client: fallbackResponse (hard-block, no main fallthrough) else no survivors or non-replayable Plugin->>Plugin: killswitchRetryAfterSeconds() Plugin-->>Client: 429 + Retry-After end end Plugin->>Main: sendWithAccessToken() Main-->>Plugin: mainResponse alt mainResponse triggers fallback Plugin->>FB: tryFallbackAccounts() with killswitch filter FB-->>Plugin: fallbackResponse end Plugin-->>Client: final responseComments Outside Diff (1)
packages/core/src/killswitch.ts, line 340-355 (link)thresholdsobject reference whenset allis usedWhen
entry.account === 'all', the samethresholdsobject is assigned toupdated.mainand to every entry inaccounts[id]. All these properties point to the same object in memory. Callers that receiveupdatedConfigand subsequently mutate threshold values (e.g., patching a single window in-place) would silently affect every account and the main entry simultaneously. Constructing a fresh object for each assignment ({ ...thresholds }) avoids the aliasing.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Reviews (13): Last reviewed commit: "feat(opencode): killswitch — block reque..." | Re-trigger Greptile