feat: add model thinking level metadata

closes #3208
This commit is contained in:
Mario Zechner
2026-05-02 01:20:01 +02:00
parent 73b7b2c564
commit 80f06d3636
28 changed files with 527 additions and 243 deletions

View File

@@ -24,7 +24,13 @@ import type {
ThinkingLevel,
} from "@mariozechner/pi-agent-core";
import type { AssistantMessage, ImageContent, Message, Model, TextContent } from "@mariozechner/pi-ai";
import { isContextOverflow, modelsAreEqual, resetApiProviders, supportsXhigh } from "@mariozechner/pi-ai";
import {
clampThinkingLevel,
getSupportedThinkingLevels,
isContextOverflow,
modelsAreEqual,
resetApiProviders,
} from "@mariozechner/pi-ai";
import { theme } from "../modes/interactive/theme/theme.js";
import { stripFrontmatter } from "../utils/frontmatter.js";
import { sleep } from "../utils/sleep.js";
@@ -230,9 +236,6 @@ interface ToolDefinitionEntry {
/** Standard thinking levels */
const THINKING_LEVELS: ThinkingLevel[] = ["off", "minimal", "low", "medium", "high"];
/** Thinking levels including xhigh (for supported models) */
const THINKING_LEVELS_WITH_XHIGH: ThinkingLevel[] = ["off", "minimal", "low", "medium", "high", "xhigh"];
// ============================================================================
// AgentSession Class
// ============================================================================
@@ -1546,15 +1549,8 @@ export class AgentSession {
* The provider will clamp to what the specific model supports internally.
*/
getAvailableThinkingLevels(): ThinkingLevel[] {
if (!this.supportsThinking()) return ["off"];
return this.supportsXhighThinking() ? THINKING_LEVELS_WITH_XHIGH : THINKING_LEVELS;
}
/**
* Check if current model supports xhigh thinking level.
*/
supportsXhighThinking(): boolean {
return this.model ? supportsXhigh(this.model) : false;
if (!this.model) return THINKING_LEVELS;
return getSupportedThinkingLevels(this.model) as ThinkingLevel[];
}
/**
@@ -1574,22 +1570,8 @@ export class AgentSession {
return this.thinkingLevel;
}
private _clampThinkingLevel(level: ThinkingLevel, availableLevels: ThinkingLevel[]): ThinkingLevel {
const ordered = THINKING_LEVELS_WITH_XHIGH;
const available = new Set(availableLevels);
const requestedIndex = ordered.indexOf(level);
if (requestedIndex === -1) {
return availableLevels[0] ?? "off";
}
for (let i = requestedIndex; i < ordered.length; i++) {
const candidate = ordered[i];
if (available.has(candidate)) return candidate;
}
for (let i = requestedIndex - 1; i >= 0; i--) {
const candidate = ordered[i];
if (available.has(candidate)) return candidate;
}
return availableLevels[0] ?? "off";
private _clampThinkingLevel(level: ThinkingLevel, _availableLevels: ThinkingLevel[]): ThinkingLevel {
return this.model ? (clampThinkingLevel(this.model, level) as ThinkingLevel) : "off";
}
// =========================================================================

View File

@@ -1359,6 +1359,8 @@ export interface ProviderModelConfig {
baseUrl?: string;
/** Whether the model supports extended thinking. */
reasoning: boolean;
/** Maps pi thinking levels to provider/model-specific values; null marks a level unsupported. */
thinkingLevelMap?: Model<Api>["thinkingLevelMap"];
/** Supported input types. */
input: ("text" | "image")[];
/** Cost per token (for tracking, can be 0). */

View File

@@ -80,20 +80,21 @@ const VercelGatewayRoutingSchema = Type.Object({
order: Type.Optional(Type.Array(Type.String())),
});
// Schema for OpenAI compatibility settings
const ReasoningEffortMapSchema = Type.Object({
minimal: Type.Optional(Type.String()),
low: Type.Optional(Type.String()),
medium: Type.Optional(Type.String()),
high: Type.Optional(Type.String()),
xhigh: Type.Optional(Type.String()),
// Schema for thinking level support and provider-specific values
const ThinkingLevelMapValueSchema = Type.Union([Type.String(), Type.Null()]);
const ThinkingLevelMapSchema = Type.Object({
off: Type.Optional(ThinkingLevelMapValueSchema),
minimal: Type.Optional(ThinkingLevelMapValueSchema),
low: Type.Optional(ThinkingLevelMapValueSchema),
medium: Type.Optional(ThinkingLevelMapValueSchema),
high: Type.Optional(ThinkingLevelMapValueSchema),
xhigh: Type.Optional(ThinkingLevelMapValueSchema),
});
const OpenAICompletionsCompatSchema = Type.Object({
supportsStore: Type.Optional(Type.Boolean()),
supportsDeveloperRole: Type.Optional(Type.Boolean()),
supportsReasoningEffort: Type.Optional(Type.Boolean()),
reasoningEffortMap: Type.Optional(ReasoningEffortMapSchema),
supportsUsageInStreaming: Type.Optional(Type.Boolean()),
maxTokensField: Type.Optional(Type.Union([Type.Literal("max_completion_tokens"), Type.Literal("max_tokens")])),
requiresToolResultName: Type.Optional(Type.Boolean()),
@@ -141,6 +142,7 @@ const ModelDefinitionSchema = Type.Object({
api: Type.Optional(Type.String({ minLength: 1 })),
baseUrl: Type.Optional(Type.String({ minLength: 1 })),
reasoning: Type.Optional(Type.Boolean()),
thinkingLevelMap: Type.Optional(ThinkingLevelMapSchema),
input: Type.Optional(Type.Array(Type.Union([Type.Literal("text"), Type.Literal("image")]))),
cost: Type.Optional(
Type.Object({
@@ -160,6 +162,7 @@ const ModelDefinitionSchema = Type.Object({
const ModelOverrideSchema = Type.Object({
name: Type.Optional(Type.String({ minLength: 1 })),
reasoning: Type.Optional(Type.Boolean()),
thinkingLevelMap: Type.Optional(ThinkingLevelMapSchema),
input: Type.Optional(Type.Array(Type.Union([Type.Literal("text"), Type.Literal("image")]))),
cost: Type.Optional(
Type.Object({
@@ -288,6 +291,9 @@ function applyModelOverride(model: Model<Api>, override: ModelOverride): Model<A
// Simple field overrides
if (override.name !== undefined) result.name = override.name;
if (override.reasoning !== undefined) result.reasoning = override.reasoning;
if (override.thinkingLevelMap !== undefined) {
result.thinkingLevelMap = { ...model.thinkingLevelMap, ...override.thinkingLevelMap };
}
if (override.input !== undefined) result.input = override.input as ("text" | "image")[];
if (override.contextWindow !== undefined) result.contextWindow = override.contextWindow;
if (override.maxTokens !== undefined) result.maxTokens = override.maxTokens;
@@ -581,6 +587,7 @@ export class ModelRegistry {
provider: providerName,
baseUrl,
reasoning: modelDef.reasoning ?? false,
thinkingLevelMap: modelDef.thinkingLevelMap,
input: (modelDef.input ?? ["text"]) as ("text" | "image")[],
cost: modelDef.cost ?? defaultCost,
contextWindow: modelDef.contextWindow ?? 128000,
@@ -878,6 +885,7 @@ export class ModelRegistry {
provider: providerName,
baseUrl: modelDef.baseUrl ?? config.baseUrl!,
reasoning: modelDef.reasoning,
thinkingLevelMap: modelDef.thinkingLevelMap,
input: modelDef.input as ("text" | "image")[],
cost: modelDef.cost,
contextWindow: modelDef.contextWindow,
@@ -926,6 +934,7 @@ export interface ProviderConfigInput {
api?: Api;
baseUrl?: string;
reasoning: boolean;
thinkingLevelMap?: Model<Api>["thinkingLevelMap"];
input: ("text" | "image")[];
cost: { input: number; output: number; cacheRead: number; cacheWrite: number };
contextWindow: number;

View File

@@ -1,6 +1,6 @@
import { join } from "node:path";
import { Agent, type AgentMessage, type ThinkingLevel } from "@mariozechner/pi-agent-core";
import { type Message, type Model, streamSimple } from "@mariozechner/pi-ai";
import { clampThinkingLevel, type Message, type Model, streamSimple } from "@mariozechner/pi-ai";
import { getAgentDir } from "../config.js";
import { AgentSession } from "./agent-session.js";
import { formatNoModelsAvailableMessage } from "./auth-guidance.js";
@@ -262,8 +262,10 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
}
// Clamp to model capabilities
if (!model || !model.reasoning) {
if (!model) {
thinkingLevel = "off";
} else {
thinkingLevel = clampThinkingLevel(model, thinkingLevel) as ThinkingLevel;
}
const defaultActiveToolNames: ToolName[] = ["read", "bash", "edit", "write"];

View File

@@ -7,7 +7,7 @@
import { resolve } from "node:path";
import { createInterface } from "node:readline";
import { type ImageContent, modelsAreEqual, supportsXhigh } from "@mariozechner/pi-ai";
import { type ImageContent, modelsAreEqual } from "@mariozechner/pi-ai";
import { ProcessTerminal, setKeybindings, TUI } from "@mariozechner/pi-tui";
import chalk from "chalk";
import { type Args, type Mode, parseArgs, printHelp } from "./cli/args.js";
@@ -595,15 +595,7 @@ export async function main(args: string[], options?: MainOptions) {
});
const cliThinkingOverride = parsed.thinking !== undefined || cliThinkingFromModel;
if (created.session.model && cliThinkingOverride) {
let effectiveThinking = created.session.thinkingLevel;
if (!created.session.model.reasoning) {
effectiveThinking = "off";
} else if (effectiveThinking === "xhigh" && !supportsXhigh(created.session.model)) {
effectiveThinking = "high";
}
if (effectiveThinking !== created.session.thinkingLevel) {
created.session.setThinkingLevel(effectiveThinking);
}
created.session.setThinkingLevel(created.session.thinkingLevel);
}
return {