Add per-model scoped killswitch threshold#109
Merged
Conversation
added 2 commits
July 5, 2026 11:12
Extend the per-account killswitch with a third, model-scoped dimension
that hard-limits traffic to a weekly scoped quota carve-out (Anthropic's
'weekly_scoped' limits — the Fable-only window today, any future
carve-out tomorrow) WITHOUT killing the account for other models.
Behavior, evaluated per-request against the request's modelId:
* 5h/7d healthy, Fable window remainingPercent <= scoped threshold
+ request IS a Fable-model → reroute to scoped-eligible fallback,
hard-block 429 if none. Account STAYS LIVE for Sonnet/Opus.
* 5h/7d below threshold → whole account killed (unchanged).
* Scoped check is additive to the 5h/7d check.
Core (packages/core/src/accounts.ts)
* KillswitchThresholds gains optional 'scoped' key
* DEFAULT_KILLSWITCH_THRESHOLDS.scoped = 0
* normalizeKillswitchThresholds exports and resolves 'scoped' with
the same finite-number guard / default fallback as 5h/7d
* getKillswitchThresholdsForAccount exports and widens return type
* killswitchPassesPolicy gains optional trailing modelId; scoped
check uses inclusive <= so the default 0 fires at exhaustion;
a missing scoped window is not 'unknown quota' (most models have
no carve-out) — only a present window at/below threshold blocks
* killswitchRetryAfterSeconds gains optional scopedModelId; the
matched scoped window's resetsAt is also considered so the retry
hint reflects the weekly reset for a scoped-driven block
Command (packages/core/src/killswitch.ts)
* KillswitchCommandAction 'set' entries gain optional scoped?: number
* parseKillswitchCommandAction regex: optional 3rd capture is scoped
* executeKillswitchCommand writes scoped into the KillswitchThresholds
when present
* buildStatusTable gains a Scoped column (<= X%)
* USAGE_TEXT documents the optional third number
Enforcement (packages/opencode/src/index.ts)
* Thread requestModelId into the THREE routing-path call sites:
- getRoutableFallbackAccounts fallback filter
- tryFallbackAccounts reactive-path fallback filter
- main killswitch gate (accountId stays undefined)
* Display/status callers (sidebar) keep account-level state — no
modelId threading there
* Terminal 429 block: scoped-aware message via new
formatKillswitchBlockMessage helper. The helper is unit-tested.
Tests
* New packages/core/src/tests/killswitch.test.ts (24 tests):
- default scoped = 0; normalizeKillswitchThresholds resolution
- matching model + scoped window at/below threshold -> false
- NON-matching model + scoped-exhausted window -> true (key proof)
- modelId absent: byte-identical to pre-change (regression lock)
- 5h/7d below threshold blocks regardless of scoped
- default 0 inclusive boundary (passes at 1, blocks at 0)
- raised threshold (20) blocks at <= 20
- non-finite scoped remainingPercent -> not blocked
- generic: Mythos carve-out behaves identically (no Fable string)
- killswitch disabled short-circuits before scoped check
- parseKillswitchCommandAction: two-number form byte-identical,
three-number form, mixed entries
- executeKillswitchCommand: per-account scoped round-trip
- buildStatusTable renders Scoped column
- killswitchRetryAfterSeconds: scoped resetsAt considered;
omitting is backward-compatible
* packages/opencode/src/tests/killswitch.test.ts (4 new):
- formatKillswitchBlockMessage: scoped-driven names the model,
account-level uses generic phrasing, generic modelName works,
retry hint formatting (m/s)
Gates
* bun run typecheck -> 0
* bun run lint -> '1 info' (pre-existing biome-schema drift, unchanged)
* core bun test -> 30 pass / 0 fail
* opencode bun test -> 763 pass / 0 fail
* pi bun test -> 47 pass / 0 fail
MUST-1: killswitchPassesPolicy bypassed the scoped check when 5h/7d
was missing/non-finite under fail-OPEN (failClosedOnUnknownQuota=false).
The function short-circuited on the unknown-5h/7d decision before
reaching the scoped check, so an exhausted scoped window was
incorrectly treated as a pass.
Fix: reorder the function so the scoped check runs BEFORE the
unknown-5h/7d fail-closed decision. The scoped check is an
independent block reason; the unknown-5h/7d decision only changes
the fall-through for accounts that did not already block on scoped.
The inclusive <= comparison and the missing-scoped-window-doesn't-
block semantics are preserved exactly.
Adds 4 tests in packages/core/src/tests/killswitch.test.ts:
* scoped check runs even when 5h/7d is unknown and fail-OPEN
* scoped check runs when one 5h/7d window is non-finite + fail-OPEN
* healthy scoped window with unknown 5h/7d + fail-OPEN still passes
* absent scoped window with unknown 5h/7d + fail-OPEN still passes
MUST-2: the 429 block message misattributed a 5h/7d-driven block as
scoped-driven whenever the request's model happened to match a scoped
window, even if that window was healthy. A Fable request with a
healthy Fable window + 5h/7d breach would produce
'Claude Fable 5 weekly limit reached' even though the actual cause
was a whole-account 5h/7d kill.
Fix: extract a new resolveScopedDrivenBlock helper in
packages/opencode/src/index.ts. It classifies a block as scoped-driven
ONLY when the request's model matches a scoped window AND that window
is at or below the per-account scoped threshold (via the existing
getKillswitchThresholdsForAccount, now imported into opencode). The
terminal block uses the helper's result to:
* pass the modelId to killswitchRetryAfterSeconds (so the weekly
reset hint is used only for genuine scoped-driven blocks)
* pass modelName to formatKillswitchBlockMessage (so the message
names the model only for genuine scoped-driven blocks)
Adds 7 tests in packages/opencode/src/tests/killswitch.test.ts:
* 5h/7d-driven block + HEALTHY Fable window -> NOT scoped-driven
* EXHAUSTED Fable window at/below scoped threshold -> IS scoped-driven
* raised scoped threshold (20) marks a 20% Fable window as scoped-driven
* same 20% threshold + 21% remaining Fable window -> NOT scoped-driven
* Sonnet request: no matching scoped window -> NOT scoped-driven
* no requestModelId -> NOT scoped-driven
* non-finite remainingPercent on a matched window -> NOT scoped-driven
All existing 5h/7d killswitch tests are unchanged in count and still
pass — the reorder only changes behavior for the (formerly buggy)
fail-OPEN + unknown-5h/7d + scoped-exhausted combination.
Gates
* bun run typecheck -> 0
* bun run lint -> '1 info' (pre-existing biome-schema drift)
* core bun test -> 34 pass / 0 fail (was 30; +4 MUST-1)
* opencode bun test -> 851 pass / 0 fail (was 844; +7 MUST-2)
* pi bun test -> 47 pass / 0 fail (unchanged)
There was a problem hiding this comment.
2 issues found across 5 files
Confidence score: 3/5
- In
packages/core/src/accounts.ts, scoped-driven killswitch blocks can return an overly aggressiveRetry-Afterby taking the minimum across unrelated 5h/7d reset windows, which may throttle users longer than necessary and cause misleading backoff behavior — computekillswitchRetryAfterSecondsfrom the matching scoped reset window only before merging. - In
packages/opencode/src/index.ts,resolveScopedDrivenBlockre-implements scoped-threshold checks already handled by corekillswitchPassesPolicy, which risks cross-package drift and inconsistent block decisions as logic evolves — de-duplicate by delegating to the core policy path (or add parity tests) before merging.
Architecture diagram
sequenceDiagram
participant Client
participant API as API Route (opencode)
participant KSW as killswitchPassesPolicy
participant ScopeHelper as getScopedQuotaWindowForModel
participant Storage as AccountStorage (config)
participant Fallback as Fallback Filter Loop
participant Resolve as resolveScopedDrivenBlock
participant Formatter as formatKillswitchBlockMessage
participant CLI as /claude-killswitch set
Note over Client,CLI: NEW: Per-model scoped killswitch threshold
%% Happy path: request with modelId, window healthy
Client->>API: POST /v1/messages (modelId: "claude-fable-5")
API->>KSW: killswitchPassesPolicy(quota, storage, accountId?, modelId)
KSW->>KSW: Check killswitch enabled, normalize thresholds<br/>including scoped (default 0)
KSW->>KSW: Evaluate five_hour (strict <) and seven_day (strict <)
alt ModelId provided
KSW->>ScopeHelper: getScopedQuotaWindowForModel(quota, modelId)
ScopeHelper-->>KSW: scopedWindow or null
alt scopedWindow exists AND remainingPercent finite AND remainingPercent <= thresholds.scoped
KSW-->>API: false (block)
else scopedWindow missing or above threshold
KSW-->>API: true (pass)
end
else No modelId
KSW-->>API: true (pass, no scoped check)
end
%% Main killed, routing to fallbacks
API->>API: Eager killswitch refresh (QuotaManager)
alt Main quota fails (killswitchPassesPolicy returned false)
API->>Fallback: Loop over fallback accounts
loop each fallback account
Fallback->>KSW: killswitchPassesPolicy(fallbackQuota, storage, account.id, modelId)
KSW-->>Fallback: true/false (scoped check included)
end
alt No fallbacks qualify
API->>Resolve: resolveScopedDrivenBlock(mainQuota, requestModelId, storage)
Resolve->>ScopeHelper: getScopedQuotaWindowForModel (same as above)
ScopeHelper-->>Resolve: matchedWindow
alt matchedWindow.remainingPercent <= thresholds.scoped
Resolve-->>API: { isScopedDriven: true, modelName, modelId }
else
Resolve-->>API: { isScopedDriven: false }
end
API->>Formatter: formatKillswitchBlockMessage(retryAfterSeconds, modelName?)
Formatter-->>API: "<Model> weekly limit reached" or "Killswitch: no routable accounts"
API->>Client: 429 { type: "rate_limit_error", message }
else Fallback account qualifies
API->>Client: Route to fallback (normal flow)
end
else Main ok
API->>Client: Normal response
end
%% CLI configuration flow
rect rgb(240, 240, 240)
Note over CLI,Storage: CLI configuration (/threshold)
Client->>CLI: /claude-killswitch set main:5,10,0
CLI->>CLI: parseKillswitchCommandAction()<br/>NEW: optional third number (scoped)
CLI->>Storage: Save killswitch config with scoped field
Storage-->>CLI: Confirmed
CLI->>Client: Status table with Scoped column
end
Note over KSW: NEW: Scoped check runs BEFORE unknown-window fail-closed return<br/>so exhausted scoped window blocks even if 5h/7d windows are missing.
Note over API: killswitchRetryAfterSeconds also includes scoped window's resetsAt<br/>when scopedModelId is provided.
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
FINDING 1 (P1 — retry-after too aggressive):
killswitchRetryAfterSeconds was collecting BOTH 5h/7d resetsAt AND the
matched scoped window's resetsAt when scopedModelId was provided, then
returning Math.min(...). For a weekly scoped block the 5h reset
(hours away) always beat the weekly reset (days away), so the client
was told 'Retry in 2h' against a block that won't clear for days
-> retry-storm.
Fix: when scopedModelId is truthy, consider ONLY the matched scoped
window's resetsAt (skip the 5h/7d collection entirely); when absent,
the 5h/7d behavior is byte-identical to before. Restructured the
per-quota loop as if/else on scopedModelId so the two branches can't
accidentally compose again.
Adds 3 tests in packages/core/src/tests/killswitch.test.ts:
* scoped branch REPLACES 5h/7d, not adds to it — main quota has
5h+7d reset in 2h + Fable reset in 2 days; the 2-day reset must win
* matched scoped window in a FALLBACK is the only source — same
2h/2days shape on the fallback quota; the fallback 5h reset must
also be ignored
* no matched scoped window in any quota -> 300 fallback (the no-reset
path, even with 5h present)
FINDING 2 (P2 — attribution ignores 5h/7d priority):
resolveScopedDrivenBlock only checked the scoped window's threshold.
It did NOT check whether 5h/7d already killed the account. When BOTH
5h/7d AND the Fable window are exhausted, the real block reason is
account-level (5h/7d), yet the function reported isScopedDriven: true
-> misleading 'Fable weekly limit reached' message + weekly retry-after
when the account is actually dead for ALL models.
Fix: guard BEFORE the scoped check with a no-modelId call to
killswitchPassesPolicy, which evaluates only 5h/7d. If it returns
false, 5h/7d drove the block -> account-level, NOT scoped-driven.
This also resolves cubic's cross-package-drift concern by reusing the
core gating priority rather than re-deriving 5h/7d logic in opencode.
Adds 3 tests in packages/opencode/src/tests/killswitch.test.ts:
* BOTH 5h/7d AND Fable exhausted -> NOT scoped-driven (the key
priority proof; the existing MUST-2 proof test is the inverse)
* same with Sonnet request (no matching scoped window) ->
NOT scoped-driven (5h/7d guard short-circuits before the
scoped check)
* HEALTHY 5h/7d + EXHAUSTED Fable -> still IS scoped-driven
(regression lock: the 5h/7d guard must not over-trigger and
turn a genuine scoped-driven block into account-level)
Existing 5h/7d behavior preserved
* killswitchRetryAfterSeconds: all 5 opencode retries (3 from
opencode tests + 2 from core tests) still pass; the no-scoped
path is byte-identical
* resolveScopedDrivenBlock: all 7 MUST-2 tests still pass
(healthy-5h/7d + exhausted-Fable remains isScopedDriven: true,
exactly as designed)
Gates
* bun run typecheck -> 0
* bun run lint -> '1 info' (pre-existing biome-schema drift)
* core bun test -> 37 pass / 0 fail (was 34; +3 FINDING 1)
* opencode bun test -> 773 pass / 0 fail (was 770; +3 FINDING 2)
* pi bun test -> 47 pass / 0 fail (unchanged)
There was a problem hiding this comment.
1 issue found across 5 files
Confidence score: 3/5
- In
packages/opencode/src/tests/killswitch.test.ts, the MUST-2 boundary case usesremainingPercentvalues equal to the 5h/7d thresholds even though the logic is described as strict<, so the test may assert behavior that doesn’t match production edge conditions and could hide a real killswitch regression at exactly-threshold values—update the fixture/expectation (or explicitly change the operator semantics) before merging.
Architecture diagram
sequenceDiagram
participant Client as API Client
participant Router as Routing Engine
participant KS as Killswitch Gate
participant Quota as Quota Manager
participant CLI as Killswitch CLI
participant Storage as Account Storage
Note over Client,Storage: NEW: Per-Model Scoped Killswitch Flow
CLI->>Storage: set main:5,10,3 (fh, sd, scoped)
Storage-->>CLI: Config updated
CLI->>CLI: normalizeKillswitchThresholds(s)
Note over CLI: Parsed thresholds: {five_hour: 5, seven_day: 10, scoped: 3}
Client->>Router: Request with modelId (e.g. claude-fable-5)
Router->>KS: killswitchPassesPolicy(quota, storage, accountId?, modelId?)
alt Killswitch disabled
KS-->>Router: true → pass through
else Killswitch enabled
KS->>KS: Check 5h window vs threshold (strict <)
alt 5h remaining < threshold
KS-->>Router: false → block (account-level)
else 5h passes
KS->>KS: Check 7d window vs threshold (strict <)
alt 7d remaining < threshold
KS-->>Router: false → block (account-level)
else 7d passes
Note over KS: NEW: Scoped check (runs BEFORE unknown-window fail-open)
alt modelId provided
KS->>Quota: getScopedQuotaWindowForModel(quota, modelId)
Quota-->>KS: matching window or null
alt window found AND remainingPercent <= scoped threshold (inclusive)
KS-->>Router: false → block (model-scoped)
else window not found or above threshold
KS->>KS: Check sawUnknownWindow
alt failClosedOnUnknown
KS-->>Router: false
else failOpen
KS-->>Router: true
end
end
else no modelId
KS->>KS: Check sawUnknownWindow (unchanged)
end
end
end
end
Note over Router: NEW: Model-scoped 429 Response
alt Block triggered
Router->>Router: resolveScopedDrivenBlock()
Note over Router: First: killswitchPassesPolicy WITHOUT modelId (5h/7d only)
alt 5h/7d alone blocks
Router->>Router: isScopedDriven = false (account-level)
else 5h/7d passes (block is scoped-only)
Router->>Router: check matched scoped window <= threshold
Router->>Router: isScopedDriven = true
end
Router->>Router: killswitchRetryAfterSeconds(quota, fallbacks, now, scopedModelId?)
alt scopedModelId present
Router->>Router: Only consider scoped window's resetsAt
Note over Router: Ignore 5h/7d resets (weekly window)
else no scopedModelId
Router->>Router: Check 5h + 7d resets (unchanged)
end
Router->>Router: formatKillswitchBlockMessage()
alt scoped-driven
Router->>Router: "Claude Fable 5 weekly limit reached..."
else account-level
Router->>Router: "Killswitch: no routable accounts..."
end
Router-->>Client: 429 + Retry-After header
end
Note over Router: NEW: Fallback Filter with modelId
alt Main killed, checking fallbacks
Router->>Router: Filter fallbacks with killswitchPassesPolicy(quota, storage, id, modelId)
Note over Router: Accounts with exhausted Fable scoped are excluded from Fable routing
alt Qualified fallbacks found
Router->>Client: Route to fallback
else No qualified fallbacks
Router->>Client: 429 (same message as above)
end
end
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
The '5h/7d-driven block + healthy Fable' test set 5h/7d remaining to exactly 5/10 against thresholds of 5/10, but 5h/7d use strict '<', so 5<5 and 10<10 never fired — the test passed only because the Fable window was healthy, never exercising the account-level priority guard (cubic P2). Lower remaining to 4/9 so the killswitch genuinely fires and the test traverses the guard branch. The both-exhausted guard lock is already covered by the existing FINDING 2 test.
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.
What
Adds a third, model-scoped dimension to the per-account killswitch: a
scopedthreshold that hard-limits traffic to a weekly scoped quota carve-out (theweekly_scoped"Fable only" window) without killing the account for other models.When the killswitch is enabled and a request's model matches a scoped window at/below the
scopedthreshold, the request reroutes to a scoped-eligible fallback and hard-blocks with a 429 if none qualify — while Sonnet/Opus traffic on the same account continues normally. Additive to the existingfive_hour/seven_daychecks; generic over any scoped window (not Fable-hardcoded).Changes
killswitchPassesPolicy(quota, storage, accountId?, modelId?)— new optionalmodelId. When present and a scoped window matches it, the window'sremainingPercentis checked against the per-accountscopedthreshold. When absent, behavior is unchanged. The scoped check runs before the unknown-5h/7d fail-open return, so an exhausted scoped window blocks independently of missing 5h/7d windows.<=(inclusive) for scoped — deliberately distinct from the strict<on 5h/7d — so the default threshold0fires at true exhaustion (matchingquotaSnapshotModelScopeIsExhausted's<= 0).KillswitchThresholdsgains an optionalscopedkey (default0);normalizeKillswitchThresholdsresolves it with the same finite-guard/default pattern. Backward-compatible: existing two-value configs readscopedas the default./claude-killswitch set main:80,10,0accepts an optional third number (fh, sd, scoped). Two-number form parses identically to before. Status table gains a Scoped column.modelIdis threaded into the three routing-path call sites (main killswitch gate + both fallback filters), leaving the sidebar/status callers model-free so account-level killed state is unchanged."<Model> weekly limit reached, …") andkillswitchRetryAfterSecondshonors the scoped window's weeklyresetsAt. A 5h/7d-driven block keeps the account-level message (classified by the same<= scoped thresholdcheck that drives the block, so message and reason never disagree).Opencode-only, consistent with the existing killswitch (Pi has no killswitch enforcement).
Verification
bun run typecheck— clean (all packages)bun run lint— cleanbun test: core 34/0, opencode 770/0, pi 47/0<=boundary at the0default and a raised threshold,modelId-absent regression lock, the unknown-window × fail-open × scoped-exhausted interaction, block-message attribution (5h/7d block with a healthy scoped window → generic message; exhausted scoped window → model-named), and config round-trip for the two- and three-value forms.Need help on this PR? Tag
/codesmithwith what you need. Autofix is disabled.Summary by cubic
Adds a per-model
scopedkillswitch threshold to enforce weekly model carve-outs (e.g., Fable) without killing the account for other models. Routing, CLI, retry hints, and 429 messages are scope-aware with correct attribution and weekly reset timing; tests lock the priority rules.New Features
scopedthreshold (default 0); inclusive<=; evaluated when a request includesmodelId; additive to 5h/7d.modelIdthrough the main gate and fallback filters; exhausted scoped windows reroute to scoped-eligible fallbacks or 429.scopedModelIdand use only the matched weekly reset; extend/claude-killswitch setto accept a third number (fh, sd, scoped); status table shows a Scoped column; thresholds normalized; block messages name the model for scoped-driven blocks.Bug Fixes
scopedthreshold and 5h/7d did not already kill the account (MUST-2, FINDING 2).scopedModelIdis provided, consider only the matched scoped window’sresetsAtso the hint reflects the weekly reset (FINDING 1).<thresholds (ensures the priority guard is exercised).Written for commit 660e47d. Summary will update on new commits.
Greptile Summary
This PR adds a
scopedthird dimension to the per-account killswitch, enabling model-specific weekly quota enforcement (e.g., Fable carve-outs) without blocking the account for other models. The change threadsmodelIdthrough the main killswitch gate and both fallback filters, and introduces scope-aware block-message attribution and retry-after hints.killswitchPassesPolicygains an optionalmodelId; when present and a scoped window matches at/below thescopedthreshold, the request is blocked independently of 5h/7d — using<=(inclusive) so the default0fires at true exhaustion.resolveScopedDrivenBlockdistinguishes a scoped-driven block from a 5h/7d-driven one by re-running policy withoutmodelId; if 5h/7d already killed the account, the block is classified as account-level regardless of the scoped window's state (addressing the previous FINDING 2).killswitchRetryAfterSecondsnow takes ascopedModelIdand, when provided, collects only the matched scoped window's weeklyresetsAt— ignoring the 5h reset that would otherwise produce a misleadingly short retry hint (addressing the previous FINDING 1).Confidence Score: 5/5
Safe to merge — the two previously flagged issues (retry-after dominated by 5h/7d resets; misclassification when both 5h/7d and scoped are simultaneously exhausted) are both resolved in this revision, with regression-lock tests co-located alongside each fix.
The scoped check is correctly ordered before the unknown-window fail-open branch, the resolveScopedDrivenBlock priority logic (5h/7d wins over scoped when both are exhausted) matches the stated invariant and is directly verified, and the retry-after split (scoped-only vs 5h/7d) prevents the retry-storm scenario. The change is backward-compatible: existing two-value configs read scoped as 0 on normalization, and callers that omit modelId see identical behavior to before.
No files require special attention.
Important Files Changed
Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart TD A[Request with modelId] --> B{killswitch enabled?} B -- No --> Z[Pass through to main] B -- Yes --> C[killswitchPassesPolicy mainQuota, storage, undefined, modelId] C --> D{5h/7d window below threshold?} D -- Yes return false --> E[Account-level kill] D -- No check scoped --> F{modelId provided and scoped window matches model?} F -- No scoped window --> G{sawUnknownWindow?} F -- Window at/below scoped threshold --> E F -- Window above scoped threshold --> G G -- failClosed=true --> E G -- failClosed=false --> Z E --> H[getRoutableFallbackAccounts filtered by modelId scoped check] H --> I{survivingFallbacks > 0?} I -- Yes --> J[Route to fallback] I -- No --> K[resolveScopedDrivenBlock check 5h/7d WITHOUT modelId] K --> L{5h/7d passes without modelId?} L -- No account-level --> M[Generic 429 message retryAfter from 5h/7d resets] L -- Yes + scoped window at/below threshold --> N[Scoped-driven 429 model-named message retryAfter from weekly resetsAt]%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%% flowchart TD A[Request with modelId] --> B{killswitch enabled?} B -- No --> Z[Pass through to main] B -- Yes --> C[killswitchPassesPolicy mainQuota, storage, undefined, modelId] C --> D{5h/7d window below threshold?} D -- Yes return false --> E[Account-level kill] D -- No check scoped --> F{modelId provided and scoped window matches model?} F -- No scoped window --> G{sawUnknownWindow?} F -- Window at/below scoped threshold --> E F -- Window above scoped threshold --> G G -- failClosed=true --> E G -- failClosed=false --> Z E --> H[getRoutableFallbackAccounts filtered by modelId scoped check] H --> I{survivingFallbacks > 0?} I -- Yes --> J[Route to fallback] I -- No --> K[resolveScopedDrivenBlock check 5h/7d WITHOUT modelId] K --> L{5h/7d passes without modelId?} L -- No account-level --> M[Generic 429 message retryAfter from 5h/7d resets] L -- Yes + scoped window at/below threshold --> N[Scoped-driven 429 model-named message retryAfter from weekly resetsAt]Comments Outside Diff (1)
packages/opencode/src/index.ts, line 886-911 (link)resolveScopedDrivenBlockmisclassifies when 5h/7d and scoped are both exhaustedresolveScopedDrivenBlockchecks only whether the scoped window is at/below threshold — it does not verify that 5h/7d windows are themselves healthy. When 5h/7d exhaustion (e.g.remainingPercent: 4vs a threshold of5) causedkillswitchPassesPolicyto returnfalse, a simultaneously exhausted scoped window still produces{ isScopedDriven: true }. The caller then emits"<Model> weekly limit reached, no routable accounts", misleading operators into thinking Sonnet requests on the same account would succeed — when in reality the account is blocked for all models due to the 5h/7d breach.The fix is to confirm that no 5h/7d window is independently below its threshold before declaring the block scoped-driven: if any finite 5h/7d window is
< thresholds[key], return{ isScopedDriven: false }immediately so the generic account-level message is used instead.Reviews (3): Last reviewed commit: "test(killswitch): make MUST-2 5h/7d prem..." | Re-trigger Greptile