fix(planner): the other four server-side calls were still on the metered key

The test that was supposed to prevent this grepped for the literal
`runtime.complete(` and passed while the phase planner (`mission_plan.rs`),
both swarm calls, and a second enhance path in `claws.rs` still billed the
pay-as-you-go account. They spell the receiver `state.runtime` or wrap the
call across lines, so the receiver name was never the thing to match. The
test now matches the METHOD, and covers all five files.

`complete_or` gains the rule that makes it safe to apply everywhere: a
`name:model` spec is an operator's explicit provider choice — the swarm
worker model is configured exactly that way — and is passed straight to
`Runtime::resolve_provider` untouched. Only a bare name is ambiguous, and a
bare name is precisely what resolves to the default provider. Hijacking a
chosen Kimi or GLM model onto Anthropic would be the same silent-substitution
bug pointed the other way.

`validator_preflight` and the evaluator judge keep calling the runtime
directly, on purpose: both exist to exercise the CONFIGURED spec.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Omar Sobh
2026-08-08 15:55:52 -07:00
co-authored by Claude Opus 5
parent deed591da6
commit ee5a939ce6
4 changed files with 86 additions and 26 deletions
+8 -2
View File
@@ -660,8 +660,14 @@ pub(crate) async fn enhance_and_publish(
let user_prompt = format!( let user_prompt = format!(
"ROLE CONTEXT: {role_context}\n\nBRAIN: {reference}\n\n=== SYSTEM PROMPT ===\n{sp}\n\n=== AGENTS.md ===\n{agent_md}\n\n=== PERSONA ===\n{persona}\n\n=== SKILLS ===\n{skills}" "ROLE CONTEXT: {role_context}\n\nBRAIN: {reference}\n\n=== SYSTEM PROMPT ===\n{sp}\n\n=== AGENTS.md ===\n{agent_md}\n\n=== PERSONA ===\n{persona}\n\n=== SKILLS ===\n{skills}"
); );
let raw = runtime let raw = crate::subscription::complete_or(
.complete(ENHANCE_SYSTEM, &user_prompt, "claude-opus-4-8", 16000, true) runtime,
ENHANCE_SYSTEM,
&user_prompt,
"claude-opus-4-8",
16000,
true,
)
.await?; .await?;
let v = extract_json(&raw).ok_or_else(|| "unparseable enhance output".to_string())?; let v = extract_json(&raw).ok_or_else(|| "unparseable enhance output".to_string())?;
let enh = v.get("enhanced").cloned().unwrap_or(Value::Null); let enh = v.get("enhanced").cloned().unwrap_or(Value::Null);
+9 -4
View File
@@ -174,13 +174,18 @@ pub async fn suggest(
PLANNABLE_KINDS.join(", "), PLANNABLE_KINDS.join(", "),
); );
let raw = state let raw = crate::subscription::complete_or(
.runtime &state.runtime,
.complete(PLAN_SYSTEM, &prompt, PLANNER_MODEL, 2000, false) PLAN_SYSTEM,
&prompt,
PLANNER_MODEL,
2000,
false,
)
.await .await
.map_err(|e| { .map_err(|e| {
eprintln!("mission {id}: plan proposal failed: {e}"); eprintln!("mission {id}: plan proposal failed: {e}");
ApiError::Internal crate::subscription::as_api_error(&e)
})?; })?;
let parsed: Value = crate::routes::claws::extract_json(&raw).ok_or_else(|| { let parsed: Value = crate::routes::claws::extract_json(&raw).ok_or_else(|| {
+39 -4
View File
@@ -68,14 +68,27 @@ pub async fn complete_or(
// web search away from two features while every test still passed. // web search away from two features while every test still passed.
web_search: bool, web_search: bool,
) -> Result<String, String> { ) -> Result<String, String> {
let Some(provider) = provider() else { // A `name:model` spec is an operator's explicit provider choice — the swarm
// worker model is literally configured that way (`kimi:kimi-k2.6`), and
// `Runtime::resolve_provider` honours it. Forcing that onto Anthropic would
// silently run someone's chosen model on the wrong provider, which is the
// same class of bug as this module exists to fix, only pointed the other
// way. Only a BARE name is ambiguous, and a bare name is what resolves to
// the default provider — the metered key.
if !is_bare_model_name(model) || provider().is_none() {
return runtime return runtime
.complete(system, user, model, max_tokens, web_search) .complete(system, user, model, max_tokens, web_search)
.await; .await;
}; }
let provider = provider().expect("checked just above");
complete_with(&provider, system, user, model, max_tokens, web_search).await complete_with(&provider, system, user, model, max_tokens, web_search).await
} }
/// Whether a model string names a model without naming a provider.
pub fn is_bare_model_name(model: &str) -> bool {
!model.contains(':')
}
/// How long to wait before each retry. Four attempts, ~30s of patience total. /// How long to wait before each retry. Four attempts, ~30s of patience total.
/// ///
/// The subscription has no credit wall, but it does have a rate limit, and a /// The subscription has no credit wall, but it does have a rate limit, and a
@@ -205,19 +218,27 @@ mod tests {
/// spec an operator configured (today `glm:glm-4.7`), and forcing it onto /// spec an operator configured (today `glm:glm-4.7`), and forcing it onto
/// Anthropic would make it prove the wrong thing — it exists to answer "is /// Anthropic would make it prove the wrong thing — it exists to answer "is
/// the configured validator reachable". /// the configured validator reachable".
/// The first version of this test grepped for the literal
/// `runtime.complete(` and passed while FOUR more call sites — the phase
/// planner, both swarm calls, and a second enhance path — still billed the
/// metered key. They were spelled `state.runtime` or wrapped across lines,
/// so the receiver name was never the thing to look for. Match the METHOD.
#[test] #[test]
fn no_server_side_call_silently_uses_the_metered_key() { fn no_server_side_call_silently_uses_the_metered_key() {
let sources = [ let sources = [
("routes/mission_roster.rs", include_str!("routes/mission_roster.rs")), ("routes/mission_roster.rs", include_str!("routes/mission_roster.rs")),
("routes/mission_plan.rs", include_str!("routes/mission_plan.rs")),
("routes/planner.rs", include_str!("routes/planner.rs")), ("routes/planner.rs", include_str!("routes/planner.rs")),
("routes/claws.rs", include_str!("routes/claws.rs")), ("routes/claws.rs", include_str!("routes/claws.rs")),
("swarm.rs", include_str!("swarm.rs")),
]; ];
for (name, src) in sources { for (name, src) in sources {
assert!( assert!(
!src.contains("runtime.complete("), !src.contains(".complete("),
"{name} calls Runtime::complete directly — a bare model name there \ "{name} calls Runtime::complete directly — a bare model name there \
resolves to the DEFAULT provider, which is the metered API key. \ resolves to the DEFAULT provider, which is the metered API key. \
Use `subscription::complete_or`." Use `subscription::complete_or`, which passes a `name:model` \
spec through untouched."
); );
} }
// And the exception stays an exception, on purpose. // And the exception stays an exception, on purpose.
@@ -254,6 +275,20 @@ mod tests {
assert!(!is_transient(&LlmError::Wire("bad json".into()))); assert!(!is_transient(&LlmError::Wire("bad json".into())));
} }
/// An operator's explicit provider choice is never hijacked.
///
/// The swarm worker model is a configured `name:model` spec. Routing that
/// onto the subscription would run someone's chosen Kimi or GLM model on
/// Anthropic and report success — the same silent-substitution bug as the
/// metered key, aimed the other way.
#[test]
fn a_provider_qualified_spec_is_left_alone() {
assert!(is_bare_model_name("claude-opus-4-8"));
assert!(is_bare_model_name("claude-haiku-4-5-20251001"));
assert!(!is_bare_model_name("kimi:kimi-k2.6"));
assert!(!is_bare_model_name("glm:glm-4.7"));
}
/// A metered key in the OAuth slot must be REFUSED, not used. /// A metered key in the OAuth slot must be REFUSED, not used.
/// ///
/// Accepting it would authenticate, work, and bill the pay-as-you-go account /// Accepting it would authenticate, work, and bill the pay-as-you-go account
+20 -6
View File
@@ -137,8 +137,14 @@ pub async fn run_swarm_job(
"GOAL:\n{goal}\n\nCHECKLIST each task's output must satisfy:\n{}{want}", "GOAL:\n{goal}\n\nCHECKLIST each task's output must satisfy:\n{}{want}",
checklist_lines(&checklist) checklist_lines(&checklist)
); );
let plan_raw = runtime let plan_raw = crate::subscription::complete_or(
.complete(PLAN_SYSTEM, &plan_user, "claude-opus-4-8", 4000, false) runtime,
PLAN_SYSTEM,
&plan_user,
"claude-opus-4-8",
4000,
false,
)
.await?; .await?;
let tasks: Vec<String> = extract_json(&plan_raw) let tasks: Vec<String> = extract_json(&plan_raw)
.and_then(|v| { .and_then(|v| {
@@ -177,8 +183,10 @@ pub async fn run_swarm_job(
let mut still: Vec<(usize, String)> = Vec::new(); let mut still: Vec<(usize, String)> = Vec::new();
let mut rejected = 0usize; let mut rejected = 0usize;
for (idx, task) in pending.iter() { for (idx, task) in pending.iter() {
let out = runtime // `worker_model` may be a `name:model` spec the operator chose;
.complete(&wsys, task, &worker_model, 4000, true) // `complete_or` passes those straight through untouched.
let out =
crate::subscription::complete_or(runtime, &wsys, task, &worker_model, 4000, true)
.await .await
.unwrap_or_else(|e| format!("worker error: {e}")); .unwrap_or_else(|e| format!("worker error: {e}"));
records.push(step( records.push(step(
@@ -190,8 +198,14 @@ pub async fn run_swarm_job(
ckpt(pool, id, &records, &totals).await; ckpt(pool, id, &records, &totals).await;
let vuser = format!("TASK:\n{task}\n\nWORKER OUTPUT:\n{out}"); let vuser = format!("TASK:\n{task}\n\nWORKER OUTPUT:\n{out}");
let v_raw = runtime let v_raw = crate::subscription::complete_or(
.complete(&vsys, &vuser, "claude-opus-4-8", 1200, true) runtime,
&vsys,
&vuser,
"claude-opus-4-8",
1200,
true,
)
.await .await
.unwrap_or_default(); .unwrap_or_default();
let v = extract_json(&v_raw); let v = extract_json(&v_raw);