feat(llm): the subscription is the default provider, with a recorded fallback chain

Two changes so an empty metered account stops being a platform outage.

1. `build_provider` prefers the subscription token over ANTHROPIC_API_KEY.
   A bare model name resolves to whatever this returns, so making it the
   subscription means no server-side call can reach the metered key by
   construction — rather than by a source-grep test that already missed four
   call sites once. The metered key remains a fallback and now warns loudly
   when it is the one in use; boot no longer requires it at all.

2. `complete_with_fallback` walks a declared chain when a model has no
   capacity: opus -> haiku -> glm:glm-4.7 by default, overridable via
   CLAWMATES_MODEL_FALLBACK, empty to disable. Measured on gw-04 today: opus
   and sonnet return 429 on the subscription while haiku, GLM and Kimi all
   return 200, so a capped window no longer means "the planner is gone".

The chain returns the model that ANSWERED, and every caller persists it —
mission_plan_proposals.author_model, mission_team_proposals.author_model, and
the swarm's step role. A plan drafted by the third link and filed as an opus
plan is a silent quality change, which is the failure shape this project keeps
paying for. Two negative controls hold the design: the chain never retries the
model that just failed as its own fallback, and it steps down ONLY for a
capacity failure — walking it on a malformed prompt would ask three models the
same bad question and report the third one's confusion.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Omar Sobh
2026-08-08 22:56:01 -07:00
co-authored by Claude Opus 5
parent ee5a939ce6
commit 9c9439a271
5 changed files with 161 additions and 20 deletions
+6 -4
View File
@@ -174,7 +174,9 @@ pub async fn suggest(
PLANNABLE_KINDS.join(", "),
);
let raw = crate::subscription::complete_or(
// The stored `author_model` is whichever link of the fallback chain
// actually answered — see `subscription::complete_with_fallback`.
let (raw, author_model) = crate::subscription::complete_with_fallback(
&state.runtime,
PLAN_SYSTEM,
&prompt,
@@ -212,7 +214,7 @@ pub async fn suggest(
id,
ws.as_uuid().to_owned(),
&stored,
PLANNER_MODEL,
&author_model,
)
.await
.map_err(|e| {
@@ -220,7 +222,7 @@ pub async fn suggest(
ApiError::Internal
})?;
eprintln!(
"mission_plan: mission {id}{PLANNER_MODEL} proposed {} phase(s): {}",
"mission_plan: mission {id}{author_model} proposed {} phase(s): {}",
plan.phases.len(),
plan.phases
.iter()
@@ -232,7 +234,7 @@ pub async fn suggest(
Ok(Json(PlanProposalResponse {
id: pid,
plan: stored,
author_model: PLANNER_MODEL.to_string(),
author_model,
status: "proposed".into(),
}))
}