diff --git a/extensions/llm-task/src/llm-task-tool.test.ts b/extensions/llm-task/src/llm-task-tool.test.ts index 6d21ec69654..0a41f0f4bad 100644 --- a/extensions/llm-task/src/llm-task-tool.test.ts +++ b/extensions/llm-task/src/llm-task-tool.test.ts @@ -1,4 +1,81 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; + +vi.mock("@sinclair/typebox", () => ({ + Type: { + Object: (schema: unknown) => schema, + String: (schema?: unknown) => schema, + Optional: (schema: unknown) => schema, + Unknown: (schema?: unknown) => schema, + Number: (schema?: unknown) => schema, + }, +})); + +vi.mock("ajv", () => ({ + default: class MockAjv { + compile(schema: unknown) { + return (value: unknown) => { + if ( + schema && + typeof schema === "object" && + !Array.isArray(schema) && + (schema as { properties?: Record }).properties?.foo?.type === + "string" + ) { + const ok = typeof (value as { foo?: unknown })?.foo === "string"; + (this as { errors?: Array<{ instancePath: string; message: string }> }).errors = ok + ? undefined + : [{ instancePath: "/foo", message: "must be string" }]; + return ok; + } + (this as { errors?: Array<{ instancePath: string; message: string }> }).errors = undefined; + return true; + }; + } + + errors?: Array<{ instancePath: string; message: string }>; + }, +})); + +vi.mock("../api.js", () => ({ + formatXHighModelHint: () => "provider models that advertise xhigh reasoning", + normalizeThinkLevel: (raw?: string | null) => { + if (!raw) { + return undefined; + } + const key = raw.trim().toLowerCase(); + const collapsed = key.replace(/[\s_-]+/g, ""); + if (collapsed === "adaptive" || collapsed === "auto") { + return "adaptive"; + } + if (collapsed === "xhigh" || collapsed === "extrahigh") { + return "xhigh"; + } + if (["off"].includes(key)) { + return "off"; + } + if (["on", "enable", "enabled"].includes(key)) { + return "low"; + } + if (["min", "minimal", "think"].includes(key)) { + return "minimal"; + } + if (["low", "thinkhard", "think-hard", "think_hard"].includes(key)) { + return "low"; + } + if (["mid", "med", "medium", "thinkharder", "think-harder", "harder"].includes(key)) { + return "medium"; + } + if ( + ["high", "ultra", "ultrathink", "think-hard", "thinkhardest", "highest", "max"].includes(key) + ) { + return "high"; + } + return undefined; + }, + resolvePreferredOpenClawTmpDir: () => "/tmp", + supportsXHighThinking: () => false, +})); + import { createLlmTaskTool } from "./llm-task-tool.js"; const runEmbeddedPiAgent = vi.fn(async () => ({ @@ -137,6 +214,7 @@ describe("llm-task tool (json-only)", () => { await expect(tool.execute("id", { prompt: "x", thinking: "banana" })).rejects.toThrow( /invalid thinking level/i, ); + expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); }); it("throws on unsupported xhigh thinking level", async () => { diff --git a/extensions/llm-task/src/llm-task-tool.ts b/extensions/llm-task/src/llm-task-tool.ts index 47c7efbea76..77d76fb2dfb 100644 --- a/extensions/llm-task/src/llm-task-tool.ts +++ b/extensions/llm-task/src/llm-task-tool.ts @@ -3,7 +3,6 @@ import path from "node:path"; import { Type } from "@sinclair/typebox"; import Ajv from "ajv"; import { - formatThinkingLevels, formatXHighModelHint, normalizeThinkLevel, resolvePreferredOpenClawTmpDir, @@ -45,6 +44,9 @@ type PluginCfg = { timeoutMs?: number; }; +const INVALID_THINKING_LEVELS_HINT = + "off, minimal, low, medium, high, adaptive, and xhigh where supported"; + export function createLlmTaskTool(api: OpenClawPluginApi) { return { name: "llm-task", @@ -125,7 +127,7 @@ export function createLlmTaskTool(api: OpenClawPluginApi) { const thinkLevel = thinkingRaw ? normalizeThinkLevel(thinkingRaw) : undefined; if (thinkingRaw && !thinkLevel) { throw new Error( - `Invalid thinking level "${thinkingRaw}". Use one of: ${formatThinkingLevels(provider, model)}.`, + `Invalid thinking level "${thinkingRaw}". Use one of: ${INVALID_THINKING_LEVELS_HINT}.`, ); } if (thinkLevel === "xhigh" && !supportsXHighThinking(provider, model)) {