Files
clawmates/crates/cm-api/src/routes/level_up.rs
T
Omar Sobh f6c3ddbf81 refactor: no feature depends on Gemini any more
Depleted Gemini prepayment credits took out PDF rendering. The same key was the
only thing standing between level-up proposals and the same fate, so both are
off it.

- `pdf_renderer` is DELETED, not disabled. Nothing sets `render_pdf: true` since
  markdown became the deliverable (821cbb8), so the worker polled forever for
  rows that can no longer exist. It was also the only caller of the Gemini
  MD->HTML conversion. A worker that cannot do anything is worse than absent: it
  reads as a feature.

- `level_up` now resolves its proposer through the provider REGISTRY
  (`Runtime::resolve_provider`), the same path the evaluator uses, defaulting to
  `glm:glm-4.7` — the validator this project measured and chose in
  scripts/judge-eval.sh. `CLAWMATES_LEVEL_UP_MODEL` takes a registry spec
  (`glm:glm-4.7`, `kimi:k2`, `claude-sonnet-5`), so every provider the platform
  can already reach works and no single vendor's billing can take it down.

The non-obvious part of that swap: Gemini was asked for
`response_mime_type: application/json` and obliged, so the old code parsed the
raw reply. Anthropic-format models are under no such obligation and wrap objects
in prose or a ```json fence. `extract_json_object` brace-counts to the matching
close — string-aware, so a `}` inside a value does not end it, and nested (these
proposals nest by design). Tested against bare, fenced, nested, brace-in-string
and absent. Parsing raw text would have worked in review and failed on the first
real proposal.

What deliberately still MENTIONS Gemini: `mission_runtime` forwards
GEMINI_API_KEY to agent containers alongside GROQ/OPENAI/ZAI/KIMI, and the claw
model selector offers it. Those are user options, not platform requirements —
the ask was to remove the NEED.

Also corrected a comment in mission_delivery that cited `pdf_renderer` as the
authority on artifact path resolution. It never was: it joined the mission id
first and produced a doubled path that never resolved.

238 lib tests, 20 test binaries.
2026-08-07 13:01:57 -07:00

110 lines
3.3 KiB
Rust

//! `/api/level-up-proposals/*` + trigger endpoints — Slice 8.5.
use axum::extract::{Path, State};
use axum::response::Json;
use serde::Deserialize;
use uuid::Uuid;
use cm_db::repo::level_up::LevelUpProposal;
use crate::{ApiError, AppState, Authed};
pub async fn list_pending(
State(state): State<AppState>,
Authed(user): Authed,
) -> Result<Json<Vec<LevelUpProposal>>, ApiError> {
let rows =
cm_db::repo::level_up::list_pending(&state.pool, user.workspace_id.as_uuid()).await?;
Ok(Json(rows))
}
pub async fn get_proposal(
State(state): State<AppState>,
Authed(user): Authed,
Path(id): Path<Uuid>,
) -> Result<Json<LevelUpProposal>, ApiError> {
let p = cm_db::repo::level_up::get(&state.pool, id, user.workspace_id.as_uuid())
.await?
.ok_or(ApiError::NotFound)?;
Ok(Json(p))
}
/// POST /api/claws/{id}/level-up — LLM proposes agent improvements.
/// Returns the new proposal id; reviewer approves via the apply route.
pub async fn propose_for_agent(
State(state): State<AppState>,
Authed(user): Authed,
Path(agent_id): Path<Uuid>,
) -> Result<Json<serde_json::Value>, ApiError> {
let id = crate::level_up::propose_agent(&state.pool, &state.runtime, user.workspace_id, user.user_id, agent_id)
.await
.map_err(|e| {
eprintln!("level_up: propose_agent {agent_id} failed: {e}");
ApiError::Internal
})?;
Ok(Json(serde_json::json!({ "proposal_id": id })))
}
/// POST /api/teams/{id}/level-up — LLM proposes team improvements.
pub async fn propose_for_team(
State(state): State<AppState>,
Authed(user): Authed,
Path(team_id): Path<Uuid>,
) -> Result<Json<serde_json::Value>, ApiError> {
let id = crate::level_up::propose_team(&state.pool, &state.runtime, user.workspace_id, user.user_id, team_id)
.await
.map_err(|e| {
eprintln!("level_up: propose_team {team_id} failed: {e}");
ApiError::Internal
})?;
Ok(Json(serde_json::json!({ "proposal_id": id })))
}
#[derive(Debug, Deserialize)]
pub struct ApplyRequest {
/// Ids from payload.suggested_items[] that the reviewer approved.
pub approved_item_ids: Vec<String>,
}
pub async fn apply_proposal(
State(state): State<AppState>,
Authed(user): Authed,
Path(id): Path<Uuid>,
Json(body): Json<ApplyRequest>,
) -> Result<Json<LevelUpProposal>, ApiError> {
crate::level_up::apply(
&state.pool,
user.workspace_id,
user.user_id,
id,
&body.approved_item_ids,
)
.await
.map_err(|e| {
eprintln!("level_up::apply {id} failed: {e}");
ApiError::Internal
})?;
let p = cm_db::repo::level_up::get(&state.pool, id, user.workspace_id.as_uuid())
.await?
.ok_or(ApiError::NotFound)?;
Ok(Json(p))
}
pub async fn reject_proposal(
State(state): State<AppState>,
Authed(user): Authed,
Path(id): Path<Uuid>,
) -> Result<Json<LevelUpProposal>, ApiError> {
cm_db::repo::level_up::mark_rejected(
&state.pool,
id,
user.workspace_id.as_uuid(),
user.user_id.as_uuid(),
)
.await?;
let p = cm_db::repo::level_up::get(&state.pool, id, user.workspace_id.as_uuid())
.await?
.ok_or(ApiError::NotFound)?;
Ok(Json(p))
}