cyberine
UsageRun recipes

Adapter liveness — is it FUNDED, not just keyed (--liveness, kwrp9)

cyberine doctor answers "is a key present". It never answered "is that key's account funded", and neither did the fleet TUI's overview, which listed an adapter whenever its key existed and printed a STATIC capability tier — so on 2026-09-08 it showed 14 adapters as usable while three lanes were funded. The router and the build fleet select from that same availability set, so a dead adapter is a run that fails after spending a turn.

cyberine doctor --liveness           # one row per adapter, probed live
cyberine doctor --liveness --json    # the same rows machine-readable
CYBERINE_OVERVIEW_PROBE=0 cyberine doctor --liveness   # cache only, no network
curl -s http://127.0.0.1:41700/api/adapters/liveness         # same rows over HTTP
curl -s 'http://127.0.0.1:41700/api/adapters/liveness?cache=1'  # ... without probing

The fleet TUI (cyberine agents) grows a fourth column on the Adapters table carrying the same cell, painted from the cache on the first frame and refreshed by one background probe.

The seven statuses. ok — a balance endpoint returned a positive number (shown: ok 2.03 USD), or a CLI subscription is logged in (shown: ok 21%, the remaining share of the most-binding rate-limit window). no-credit — a balance endpoint returned <= 0, or a call was refused for billing. no-key — no credential configured (an unset env key, or a CLI that is not logged in). key-rejected — a credential is configured but the provider refused it (401/403), so the fix is to replace the key, not to set one. rate-limited — 429, with the reset when the provider sends one. funding-unknown — the key authenticates and the endpoint answers, but this provider publishes no balance endpoint. unknown — the probe itself failed (timeout, DNS, an unparseable body), or the provider authenticates out of band (copilot's device-code OAuth) so a key probe cannot see it.

Why a 2xx is NEVER ok. Measured 2026-09-09 (.local/research/kwrp9-liveness-probe-measurements-2026-09-09.md): DeepSeek's balance is -0.01 USD and its own API reports is_available: false, yet GET /models returns 200 with the full catalog; MiniMax, retired for "402 insufficient balance on every dispatch", also returns 200. Classifying a list-models 2xx as ok would therefore reproduce the exact bug this feature removes, so it earns funding-unknown instead. Only a number from a balance endpoint, or a logged-in subscription, is evidence of funding.

Where a number comes from. Cheapest-first and all free: DeepSeek GET /user/balance; OpenRouter GET /credits AND GET /key, reported as the MINIMUM of the two — /credits is the ACCOUNT (total_credits - total_usage) and /key is that key's own monthly cap, and reading only the latter overstated the measured account by about 4x; Moonshot GET /v1/users/me/balance (a non-OpenAI body shape). Everything else falls back to the dialect's list-models call (ProviderSpec.api decides: Anthropic gets /v1/models with x-api-key + anthropic-version, not a bearer on /models). Nothing sends a completion, so no probe counts toward the daily spend cap — but every probe IS network egress per adapter, which is why the cache and the env switch exist.

Two things the status reflects that key presence cannot. A CLI logged in via a METERED key with no subscription reads funding-unknown, not ok: checkCliLogin reports ANTHROPIC_API_KEY set before it looks for the claude.ai OAuth record, which is right for its own question and wrong for this one. And a logged-in CLI whose worker sits in RETIRED_WORKERS reads no-credit with the operator's own retirement reason — gemini is logged in with a revoked tier, muse is logged in with a 402 on every completion.

Quota attribution (kmt6c). The claude row's cached rate-limit reading is shown as THIS seat's only when its accountKey equals the current resolveQuotaAccountKey(); a reading keyed to a different account is withheld entirely (the row falls back to a bare ok with no quota number rather than ever showing another seat's percentage as this one's). A reading with no accountKey at all — a cache file written before account keying existed — is still shown, but its evidence line carries the literal marker unattributed (pre-account cache) so a reader knows the number may not belong to the seat currently running.

Cache. One file per adapter at $CYBERINE_HOME/liveness/<adapter>.json carrying the reading plus observedAt, so a cold open renders the last reading immediately and refreshes behind it. A missing or corrupt file reads as "no reading", never an error; an unwritable CYBERINE_HOME degrades to "no cache", never a failed overview. Adapter ids are sanitised into the directory, so a crafted id cannot write above it.

Known Limitations and Deferred Work. A provider with no balance endpoint can never report a number — funding-unknown is the honest ceiling there, not a bug to fix later, and openai, minimax, xiaomi, meta and anthropic all sit at it today. The per-adapter timeout is 3 s and probes run in parallel, so a whole sweep is bounded by the slowest single provider, not their sum; a provider that hangs shows its cached reading. The reading is a snapshot: an account can drain between the probe and the dispatch, so this narrows the window rather than closing it. Nothing here writes to RETIRED_WORKERS — a measurement that contradicts the operator's retirement list (on 2026-09-09, OpenRouter measured ~$2.03 remaining while a prior operator statement called it out of credit) is reported, never acted on. And the fleet TUI's fourth column has NOT been eyeballed on a real TTY: the column, its widths and its colours are typecheck- and smoke-verified only, and the row-detail evidence line is rendered from the same reading but likewise unseen. A legacy pre-account quota cache file is never migrated or deleted by this feature — it simply keeps rendering unattributed until something overwrites it with a keyed reading. And a machine running multiple Claude seats still has exactly one $CYBERINE_HOME/quota/ directory: each seat gets its own claude-<accountKey>.json, but the pre-account claude.json is shared, so its unattributed reading cannot be traced back to whichever seat last wrote it.

npm run selftest:adapter-liveness is the guard: 60 checks against a real stub HTTP server (real statuses, headers and sockets, not a mocked fetch), covering every status, both balance shapes, the Anthropic dialect, the CLI subscription/metered/retired split, the cache round-trip, the quota-file parse and the row-merge id matching. Positive-controlled by making a 200 read as ok, which reddens 4 checks.