claws: PATCH /api/claws/{id}/model — swap the runtime-bound model
ci / gates (push) Successful in 5s
ci / frontend (push) Successful in 40s
ci / rust (push) Successful in 3m20s
ci / e2e (push) Skipped
ci / publish (push) Successful in 2m18s

Auto-provisioned agents (via the research + loops team wizard) show
up on the Agents page (same agents::insert → agents::roster path),
and PATCH /api/claws/{id} already handled name/job_title/system_prompt/
avatar/accent/wallpaper. The one thing that was NOT reachable from the
Agents page: swapping the model.

Add a dedicated endpoint:
- PATCH /api/claws/{id}/model { model: string }
- Persists via agents::set_model_binding (DB)
- Best-effort runtime rebind via RuntimeProvisioner::provision_claw
  (idempotent — overwrites agents.<alias>.model_provider on the
  shared ZeroClaw config)
- Audit-logged as 'agent.model_changed' with the new model in payload

Now an operator can open the Agents page, click a claw that was
auto-provisioned by the team wizard, and swap its model
(claude-sonnet-5 → glm-5.2 → whatever) without recreating the team.
Same DB row, same claw_id, same brain — just a new provider on the
next turn.

Frontend affordance not shipped in this commit — the endpoint is
usable via curl/psql/scripts today; a UI 'Model' picker on the claw
detail card can land in the next Agents-page pass.
This commit is contained in:
Omar Sobh
2026-07-17 19:26:27 -07:00
parent ac8c689f50
commit 6e37802f73
2 changed files with 56 additions and 0 deletions
+55
View File
@@ -957,6 +957,61 @@ pub async fn patch(
Ok(Json(updated))
}
#[derive(Deserialize)]
pub struct SetModelRequest {
/// Model selector (claude / glm / glm-5.2 / kimi / gemini / groq /
/// specific model id like `claude-sonnet-5`). Resolved through the
/// same RuntimeProvisioner::provider_alias_for that team creation
/// uses, so shorthand + fully-qualified ids both work.
pub model: String,
}
/// `PATCH /api/claws/{id}/model` — swap the model bound to a claw
/// (both the DB row + the live ZeroClaw runtime agent). Idempotent:
/// re-runs safely if the runtime agent was previously deprovisioned.
///
/// This is the missing piece that lets an operator open the Agents
/// page, click a claw that was auto-provisioned via the research or
/// loops team wizard, and swap its model without going through the
/// team-recreation flow. Complements `PATCH /api/claws/:id` (profile
/// fields) which was already there.
pub async fn set_model(
State(state): State<AppState>,
Authed(user): Authed,
Path(id): Path<AgentId>,
Json(body): Json<SetModelRequest>,
) -> Result<Json<Agent>, ApiError> {
let agent = workspace_agent(&state, &user, id).await?;
let model = body.model.trim();
if model.is_empty() {
return Err(ApiError::BadRequest);
}
// 1. Persist the DB binding first — the runtime provision is
// idempotent and non-critical for read-only Agents-page display.
cm_db::repo::agents::set_model_binding(&state.pool, id, model).await?;
// 2. Best-effort runtime rebind so live sessions pick up the new
// model on their next turn. provision_claw overwrites
// agents.<alias>.model_provider on the shared ZeroClaw config.
if let Some(provisioner) = crate::runtime_provision::RuntimeProvisioner::from_env() {
if let Err(e) = provisioner.provision_claw(id.as_uuid(), model).await {
eprintln!("set_model({id}): runtime rebind failed: {e}");
}
}
cm_db::repo::audit::append(
&state.pool,
user.workspace_id,
Actor::User(user.user_id),
"agent.model_changed",
"agent",
&id.to_string(),
json!({ "model": model }),
)
.await?;
Ok(Json(agent))
}
/// DELETE /api/claws/{id} — destructive (§7.7): workspace owners or the
/// claw's manager only. Soft delete keeps rows for audit.
pub async fn delete(