From 8f0fc59a48fc14834caf2eed044cd116e60fb5ca Mon Sep 17 00:00:00 2001 From: Bennet Fenner Date: Wed, 15 Oct 2025 12:21:30 +0200 Subject: [PATCH] Add model selection support (#93) This part of ACP is not yet stabilized, so marking as Draft for now --------- Co-authored-by: Ben Brandt Co-authored-by: Smit Barmase --- CHANGELOG.md | 1 + package-lock.json | 8 ++++---- package.json | 2 +- src/acp-agent.ts | 35 +++++++++++++++++++++++++++++++++++ src/tests/acp-agent.test.ts | 11 ++++++++++- 5 files changed, 51 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12e72448..c15b28fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## 0.5.6 +- Provide a model selector. We use the "default" model by default, and the user can change it via the client. - Make sure writes require permissions when necessary: https://github.com/zed-industries/claude-code-acp/pull/92 - Add support for appending or overriding the system prompt: https://github.com/zed-industries/claude-code-acp/pull/91 - Update to @anthropic-ai/claude-agent-sdk@v0.1.15 diff --git a/package-lock.json b/package-lock.json index 0673986b..8589445c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.5.6", "license": "Apache-2.0", "dependencies": { - "@agentclientprotocol/sdk": "0.4.7", + "@agentclientprotocol/sdk": "0.4.8", "@anthropic-ai/claude-agent-sdk": "0.1.15", "@anthropic-ai/claude-code": "2.0.15", "@modelcontextprotocol/sdk": "1.20.0", @@ -40,9 +40,9 @@ } }, "node_modules/@agentclientprotocol/sdk": { - "version": "0.4.7", - "resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-0.4.7.tgz", - "integrity": "sha512-tCKNzsHs4TnpCKPjR1eU9qK6LorYs2bxff3bUUREaUgX5JpeH2YpYn0QSNo5qTTMKCKKs55HdjH+/my/ejsZBQ==", + "version": "0.4.8", + "resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-0.4.8.tgz", + "integrity": "sha512-2hN2CMMWtYVHrUeuapwLu66S2wwOPhq+bLqAryl1cQjKjL5Mzkfq+oxFYFvGdPvBoKpJSxM2pIIQiUH8tGZ3rg==", "license": "Apache-2.0", "dependencies": { "zod": "^3.0.0" diff --git a/package.json b/package.json index 4a0ed2f8..6b95297e 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "author": "Zed Industries", "license": "Apache-2.0", "dependencies": { - "@agentclientprotocol/sdk": "0.4.7", + "@agentclientprotocol/sdk": "0.4.8", "@anthropic-ai/claude-agent-sdk": "0.1.15", "@anthropic-ai/claude-code": "2.0.15", "@modelcontextprotocol/sdk": "1.20.0", diff --git a/src/acp-agent.ts b/src/acp-agent.ts index 12b9cbdb..c902dc24 100644 --- a/src/acp-agent.ts +++ b/src/acp-agent.ts @@ -15,6 +15,9 @@ import { ReadTextFileRequest, ReadTextFileResponse, RequestError, + SessionModelState, + SetSessionModelRequest, + SetSessionModelResponse, SetSessionModeRequest, SetSessionModeResponse, TerminalHandle, @@ -74,6 +77,8 @@ type ToolUseCache = { [key: string]: { type: "tool_use"; id: string; name: string; input: any }; }; +const DEFAULT_MODEL_ID = "default"; + // Implement the ACP Agent interface export class ClaudeAcpAgent implements Agent { sessions: { @@ -240,8 +245,11 @@ export class ClaudeAcpAgent implements Agent { }); }); + const models = await getAvailableModels(q); + return { sessionId, + models, modes: { currentModeId: "default", availableModes: [ @@ -388,6 +396,13 @@ export class ClaudeAcpAgent implements Agent { await this.sessions[params.sessionId].query.interrupt(); } + async setSessionModel(params: SetSessionModelRequest): Promise { + if (!this.sessions[params.sessionId]) { + throw new Error("Session not found"); + } + await this.sessions[params.sessionId].query.setModel(params.modelId); + } + async setSessionMode(params: SetSessionModeRequest): Promise { if (!this.sessions[params.sessionId]) { throw new Error("Session not found"); @@ -428,6 +443,26 @@ export class ClaudeAcpAgent implements Agent { } } +async function getAvailableModels(query: Query): Promise { + const models = await query.supportedModels(); + + // Query doesn't give us access to the currently selected model, so we default to "default" if it is there, + // otherwise we just choose the first model in the list. + const currentModel = models.find((model) => model.value === DEFAULT_MODEL_ID) || models[0]; + await query.setModel(currentModel.value); + + const availableModels = models.map((model) => ({ + modelId: model.value, + name: model.displayName, + description: model.description, + })); + + return { + availableModels, + currentModelId: currentModel.value, + }; +} + async function getAvailableSlashCommands(query: Query): Promise { const UNSUPPORTED_COMMANDS = [ "add-dir", diff --git a/src/tests/acp-agent.test.ts b/src/tests/acp-agent.test.ts index 25f10225..02146142 100644 --- a/src/tests/acp-agent.test.ts +++ b/src/tests/acp-agent.test.ts @@ -18,7 +18,7 @@ import { import { nodeToWebWritable, nodeToWebReadable } from "../utils.js"; import { markdownEscape, toolInfoFromToolUse, toolUpdateFromToolResult } from "../tools.js"; import { toAcpNotifications } from "../acp-agent.js"; -import { SDKAssistantMessage } from "@anthropic-ai/claude-agent-sdk"; +import { query, SDKAssistantMessage } from "@anthropic-ai/claude-agent-sdk"; describe.skipIf(!process.env.RUN_INTEGRATION_TESTS)("ACP subprocess integration", () => { let child: ReturnType; @@ -780,3 +780,12 @@ describe("escape markdown", () => { expect(escaped).toEqual("````\nfor example:\n```markdown\nHello *world*!\n```\n````"); }); }); + +describe("SDK behavior", () => { + it("query has a 'default' model", async () => { + const q = query({ prompt: "hi" }); + const models = await q.supportedModels(); + const defaultModel = models.find((m) => m.value === "default"); + expect(defaultModel).toBeDefined(); + }); +});