Compare commits

...

2 Commits

Author SHA1 Message Date
Tak Hoffman
fff82599fc
fix llm-task invalid thinking timeout 2026-03-18 08:31:09 -05:00
Tak Hoffman
7b8b268079
Build: narrow tsdown unresolved import guard 2026-03-18 08:29:55 -05:00
3 changed files with 104 additions and 6 deletions

View File

@ -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<string, { type?: string }> }).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 () => {

View File

@ -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)) {

View File

@ -6,6 +6,23 @@ const logLevel = process.env.OPENCLAW_BUILD_VERBOSE ? "info" : "warn";
const extraArgs = process.argv.slice(2);
const INEFFECTIVE_DYNAMIC_IMPORT_RE = /\[INEFFECTIVE_DYNAMIC_IMPORT\]/;
const UNRESOLVED_IMPORT_RE = /\[UNRESOLVED_IMPORT\]/;
const ANSI_ESCAPE_RE = new RegExp(String.raw`\u001B\[[0-9;]*m`, "g");
function findFatalUnresolvedImport(lines) {
for (const line of lines) {
if (!UNRESOLVED_IMPORT_RE.test(line)) {
continue;
}
const normalizedLine = line.replace(ANSI_ESCAPE_RE, "");
if (!normalizedLine.includes("extensions/")) {
return normalizedLine;
}
}
return null;
}
const result = spawnSync(
"pnpm",
["exec", "tsdown", "--config-loader", "unrun", "--logLevel", logLevel, ...extraArgs],
@ -32,10 +49,11 @@ if (result.status === 0 && INEFFECTIVE_DYNAMIC_IMPORT_RE.test(`${stdout}\n${stde
process.exit(1);
}
if (result.status === 0 && UNRESOLVED_IMPORT_RE.test(`${stdout}\n${stderr}`)) {
console.error(
"Build emitted [UNRESOLVED_IMPORT]. Declare or bundle the missing dependency instead of silently externalizing it.",
);
const fatalUnresolvedImport =
result.status === 0 ? findFatalUnresolvedImport(`${stdout}\n${stderr}`.split("\n")) : null;
if (fatalUnresolvedImport) {
console.error(`Build emitted [UNRESOLVED_IMPORT] outside extensions: ${fatalUnresolvedImport}`);
process.exit(1);
}