mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Allow turning thinking off (#2257)
* feat(providers): allow turning thinking off Expose Off only for models that support disabled thinking while preserving Low as the default. * fix(providers): reset unsupported thinking on model change Keep the selected thinking option only when the new model advertises it; otherwise use that model's default and persist the runtime change. * fix(providers): isolate custom model capabilities Use strict manifest identity when reconciling thinking options so provider-prefixed custom models cannot inherit capabilities they do not advertise. * fix(providers): clear thinking with default model * fix(providers): validate disabled thinking support * fix(providers): validate initial thinking config * refactor(providers): centralize thinking capability * fix(server): order config mutation events
This commit is contained in:
@@ -3103,6 +3103,53 @@ test("persists live mode, model, and thinking changes without an external snapsh
|
||||
expect(persisted?.runtimeInfo?.model).toBe("gpt-5.4");
|
||||
});
|
||||
|
||||
test("later explicit config mutations win over events emitted by earlier mutations", async () => {
|
||||
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-config-mutation-order-"));
|
||||
class ConfigMutationSession extends TestAgentSession {
|
||||
async setModel(): Promise<void> {
|
||||
this.pushEvent({
|
||||
type: "timeline",
|
||||
provider: "codex",
|
||||
item: { type: "assistant_message", text: "model changed" },
|
||||
});
|
||||
this.pushEvent({
|
||||
type: "thinking_option_changed",
|
||||
provider: "codex",
|
||||
thinkingOptionId: "low",
|
||||
});
|
||||
}
|
||||
|
||||
async setThinkingOption(): Promise<void> {}
|
||||
}
|
||||
class ConfigMutationClient extends TestAgentClient {
|
||||
override async createSession(config: AgentSessionConfig): Promise<AgentSession> {
|
||||
return new ConfigMutationSession(config);
|
||||
}
|
||||
}
|
||||
|
||||
const manager = new AgentManager({
|
||||
clients: { codex: new ConfigMutationClient() },
|
||||
logger,
|
||||
idFactory: () => "00000000-0000-4000-8000-000000000134",
|
||||
});
|
||||
const snapshot = await manager.createAgent(
|
||||
{
|
||||
provider: "codex",
|
||||
cwd: workdir,
|
||||
model: "gpt-5.2-codex",
|
||||
thinkingOptionId: "off",
|
||||
},
|
||||
undefined,
|
||||
{ workspaceId: undefined },
|
||||
);
|
||||
|
||||
await manager.setAgentModel(snapshot.id, "gpt-5.4");
|
||||
await manager.setAgentThinkingOption(snapshot.id, "high");
|
||||
await manager.flush();
|
||||
|
||||
expect(manager.getAgent(snapshot.id)?.config.thinkingOptionId).toBe("high");
|
||||
});
|
||||
|
||||
test("session config drift events update state through the stream channel", async () => {
|
||||
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-session-config-events-"));
|
||||
let capturedSession: TestAgentSession | null = null;
|
||||
@@ -3171,6 +3218,7 @@ test("session config drift events update state through the stream channel", asyn
|
||||
|
||||
const agent = manager.getAgent(snapshot.id);
|
||||
expect(agent?.currentModeId).toBe("build");
|
||||
expect(agent?.config.thinkingOptionId).toBe("high");
|
||||
expect(agent?.availableModes).toEqual([
|
||||
{ id: "plan", label: "Plan" },
|
||||
{ id: "build", label: "Build" },
|
||||
|
||||
@@ -1579,6 +1579,7 @@ export class AgentManager {
|
||||
async setAgentMode(agentId: string, modeId: string): Promise<AgentProviderNotice | null> {
|
||||
const agent = this.requireSessionAgent(agentId);
|
||||
const notice = (await agent.session.setMode(modeId)) ?? null;
|
||||
await this.drainSessionEvents(agentId);
|
||||
const currentMode = (await agent.session.getCurrentMode()) ?? modeId;
|
||||
agent.config.modeId = currentMode ?? undefined;
|
||||
agent.currentModeId = currentMode;
|
||||
@@ -1599,6 +1600,7 @@ export class AgentManager {
|
||||
if (agent.session.setModel) {
|
||||
await agent.session.setModel(normalizedModelId);
|
||||
}
|
||||
await this.drainSessionEvents(agentId);
|
||||
|
||||
agent.config.model = normalizedModelId ?? undefined;
|
||||
if (agent.runtimeInfo) {
|
||||
@@ -1622,6 +1624,7 @@ export class AgentManager {
|
||||
if (agent.session.setThinkingOption) {
|
||||
notice = (await agent.session.setThinkingOption(normalizedThinkingOptionId)) ?? null;
|
||||
}
|
||||
await this.drainSessionEvents(agentId);
|
||||
|
||||
agent.config.thinkingOptionId = normalizedThinkingOptionId ?? undefined;
|
||||
if (agent.runtimeInfo) {
|
||||
@@ -1643,6 +1646,7 @@ export class AgentManager {
|
||||
}
|
||||
|
||||
await agent.session.setFeature(featureId, value);
|
||||
await this.drainSessionEvents(agentId);
|
||||
agent.config.featureValues = { ...agent.config.featureValues, [featureId]: value };
|
||||
this.touchUpdatedAt(agent);
|
||||
this.emitState(agent);
|
||||
@@ -3009,6 +3013,24 @@ export class AgentManager {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Provider mutations may synchronously emit config events that are processed through the
|
||||
* asynchronous session queue. Apply those events before committing the mutation's explicit
|
||||
* manager state so call order remains authoritative.
|
||||
*/
|
||||
private async drainSessionEvents(agentId: string): Promise<void> {
|
||||
while (true) {
|
||||
const tail = this.sessionEventTails.get(agentId);
|
||||
if (!tail) {
|
||||
return;
|
||||
}
|
||||
await tail;
|
||||
if (this.sessionEventTails.get(agentId) === tail) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async dispatchSessionEvent(
|
||||
agent: ActiveManagedAgent,
|
||||
event: AgentStreamEvent,
|
||||
@@ -3431,6 +3453,7 @@ export class AgentManager {
|
||||
this.emitState(agent);
|
||||
return undefined;
|
||||
case "thinking_option_changed":
|
||||
agent.config.thinkingOptionId = event.thinkingOptionId ?? undefined;
|
||||
if (agent.runtimeInfo) {
|
||||
agent.runtimeInfo = {
|
||||
...agent.runtimeInfo,
|
||||
|
||||
@@ -583,8 +583,12 @@ describe("ClaudeAgentSession features", () => {
|
||||
};
|
||||
},
|
||||
};
|
||||
const queryFactory = vi.fn(() => queryMock);
|
||||
return { queryFactory, queryMock };
|
||||
const launches: Array<{ options: Record<string, unknown> }> = [];
|
||||
const queryFactory = vi.fn((input) => {
|
||||
launches.push(input);
|
||||
return queryMock;
|
||||
});
|
||||
return { queryFactory, queryMock, launches };
|
||||
}
|
||||
|
||||
test("lists fast mode only for supported Opus models", async () => {
|
||||
@@ -686,6 +690,96 @@ describe("ClaudeAgentSession features", () => {
|
||||
await session.close();
|
||||
});
|
||||
|
||||
test("turns Claude thinking off without retaining an effort level", async () => {
|
||||
const { queryFactory, launches } = createQueryMock();
|
||||
const client = new ClaudeAgentClient({
|
||||
logger,
|
||||
queryFactory,
|
||||
resolveBinary: async () => "/test/claude/bin",
|
||||
});
|
||||
const session = await client.createSession({
|
||||
provider: "claude",
|
||||
cwd: process.cwd(),
|
||||
model: "claude-sonnet-5",
|
||||
thinkingOptionId: "off",
|
||||
});
|
||||
|
||||
await expect(session.startTurn("hello")).resolves.toEqual({
|
||||
turnId: expect.stringMatching(/^foreground-turn-/),
|
||||
});
|
||||
|
||||
expect(launches[0]?.options.thinking).toEqual({ type: "disabled" });
|
||||
expect(launches[0]?.options).not.toHaveProperty("effort");
|
||||
|
||||
await session.close();
|
||||
});
|
||||
|
||||
test.each([
|
||||
["supported model", "claude-opus-4-8", { type: "disabled" }, undefined],
|
||||
["unsupported model", "claude-fable-5", { type: "adaptive" }, "low"],
|
||||
["custom model", "openrouter/anthropic/claude-opus-4-8", undefined, undefined],
|
||||
["provider default", null, undefined, undefined],
|
||||
])("reconciles Off when switching to a %s", async (_label, modelId, thinking, effort) => {
|
||||
const { queryFactory, launches } = createQueryMock();
|
||||
const client = new ClaudeAgentClient({
|
||||
logger,
|
||||
queryFactory,
|
||||
resolveBinary: async () => "/test/claude/bin",
|
||||
});
|
||||
const session = await client.createSession({
|
||||
provider: "claude",
|
||||
cwd: process.cwd(),
|
||||
model: "claude-sonnet-5",
|
||||
thinkingOptionId: "off",
|
||||
});
|
||||
|
||||
await session.setModel?.(modelId);
|
||||
await session.startTurn("hello");
|
||||
|
||||
expect(launches.at(-1)?.options.thinking).toEqual(thinking);
|
||||
expect(launches.at(-1)?.options.effort).toBe(effort);
|
||||
|
||||
await session.close();
|
||||
});
|
||||
|
||||
test("rejects disabled thinking when the active model does not support it", async () => {
|
||||
const { queryFactory } = createQueryMock();
|
||||
const client = new ClaudeAgentClient({
|
||||
logger,
|
||||
queryFactory,
|
||||
resolveBinary: async () => "/test/claude/bin",
|
||||
});
|
||||
const session = await client.createSession({
|
||||
provider: "claude",
|
||||
cwd: process.cwd(),
|
||||
model: "claude-fable-5",
|
||||
});
|
||||
|
||||
await expect(session.setThinkingOption?.("off")).rejects.toThrow(
|
||||
"Thinking option 'off' is not available for model 'claude-fable-5'",
|
||||
);
|
||||
|
||||
await session.close();
|
||||
});
|
||||
|
||||
test("rejects an initial disabled-thinking config for an unsupported model", async () => {
|
||||
const { queryFactory } = createQueryMock();
|
||||
const client = new ClaudeAgentClient({
|
||||
logger,
|
||||
queryFactory,
|
||||
resolveBinary: async () => "/test/claude/bin",
|
||||
});
|
||||
|
||||
await expect(
|
||||
client.createSession({
|
||||
provider: "claude",
|
||||
cwd: process.cwd(),
|
||||
model: "claude-fable-5",
|
||||
thinkingOptionId: "off",
|
||||
}),
|
||||
).rejects.toThrow("Thinking option 'off' is not available for model 'claude-fable-5'");
|
||||
});
|
||||
|
||||
test("returns a next-turn notice when changing Claude thinking during an active turn", async () => {
|
||||
const { queryFactory } = createQueryMock();
|
||||
const client = new ClaudeAgentClient({
|
||||
|
||||
@@ -34,7 +34,11 @@ import {
|
||||
getClaudeModelsWithSettings,
|
||||
normalizeClaudeRuntimeModelId,
|
||||
} from "./models.js";
|
||||
import { CLAUDE_ULTRACODE_THINKING_OPTION_ID } from "./model-manifest.js";
|
||||
import {
|
||||
CLAUDE_DISABLED_THINKING_OPTION_ID,
|
||||
CLAUDE_ULTRACODE_THINKING_OPTION_ID,
|
||||
resolveClaudeDisabledThinkingForModel,
|
||||
} from "./model-manifest.js";
|
||||
import { parsePartialJsonObject } from "./partial-json.js";
|
||||
import { ClaudeSidechainTracker } from "./sidechain-tracker.js";
|
||||
import { buildClaudeFeatures, claudeModelSupportsFastMode } from "./feature-definitions.js";
|
||||
@@ -375,7 +379,10 @@ interface ClaudeAgentSessionOptions {
|
||||
}
|
||||
|
||||
type ClaudeThinkingEffort = "low" | "medium" | "high" | "xhigh" | "max";
|
||||
type ClaudeThinkingOption = ClaudeThinkingEffort | typeof CLAUDE_ULTRACODE_THINKING_OPTION_ID;
|
||||
type ClaudeThinkingOption =
|
||||
| ClaudeThinkingEffort
|
||||
| typeof CLAUDE_DISABLED_THINKING_OPTION_ID
|
||||
| typeof CLAUDE_ULTRACODE_THINKING_OPTION_ID;
|
||||
|
||||
function resolvePathEnvKey(): "Path" | "PATH" | null {
|
||||
if (process.env["Path"] !== undefined) return "Path";
|
||||
@@ -423,7 +430,26 @@ function isClaudeThinkingEffort(value: string | null | undefined): value is Clau
|
||||
}
|
||||
|
||||
function isClaudeThinkingOption(value: string | null | undefined): value is ClaudeThinkingOption {
|
||||
return value === CLAUDE_ULTRACODE_THINKING_OPTION_ID || isClaudeThinkingEffort(value);
|
||||
return (
|
||||
value === CLAUDE_DISABLED_THINKING_OPTION_ID ||
|
||||
value === CLAUDE_ULTRACODE_THINKING_OPTION_ID ||
|
||||
isClaudeThinkingEffort(value)
|
||||
);
|
||||
}
|
||||
|
||||
function assertClaudeThinkingOptionSupported(
|
||||
modelId: string | null | undefined,
|
||||
thinkingOptionId: string | null | undefined,
|
||||
): void {
|
||||
if (
|
||||
thinkingOptionId !== CLAUDE_DISABLED_THINKING_OPTION_ID ||
|
||||
resolveClaudeDisabledThinkingForModel(modelId).supported
|
||||
) {
|
||||
return;
|
||||
}
|
||||
throw new Error(
|
||||
`Thinking option '${thinkingOptionId}' is not available for model '${modelId ?? "default"}'`,
|
||||
);
|
||||
}
|
||||
|
||||
interface ClaudeOptionsLogSummary {
|
||||
@@ -1946,6 +1972,7 @@ class ClaudeAgentSession implements AgentSession {
|
||||
|
||||
constructor(config: ClaudeAgentConfig, options: ClaudeAgentSessionOptions) {
|
||||
this.config = config;
|
||||
assertClaudeThinkingOptionSupported(config.model, config.thinkingOptionId);
|
||||
this.launchEnv = options.launchEnv;
|
||||
this.agentId = options.agentId;
|
||||
this.defaults = options.defaults;
|
||||
@@ -2203,6 +2230,7 @@ class ClaudeAgentSession implements AgentSession {
|
||||
const activeQuery = await this.ensureQuery();
|
||||
await activeQuery.setModel(normalizedModelId ?? undefined);
|
||||
this.config.model = normalizedModelId ?? undefined;
|
||||
this.reconcileThinkingOptionForModel(normalizedModelId);
|
||||
if (!claudeModelSupportsFastMode(this.config.model) && this.config.featureValues?.fast_mode) {
|
||||
await this.applyFastModeFeature(false, activeQuery);
|
||||
}
|
||||
@@ -2216,6 +2244,26 @@ class ClaudeAgentSession implements AgentSession {
|
||||
this.persistence = null;
|
||||
}
|
||||
|
||||
private reconcileThinkingOptionForModel(modelId: string | null): void {
|
||||
const thinkingOptionId = this.config.thinkingOptionId;
|
||||
if (thinkingOptionId !== CLAUDE_DISABLED_THINKING_OPTION_ID) {
|
||||
return;
|
||||
}
|
||||
|
||||
const resolution = resolveClaudeDisabledThinkingForModel(modelId);
|
||||
if (resolution.supported) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.config.thinkingOptionId = resolution.fallbackThinkingOptionId;
|
||||
this.queryRestartNeeded = true;
|
||||
this.pushEvent({
|
||||
type: "thinking_option_changed",
|
||||
provider: "claude",
|
||||
thinkingOptionId: this.config.thinkingOptionId ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
async setThinkingOption(thinkingOptionId: string | null): Promise<void | AgentProviderNotice> {
|
||||
const normalizedThinkingOptionId =
|
||||
typeof thinkingOptionId === "string" && thinkingOptionId.trim().length > 0
|
||||
@@ -2225,6 +2273,7 @@ class ClaudeAgentSession implements AgentSession {
|
||||
if (!normalizedThinkingOptionId || normalizedThinkingOptionId === "default") {
|
||||
this.config.thinkingOptionId = undefined;
|
||||
} else if (isClaudeThinkingOption(normalizedThinkingOptionId)) {
|
||||
assertClaudeThinkingOptionSupported(this.config.model, normalizedThinkingOptionId);
|
||||
this.config.thinkingOptionId = normalizedThinkingOptionId;
|
||||
} else {
|
||||
throw new Error(`Unknown thinking option: ${normalizedThinkingOptionId}`);
|
||||
@@ -2899,6 +2948,10 @@ class ClaudeAgentSession implements AgentSession {
|
||||
this.config.thinkingOptionId && this.config.thinkingOptionId !== "default"
|
||||
? this.config.thinkingOptionId
|
||||
: undefined;
|
||||
assertClaudeThinkingOptionSupported(this.config.model, thinkingOptionId);
|
||||
if (thinkingOptionId === CLAUDE_DISABLED_THINKING_OPTION_ID) {
|
||||
return { thinking: { type: "disabled" }, effort: undefined, ultracode: false };
|
||||
}
|
||||
if (thinkingOptionId === CLAUDE_ULTRACODE_THINKING_OPTION_ID) {
|
||||
return { thinking: { type: "adaptive" }, effort: "xhigh", ultracode: true };
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ interface ClaudeModelManifestEntry {
|
||||
isDefault?: boolean;
|
||||
contextWindowMaxTokens?: number;
|
||||
effortLevels?: readonly ClaudeEffortLevel[];
|
||||
supportsThinkingDisabled?: boolean;
|
||||
supportsFastMode?: boolean;
|
||||
}
|
||||
|
||||
@@ -25,6 +26,7 @@ const CLAUDE_EFFORT_LABELS = {
|
||||
max: "Max",
|
||||
} as const satisfies Record<ClaudeEffortLevel, string>;
|
||||
|
||||
export const CLAUDE_DISABLED_THINKING_OPTION_ID = "off";
|
||||
export const CLAUDE_ULTRACODE_THINKING_OPTION_ID = "ultracode";
|
||||
|
||||
export const CLAUDE_MODEL_MANIFEST = [
|
||||
@@ -41,6 +43,7 @@ export const CLAUDE_MODEL_MANIFEST = [
|
||||
description: "Opus 4.8 with 1M context window",
|
||||
contextWindowMaxTokens: 1_000_000,
|
||||
effortLevels: CLAUDE_EFFORT_LEVELS.xhigh,
|
||||
supportsThinkingDisabled: true,
|
||||
supportsFastMode: true,
|
||||
},
|
||||
{
|
||||
@@ -50,6 +53,7 @@ export const CLAUDE_MODEL_MANIFEST = [
|
||||
isDefault: true,
|
||||
contextWindowMaxTokens: 200_000,
|
||||
effortLevels: CLAUDE_EFFORT_LEVELS.xhigh,
|
||||
supportsThinkingDisabled: true,
|
||||
supportsFastMode: true,
|
||||
},
|
||||
{
|
||||
@@ -58,6 +62,7 @@ export const CLAUDE_MODEL_MANIFEST = [
|
||||
description: "Sonnet 5 · Best for everyday tasks",
|
||||
contextWindowMaxTokens: 1_000_000,
|
||||
effortLevels: CLAUDE_EFFORT_LEVELS.xhigh,
|
||||
supportsThinkingDisabled: true,
|
||||
},
|
||||
{
|
||||
id: "claude-opus-4-7[1m]",
|
||||
@@ -65,6 +70,7 @@ export const CLAUDE_MODEL_MANIFEST = [
|
||||
description: "Opus 4.7 with 1M context window",
|
||||
contextWindowMaxTokens: 1_000_000,
|
||||
effortLevels: CLAUDE_EFFORT_LEVELS.xhigh,
|
||||
supportsThinkingDisabled: true,
|
||||
supportsFastMode: true,
|
||||
},
|
||||
{
|
||||
@@ -73,6 +79,7 @@ export const CLAUDE_MODEL_MANIFEST = [
|
||||
description: "Opus 4.7 · Previous release",
|
||||
contextWindowMaxTokens: 200_000,
|
||||
effortLevels: CLAUDE_EFFORT_LEVELS.xhigh,
|
||||
supportsThinkingDisabled: true,
|
||||
supportsFastMode: true,
|
||||
},
|
||||
{
|
||||
@@ -81,6 +88,7 @@ export const CLAUDE_MODEL_MANIFEST = [
|
||||
description: "Opus 4.6 with 1M context window",
|
||||
contextWindowMaxTokens: 1_000_000,
|
||||
effortLevels: CLAUDE_EFFORT_LEVELS.standard,
|
||||
supportsThinkingDisabled: true,
|
||||
supportsFastMode: true,
|
||||
},
|
||||
{
|
||||
@@ -89,6 +97,7 @@ export const CLAUDE_MODEL_MANIFEST = [
|
||||
description: "Opus 4.6 · Most capable for complex work",
|
||||
contextWindowMaxTokens: 200_000,
|
||||
effortLevels: CLAUDE_EFFORT_LEVELS.standard,
|
||||
supportsThinkingDisabled: true,
|
||||
supportsFastMode: true,
|
||||
},
|
||||
{
|
||||
@@ -97,6 +106,7 @@ export const CLAUDE_MODEL_MANIFEST = [
|
||||
description: "Sonnet 4.6 with 1M context window",
|
||||
contextWindowMaxTokens: 1_000_000,
|
||||
effortLevels: CLAUDE_EFFORT_LEVELS.standard,
|
||||
supportsThinkingDisabled: true,
|
||||
},
|
||||
{
|
||||
id: "claude-sonnet-4-6",
|
||||
@@ -104,6 +114,7 @@ export const CLAUDE_MODEL_MANIFEST = [
|
||||
description: "Sonnet 4.6 · Best for everyday tasks",
|
||||
contextWindowMaxTokens: 200_000,
|
||||
effortLevels: CLAUDE_EFFORT_LEVELS.standard,
|
||||
supportsThinkingDisabled: true,
|
||||
},
|
||||
{
|
||||
id: "claude-haiku-4-5",
|
||||
@@ -115,15 +126,19 @@ export const CLAUDE_MODEL_MANIFEST = [
|
||||
|
||||
function buildThinkingOptions(
|
||||
effortLevels: readonly ClaudeEffortLevel[] | undefined,
|
||||
supportsThinkingDisabled: boolean,
|
||||
): AgentSelectOption[] | undefined {
|
||||
if (!effortLevels) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const options: AgentSelectOption[] = effortLevels.map((id) => ({
|
||||
id,
|
||||
label: CLAUDE_EFFORT_LABELS[id],
|
||||
}));
|
||||
const options: AgentSelectOption[] = [
|
||||
...(supportsThinkingDisabled ? [{ id: CLAUDE_DISABLED_THINKING_OPTION_ID, label: "Off" }] : []),
|
||||
...effortLevels.map((id) => ({
|
||||
id,
|
||||
label: CLAUDE_EFFORT_LABELS[id],
|
||||
})),
|
||||
];
|
||||
|
||||
if (effortLevels.includes("xhigh")) {
|
||||
options.push({ id: CLAUDE_ULTRACODE_THINKING_OPTION_ID, label: "Ultra Code" });
|
||||
@@ -136,6 +151,7 @@ export function getClaudeManifestModels(): AgentModelDefinition[] {
|
||||
return CLAUDE_MODEL_MANIFEST.map((model) => {
|
||||
const thinkingOptions = buildThinkingOptions(
|
||||
"effortLevels" in model ? model.effortLevels : undefined,
|
||||
"supportsThinkingDisabled" in model && model.supportsThinkingDisabled,
|
||||
);
|
||||
return {
|
||||
provider: "claude",
|
||||
@@ -146,11 +162,40 @@ export function getClaudeManifestModels(): AgentModelDefinition[] {
|
||||
...(model.contextWindowMaxTokens !== undefined
|
||||
? { contextWindowMaxTokens: model.contextWindowMaxTokens }
|
||||
: {}),
|
||||
...(thinkingOptions ? { thinkingOptions } : {}),
|
||||
...(thinkingOptions
|
||||
? {
|
||||
thinkingOptions,
|
||||
defaultThinkingOptionId: "effortLevels" in model ? model.effortLevels?.[0] : undefined,
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export interface ClaudeDisabledThinkingResolution {
|
||||
supported: boolean;
|
||||
fallbackThinkingOptionId: string | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the disabled-thinking capability from the curated manifest only. Runtime/provider
|
||||
* model aliases intentionally do not inherit this capability.
|
||||
*/
|
||||
export function resolveClaudeDisabledThinkingForModel(
|
||||
modelId: string | null | undefined,
|
||||
): ClaudeDisabledThinkingResolution {
|
||||
const normalizedModelId = normalizeClaudeManifestModelId(modelId);
|
||||
const model = normalizedModelId
|
||||
? CLAUDE_MODEL_MANIFEST.find((candidate) => candidate.id === normalizedModelId)
|
||||
: undefined;
|
||||
return {
|
||||
supported:
|
||||
!!model && "supportsThinkingDisabled" in model && model.supportsThinkingDisabled === true,
|
||||
fallbackThinkingOptionId:
|
||||
model && "effortLevels" in model ? model.effortLevels?.[0] : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export function isClaudeManifestModelId(modelId: string): boolean {
|
||||
return CLAUDE_MODEL_MANIFEST.some((model) => model.id === modelId);
|
||||
}
|
||||
|
||||
@@ -6,9 +6,11 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { createTestLogger } from "../../../../test-utils/test-logger.js";
|
||||
import { ClaudeAgentClient } from "./agent.js";
|
||||
import {
|
||||
CLAUDE_DISABLED_THINKING_OPTION_ID,
|
||||
CLAUDE_ULTRACODE_THINKING_OPTION_ID,
|
||||
claudeManifestModelSupportsFastMode,
|
||||
normalizeClaudeManifestModelId,
|
||||
resolveClaudeDisabledThinkingForModel,
|
||||
} from "./model-manifest.js";
|
||||
import { findClaudeModel, getClaudeModels, normalizeClaudeRuntimeModelId } from "./models.js";
|
||||
|
||||
@@ -87,6 +89,7 @@ describe("getClaudeModels", () => {
|
||||
const models = new Map(getClaudeModels().map((model) => [model.id, model]));
|
||||
|
||||
expect(models.get("claude-sonnet-5")?.thinkingOptions?.map((option) => option.id)).toEqual([
|
||||
CLAUDE_DISABLED_THINKING_OPTION_ID,
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
@@ -100,8 +103,10 @@ describe("getClaudeModels", () => {
|
||||
?.thinkingOptions?.find((option) => option.id === CLAUDE_ULTRACODE_THINKING_OPTION_ID)
|
||||
?.label,
|
||||
).toBe("Ultra Code");
|
||||
expect(models.get("claude-sonnet-5")?.defaultThinkingOptionId).toBe("low");
|
||||
|
||||
expect(models.get("claude-opus-4-7")?.thinkingOptions?.map((option) => option.id)).toEqual([
|
||||
CLAUDE_DISABLED_THINKING_OPTION_ID,
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
@@ -110,14 +115,32 @@ describe("getClaudeModels", () => {
|
||||
CLAUDE_ULTRACODE_THINKING_OPTION_ID,
|
||||
]);
|
||||
expect(models.get("claude-sonnet-4-6")?.thinkingOptions?.map((option) => option.id)).toEqual([
|
||||
CLAUDE_DISABLED_THINKING_OPTION_ID,
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"max",
|
||||
]);
|
||||
expect(models.get("claude-fable-5")?.thinkingOptions?.map((option) => option.id)).not.toContain(
|
||||
CLAUDE_DISABLED_THINKING_OPTION_ID,
|
||||
);
|
||||
expect(models.get("claude-haiku-4-5")?.thinkingOptions).toBeUndefined();
|
||||
});
|
||||
|
||||
it.each([
|
||||
["claude-sonnet-5", true, "low"],
|
||||
["claude-sonnet-5-20260101", true, "low"],
|
||||
["claude-fable-5", false, "low"],
|
||||
["claude-haiku-4-5", false, undefined],
|
||||
["openrouter/anthropic/claude-opus-4-8", false, undefined],
|
||||
[null, false, undefined],
|
||||
])("resolves disabled thinking for model %s", (modelId, supported, fallbackThinkingOptionId) => {
|
||||
expect(resolveClaudeDisabledThinkingForModel(modelId)).toEqual({
|
||||
supported,
|
||||
fallbackThinkingOptionId,
|
||||
});
|
||||
});
|
||||
|
||||
it("returns fresh copies each call", () => {
|
||||
const a = getClaudeModels();
|
||||
const b = getClaudeModels();
|
||||
|
||||
Reference in New Issue
Block a user