From e003191df4a3bc990b77ce847f4a536e720246a7 Mon Sep 17 00:00:00 2001 From: chillerno1 Date: Sat, 25 Jul 2026 10:10:24 +1000 Subject: [PATCH] fix(buzz-acp): accept id-keyed config options when resolving model switch resolve_model_switch_method only read the configId key from session/new configOptions entries. claude-agent-acp keys its entries with id, so every entry was skipped, the desired model was never matched, and sessions fell back to the CLI default model. Accept either spelling and cover the id-keyed shape with a test mirroring the real adapter response. Co-Authored-By: Claude Fable 5 Signed-off-by: chillerno1 --- crates/buzz-acp/src/acp.rs | 42 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/crates/buzz-acp/src/acp.rs b/crates/buzz-acp/src/acp.rs index 78db7ff718..3423ad02a7 100644 --- a/crates/buzz-acp/src/acp.rs +++ b/crates/buzz-acp/src/acp.rs @@ -1846,7 +1846,8 @@ pub enum ModelSwitchMethod { /// Extract `configOptions` entries with `category == "model"` from a `session/new` result. /// -/// Returns the raw JSON array entries. Each entry has `configId`, `displayName`, +/// Returns the raw JSON array entries. Each entry has `configId` (spelled `id` +/// by some adapters, e.g. claude-agent-acp), `displayName`, /// `options: [{ value, displayName }]`, etc. pub fn extract_model_config_options(result: &serde_json::Value) -> Vec { result["configOptions"] @@ -1880,7 +1881,14 @@ pub fn resolve_model_switch_method( // 1. Search stable configOptions for a "model"-category entry whose // options contain a value matching desired_model. for config_opt in extract_model_config_options(session_new_result) { - let config_id = match config_opt.get("configId").and_then(|v| v.as_str()) { + // Adapters disagree on the key: the ACP spec says `configId`, but + // claude-agent-acp emits `id`. Accept both; the set request always + // uses `configId` on the wire. + let config_id = match config_opt + .get("configId") + .or_else(|| config_opt.get("id")) + .and_then(|v| v.as_str()) + { Some(id) => id, None => continue, }; @@ -2456,6 +2464,36 @@ mod tests { ); } + #[test] + fn resolve_accepts_id_keyed_config_options() { + // claude-agent-acp (observed on v0.61.0) keys config options with + // `id` instead of the spec's `configId`. Payload mirrors its real + // `session/new` response. + let result = serde_json::json!({ + "configOptions": [{ + "id": "model", + "name": "Model", + "category": "model", + "type": "select", + "currentValue": "default", + "options": [ + { "value": "default", "name": "Default" }, + { "value": "opus[1m]", "name": "Opus" }, + { "value": "sonnet", "name": "Sonnet" } + ] + }], + "models": null + }); + let method = super::resolve_model_switch_method(&result, "opus[1m]"); + assert_eq!( + method, + Some(super::ModelSwitchMethod::ConfigOption { + config_id: "model".to_string(), + option_value: "opus[1m]".to_string(), + }) + ); + } + #[test] fn resolve_falls_back_to_unstable() { let result = serde_json::json!({