refine: switch from Gemini to Claude Opus 4.8
ci / gates (push) Successful in 7s
ci / frontend (push) Successful in 27s
ci / rust (push) Successful in 4m25s
ci / e2e (push) Skipped
ci / publish (push) Successful in 4m16s

Prod's Gemini prepayment credits are depleted (429 on every refine
attempt). Switching to Anthropic Claude Opus 4.8 for the mission
Refine flow — ANTHROPIC_API_KEY is already set in prod for ZeroClaw's
provider config, so no new secret plumbing.

  - crates/cm-api/src/mission_refiner.rs:
    * DEFAULT_MODEL: gemini-2.5-flash → claude-opus-4-8
    * call_gemini → call_anthropic against
      https://api.anthropic.com/v1/messages with the standard
      x-api-key + anthropic-version headers
    * Response parser reads content[type='text'].text (Messages API
      block shape) instead of Gemini's candidates path
    * Timeout raised 60s → 90s (Opus can be slower than Flash on
      long briefs; still bounded so a stuck call fails fast)
  - deploy/compose/.env.example: doc block rewritten. Refine now
    reuses ANTHROPIC_API_KEY; Level-Up keeps GEMINI_API_KEY because
    it needs JSON-mode structured output.

Level-Up is NOT switched in this commit — it uses Gemini's JSON mode
which has no drop-in Anthropic equivalent (needs tool-use rewrite).
Filed as a separate concern; Refine is what was actively broken.

