6ff37e346a
- Added `off` and `auto` as valid inputs for the `--thinking` CLI flag. - Centralized thinking level definitions in `CLI_THINKING_LEVELS` to keep flag options, shell completions, and validation in sync. - Configured CLI parsing to reject `inherit` as an explicit input to prevent unintended configuration suppression.
158 lines
5.7 KiB
TypeScript
158 lines
5.7 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from "bun:test";
|
|
import * as path from "node:path";
|
|
import { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
import { Effort, type Model } from "@oh-my-pi/pi-ai";
|
|
import { getBundledModel } from "@oh-my-pi/pi-catalog/models";
|
|
import {
|
|
classifyDifficulty,
|
|
parseDifficultyBucket,
|
|
parseDifficultyLevel,
|
|
} from "@oh-my-pi/pi-coding-agent/auto-thinking/classifier";
|
|
import { ModelRegistry } from "@oh-my-pi/pi-coding-agent/config/model-registry";
|
|
import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings";
|
|
import { AuthStorage } from "@oh-my-pi/pi-coding-agent/session/auth-storage";
|
|
import {
|
|
AUTO_THINKING,
|
|
clampAutoThinkingEffort,
|
|
parseCliThinkingLevel,
|
|
parseConfiguredThinkingLevel,
|
|
parseEffort,
|
|
parseThinkingLevel,
|
|
} from "@oh-my-pi/pi-coding-agent/thinking";
|
|
import type { TinyMemoryLocalModelKey } from "@oh-my-pi/pi-coding-agent/tiny/models";
|
|
import { tinyModelClient } from "@oh-my-pi/pi-coding-agent/tiny/title-client";
|
|
import { TempDir } from "@oh-my-pi/pi-utils";
|
|
|
|
describe("auto thinking classifier helpers", () => {
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
interface LocalClassifierFixture {
|
|
settings: Settings;
|
|
registry: ModelRegistry;
|
|
model: Model;
|
|
cleanup: () => void;
|
|
}
|
|
|
|
async function createLocalClassifierFixture(
|
|
autoThinkingModel: TinyMemoryLocalModelKey,
|
|
): Promise<LocalClassifierFixture> {
|
|
const tempDir = TempDir.createSync("@pi-auto-thinking-classifier-");
|
|
const authStorage = await AuthStorage.create(path.join(tempDir.path(), "auth.db"));
|
|
const model = getBundledModel("anthropic", "claude-sonnet-4-6");
|
|
if (!model) {
|
|
authStorage.close();
|
|
tempDir.removeSync();
|
|
throw new Error("Expected bundled Claude Sonnet 4.6 model");
|
|
}
|
|
|
|
return {
|
|
settings: Settings.isolated({ "providers.autoThinkingModel": autoThinkingModel }),
|
|
registry: new ModelRegistry(authStorage, path.join(tempDir.path(), "models.yml")),
|
|
model,
|
|
cleanup: () => {
|
|
authStorage.close();
|
|
tempDir.removeSync();
|
|
},
|
|
};
|
|
}
|
|
|
|
it("parses configured thinking without widening provider-facing thinking selectors", () => {
|
|
expect(parseConfiguredThinkingLevel(AUTO_THINKING)).toBe(AUTO_THINKING);
|
|
expect(parseConfiguredThinkingLevel(Effort.High)).toBe(Effort.High);
|
|
expect(parseConfiguredThinkingLevel("bogus")).toBeUndefined();
|
|
expect(parseThinkingLevel(AUTO_THINKING)).toBeUndefined();
|
|
expect(parseThinkingLevel(ThinkingLevel.Off)).toBe(ThinkingLevel.Off);
|
|
});
|
|
|
|
it("parses CLI --thinking selectors while rejecting inherit", () => {
|
|
expect(parseCliThinkingLevel(ThinkingLevel.Off)).toBe(ThinkingLevel.Off);
|
|
expect(parseCliThinkingLevel(AUTO_THINKING)).toBe(AUTO_THINKING);
|
|
expect(parseCliThinkingLevel("max")).toBe(ThinkingLevel.XHigh);
|
|
expect(parseCliThinkingLevel(ThinkingLevel.Inherit)).toBeUndefined();
|
|
expect(parseCliThinkingLevel("bogus")).toBeUndefined();
|
|
});
|
|
|
|
it("maps online 4-way classifier labels to effort levels", () => {
|
|
expect(parseDifficultyLevel("x-high")).toBe(Effort.XHigh);
|
|
expect(parseDifficultyLevel("The answer is HIGH.")).toBe(Effort.High);
|
|
expect(parseDifficultyLevel("med")).toBe(Effort.Medium);
|
|
expect(parseDifficultyLevel("low")).toBe(Effort.Low);
|
|
expect(parseDifficultyLevel("unknown")).toBeUndefined();
|
|
});
|
|
|
|
it("maps local 3-bucket labels to coarse effort levels", () => {
|
|
expect(parseDifficultyBucket("trivial")).toBe(Effort.Low);
|
|
expect(parseDifficultyBucket("moderate")).toBe(Effort.High);
|
|
expect(parseDifficultyBucket("hard")).toBe(Effort.XHigh);
|
|
expect(parseDifficultyBucket("medium")).toBeUndefined();
|
|
});
|
|
|
|
it("expands the local reasoning classifier budget", async () => {
|
|
let maxTokens: number | undefined;
|
|
const fixture = await createLocalClassifierFixture("qwen3-1.7b");
|
|
vi.spyOn(tinyModelClient, "complete").mockImplementation(async (_modelKey, _prompt, options) => {
|
|
maxTokens = options?.maxTokens;
|
|
return "moderate";
|
|
});
|
|
|
|
try {
|
|
const effort = await classifyDifficulty("fix the local classifier token budget", {
|
|
settings: fixture.settings,
|
|
registry: fixture.registry,
|
|
model: fixture.model,
|
|
});
|
|
|
|
expect(effort).toBe(Effort.High);
|
|
expect(maxTokens).toBe(1024);
|
|
} finally {
|
|
fixture.cleanup();
|
|
}
|
|
});
|
|
|
|
it("uses a larger local non-reasoning classifier floor", async () => {
|
|
let maxTokens: number | undefined;
|
|
const fixture = await createLocalClassifierFixture("qwen2.5-1.5b");
|
|
vi.spyOn(tinyModelClient, "complete").mockImplementation(async (_modelKey, _prompt, options) => {
|
|
maxTokens = options?.maxTokens;
|
|
return "moderate";
|
|
});
|
|
|
|
try {
|
|
const effort = await classifyDifficulty("rename a local helper", {
|
|
settings: fixture.settings,
|
|
registry: fixture.registry,
|
|
model: fixture.model,
|
|
});
|
|
|
|
expect(effort).toBe(Effort.High);
|
|
expect(maxTokens).toBe(16);
|
|
} finally {
|
|
fixture.cleanup();
|
|
}
|
|
});
|
|
|
|
it("clamps auto effort to model support while never resolving below low", () => {
|
|
const model = getBundledModel("anthropic", "claude-sonnet-4-6");
|
|
if (!model) throw new Error("Expected bundled Claude Sonnet 4.6 model");
|
|
|
|
expect(clampAutoThinkingEffort(model, Effort.XHigh)).toBe(Effort.High);
|
|
expect(clampAutoThinkingEffort(model, Effort.Minimal)).toBe(Effort.Low);
|
|
});
|
|
|
|
it("accepts max as the top configured thinking alias", () => {
|
|
expect(parseEffort("max")).toBe(Effort.XHigh);
|
|
expect(parseThinkingLevel("max")).toBeUndefined();
|
|
expect(parseConfiguredThinkingLevel("max")).toBe(ThinkingLevel.XHigh);
|
|
});
|
|
|
|
it("rejects inherited object keys as thinking selectors", () => {
|
|
for (const selector of ["toString", "constructor", "__proto__"]) {
|
|
expect(parseEffort(selector)).toBeUndefined();
|
|
expect(parseThinkingLevel(selector)).toBeUndefined();
|
|
expect(parseConfiguredThinkingLevel(selector)).toBeUndefined();
|
|
}
|
|
});
|
|
});
|