diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b222ef837c..68cbdc6b19b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Providers/OpenRouter: default reasoning to enabled when the selected model advertises `reasoning: true` and no session/directive override is set. (#22513) Thanks @zwffff. - Providers/OpenRouter: map `/think` levels to `reasoning.effort` in embedded runs while preserving explicit `reasoning.max_tokens` payloads. (#17236) Thanks @robbyczgw-cla. - Gateway/OpenRouter: preserve stored session provider when model IDs are vendor-prefixed (for example, `anthropic/...`) so follow-up turns do not incorrectly route to direct provider APIs. (#22753) Thanks @dndodson. - Providers/OpenRouter: preserve the required `openrouter/` prefix for OpenRouter-native model IDs during model-ref normalization. (#12942) Thanks @omair445. diff --git a/src/agents/model-selection.ts b/src/agents/model-selection.ts index 2df5acb14ea..6f6773d5c61 100644 --- a/src/agents/model-selection.ts +++ b/src/agents/model-selection.ts @@ -529,6 +529,21 @@ export function resolveThinkingDefault(params: { return "off"; } +/** Default reasoning level when session/directive do not set it: "on" if model supports reasoning, else "off". */ +export function resolveReasoningDefault(params: { + provider: string; + model: string; + catalog?: ModelCatalogEntry[]; +}): "on" | "off" { + const key = modelKey(params.provider, params.model); + const candidate = params.catalog?.find( + (entry) => + (entry.provider === params.provider && entry.id === params.model) || + (entry.provider === key && entry.id === params.model), + ); + return candidate?.reasoning === true ? "on" : "off"; +} + /** * Resolve the model configured for Gmail hook processing. * Returns null if hooks.gmail.model is not set. diff --git a/src/auto-reply/reply/get-reply-directives.ts b/src/auto-reply/reply/get-reply-directives.ts index 57d1808d495..f421ed92eae 100644 --- a/src/auto-reply/reply/get-reply-directives.ts +++ b/src/auto-reply/reply/get-reply-directives.ts @@ -345,7 +345,7 @@ export async function resolveReplyDirectives(params: { directives.verboseLevel ?? (sessionEntry?.verboseLevel as VerboseLevel | undefined) ?? (agentCfg?.verboseDefault as VerboseLevel | undefined); - const resolvedReasoningLevel: ReasoningLevel = + let resolvedReasoningLevel: ReasoningLevel = directives.reasoningLevel ?? (sessionEntry?.reasoningLevel as ReasoningLevel | undefined) ?? "off"; @@ -389,6 +389,14 @@ export async function resolveReplyDirectives(params: { provider = modelState.provider; model = modelState.model; + // When neither directive nor session set reasoning, default to model capability (e.g. OpenRouter with reasoning: true). + const reasoningExplicitlySet = + directives.reasoningLevel !== undefined || + (sessionEntry?.reasoningLevel !== undefined && sessionEntry?.reasoningLevel !== null); + if (!reasoningExplicitlySet && resolvedReasoningLevel === "off") { + resolvedReasoningLevel = await modelState.resolveDefaultReasoningLevel(); + } + let contextTokens = resolveContextTokens({ agentCfg, model, diff --git a/src/auto-reply/reply/model-selection.test.ts b/src/auto-reply/reply/model-selection.test.ts index b4f5f3577d4..493adec0515 100644 --- a/src/auto-reply/reply/model-selection.test.ts +++ b/src/auto-reply/reply/model-selection.test.ts @@ -264,3 +264,35 @@ describe("createModelSelectionState respects session model override", () => { expect(state.model).toBe("deepseek-v3-4bit-mlx"); }); }); + +describe("createModelSelectionState resolveDefaultReasoningLevel", () => { + it("returns on when catalog model has reasoning true", async () => { + const { loadModelCatalog } = await import("../../agents/model-catalog.js"); + vi.mocked(loadModelCatalog).mockResolvedValueOnce([ + { provider: "openrouter", id: "x-ai/grok-4.1-fast", name: "Grok", reasoning: true }, + ]); + const state = await createModelSelectionState({ + cfg: {} as OpenClawConfig, + agentCfg: undefined, + defaultProvider: "openrouter", + defaultModel: "x-ai/grok-4.1-fast", + provider: "openrouter", + model: "x-ai/grok-4.1-fast", + hasModelDirective: false, + }); + await expect(state.resolveDefaultReasoningLevel()).resolves.toBe("on"); + }); + + it("returns off when catalog model has no reasoning", async () => { + const state = await createModelSelectionState({ + cfg: {} as OpenClawConfig, + agentCfg: undefined, + defaultProvider: "openai", + defaultModel: "gpt-4o-mini", + provider: "openai", + model: "gpt-4o-mini", + hasModelDirective: false, + }); + await expect(state.resolveDefaultReasoningLevel()).resolves.toBe("off"); + }); +}); diff --git a/src/auto-reply/reply/model-selection.ts b/src/auto-reply/reply/model-selection.ts index c41abd31b46..1b666b6ded5 100644 --- a/src/auto-reply/reply/model-selection.ts +++ b/src/auto-reply/reply/model-selection.ts @@ -8,6 +8,7 @@ import { modelKey, normalizeProviderId, resolveModelRefFromString, + resolveReasoningDefault, resolveThinkingDefault, } from "../../agents/model-selection.js"; import type { OpenClawConfig } from "../../config/config.js"; @@ -32,6 +33,8 @@ type ModelSelectionState = { allowedModelCatalog: ModelCatalog; resetModelOverride: boolean; resolveDefaultThinkingLevel: () => Promise; + /** Default reasoning level from model capability: "on" if model has reasoning, else "off". */ + resolveDefaultReasoningLevel: () => Promise<"on" | "off">; needsModelCatalog: boolean; }; @@ -397,6 +400,19 @@ export async function createModelSelectionState(params: { return defaultThinkingLevel; }; + const resolveDefaultReasoningLevel = async (): Promise<"on" | "off"> => { + let catalogForReasoning = modelCatalog ?? allowedModelCatalog; + if (!catalogForReasoning || catalogForReasoning.length === 0) { + modelCatalog = await loadModelCatalog({ config: cfg }); + catalogForReasoning = modelCatalog; + } + return resolveReasoningDefault({ + provider, + model, + catalog: catalogForReasoning, + }); + }; + return { provider, model, @@ -404,6 +420,7 @@ export async function createModelSelectionState(params: { allowedModelCatalog, resetModelOverride, resolveDefaultThinkingLevel, + resolveDefaultReasoningLevel, needsModelCatalog, }; }