Verified: SQLX_OFFLINE=true cargo check -p cm-api clean;
cargo fmt --all clean.
This commit is contained in:
Omar Sobh
2026-07-20 14:04:43 -07:00
parent d8c8793c4a
commit 1cbbbbd3e5
2 changed files with 52 additions and 29 deletions
+42 -24
View File
@@ -2,14 +2,16 @@
//! mission and rewrite it into a coherent, sectioned Markdown brief //! mission and rewrite it into a coherent, sectioned Markdown brief
//! that downstream research + coding agents can ingest cleanly. //! that downstream research + coding agents can ingest cleanly.
//! //!
//! Uses Gemini 2.5 Flash (same call shape as level_up.rs) but with a //! Calls Anthropic Claude Opus 4.8 by default. Prod already carries
//! text-mode response — we want Markdown out, not JSON. //! ANTHROPIC_API_KEY for ZeroClaw's provider config, so no separate
//! env is needed.
use serde_json::json; use serde_json::json;
use sqlx::PgPool; use sqlx::PgPool;
use uuid::Uuid; use uuid::Uuid;
const DEFAULT_MODEL: &str = "gemini-2.5-flash"; const DEFAULT_MODEL: &str = "claude-opus-4-8";
const ANTHROPIC_API_VERSION: &str = "2023-06-01";
fn model_name() -> String { fn model_name() -> String {
std::env::var("CLAWMATES_REFINER_MODEL").unwrap_or_else(|_| DEFAULT_MODEL.to_string()) std::env::var("CLAWMATES_REFINER_MODEL").unwrap_or_else(|_| DEFAULT_MODEL.to_string())
@@ -51,7 +53,8 @@ pub async fn refine(
.map(|p| p.kind) .map(|p| p.kind)
.collect(); .collect();
let refined = call_gemini(&mission.title, &mission.template_kind, &phase_kinds, &raw).await?; let refined =
call_anthropic(&mission.title, &mission.template_kind, &phase_kinds, &raw).await?;
Ok(RefineResult { Ok(RefineResult {
original: raw, original: raw,
@@ -59,18 +62,15 @@ pub async fn refine(
}) })
} }
async fn call_gemini( async fn call_anthropic(
title: &str, title: &str,
template_kind: &str, template_kind: &str,
phase_kinds: &[String], phase_kinds: &[String],
raw: &str, raw: &str,
) -> Result<String, String> { ) -> Result<String, String> {
let api_key = let api_key =
std::env::var("GEMINI_API_KEY").map_err(|_| "GEMINI_API_KEY unset".to_string())?; std::env::var("ANTHROPIC_API_KEY").map_err(|_| "ANTHROPIC_API_KEY unset".to_string())?;
let model = model_name(); let model = model_name();
let url = format!(
"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={api_key}"
);
let system = "You are a technical brief editor for an autonomous software \ let system = "You are a technical brief editor for an autonomous software \
engineering platform. Rewrite the user's raw mission description into a \ engineering platform. Rewrite the user's raw mission description into a \
@@ -131,38 +131,56 @@ async fn call_gemini(
); );
let body = json!({ let body = json!({
"system_instruction": { "parts": [{ "text": system }] }, "model": model,
"contents": [{ "role": "user", "parts": [{ "text": user }] }], "max_tokens": 4096,
"generationConfig": { "temperature": 0.3,
"temperature": 0.3, "system": system,
"maxOutputTokens": 4096, "messages": [
} { "role": "user", "content": user }
]
}); });
let client = reqwest::Client::builder() let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(60)) .timeout(std::time::Duration::from_secs(90))
.build() .build()
.map_err(|e| format!("http client: {e}"))?; .map_err(|e| format!("http client: {e}"))?;
let resp = client let resp = client
.post(&url) .post("https://api.anthropic.com/v1/messages")
.header("x-api-key", &api_key)
.header("anthropic-version", ANTHROPIC_API_VERSION)
.header("content-type", "application/json")
.json(&body) .json(&body)
.send() .send()
.await .await
.map_err(|e| format!("gemini call: {e}"))?; .map_err(|e| format!("anthropic call: {e}"))?;
if !resp.status().is_success() { if !resp.status().is_success() {
let code = resp.status(); let code = resp.status();
let body = resp.text().await.unwrap_or_default(); let body = resp.text().await.unwrap_or_default();
return Err(format!("gemini {code}: {}", &body[..body.len().min(500)])); return Err(format!(
"anthropic {code}: {}",
&body[..body.len().min(500)]
));
} }
let json: serde_json::Value = resp.json().await.map_err(|e| format!("gemini json: {e}"))?; let json: serde_json::Value = resp
.json()
.await
.map_err(|e| format!("anthropic json: {e}"))?;
// Anthropic Messages API returns content as an array of blocks;
// the first text block holds the assistant's reply.
let text = json let text = json
.pointer("/candidates/0/content/parts/0/text") .get("content")
.and_then(|v| v.as_str()) .and_then(|c| c.as_array())
.ok_or_else(|| "gemini response missing text".to_string())? .and_then(|arr| {
arr.iter()
.find(|b| b.get("type").and_then(|t| t.as_str()) == Some("text"))
})
.and_then(|b| b.get("text"))
.and_then(|t| t.as_str())
.ok_or_else(|| "anthropic response missing text block".to_string())?
.trim() .trim()
.to_string(); .to_string();
if text.is_empty() { if text.is_empty() {
return Err("gemini returned empty text".into()); return Err("anthropic returned empty text".into());
} }
Ok(text) Ok(text)
} }
+10 -5
View File
@@ -25,12 +25,17 @@ CLAWMATES_BOOTSTRAP_CREDITS=1250
# and provide the key here (uncomment): # and provide the key here (uncomment):
# ANTHROPIC_API_KEY=sk-ant-... # ANTHROPIC_API_KEY=sk-ant-...
# --- Gemini (mission Refine + agent/team Level-Up) -------------------------- # --- Refine + Level-Up LLM providers ----------------------------------------
# Both features call Gemini via generativelanguage.googleapis.com. Without a # Refine (mission description rewrites) calls Anthropic Claude Opus 4.8 by
# key, the Refine button and Level-Up buttons return 500. # default. The existing ANTHROPIC_API_KEY (used by ZeroClaw providers) is
# Model overrides default to gemini-2.5-flash (cheap, JSON-mode-native). # reused — no separate key needed. Override model with CLAWMATES_REFINER_MODEL.
#
# Level-Up (per-agent + per-team improvement proposals) still calls Gemini
# 2.5 Flash because it needs JSON-mode structured output. If Gemini credits
# lapse, set CLAWMATES_LEVEL_UP_MODEL to another supported Gemini model
# with quota — or plan to swap this to Anthropic tool-use in a later slice.
# GEMINI_API_KEY=AIza... # GEMINI_API_KEY=AIza...
# CLAWMATES_REFINER_MODEL=gemini-2.5-flash # CLAWMATES_REFINER_MODEL=claude-opus-4-8
# CLAWMATES_LEVEL_UP_MODEL=gemini-2.5-flash # CLAWMATES_LEVEL_UP_MODEL=gemini-2.5-flash
# --- Auth mode (optional) --------------------------------------------------- # --- Auth mode (optional) ---------------------------------------------------