Repository navigation
Choose ns-run's Anthropic slot in fill-first order and move on when credit is exhausted - #1103
Merged
seathatflowsinourveins merged 2 commits intoOct 14, 2026
Conversation
…redit is exhausted The command center's incident of 2026-10-14 (01:01:52Z): an ns-run pi researcher run on anthropic-api-4 was refused with Anthropic's 400 "credit balance is too low", billed nothing, and stopped. choose_slot() walked a stale api-4/3/2 list and took the first slot whose no-token check (GET /v1/models) read "valid", which an exhausted key still does, and the run never moved on. - choose_slot() walks the review worker's DEFAULT_KEYS (one fill-first order for every Console consumer) and screens out only a key that is not stored, invalid (401) or forbidden (403); "valid" no longer decides. - A print run whose first call is refused as credit-exhausted before any billed output starts again on the next usable slot; the receipt lists each refused slot. Pi reports the refusal as an assistant message_end with stopReason "error" and errorMessage "<status> <body>"; OpenCode as an error event. Any other refusal, or a call that billed usage, ends the run on its slot. - The classifier is the review worker's credit_exhausted(status, text) (402, or 400 "credit balance is too low"), now a named function analyze() uses and runtime_profiles imports. FillFirstSlotTests: on main's code the credit refusal ends the run on anthropic-api-4 and the screening gives up after 4/3/2; here the run moves to the next slot. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…used slot Both review families' blockers on #1103 at 0de55d7, in one commit. - A timed-out or signalled attempt keeps its own receipt and exit status, whatever its log holds: only an attempt that ended on its own with a credit refusal moves the run to another slot (cut_short: timeout's 124, 125, 126, 127 and 137, the credential runner's 128+N, a negative subprocess status). - The refused slot is recorded before a successor is looked for, so the last exhausted slot is in the receipt's credit_refused_slots when none follows. - first_call_refusal cites the refusal-event sources at the qualified pins: earendil-works/pi@04566631 (with @anthropic-ai/sdk 0.129.0) and anomalyco/opencode@53d1eabb. Tests fail on the 0de55d7 code (failures=6: five cut-short cases and the last-slot case) and pass here. Co-Authored-By: Claude Opus 5.5 <[email protected]>
seathatflowsinourveins
marked this pull request as ready for review
October 14, 2026 02:26
5 tasks done
seathatflowsinourveins
deleted the
claude/api-actions-ns-run-fill-first-20261014
branch
October 14, 2026 04:14
5 tasks done
seathatflowsinourveins
added a commit
that referenced
this pull request
Oct 14, 2026
A follow-up to #1103, ruled so by the command center: the local delta review of #1103 at ef50b1a found that OpenCode retries a failed stream itself (anomalyco/opencode@53d1eabb, packages/opencode/src/session/processor.ts: 649-689). A credit refusal can therefore follow text, reasoning or a tool call. first_call_refusal stopped only at a finished step, so such a run would start again on another slot, replaying the prompt in the same worktree. first_call_refusal now stops at any step_finish, text, reasoning or tool_use event before the error (run.ts:750, :754, :767, :725 at that pin); a step_start alone does not count. Pi needs no change: partial output there follows message_start, so usage is non-zero and the usage check already stops it. The new subcases fail on the #1103 code (failures=3: text, reasoning, tool_use) and pass here. Co-Authored-By: Claude Opus 5.5 <[email protected]>
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.
Scope
tools/adoption/runtime_profiles.py) now chooses its Anthropic Console slot in the fill-first order the review worker uses. When the first call is refused as credit-exhausted, before any billed output, the run moves to the next usable slot instead of stopping.ef2fea18(main). Two commits:0de55d7f(the change) andef50b1a2(the review round's blockers). The head merges cleanly with main000ae0e3(git merge-treerc 0). The merged-tree checks below ran at main04926b5d; the one commit since, Retain exact seq2 replay sources for a git-bound inventory #1102, touches onlyblueprints/us-equities/paper-open-e2e/seq2-replay/.lane:foundationtools/adoption/runtime_profiles.py:fill_first_order,choose_slot(after=),first_call_refusal,credit_refused,cut_short, and the print-run retry;tools/claude-review-worker/worker.py:credit_exhausted(status, text), named and used byanalyze()(no behavior change);tests/test_runtime_profiles.py(FillFirstSlotTests) andtests/test_claude_review_worker.py(a classifier test);manifests/evidence.json: re-registration.The incident. The command center's ns-run pi researcher run, on the
pi-claudeprofile at 2026-10-14T01:01:52Z, was refused with Anthropic's 400invalid_request_error"Your credit balance is too low to access the Anthropic API". It billed 0 tokens and stopped. The cause waschoose_slot():GET /v1/models) read "valid". A key whose prepaid credit is exhausted still answers 200 there, so it choseanthropic-api-4, which was out of credit;The api-actions lane's ledgered one-token probes at 01:04:52-01:04:57Z confirmed the state:
anthropic-api-4andanthropic-api-2refused for credit, whileanthropic-api-7,-5,anthropic-apiand-6had credit.credential_run.pyinjects one named slot by design and never switches; switching belongs to its caller.The change.
choose_slot()walksfill_first_order(), which is the review worker'sDEFAULT_KEYS, so every Console consumer follows one order. This PR does not change that order; it changes only through Take up the command center's two remaining notes on the key slots #1050, on its own review, and ns-run then follows with no further change.invalid(401) orforbidden(403). "valid" no longer decides; a rate-limited or unreachable answer does not remove a slot either.message_endwithstopReason: "error"anderrorMessage"<status> <body>", with zero usage, and exits 0. That is the incident's own run record.errorevent.credit_refused_slots, and the run starts again onchoose_slot(after=<that slot>).cut_short: timeout's 124, 125, 126, 127 and 137, the credential runner's 128+N, a negative subprocess status).credit_exhausted(status, text).analyze()uses it, andruntime_profiles.pyimports it rather than copying it.Review round at
0de55d7f. The command center's Claude micro passed. The local review (pr1103-0de55d7f/review_output.json) and the Astra read (full1-pr1103-0de55d7f4/GPT-VERDICT.md) asked for changes, with three distinct P2 blockers and no P1. Commitef50b1a2takes all three:cut_shortcheck runs before the credit check.first_call_refusalcites them, as listed under SOTA sources.Delta round at
ef50b1a2. The command center's Claude micro passed, and the Astra delta read (fix1-pr1103-ef50b1a23/GPT-VERDICT.md) acknowledged the head with no blocker. The local delta review (pr1103-ef50b1a2/review_output.json) raised one P2, which the command center ruled a follow-up. It is the first item below.Follow-ups (not in this change):
anomalyco/opencode@53d1eabb,packages/opencode/src/session/processor.ts:649-689), so a credit refusal can follow text, reasoning or a tool call. The parser stops only at a finished step, so such a run would start again on another slot. The fix stops at atext,reasoningortool_useevent too. It is prepared with a fail-first test and goes up as its own PR once this one lands.tools/adoption/runtime_acceptance.pyalso callschoose_slot(). It gets the order and the screening, but not the retry. Itsloader_health_readynow means a stored slot that was not rejected outright, not shown credit.runtime_profiles.py:482), so this is not a reproduced billing case; whole-run usage and recovery-order tests remain to add (Astra's P3).~/.local/bin/ns-runrunsruntime_profiles.pyfrom the runtime-profiles lane's worktree, not main. After landing it must be re-pointed to main's file, by that lane's install or by api-actions. The record of which says so.SOTA sources
tools/claude-review-worker/worker.py(DEFAULT_KEYS,analyze()), which this change reuses instead of duplicating.earendil-works/pi@04566631(the qualified release):packages/ai/src/api/anthropic-messages.ts:893-895setsstopReason: "error"anderrorMessagefrom the SDK error;:589-596starts usage at zero;packages/agent/src/agent-loop.ts:443-455emits that message asmessage_end;packages/coding-agent/src/modes/print-mode.ts:108-112writes each event as a JSON line;@anthropic-ai/sdk0.129.0 (pinned atpackages/ai/package.json:70),src/core/error.ts:40-50: the error message is"<status> <body>".anomalyco/opencode@53d1eabb(the qualified release):packages/opencode/src/cli/cmd/run.ts:750emitsstep_finish,:789emits theerrorevent, and:842sets exit status 1;packages/opencode/src/session/message-v2.ts:707-717andpackages/core/src/util/error.ts:55-60: a provider refusal is{name, data: {statusCode, ...}}.timeout --help, "Exit status" (124, 125, 126, 127, 137), andtools/credentials/credential_run.py, "Exit status" (the command's own; 128+N for a signal death).billing_error. The 400 "credit balance is too low" form is the one the incident's own run recorded (request id in the command center's incident note, cited by name).Evidence-class table
ef2fea18a credit-refused first slot ends the run on api-4, and the screening gives up after 4/3/2local_integrationruntime_profiles.pyandworker.py:FAILED (failures=2, errors=7); the two failures are['anthropic-api-4'] != ['anthropic-api-4', 'anthropic-api-3', ...]0de55d7fa cut-short attempt still moved on, and the last refused slot was missinglocal_integrationFillFirstSlotTestswith the0de55d7fruntime_profiles.py:FAILED (failures=6): five cut-short cases (the watchdog, 124, 137, 143, -15) and the last-slot case (9 of 10 rows)local_integrationFillFirstSlotTests: 8 tests OKnative_proven04926b5dlocal_integrationLocal commands run
CI at
0de55d7ffailedvalidate,validate-shard-0andvalidate-shard-5on one line, the date-based model-currency rule forclaude-fable-5-1. #1101 fixed that on main (04926b5d). CI atef50b1a2passes: 26 checks, 5 skipped.Decision record
None: this implements the fill-first rule already recorded in
docs/decisions/2026-10-10-anthropic-key-slots-and-practice.mdanddocs/secret-storage.md(item 10, "Fill-first failover") for the ns-run profiles.Host evidence
Not applicable (no
evidence/hosts/change).Checklist
version comment (no floating tags). (none changed)
permissions: {}and grant each jobonly what it needs (
contents: read, or an explicitly justified addition). (none changed)added without a documented owner.
moved or overwritten).
🤖 Generated with Claude Code