claws: PATCH /api/claws/{id}/model — swap the runtime-bound model
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:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user