fix(planner): server-side model calls run on the subscription, not the metered key

The roster planner died with `400 — "Your credit balance is too low to access
the Anthropic API"` while every mission on the same machine kept running. Two
Anthropic credentials reach this server and they bill differently:
`ANTHROPIC_API_KEY` (sk-ant-api, metered, runs out) and the Claude Code
subscription token (sk-ant-oat) that every VM already uses.

`Runtime::complete` with a BARE model name — "claude-opus-4-8" — resolves to the
default provider, which is the metered key. Three server-side callers did that:
the roster planner, the Master Planner, and the claw enhancer. Missions were
never affected because `mission_runtime` deliberately sends only the
subscription token into a guest; the server had no equivalent rule.

`subscription::complete_or` is now that rule, and it is the ONE place a
subscription token becomes a provider — `evaluator::subscription_judge` had its
own copy, and two of them is how one ends up with a prefix check the other
lacks.

The `sk-ant-oat` prefix is checked rather than the variable name trusted: an
API key pasted into the OAuth slot would authenticate, work, and bill the
metered account — the same failure again, discovered weeks later.

`web_search` is carried explicitly rather than defaulted. The Master Planner and
the claw enhancer both pass `true`, and a helper that quietly dropped it would
have taken web search away from two features while every test still passed.

`validator_preflight` deliberately keeps `Runtime::complete`: it probes whatever
validator spec is configured (today `glm:glm-4.7`), and forcing it onto Anthropic
would make it prove the wrong thing. A test pins both halves — no other
server-side caller may regress to the metered key, and preflight must keep
probing the configured spec.

258 lib tests.
This commit is contained in:
Omar Sobh
2026-08-08 09:34:19 -07:00
parent d84d17207f
commit 72046e7985
6 changed files with 189 additions and 20 deletions
+13 -4
View File
@@ -111,10 +111,19 @@ pub async fn suggest(
},
);
let raw = state
.runtime
.complete(ROSTER_SYSTEM, &prompt, PLANNER_MODEL, 2000, false)
.await
// On the SUBSCRIPTION, like every mission VM — not the metered API key.
// `Runtime::complete` with a bare model name resolves to the default
// provider, which is the pay-as-you-go key; this planner died with
// "credit balance is too low" while missions on the same box ran fine.
let raw = crate::subscription::complete_or(
&state.runtime,
ROSTER_SYSTEM,
&prompt,
PLANNER_MODEL,
2000,
false,
)
.await
.map_err(|e| {
eprintln!("mission {id}: roster proposal failed: {e}");
ApiError::Internal