refine: switch from Gemini to Claude Opus 4.8
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:
@@ -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,
|
||||||
"maxOutputTokens": 4096,
|
"system": system,
|
||||||
}
|
"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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) ---------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user