diff --git a/src/agents/model-selection.test.ts b/src/agents/model-selection.test.ts index f97baebc956..6e95a1e622b 100644 --- a/src/agents/model-selection.test.ts +++ b/src/agents/model-selection.test.ts @@ -11,6 +11,7 @@ import { modelKey, resolveAllowedModelRef, resolveConfiguredModelRef, + resolveThinkingDefault, resolveModelRefFromString, } from "./model-selection.js"; @@ -470,6 +471,39 @@ describe("model-selection", () => { expect(result).toEqual({ provider: "openai", model: "gpt-4" }); }); }); + + describe("resolveThinkingDefault", () => { + it("prefers per-model params.thinking over global thinkingDefault", () => { + const cfg = { + agents: { + defaults: { + thinkingDefault: "low", + models: { + "anthropic/claude-opus-4-6": { + params: { thinking: "high" }, + }, + }, + }, + }, + } as OpenClawConfig; + + expect( + resolveThinkingDefault({ + cfg, + provider: "anthropic", + model: "claude-opus-4-6", + catalog: [ + { + provider: "anthropic", + id: "claude-opus-4-6", + name: "Claude Opus 4.6", + reasoning: true, + }, + ], + }), + ).toBe("high"); + }); + }); }); describe("normalizeModelSelection", () => { diff --git a/src/agents/model-selection.ts b/src/agents/model-selection.ts index 238b8276504..e7f6ae9757f 100644 --- a/src/agents/model-selection.ts +++ b/src/agents/model-selection.ts @@ -525,6 +525,19 @@ export function resolveThinkingDefault(params: { model: string; catalog?: ModelCatalogEntry[]; }): ThinkLevel { + const perModelThinking = + params.cfg.agents?.defaults?.models?.[modelKey(params.provider, params.model)]?.params + ?.thinking; + if ( + perModelThinking === "off" || + perModelThinking === "minimal" || + perModelThinking === "low" || + perModelThinking === "medium" || + perModelThinking === "high" || + perModelThinking === "xhigh" + ) { + return perModelThinking; + } const configured = params.cfg.agents?.defaults?.thinkingDefault; if (configured) { return configured; diff --git a/src/auto-reply/reply/directive-handling.levels.test.ts b/src/auto-reply/reply/directive-handling.levels.test.ts new file mode 100644 index 00000000000..204d2685005 --- /dev/null +++ b/src/auto-reply/reply/directive-handling.levels.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, it, vi } from "vitest"; +import { resolveCurrentDirectiveLevels } from "./directive-handling.levels.js"; + +describe("resolveCurrentDirectiveLevels", () => { + it("prefers resolved model default over agent thinkingDefault", async () => { + const resolveDefaultThinkingLevel = vi.fn().mockResolvedValue("high"); + + const result = await resolveCurrentDirectiveLevels({ + sessionEntry: {}, + agentCfg: { + thinkingDefault: "low", + }, + resolveDefaultThinkingLevel, + }); + + expect(result.currentThinkLevel).toBe("high"); + expect(resolveDefaultThinkingLevel).toHaveBeenCalledTimes(1); + }); + + it("keeps session thinking override without consulting defaults", async () => { + const resolveDefaultThinkingLevel = vi.fn().mockResolvedValue("high"); + + const result = await resolveCurrentDirectiveLevels({ + sessionEntry: { + thinkingLevel: "minimal", + }, + agentCfg: { + thinkingDefault: "low", + }, + resolveDefaultThinkingLevel, + }); + + expect(result.currentThinkLevel).toBe("minimal"); + expect(resolveDefaultThinkingLevel).not.toHaveBeenCalled(); + }); +}); diff --git a/src/auto-reply/reply/directive-handling.levels.ts b/src/auto-reply/reply/directive-handling.levels.ts index 61f9aef1c79..ee7b1108e83 100644 --- a/src/auto-reply/reply/directive-handling.levels.ts +++ b/src/auto-reply/reply/directive-handling.levels.ts @@ -21,8 +21,8 @@ export async function resolveCurrentDirectiveLevels(params: { }> { const resolvedDefaultThinkLevel = (params.sessionEntry?.thinkingLevel as ThinkLevel | undefined) ?? - (params.agentCfg?.thinkingDefault as ThinkLevel | undefined) ?? - (await params.resolveDefaultThinkingLevel()); + (await params.resolveDefaultThinkingLevel()) ?? + (params.agentCfg?.thinkingDefault as ThinkLevel | undefined); const currentThinkLevel = resolvedDefaultThinkLevel; const currentVerboseLevel = (params.sessionEntry?.verboseLevel as VerboseLevel | undefined) ?? diff --git a/src/auto-reply/reply/get-reply-directives.ts b/src/auto-reply/reply/get-reply-directives.ts index dd288ef83d5..4c9da28deae 100644 --- a/src/auto-reply/reply/get-reply-directives.ts +++ b/src/auto-reply/reply/get-reply-directives.ts @@ -339,9 +339,7 @@ export async function resolveReplyDirectives(params: { }); const defaultActivation = defaultGroupActivation(requireMention); const resolvedThinkLevel = - directives.thinkLevel ?? - (sessionEntry?.thinkingLevel as ThinkLevel | undefined) ?? - (agentCfg?.thinkingDefault as ThinkLevel | undefined); + directives.thinkLevel ?? (sessionEntry?.thinkingLevel as ThinkLevel | undefined); const resolvedVerboseLevel = directives.verboseLevel ?? @@ -390,6 +388,10 @@ export async function resolveReplyDirectives(params: { }); provider = modelState.provider; model = modelState.model; + const resolvedThinkLevelWithDefault = + resolvedThinkLevel ?? + (await modelState.resolveDefaultThinkingLevel()) ?? + (agentCfg?.thinkingDefault as ThinkLevel | undefined); // When neither directive nor session set reasoning, default to model capability // (e.g. OpenRouter with reasoning: true). Skip auto-enabling when thinking is @@ -398,9 +400,7 @@ export async function resolveReplyDirectives(params: { const reasoningExplicitlySet = directives.reasoningLevel !== undefined || (sessionEntry?.reasoningLevel !== undefined && sessionEntry?.reasoningLevel !== null); - const effectiveThinkingForReasoning = - resolvedThinkLevel ?? (await modelState.resolveDefaultThinkingLevel()); - const thinkingActive = effectiveThinkingForReasoning !== "off"; + const thinkingActive = resolvedThinkLevelWithDefault !== "off"; if (!reasoningExplicitlySet && resolvedReasoningLevel === "off" && !thinkingActive) { resolvedReasoningLevel = await modelState.resolveDefaultReasoningLevel(); } @@ -477,7 +477,7 @@ export async function resolveReplyDirectives(params: { elevatedAllowed, elevatedFailures, defaultActivation, - resolvedThinkLevel, + resolvedThinkLevel: resolvedThinkLevelWithDefault, resolvedVerboseLevel, resolvedReasoningLevel, resolvedElevatedLevel, diff --git a/src/commands/agent.test.ts b/src/commands/agent.test.ts index b60eaffa9c3..e168f887039 100644 --- a/src/commands/agent.test.ts +++ b/src/commands/agent.test.ts @@ -734,6 +734,25 @@ describe("agentCommand", () => { }); }); + it("prefers per-model thinking over global thinkingDefault", async () => { + await withTempHome(async (home) => { + const store = path.join(home, "sessions.json"); + mockConfig(home, store, { + thinkingDefault: "low", + models: { + "anthropic/claude-opus-4-5": { + params: { thinking: "high" }, + }, + }, + }); + + await agentCommand({ message: "hi", to: "+1555" }, runtime); + + const callArgs = vi.mocked(runEmbeddedPiAgent).mock.calls.at(-1)?.[0]; + expect(callArgs?.thinkLevel).toBe("high"); + }); + }); + it("prints JSON payload when requested", async () => { await withTempHome(async (home) => { vi.mocked(runEmbeddedPiAgent).mockResolvedValue({ diff --git a/src/commands/agent.ts b/src/commands/agent.ts index 0ebde04fe1e..3d669bbac0f 100644 --- a/src/commands/agent.ts +++ b/src/commands/agent.ts @@ -588,11 +588,7 @@ export async function agentCommand( }); } - let resolvedThinkLevel = - thinkOnce ?? - thinkOverride ?? - persistedThinking ?? - (agentCfg?.thinkingDefault as ThinkLevel | undefined); + let resolvedThinkLevel = thinkOnce ?? thinkOverride ?? persistedThinking; const resolvedVerboseLevel = verboseOverride ?? persistedVerbose ?? (agentCfg?.verboseDefault as VerboseLevel | undefined); diff --git a/src/cron/isolated-agent/run.ts b/src/cron/isolated-agent/run.ts index adde8db59c2..623cc6e3eb2 100644 --- a/src/cron/isolated-agent/run.ts +++ b/src/cron/isolated-agent/run.ts @@ -299,16 +299,15 @@ export async function runCronIsolatedAgentTurn(params: { } } - // Resolve thinking level - job thinking > hooks.gmail.thinking > agent default + // Resolve thinking level - job thinking > hooks.gmail.thinking > model/global defaults const hooksGmailThinking = isGmailHook ? normalizeThinkLevel(params.cfg.hooks?.gmail?.thinking) : undefined; - const thinkOverride = normalizeThinkLevel(agentCfg?.thinkingDefault); const jobThink = normalizeThinkLevel( (params.job.payload.kind === "agentTurn" ? params.job.payload.thinking : undefined) ?? undefined, ); - let thinkLevel = jobThink ?? hooksGmailThinking ?? thinkOverride; + let thinkLevel = jobThink ?? hooksGmailThinking; if (!thinkLevel) { thinkLevel = resolveThinkingDefault({ cfg: cfgWithAgentDefaults, diff --git a/src/gateway/server-methods/chat.ts b/src/gateway/server-methods/chat.ts index c7773a873b4..e5202392a36 100644 --- a/src/gateway/server-methods/chat.ts +++ b/src/gateway/server-methods/chat.ts @@ -574,20 +574,15 @@ export const chatHandlers: GatewayRequestHandlers = { } let thinkingLevel = entry?.thinkingLevel; if (!thinkingLevel) { - const configured = cfg.agents?.defaults?.thinkingDefault; - if (configured) { - thinkingLevel = configured; - } else { - const sessionAgentId = resolveSessionAgentId({ sessionKey, config: cfg }); - const { provider, model } = resolveSessionModelRef(cfg, entry, sessionAgentId); - const catalog = await context.loadGatewayModelCatalog(); - thinkingLevel = resolveThinkingDefault({ - cfg, - provider, - model, - catalog, - }); - } + const sessionAgentId = resolveSessionAgentId({ sessionKey, config: cfg }); + const { provider, model } = resolveSessionModelRef(cfg, entry, sessionAgentId); + const catalog = await context.loadGatewayModelCatalog(); + thinkingLevel = resolveThinkingDefault({ + cfg, + provider, + model, + catalog, + }); } const verboseLevel = entry?.verboseLevel ?? cfg.agents?.defaults?.verboseDefault; respond(true, {