mirror of
https://github.com/agentclientprotocol/claude-agent-acp.git
synced 2026-10-28 13:01:20 +00:00
feat: Support effort levels (#464)
This commit is contained in:
131
src/acp-agent.ts
131
src/acp-agent.ts
@@ -53,6 +53,7 @@ import {
|
||||
PermissionMode,
|
||||
Query,
|
||||
query,
|
||||
Settings,
|
||||
SDKPartialAssistantMessage,
|
||||
SDKUserMessage,
|
||||
SlashCommand,
|
||||
@@ -135,6 +136,7 @@ type Session = {
|
||||
accumulatedUsage: AccumulatedUsage;
|
||||
modes: SessionModeState;
|
||||
models: SessionModelState;
|
||||
modelInfos: ModelInfo[];
|
||||
configOptions: SessionConfigOption[];
|
||||
promptRunning: boolean;
|
||||
pendingMessages: Map<string, { resolve: (cancelled: boolean) => void; order: number }>;
|
||||
@@ -1088,11 +1090,17 @@ export class ClaudeAcpAgent implements Agent {
|
||||
async unstable_setSessionModel(
|
||||
params: SetSessionModelRequest,
|
||||
): Promise<SetSessionModelResponse | void> {
|
||||
if (!this.sessions[params.sessionId]) {
|
||||
const session = this.sessions[params.sessionId];
|
||||
if (!session) {
|
||||
throw new Error("Session not found");
|
||||
}
|
||||
await this.sessions[params.sessionId].query.setModel(params.modelId);
|
||||
await this.updateConfigOption(params.sessionId, "model", params.modelId);
|
||||
// Resolve aliases (e.g. "opus", "opus[1m]") to canonical model IDs so
|
||||
// downstream lookups in modelInfos succeed and the effort option isn't
|
||||
// silently dropped.
|
||||
const resolved = resolveModelPreference(session.modelInfos, params.modelId);
|
||||
const modelId = resolved?.value ?? params.modelId;
|
||||
await session.query.setModel(modelId);
|
||||
await this.updateConfigOption(params.sessionId, "model", modelId);
|
||||
}
|
||||
|
||||
async setSessionMode(params: SetSessionModeRequest): Promise<SetSessionModeResponse> {
|
||||
@@ -1162,14 +1170,11 @@ export class ClaudeAcpAgent implements Agent {
|
||||
} else if (params.configId === "model") {
|
||||
await this.sessions[params.sessionId].query.setModel(resolvedValue);
|
||||
}
|
||||
// Effort SDK sync is handled inside applyConfigOptionValue so that direct
|
||||
// effort changes and effort changes induced by a model switch go through
|
||||
// the same path.
|
||||
|
||||
this.syncSessionConfigState(session, params.configId, params.value);
|
||||
|
||||
session.configOptions = session.configOptions.map((o) =>
|
||||
o.id === params.configId && typeof o.currentValue === "string"
|
||||
? { ...o, currentValue: resolvedValue }
|
||||
: o,
|
||||
);
|
||||
await this.applyConfigOptionValue(session, params.configId, resolvedValue);
|
||||
|
||||
return { configOptions: session.configOptions };
|
||||
}
|
||||
@@ -1401,11 +1406,7 @@ export class ClaudeAcpAgent implements Agent {
|
||||
const session = this.sessions[sessionId];
|
||||
if (!session) return;
|
||||
|
||||
this.syncSessionConfigState(session, configId, value);
|
||||
|
||||
session.configOptions = session.configOptions.map((o) =>
|
||||
o.id === configId && typeof o.currentValue === "string" ? { ...o, currentValue: value } : o,
|
||||
);
|
||||
await this.applyConfigOptionValue(session, configId, value);
|
||||
|
||||
await this.client.sessionUpdate({
|
||||
sessionId,
|
||||
@@ -1416,7 +1417,12 @@ export class ClaudeAcpAgent implements Agent {
|
||||
});
|
||||
}
|
||||
|
||||
private syncSessionConfigState(session: Session, configId: string, value: string): void {
|
||||
private async applyConfigOptionValue(
|
||||
session: Session,
|
||||
configId: string,
|
||||
value: string,
|
||||
): Promise<void> {
|
||||
// Sync top-level session state
|
||||
if (configId === "mode") {
|
||||
session.modes = { ...session.modes, currentModeId: value };
|
||||
} else if (configId === "model") {
|
||||
@@ -1429,6 +1435,39 @@ export class ClaudeAcpAgent implements Agent {
|
||||
}
|
||||
session.models = { ...session.models, currentModelId: value };
|
||||
}
|
||||
|
||||
// Update configOptions
|
||||
if (configId === "model") {
|
||||
// Rebuild config options since effort levels depend on the selected model
|
||||
const effortOpt = session.configOptions.find((o) => o.id === "effort");
|
||||
const currentEffort =
|
||||
typeof effortOpt?.currentValue === "string" ? effortOpt.currentValue : undefined;
|
||||
session.configOptions = buildConfigOptions(
|
||||
session.modes,
|
||||
session.models,
|
||||
session.modelInfos,
|
||||
currentEffort,
|
||||
);
|
||||
|
||||
// Sync effort with the SDK if it changed after the model switch
|
||||
const newEffortOpt = session.configOptions.find((o) => o.id === "effort");
|
||||
const newEffort =
|
||||
typeof newEffortOpt?.currentValue === "string" ? newEffortOpt.currentValue : undefined;
|
||||
if (newEffort !== currentEffort) {
|
||||
await session.query.applyFlagSettings({
|
||||
effortLevel: newEffort as Settings["effortLevel"],
|
||||
});
|
||||
}
|
||||
} else {
|
||||
session.configOptions = session.configOptions.map((o) =>
|
||||
o.id === configId && typeof o.currentValue === "string" ? { ...o, currentValue: value } : o,
|
||||
);
|
||||
if (configId === "effort") {
|
||||
await session.query.applyFlagSettings({
|
||||
effortLevel: value as Settings["effortLevel"],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async getOrCreateSession(params: {
|
||||
@@ -1705,7 +1744,20 @@ export class ClaudeAcpAgent implements Agent {
|
||||
availableModes,
|
||||
};
|
||||
|
||||
const configOptions = buildConfigOptions(modes, models);
|
||||
const configOptions = buildConfigOptions(
|
||||
modes,
|
||||
models,
|
||||
initializationResult.models,
|
||||
settingsManager.getSettings().effortLevel,
|
||||
);
|
||||
|
||||
// Apply the initial effort level to the SDK so it matches the UI default
|
||||
const initialEffort = configOptions.find((o) => o.id === "effort");
|
||||
if (initialEffort && typeof initialEffort.currentValue === "string") {
|
||||
await q.applyFlagSettings({
|
||||
effortLevel: initialEffort.currentValue as Settings["effortLevel"],
|
||||
});
|
||||
}
|
||||
|
||||
this.sessions[sessionId] = {
|
||||
query: q,
|
||||
@@ -1722,6 +1774,7 @@ export class ClaudeAcpAgent implements Agent {
|
||||
},
|
||||
modes,
|
||||
models,
|
||||
modelInfos: initializationResult.models,
|
||||
configOptions,
|
||||
promptRunning: false,
|
||||
pendingMessages: new Map(),
|
||||
@@ -1817,8 +1870,10 @@ function createEnvForGateway(gatewayMeta?: GatewayAuthMeta) {
|
||||
function buildConfigOptions(
|
||||
modes: SessionModeState,
|
||||
models: SessionModelState,
|
||||
modelInfos: ModelInfo[],
|
||||
currentEffortLevel?: string,
|
||||
): SessionConfigOption[] {
|
||||
return [
|
||||
const options: SessionConfigOption[] = [
|
||||
{
|
||||
id: "mode",
|
||||
name: "Mode",
|
||||
@@ -1846,6 +1901,46 @@ function buildConfigOptions(
|
||||
})),
|
||||
},
|
||||
];
|
||||
|
||||
// Add effort level option based on the currently selected model
|
||||
const currentModelInfo = modelInfos.find((m) => m.value === models.currentModelId);
|
||||
const supportedLevels = currentModelInfo?.supportsEffort
|
||||
? (currentModelInfo.supportedEffortLevels ?? [])
|
||||
: [];
|
||||
|
||||
if (supportedLevels.length > 0) {
|
||||
const effortOptions = supportedLevels.map((level) => ({
|
||||
value: level,
|
||||
name: level
|
||||
.split(/[_-]/)
|
||||
.map((part) => (part ? part.charAt(0).toUpperCase() + part.slice(1) : part))
|
||||
.join(" "),
|
||||
}));
|
||||
|
||||
// Keep the current level if valid, otherwise prefer xhigh (Claude Code's
|
||||
// recommended default for capable models), then high (the API default).
|
||||
const includes = (l: string) => (supportedLevels as string[]).includes(l);
|
||||
const validEffort =
|
||||
currentEffortLevel && includes(currentEffortLevel)
|
||||
? currentEffortLevel
|
||||
: includes("xhigh")
|
||||
? "xhigh"
|
||||
: includes("high")
|
||||
? "high"
|
||||
: supportedLevels[0];
|
||||
|
||||
options.push({
|
||||
id: "effort",
|
||||
name: "Effort",
|
||||
description: "Available effort levels for this model",
|
||||
category: "effort",
|
||||
type: "select",
|
||||
currentValue: validEffort,
|
||||
options: effortOptions,
|
||||
});
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
// Claude Code CLI persists display strings like "opus[1m]" in settings,
|
||||
|
||||
@@ -22,6 +22,7 @@ export interface ClaudeCodeSettings {
|
||||
permissions?: PermissionSettings;
|
||||
env?: Record<string, string>;
|
||||
model?: string;
|
||||
effortLevel?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -188,6 +189,10 @@ export class SettingsManager {
|
||||
merged.model = settings.model;
|
||||
}
|
||||
|
||||
if (settings.effortLevel !== undefined) {
|
||||
merged.effortLevel = settings.effortLevel;
|
||||
}
|
||||
|
||||
if (settings.permissions?.defaultMode !== undefined) {
|
||||
merged.permissions = {
|
||||
...merged.permissions,
|
||||
|
||||
@@ -1336,6 +1336,7 @@ describe("stop reason propagation", () => {
|
||||
currentModelId: "default",
|
||||
availableModels: [],
|
||||
},
|
||||
modelInfos: [],
|
||||
settingsManager: { dispose: vi.fn() } as any,
|
||||
accumulatedUsage: {
|
||||
inputTokens: 0,
|
||||
@@ -1478,6 +1479,7 @@ describe("stop reason propagation", () => {
|
||||
currentModelId: "default",
|
||||
availableModels: [],
|
||||
},
|
||||
modelInfos: [],
|
||||
settingsManager: { dispose: vi.fn() } as any,
|
||||
accumulatedUsage: {
|
||||
inputTokens: 0,
|
||||
@@ -1554,6 +1556,7 @@ describe("session/close", () => {
|
||||
currentModelId: "default",
|
||||
availableModels: [],
|
||||
},
|
||||
modelInfos: [],
|
||||
settingsManager: { dispose: vi.fn() } as any,
|
||||
accumulatedUsage: {
|
||||
inputTokens: 0,
|
||||
@@ -1649,6 +1652,7 @@ describe("getOrCreateSession param change detection", () => {
|
||||
}),
|
||||
modes: { currentModeId: "default", availableModes: [] },
|
||||
models: { currentModelId: "default", availableModels: [] },
|
||||
modelInfos: [],
|
||||
settingsManager: { dispose: vi.fn() } as any,
|
||||
accumulatedUsage: {
|
||||
inputTokens: 0,
|
||||
@@ -1882,6 +1886,7 @@ describe("usage_update computation", () => {
|
||||
currentModelId: "default",
|
||||
availableModels: [],
|
||||
},
|
||||
modelInfos: [],
|
||||
settingsManager: {} as any,
|
||||
accumulatedUsage: {
|
||||
inputTokens: 0,
|
||||
@@ -2235,7 +2240,7 @@ describe("usage_update computation", () => {
|
||||
const session = agent.sessions["test-session"];
|
||||
expect(session.contextWindowSize).toBe(200000);
|
||||
|
||||
(agent as any).syncSessionConfigState(session, "model", "claude-opus-4-6-1m");
|
||||
await (agent as any).applyConfigOptionValue(session, "model", "claude-opus-4-6-1m");
|
||||
expect(session.contextWindowSize).toBe(1000000);
|
||||
|
||||
await agent.prompt({ sessionId: "test-session", prompt: [{ type: "text", text: "test" }] });
|
||||
@@ -2286,7 +2291,7 @@ describe("usage_update computation", () => {
|
||||
it("switching the session's model invalidates the learned context window", async () => {
|
||||
// When the user switches models mid-session, the window learned for the
|
||||
// previous model would otherwise persist into the next prompt's first
|
||||
// mid-stream update. syncSessionConfigState should reset it so the next
|
||||
// mid-stream update. applyConfigOptionValue should reset it so the next
|
||||
// turn's first update falls back to the heuristic (here: 200k default).
|
||||
const { agent, updates } = createMockAgentWithCapture();
|
||||
injectSession(agent, [
|
||||
@@ -2320,7 +2325,7 @@ describe("usage_update computation", () => {
|
||||
session.models = { ...session.models, currentModelId: "claude-opus-4-6-1m" };
|
||||
|
||||
// User flips the selector to a 200k model.
|
||||
(agent as any).syncSessionConfigState(session, "model", "claude-sonnet-4-6");
|
||||
await (agent as any).applyConfigOptionValue(session, "model", "claude-sonnet-4-6");
|
||||
|
||||
await agent.prompt({ sessionId: "test-session", prompt: [{ type: "text", text: "test" }] });
|
||||
|
||||
@@ -2770,6 +2775,7 @@ describe("emitRawSDKMessages", () => {
|
||||
sessionFingerprint: JSON.stringify({ cwd: "/test", mcpServers: [] }),
|
||||
modes: { currentModeId: "default", availableModes: [] },
|
||||
models: { currentModelId: "default", availableModels: [] },
|
||||
modelInfos: [],
|
||||
settingsManager: { dispose: vi.fn() } as any,
|
||||
accumulatedUsage: {
|
||||
inputTokens: 0,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||
import { AgentSideConnection, SessionNotification } from "@agentclientprotocol/sdk";
|
||||
import type { ModelInfo } from "@anthropic-ai/claude-agent-sdk";
|
||||
import type { ClaudeAcpAgent as ClaudeAcpAgentType } from "../acp-agent.js";
|
||||
|
||||
const { registerHookCallbackSpy } = vi.hoisted(() => ({
|
||||
@@ -58,6 +59,19 @@ const MOCK_CONFIG_OPTIONS = [
|
||||
description: m.description,
|
||||
})),
|
||||
},
|
||||
{
|
||||
id: "effort",
|
||||
name: "Effort",
|
||||
description: "Available effort levels for this model",
|
||||
type: "select",
|
||||
category: "effort",
|
||||
currentValue: "high",
|
||||
options: [
|
||||
{ value: "low", name: "Low" },
|
||||
{ value: "medium", name: "Medium" },
|
||||
{ value: "high", name: "High" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
describe("session config options", () => {
|
||||
@@ -67,6 +81,7 @@ describe("session config options", () => {
|
||||
let createSessionSpy: ReturnType<typeof vi.fn>;
|
||||
let setPermissionModeSpy: ReturnType<typeof vi.fn>;
|
||||
let setModelSpy: ReturnType<typeof vi.fn>;
|
||||
let applyFlagSettingsSpy: ReturnType<typeof vi.fn>;
|
||||
|
||||
function createMockClient(): AgentSideConnection {
|
||||
return {
|
||||
@@ -82,11 +97,13 @@ describe("session config options", () => {
|
||||
function populateSession() {
|
||||
setPermissionModeSpy = vi.fn();
|
||||
setModelSpy = vi.fn();
|
||||
applyFlagSettingsSpy = vi.fn();
|
||||
|
||||
(agent as unknown as { sessions: Record<string, unknown> }).sessions[SESSION_ID] = {
|
||||
query: {
|
||||
setPermissionMode: setPermissionModeSpy,
|
||||
setModel: setModelSpy,
|
||||
applyFlagSettings: applyFlagSettingsSpy,
|
||||
supportedCommands: async () => [],
|
||||
},
|
||||
input: null,
|
||||
@@ -95,6 +112,15 @@ describe("session config options", () => {
|
||||
settingsManager: {},
|
||||
modes: structuredClone(MOCK_MODES),
|
||||
models: structuredClone(MOCK_MODELS),
|
||||
modelInfos: MOCK_MODELS.availableModels.map(
|
||||
(m): ModelInfo => ({
|
||||
value: m.modelId,
|
||||
displayName: m.name,
|
||||
description: m.description,
|
||||
supportsEffort: true,
|
||||
supportedEffortLevels: ["low", "medium", "high"],
|
||||
}),
|
||||
),
|
||||
configOptions: structuredClone(MOCK_CONFIG_OPTIONS),
|
||||
contextWindowSize: 200000,
|
||||
};
|
||||
@@ -387,6 +413,101 @@ describe("session config options", () => {
|
||||
const modelOption = session.configOptions.find((o) => o.id === "model");
|
||||
expect(modelOption?.currentValue).toBe("claude-sonnet-4-5");
|
||||
});
|
||||
|
||||
it("includes updated effort in config_option_update when model drops effort support", async () => {
|
||||
const session = (agent as unknown as { sessions: Record<string, any> }).sessions[SESSION_ID];
|
||||
session.modelInfos = [
|
||||
{
|
||||
value: "claude-opus-4-5",
|
||||
displayName: "Claude Opus",
|
||||
description: "Most capable",
|
||||
supportsEffort: true,
|
||||
supportedEffortLevels: ["low", "medium", "high"],
|
||||
},
|
||||
{
|
||||
value: "claude-sonnet-4-5",
|
||||
displayName: "Claude Sonnet",
|
||||
description: "Balanced",
|
||||
supportsEffort: false,
|
||||
},
|
||||
];
|
||||
|
||||
await agent.unstable_setSessionModel({
|
||||
sessionId: SESSION_ID,
|
||||
modelId: "claude-sonnet-4-5",
|
||||
});
|
||||
|
||||
const configUpdate = sessionUpdates.find(
|
||||
(n) => n.update.sessionUpdate === "config_option_update",
|
||||
);
|
||||
expect(configUpdate).toBeDefined();
|
||||
const effortOption = (configUpdate?.update as any).configOptions.find(
|
||||
(o: any) => o.id === "effort",
|
||||
);
|
||||
expect(effortOption).toBeUndefined();
|
||||
expect(applyFlagSettingsSpy).toHaveBeenCalledWith({ effortLevel: undefined });
|
||||
});
|
||||
|
||||
it("clamps effort in config_option_update when new model has different supported levels", async () => {
|
||||
// Set current effort to "max" which the new model won't support
|
||||
const session = (agent as unknown as { sessions: Record<string, any> }).sessions[SESSION_ID];
|
||||
const effortOpt = session.configOptions.find((o: any) => o.id === "effort");
|
||||
if (effortOpt) effortOpt.currentValue = "max";
|
||||
|
||||
session.modelInfos = [
|
||||
{
|
||||
value: "claude-opus-4-5",
|
||||
displayName: "Claude Opus",
|
||||
description: "Most capable",
|
||||
supportsEffort: true,
|
||||
supportedEffortLevels: ["low", "medium", "high", "max"],
|
||||
},
|
||||
{
|
||||
value: "claude-sonnet-4-5",
|
||||
displayName: "Claude Sonnet",
|
||||
description: "Balanced",
|
||||
supportsEffort: true,
|
||||
supportedEffortLevels: ["low", "medium", "high"],
|
||||
},
|
||||
];
|
||||
|
||||
await agent.unstable_setSessionModel({
|
||||
sessionId: SESSION_ID,
|
||||
modelId: "claude-sonnet-4-5",
|
||||
});
|
||||
|
||||
const configUpdate = sessionUpdates.find(
|
||||
(n) => n.update.sessionUpdate === "config_option_update",
|
||||
);
|
||||
const effortOption = (configUpdate?.update as any).configOptions.find(
|
||||
(o: any) => o.id === "effort",
|
||||
);
|
||||
expect(effortOption).toBeDefined();
|
||||
expect(effortOption.currentValue).toBe("high");
|
||||
expect(applyFlagSettingsSpy).toHaveBeenCalledWith({ effortLevel: "high" });
|
||||
});
|
||||
|
||||
it("preserves effort in config_option_update when new model supports same level", async () => {
|
||||
// Set effort to "low" first
|
||||
const session = (agent as unknown as { sessions: Record<string, any> }).sessions[SESSION_ID];
|
||||
const effortOpt = session.configOptions.find((o: any) => o.id === "effort");
|
||||
if (effortOpt) effortOpt.currentValue = "low";
|
||||
|
||||
await agent.unstable_setSessionModel({
|
||||
sessionId: SESSION_ID,
|
||||
modelId: "claude-sonnet-4-5",
|
||||
});
|
||||
|
||||
const configUpdate = sessionUpdates.find(
|
||||
(n) => n.update.sessionUpdate === "config_option_update",
|
||||
);
|
||||
const effortOption = (configUpdate?.update as any).configOptions.find(
|
||||
(o: any) => o.id === "effort",
|
||||
);
|
||||
expect(effortOption?.currentValue).toBe("low");
|
||||
// Effort didn't change, so applyFlagSettings should NOT be called
|
||||
expect(applyFlagSettingsSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("no config_option_update notification when using setSessionConfigOption", () => {
|
||||
@@ -421,6 +542,223 @@ describe("session config options", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("setSessionConfigOption for effort", () => {
|
||||
beforeEach(() => {
|
||||
populateSession();
|
||||
});
|
||||
|
||||
it("calls applyFlagSettings with effortLevel", async () => {
|
||||
await agent.setSessionConfigOption({
|
||||
sessionId: SESSION_ID,
|
||||
configId: "effort",
|
||||
value: "low",
|
||||
});
|
||||
|
||||
expect(applyFlagSettingsSpy).toHaveBeenCalledWith({ effortLevel: "low" });
|
||||
});
|
||||
|
||||
it("updates effort currentValue in returned configOptions", async () => {
|
||||
const response = await agent.setSessionConfigOption({
|
||||
sessionId: SESSION_ID,
|
||||
configId: "effort",
|
||||
value: "medium",
|
||||
});
|
||||
|
||||
const effortOption = response.configOptions.find((o) => o.id === "effort");
|
||||
expect(effortOption?.currentValue).toBe("medium");
|
||||
});
|
||||
|
||||
it("throws for invalid effort value", async () => {
|
||||
await expect(
|
||||
agent.setSessionConfigOption({
|
||||
sessionId: SESSION_ID,
|
||||
configId: "effort",
|
||||
value: "turbo",
|
||||
}),
|
||||
).rejects.toThrow("Invalid value for config option effort: turbo");
|
||||
});
|
||||
|
||||
it("does not send config_option_update notification", async () => {
|
||||
await agent.setSessionConfigOption({
|
||||
sessionId: SESSION_ID,
|
||||
configId: "effort",
|
||||
value: "low",
|
||||
});
|
||||
|
||||
const configUpdates = sessionUpdates.filter(
|
||||
(n) => n.update.sessionUpdate === "config_option_update",
|
||||
);
|
||||
expect(configUpdates).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("other options are unchanged when effort is updated", async () => {
|
||||
const response = await agent.setSessionConfigOption({
|
||||
sessionId: SESSION_ID,
|
||||
configId: "effort",
|
||||
value: "low",
|
||||
});
|
||||
|
||||
const modeOption = response.configOptions.find((o) => o.id === "mode");
|
||||
expect(modeOption?.currentValue).toBe("default");
|
||||
const modelOption = response.configOptions.find((o) => o.id === "model");
|
||||
expect(modelOption?.currentValue).toBe("claude-opus-4-5");
|
||||
});
|
||||
});
|
||||
|
||||
describe("effort level and model switch interactions", () => {
|
||||
beforeEach(() => {
|
||||
populateSession();
|
||||
});
|
||||
|
||||
it("drops effort option when switching to a model without effort support", async () => {
|
||||
// Make sonnet not support effort
|
||||
const session = (agent as unknown as { sessions: Record<string, any> }).sessions[SESSION_ID];
|
||||
session.modelInfos = [
|
||||
{
|
||||
value: "claude-opus-4-5",
|
||||
displayName: "Claude Opus",
|
||||
description: "Most capable",
|
||||
supportsEffort: true,
|
||||
supportedEffortLevels: ["low", "medium", "high"],
|
||||
},
|
||||
{
|
||||
value: "claude-sonnet-4-5",
|
||||
displayName: "Claude Sonnet",
|
||||
description: "Balanced",
|
||||
supportsEffort: false,
|
||||
},
|
||||
];
|
||||
|
||||
const response = await agent.setSessionConfigOption({
|
||||
sessionId: SESSION_ID,
|
||||
configId: "model",
|
||||
value: "claude-sonnet-4-5",
|
||||
});
|
||||
|
||||
const effortOption = response.configOptions.find((o) => o.id === "effort");
|
||||
expect(effortOption).toBeUndefined();
|
||||
});
|
||||
|
||||
it("clears effort via applyFlagSettings when switching to a model without effort", async () => {
|
||||
const session = (agent as unknown as { sessions: Record<string, any> }).sessions[SESSION_ID];
|
||||
session.modelInfos = [
|
||||
{
|
||||
value: "claude-opus-4-5",
|
||||
displayName: "Claude Opus",
|
||||
description: "Most capable",
|
||||
supportsEffort: true,
|
||||
supportedEffortLevels: ["low", "medium", "high"],
|
||||
},
|
||||
{
|
||||
value: "claude-sonnet-4-5",
|
||||
displayName: "Claude Sonnet",
|
||||
description: "Balanced",
|
||||
supportsEffort: false,
|
||||
},
|
||||
];
|
||||
|
||||
await agent.setSessionConfigOption({
|
||||
sessionId: SESSION_ID,
|
||||
configId: "model",
|
||||
value: "claude-sonnet-4-5",
|
||||
});
|
||||
|
||||
expect(applyFlagSettingsSpy).toHaveBeenCalledWith({ effortLevel: undefined });
|
||||
});
|
||||
|
||||
it("adds effort option when switching to a model that supports effort", async () => {
|
||||
const session = (agent as unknown as { sessions: Record<string, any> }).sessions[SESSION_ID];
|
||||
// Start with sonnet (no effort) as current
|
||||
session.models = { ...session.models, currentModelId: "claude-sonnet-4-5" };
|
||||
session.modelInfos = [
|
||||
{
|
||||
value: "claude-opus-4-5",
|
||||
displayName: "Claude Opus",
|
||||
description: "Most capable",
|
||||
supportsEffort: true,
|
||||
supportedEffortLevels: ["low", "medium", "high"],
|
||||
},
|
||||
{
|
||||
value: "claude-sonnet-4-5",
|
||||
displayName: "Claude Sonnet",
|
||||
description: "Balanced",
|
||||
supportsEffort: false,
|
||||
},
|
||||
];
|
||||
// Remove effort from current config options
|
||||
session.configOptions = session.configOptions.filter((o: any) => o.id !== "effort");
|
||||
|
||||
const response = await agent.setSessionConfigOption({
|
||||
sessionId: SESSION_ID,
|
||||
configId: "model",
|
||||
value: "claude-opus-4-5",
|
||||
});
|
||||
|
||||
const effortOption = response.configOptions.find((o) => o.id === "effort");
|
||||
expect(effortOption).toBeDefined();
|
||||
// No previous effort, so defaults to "high" (the API default)
|
||||
expect(effortOption?.currentValue).toBe("high");
|
||||
});
|
||||
|
||||
it("clamps effort to valid value when new model has different supported levels", async () => {
|
||||
const session = (agent as unknown as { sessions: Record<string, any> }).sessions[SESSION_ID];
|
||||
// Set current effort to "max" (not supported by sonnet in our mock)
|
||||
const effortOpt = session.configOptions.find((o: any) => o.id === "effort");
|
||||
if (effortOpt) effortOpt.currentValue = "max";
|
||||
|
||||
session.modelInfos = [
|
||||
{
|
||||
value: "claude-opus-4-5",
|
||||
displayName: "Claude Opus",
|
||||
description: "Most capable",
|
||||
supportsEffort: true,
|
||||
supportedEffortLevels: ["low", "medium", "high", "max"],
|
||||
},
|
||||
{
|
||||
value: "claude-sonnet-4-5",
|
||||
displayName: "Claude Sonnet",
|
||||
description: "Balanced",
|
||||
supportsEffort: true,
|
||||
supportedEffortLevels: ["low", "medium", "high"],
|
||||
},
|
||||
];
|
||||
|
||||
const response = await agent.setSessionConfigOption({
|
||||
sessionId: SESSION_ID,
|
||||
configId: "model",
|
||||
value: "claude-sonnet-4-5",
|
||||
});
|
||||
|
||||
const effortOption = response.configOptions.find((o) => o.id === "effort");
|
||||
expect(effortOption).toBeDefined();
|
||||
// "max" is not in sonnet's levels, so should fall back to "high" (the API default)
|
||||
expect(effortOption?.currentValue).toBe("high");
|
||||
// SDK should be told about the clamped value
|
||||
expect(applyFlagSettingsSpy).toHaveBeenCalledWith({ effortLevel: "high" });
|
||||
});
|
||||
|
||||
it("preserves effort value when new model supports the same level", async () => {
|
||||
// Set effort to "low"
|
||||
await agent.setSessionConfigOption({
|
||||
sessionId: SESSION_ID,
|
||||
configId: "effort",
|
||||
value: "low",
|
||||
});
|
||||
|
||||
// Switch model — both support "low"
|
||||
const response = await agent.setSessionConfigOption({
|
||||
sessionId: SESSION_ID,
|
||||
configId: "model",
|
||||
value: "claude-sonnet-4-5",
|
||||
});
|
||||
|
||||
const effortOption = response.configOptions.find((o) => o.id === "effort");
|
||||
expect(effortOption?.currentValue).toBe("low");
|
||||
// applyFlagSettings was called once for the effort change, but not again for the model switch
|
||||
expect(applyFlagSettingsSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("bidirectional consistency", () => {
|
||||
beforeEach(() => {
|
||||
populateSession();
|
||||
|
||||
Reference in New Issue
Block a user