Compare commits

...
3 changed files with 162 additions and 27 deletions
+106
View File
@@ -1009,6 +1009,76 @@ describe("runCli lightweight command dispatch", () => {
);
});
it("defaults thinking to medium for reasoning-capable selected models", async () => {
mockState.runAgentCalls = 0;
runtimeMocks.runAgent.mockClear();
llmMocks.resolveProviderConfig.mockResolvedValue({
knownModels: {
"openai/gpt-5": {
id: "openai/gpt-5",
name: "GPT-5",
capabilities: ["tools", "reasoning"],
},
},
});
forcePromptModeInput();
process.argv = ["bun", "src/index.ts", "-m", "openai/gpt-5", "hello"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(mockState.runAgentCalls).toBe(1);
expect(runtimeMocks.runAgent).toHaveBeenCalledWith(
"hello",
expect.objectContaining({
modelId: "openai/gpt-5",
thinking: true,
reasoningEffort: "medium",
}),
expect.anything(),
);
});
it("keeps thinking disabled when explicitly set to none for reasoning models", async () => {
mockState.runAgentCalls = 0;
runtimeMocks.runAgent.mockClear();
llmMocks.resolveProviderConfig.mockResolvedValue({
knownModels: {
"openai/gpt-5": {
id: "openai/gpt-5",
name: "GPT-5",
capabilities: ["tools", "reasoning"],
},
},
});
forcePromptModeInput();
process.argv = [
"bun",
"src/index.ts",
"-m",
"openai/gpt-5",
"--thinking",
"none",
"hello",
];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(mockState.runAgentCalls).toBe(1);
expect(runtimeMocks.runAgent).toHaveBeenCalledWith(
"hello",
expect.objectContaining({
modelId: "openai/gpt-5",
thinking: false,
reasoningEffort: undefined,
}),
expect.anything(),
);
});
it("maps --thinking to medium effort", async () => {
mockState.runAgentCalls = 0;
runtimeMocks.runAgent.mockClear();
@@ -1060,6 +1130,42 @@ describe("runCli lightweight command dispatch", () => {
);
});
it("keeps persisted disabled reasoning when --thinking is not provided", async () => {
mockState.runAgentCalls = 0;
runtimeMocks.runAgent.mockClear();
providerSettingsMocks.getProviderSettings.mockReturnValue({
provider: "cline",
model: "openai/gpt-5",
reasoning: { enabled: false },
});
llmMocks.resolveProviderConfig.mockResolvedValue({
knownModels: {
"openai/gpt-5": {
id: "openai/gpt-5",
name: "GPT-5",
capabilities: ["tools", "reasoning"],
},
},
});
forcePromptModeInput();
process.argv = ["bun", "src/index.ts", "hello"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(mockState.runAgentCalls).toBe(1);
expect(runtimeMocks.runAgent).toHaveBeenCalledWith(
"hello",
expect.objectContaining({
modelId: "openai/gpt-5",
thinking: false,
reasoningEffort: undefined,
}),
expect.anything(),
);
});
it("prefers explicit --thinking over persisted reasoning effort", async () => {
mockState.runAgentCalls = 0;
runtimeMocks.runAgent.mockClear();
+27 -6
View File
@@ -61,6 +61,13 @@ export function stdinHasPipedInput(): boolean {
}
}
function modelSupportsReasoning(
knownModels: Config["knownModels"],
modelId: string,
): boolean {
return knownModels?.[modelId]?.capabilities?.includes("reasoning") ?? false;
}
async function createProviderSettingsManager() {
const { ProviderSettingsManager } = await import("@cline/core");
return new ProviderSettingsManager();
@@ -927,8 +934,17 @@ export async function runCli(): Promise<void> {
);
}
const knownModelIds = knownModels ? Object.keys(knownModels) : [];
const selectedModelId =
args.model ??
selectedProviderSettings?.model ??
knownModelIds[0] ??
"anthropic/claude-sonnet-4.6";
const persistedReasoning = selectedProviderSettings?.reasoning;
const persistedReasoningEffort = persistedReasoning?.effort;
const hasPersistedReasoning =
persistedReasoning?.enabled !== undefined ||
persistedReasoning?.effort !== undefined ||
persistedReasoning?.budgetTokens !== undefined;
const reasoningEffortFromSettings =
persistedReasoning?.enabled === false
? "none"
@@ -937,9 +953,18 @@ export async function runCli(): Promise<void> {
: persistedReasoning?.enabled === true
? "medium"
: "none";
const reasoningEffortFromModel = modelSupportsReasoning(
knownModels,
selectedModelId,
)
? "medium"
: "none";
const effectiveReasoningEffort = args.thinkingExplicitlySet
? (args.reasoningEffort ?? "none")
: (args.reasoningEffort ?? reasoningEffortFromSettings);
: (args.reasoningEffort ??
(hasPersistedReasoning
? reasoningEffortFromSettings
: reasoningEffortFromModel));
const { createCliLoggerAdapter } = await import("./logging/adapter");
const loggerAdapter = createCliLoggerAdapter({
runtime: "cli",
@@ -953,11 +978,7 @@ export async function runCli(): Promise<void> {
const config: Config = {
providerId: provider,
modelId:
args.model ??
selectedProviderSettings?.model ??
knownModelIds[0] ??
"anthropic/claude-sonnet-4.6",
modelId: selectedModelId,
apiKey: apiKey ?? "",
knownModels,
systemPrompt: await resolveSystemPrompt({
+29 -21
View File
@@ -74,6 +74,21 @@ function clearReasoningConfig(config: Config): void {
config.reasoningEffort = undefined;
}
function resolveDefaultThinkingLevel(
config: Pick<Config, "modelId" | "reasoningEffort" | "thinking">,
selectedModelId: string,
): ThinkingLevel {
if (config.reasoningEffort) {
return config.reasoningEffort as ThinkingLevel;
}
if (selectedModelId === config.modelId && !config.thinking) {
return "none";
}
return "medium";
}
function usesModelIdInput(providerId: string): boolean {
return providerId === "openai-compatible";
}
@@ -331,23 +346,21 @@ export function useModelSelector(opts: {
await changeProvider();
continue;
}
config.modelId = browseResult;
const browseModel = modelOptions.find(
(m: ModelOption) => m.key === browseResult,
);
if (browseModel?.supportsReasoning) {
const lvl: ThinkingLevel = config.reasoningEffort
? (config.reasoningEffort as ThinkingLevel)
: config.thinking
? "medium"
: "none";
const currentLevel = resolveDefaultThinkingLevel(
config,
browseResult,
);
const pick = await dialog.choice<ThinkingLevel>({
style: { maxHeight: termHeight - 2 },
content: (ctx: ChoiceContext<ThinkingLevel>) => (
<ThinkingLevelContent
{...ctx}
modelName={browseModel.name}
currentLevel={lvl}
currentLevel={currentLevel}
/>
),
});
@@ -361,6 +374,7 @@ export function useModelSelector(opts: {
}
}
}
config.modelId = browseResult;
if (!browseModel?.supportsReasoning) {
clearReasoningConfig(config);
}
@@ -368,16 +382,14 @@ export function useModelSelector(opts: {
continue;
}
config.modelId = clineResult;
const selectedModel = modelOptions.find(
(m: ModelOption) => m.key === clineResult,
);
if (selectedModel?.supportsReasoning) {
const currentLevel: ThinkingLevel = config.reasoningEffort
? (config.reasoningEffort as ThinkingLevel)
: config.thinking
? "medium"
: "none";
const currentLevel = resolveDefaultThinkingLevel(
config,
clineResult,
);
const thinkingLevel = await dialog.choice<ThinkingLevel>({
style: { maxHeight: termHeight - 2 },
content: (ctx: ChoiceContext<ThinkingLevel>) => (
@@ -398,6 +410,7 @@ export function useModelSelector(opts: {
}
}
}
config.modelId = clineResult;
if (!selectedModel?.supportsReasoning) {
clearReasoningConfig(config);
}
@@ -426,23 +439,17 @@ export function useModelSelector(opts: {
continue;
}
config.modelId = selectedKey;
const selectedModel = modelOptions.find(
(m: ModelOption) => m.key === selectedKey,
);
if (!selectedModel?.supportsReasoning) {
config.modelId = selectedKey;
clearReasoningConfig(config);
pickingModel = false;
break;
}
const currentLevel: ThinkingLevel = config.reasoningEffort
? (config.reasoningEffort as ThinkingLevel)
: config.thinking
? "medium"
: "none";
const currentLevel = resolveDefaultThinkingLevel(config, selectedKey);
const thinkingLevel = await dialog.choice<ThinkingLevel>({
style: { maxHeight: termHeight - 2 },
content: (ctx: ChoiceContext<ThinkingLevel>) => (
@@ -465,6 +472,7 @@ export function useModelSelector(opts: {
config.thinking = true;
config.reasoningEffort = thinkingLevel;
}
config.modelId = selectedKey;
pickingModel = false;
}