Skip to content

buzz-acp model switch never matches claude-agent-acp config options: expects configId, adapter sends id #2794

Description

@chillerno1

Summary

resolve_model_switch_method() in crates/buzz-acp/src/acp.rs reads the configId key from each session/new configOptions entry and skips any entry that lacks it. The claude-agent-acp adapter (v0.61.0) keys those entries with id, not configId. Every model-category entry is therefore skipped, the desired model never matches, and the session silently falls back to the CLI default from the user's ~/.claude/settings.json.

Reproduction

Buzz Desktop 0.4.24, macOS arm64, claude-agent-acp 0.61.0, Claude Code CLI 2.1.219.

  1. Set a Claude Code agent's model to Opus in Buzz Desktop (per-agent or global default).
  2. Restart the agent. buzz-acp is launched with BUZZ_ACP_MODEL=opus[1m] as expected.
  3. Ask the agent what model it is running. It reports whatever ~/.claude/settings.json pins (in my case claude-fable-5[1m]), not Opus.

Harness log shows the fallback:

WARN pool::model: desired model opus[1m] not found in agent's available models - proceeding with agent default

The pool::model tracing target is not written to the per-agent log files, so nothing user-visible records the miss.

Root cause

The adapter's actual session/new response (captured with a stdio JSON-RPC probe against the bundled adapter):

{
  "configOptions": [{
    "id": "model",
    "name": "Model",
    "category": "model",
    "type": "select",
    "currentValue": "claude-fable-5[1m]",
    "options": [
      { "value": "default" },
      { "value": "opus[1m]" },
      { "value": "claude-fable-5[1m]" },
      { "value": "sonnet" },
      { "value": "haiku" }
    ]
  }],
  "models": null
}

Note the key is id. But resolve_model_switch_method() does:

let config_id = match config_opt.get("configId").and_then(|v| v.as_str()) {
    Some(id) => id,
    None => continue,   // claude-agent-acp entries always take this branch
};

So opus[1m] is a valid option value sitting right there in the response, and the exact-match loop never sees it. The unstable models path cannot rescue it either because the adapter returns models: null.

The unit tests all use configId-keyed fixtures, which is why they pass despite the drift.

The set side is unaffected: the ACP SDK's SetSessionConfigOptionRequest schema takes configId as the request param, and the adapter's setSessionConfigOption handler looks entries up by params.configId against o.id. Only the read side in buzz-acp is wrong.

Relationship to existing issues and PRs

Fix

Accept both spellings when extracting the config id:

config_opt.get("configId").or_else(|| config_opt.get("id")).and_then(|v| v.as_str())

PR incoming with this change plus a regression test using an id-keyed payload that mirrors the real adapter response.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions