From 8a8ea037a4e26714919c16674b5ebe9f31464edc Mon Sep 17 00:00:00 2001 From: Kesava Date: Sat, 25 Jul 2026 13:45:16 +0530 Subject: [PATCH] feat(auto-reply): show thinking level in model summary - Add thinking level display to /model summary output - Resolve effective thinking level from provider catalog and runtime - Move thinkingCatalog construction before maybeHandleModelDirectiveInfo call - Align thinkingCatalog type to ThinkingCatalogEntry[] - Make runtimePolicySessionKey optional - Add tests for thinking level in model summary and channel-specific summaries Rebased from PR #111709 (commits addba5549ba, a9f9f46406e, 72e75e994c6) onto origin/main. --- .../reply/directive-handling.impl.ts | 16 +++--- .../reply/directive-handling.model.test.ts | 57 +++++++++++++++++++ .../reply/directive-handling.model.ts | 25 ++++++++ 3 files changed, 91 insertions(+), 7 deletions(-) diff --git a/src/auto-reply/reply/directive-handling.impl.ts b/src/auto-reply/reply/directive-handling.impl.ts index f2150ee19307..c2743a2c02d2 100644 --- a/src/auto-reply/reply/directive-handling.impl.ts +++ b/src/auto-reply/reply/directive-handling.impl.ts @@ -119,7 +119,12 @@ export async function handleDirectiveOnly( commandAuthorized: params.commandAuthorized, senderIsOwner: params.senderIsOwner, }); - + const thinkingCatalog = + params.thinkingCatalog && params.thinkingCatalog.length > 0 + ? params.thinkingCatalog + : allowedModelCatalog.length > 0 + ? allowedModelCatalog + : undefined; const modelInfo = await maybeHandleModelDirectiveInfo({ directives, cfg: params.cfg, @@ -133,6 +138,9 @@ export async function handleDirectiveOnly( policyAliasIndex, allowedModelKeys, allowedModelCatalog, + currentThinkLevel: currentThinkLevel ?? "off", + thinkingCatalog, + runtimePolicySessionKey, resetModelOverride, workspaceDir: params.workspaceDir, surface: params.surface, @@ -186,12 +194,6 @@ export async function handleDirectiveOnly( sessionKey: runtimePolicySessionKey, sessionEntry: prospectiveSessionEntry, }); - const thinkingCatalog = - params.thinkingCatalog && params.thinkingCatalog.length > 0 - ? params.thinkingCatalog - : allowedModelCatalog.length > 0 - ? allowedModelCatalog - : undefined; const fastModeState = resolveFastModeState({ cfg: params.cfg, provider: resolvedProvider, diff --git a/src/auto-reply/reply/directive-handling.model.test.ts b/src/auto-reply/reply/directive-handling.model.test.ts index 8801dd69ada0..0723d4c832cc 100644 --- a/src/auto-reply/reply/directive-handling.model.test.ts +++ b/src/auto-reply/reply/directive-handling.model.test.ts @@ -638,6 +638,8 @@ async function resolveModelInfoReply( aliasIndex: baseAliasIndex(), allowedModelKeys: new Set(), allowedModelCatalog: [], + currentThinkLevel: "medium", + runtimePolicySessionKey: "agent:main:main", resetModelOverride: false, ...overrides, }); @@ -648,10 +650,65 @@ describe("/model chat UX", () => { const reply = await resolveModelInfoReply(); expect(reply?.text).toContain("Current:"); + expect(reply?.text).toContain("Think: medium (change with /think )"); expect(reply?.text).toContain("Browse: /models"); expect(reply?.text).toContain("Switch: /model "); }); + it("includes the thinking level in channel-specific model summaries", async () => { + const registry = createEmptyPluginRegistry(); + registry.channels = [ + { + pluginId: "test", + plugin: { + id: "telegram", + commands: { + buildModelBrowseChannelData: () => ({ telegram: { inlineKeyboard: [] } }), + }, + }, + source: "test", + }, + ] as never; + setActivePluginRegistry(registry); + + const reply = await resolveModelInfoReply({ surface: "telegram" }); + + expect(reply?.channelData).toBeDefined(); + expect(reply?.text).toContain("Think: medium (change with /think )"); + }); + + it("shows the effective thinking level for the selected runtime", async () => { + setDirectiveTestProviders([ + { + id: "openai", + label: "OpenAI", + auth: [], + resolveThinkingProfile: ({ agentRuntime }) => ({ + levels: [ + { id: "off" }, + { id: "low" }, + { id: "medium" }, + { id: "high" }, + { id: "max" }, + ...(agentRuntime === "openclaw" ? ([{ id: "ultra" }] as const) : []), + ], + }), + }, + ]); + + const reply = await resolveModelInfoReply({ + provider: "openai", + model: "gpt-5.6-luna", + defaultProvider: "openai", + defaultModel: "gpt-5.6-luna", + currentThinkLevel: "ultra", + sessionEntry: { agentRuntimeOverride: "codex" }, + }); + + expect(reply?.text).toContain("Think: max (change with /think )"); + expect(reply?.text).not.toContain("Think: ultra"); + }); + it("treats /model list as a models browser alias, not a model id", async () => { const reply = await resolveModelInfoReply({ directives: parseInlineDirectives("/model list"), diff --git a/src/auto-reply/reply/directive-handling.model.ts b/src/auto-reply/reply/directive-handling.model.ts index 4ce20a32560c..5a08e846279c 100644 --- a/src/auto-reply/reply/directive-handling.model.ts +++ b/src/auto-reply/reply/directive-handling.model.ts @@ -21,11 +21,14 @@ import { import { RUNTIME_MODEL_VISIBILITY_NORMALIZATION } from "../../agents/model-visibility-policy.js"; import { buildAgentRuntimeAuthPlan } from "../../agents/runtime-plan/auth.js"; import { resolveSessionRuntimeOverrideForProvider } from "../../agents/session-runtime-compat.js"; +import { resolveEffectiveAgentRuntime } from "../../agents/thinking-runtime.js"; import { getChannelPlugin } from "../../channels/plugins/index.js"; import type { SessionEntry } from "../../config/sessions.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { shortenHomePath } from "../../utils.js"; import { resolveSelectedAndActiveModel } from "../model-runtime.js"; +import { resolveSupportedThinkingLevel } from "../thinking.js"; +import type { ThinkingCatalogEntry } from "../thinking.shared.js"; import type { ReplyPayload } from "../types.js"; import { resolveModelsCommandReply } from "./commands-models.js"; import { @@ -38,6 +41,7 @@ import { resolveProviderEndpointLabel, } from "./directive-handling.model-picker.js"; import type { InlineDirectives } from "./directive-handling.parse.js"; +import type { ThinkLevel } from "./directives.js"; function isMissingAuthLabel(auth: { label: string; source: string }): boolean { return auth.label === "missing" && auth.source === "missing"; @@ -380,6 +384,9 @@ export async function maybeHandleModelDirectiveInfo(params: { policyAliasIndex?: ModelAliasIndex; allowedModelKeys: ReadonlySet; allowedModelCatalog: Array<{ provider: string; id?: string; name?: string }>; + currentThinkLevel: ThinkLevel; + thinkingCatalog?: ThinkingCatalogEntry[]; + runtimePolicySessionKey?: string; resetModelOverride: boolean; workspaceDir?: string; surface?: string; @@ -435,6 +442,22 @@ export async function maybeHandleModelDirectiveInfo(params: { sessionEntry: params.sessionEntry, }); const current = modelRefs.selected.label; + const thinkingRuntime = resolveEffectiveAgentRuntime({ + cfg: params.cfg, + provider: params.provider, + modelId: params.model, + agentId: params.activeAgentId, + sessionKey: params.runtimePolicySessionKey, + sessionEntry: params.sessionEntry, + }); + const effectiveThinkLevel = resolveSupportedThinkingLevel({ + provider: params.provider, + model: params.model, + level: params.currentThinkLevel, + catalog: params.thinkingCatalog, + agentRuntime: thinkingRuntime, + }); + const thinkingLine = `Think: ${effectiveThinkLevel} (change with /think )`; const activeRuntimeLine = modelRefs.activeDiffers ? `Active: ${modelRefs.active.label} (runtime)` : null; @@ -445,6 +468,7 @@ export async function maybeHandleModelDirectiveInfo(params: { text: [ `Current: ${current}${modelRefs.activeDiffers ? " (selected)" : ""}`, activeRuntimeLine, + thinkingLine, "", "Tap below to browse models, or use:", "/model to switch", @@ -461,6 +485,7 @@ export async function maybeHandleModelDirectiveInfo(params: { text: [ `Current: ${current}${modelRefs.activeDiffers ? " (selected)" : ""}`, activeRuntimeLine, + thinkingLine, "", "Switch: /model ", "Runtime: /model --runtime ",