refactor: migrated thinking configuration to centralized pi-ai module

- Extracted thinking module with ThinkingEffort, ThinkingLevel, and ThinkingMode types to centralize reasoning configuration across packages.
- Migrated ThinkingLevel type from pi-agent-core to pi-ai package with new validation functions parseThinkingLevel() and getAvailableThinkingLevel().
- Consolidated thinking level constants and descriptions into reusable exports (ALL_THINKING_LEVELS, THINKING_MODE_DESCRIPTIONS) for consistent UI display.
- Removed local thinking-effort-label utility and replaced formatThinkingEffortLabel() with centralized formatThinking() function from pi-ai.
- Refactored thinking mode handling to distinguish ThinkingSelector (user-facing with 'off' option) from ThinkingEffort (provider-level).
This commit is contained in:
can1357
2026-03-05 00:03:40 +01:00
parent 2e001bbf8d
commit e1897ce013
33 changed files with 326 additions and 259 deletions
+2 -3
View File
@@ -1,5 +1,4 @@
/**
* Agent class that uses the agent-loop directly.
/** Agent class that uses the agent-loop directly.
* No transport abstraction - calls streamSimple via the loop.
*/
import {
@@ -14,6 +13,7 @@ import {
streamSimple,
type TextContent,
type ThinkingBudgets,
type ThinkingLevel,
type ToolChoice,
type ToolResultMessage,
} from "@oh-my-pi/pi-ai";
@@ -27,7 +27,6 @@ import type {
AgentTool,
AgentToolContext,
StreamFn,
ThinkingLevel,
ToolCallContext,
} from "./types";
+1 -6
View File
@@ -7,6 +7,7 @@ import type {
SimpleStreamOptions,
streamSimple,
TextContent,
ThinkingLevel,
Tool,
ToolChoice,
ToolResultMessage,
@@ -139,12 +140,6 @@ export interface ToolCallContext {
toolCalls: Array<{ id: string; name: string }>;
}
/**
* Thinking/reasoning level for models that support it.
* Note: "xhigh" is only supported by OpenAI gpt-5.1-codex-max, gpt-5.2, gpt-5.2-codex, gpt-5.3, and gpt-5.3-codex models.
*/
export type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
/**
* Extensible interface for custom app messages.
* Apps can extend via declaration merging:
+20
View File
@@ -1,6 +1,21 @@
# Changelog
## [Unreleased]
### Added
- Exported new thinking module with `ThinkingEffort`, `ThinkingLevel`, and `ThinkingMode` types for managing reasoning effort levels
- Added `getAvailableThinkingEffort()` function to determine supported thinking effort levels based on model capabilities
- Added `parseThinkingEffort()`, `parseThinkingLevel()`, and `parseThinkingMode()` functions for parsing thinking configuration strings
- Added `THINKING_LEVELS`, `ALL_THINKING_LEVELS`, and `ALL_THINKING_MODES` constants for iterating over available thinking options
- Added `THINKING_MODE_DESCRIPTIONS` and `THINKING_MODE_LABELS` for displaying thinking modes in user interfaces
- Added `formatThinking()` function to format thinking modes as compact display labels
### Changed
- Refactored thinking level handling to distinguish between `ThinkingEffort` (provider-level, no "off") and `ThinkingLevel` (user-facing, includes "off")
- Updated `ThinkingBudgets` type to use `ThinkingEffort` instead of `ThinkingLevel` for more precise token budget configuration
- Improved reasoning option handling to explicitly support "off" value for disabling reasoning across all providers
- Simplified thinking effort mapping logic by centralizing provider-specific clamping behavior
## [13.7.8] - 2026-03-04
@@ -9,6 +24,7 @@
- Added ZenMux provider support with mixed API routing: Anthropic-owned models discovered from `https://zenmux.ai/api/v1/models` now use the Anthropic transport (`https://zenmux.ai/api/anthropic`), while other ZenMux models use the OpenAI-compatible transport.
## [13.7.7] - 2026-03-04
### Changed
- Modified response ID normalization to preserve existing item ID prefixes when truncating oversized IDs
@@ -19,6 +35,7 @@
- Fixed handling of reasoning item IDs to remain untouched during response normalization while function call IDs are properly normalized
## [13.7.2] - 2026-03-04
### Added
- Added support for Kagi API key authentication via `login kagi` command
@@ -31,6 +48,7 @@
- Tool schema compilation is now cached per schema identity, eliminating redundant recompilation on every tool call
## [13.6.0] - 2026-03-03
### Added
- Added Anthropic Foundry gateway mode controlled by `CLAUDE_CODE_USE_FOUNDRY`, with support for `FOUNDRY_BASE_URL`, `ANTHROPIC_FOUNDRY_API_KEY`, `ANTHROPIC_CUSTOM_HEADERS`, and optional mTLS material (`CLAUDE_CODE_CLIENT_CERT`, `CLAUDE_CODE_CLIENT_KEY`, `NODE_EXTRA_CA_CERTS`)
@@ -43,6 +61,7 @@
- Anthropic auth base-URL fallback now prefers `FOUNDRY_BASE_URL` when `CLAUDE_CODE_USE_FOUNDRY` is enabled
## [13.5.8] - 2026-03-02
### Fixed
- Fixed schema compatibility issue where patternProperties in tool parameters caused failures when converting to legacy Antigravity format
@@ -59,6 +78,7 @@
- Anthropic cache-control normalization now removes later `ttl: "1h"` entries when a default/5m block has already appeared earlier in evaluation order
## [13.5.3] - 2026-03-01
### Fixed
- Fixed tool argument coercion to handle malformed JSON with trailing wrapper braces by parsing leading JSON containers
+1
View File
@@ -21,6 +21,7 @@ export * from "./providers/openai-responses";
export * from "./providers/synthetic";
export * from "./rate-limit-utils";
export * from "./stream";
export * from "./thinking";
export * from "./types";
export * from "./usage";
export * from "./usage/claude";
+7 -6
View File
@@ -22,6 +22,7 @@ import {
import { $env } from "@oh-my-pi/pi-utils";
import { NodeHttpHandler } from "@smithy/node-http-handler";
import { calculateCost } from "../models";
import type { ThinkingEffort, ThinkingLevel } from "../thinking";
import type {
Api,
AssistantMessage,
@@ -35,7 +36,6 @@ import type {
TextContent,
ThinkingBudgets,
ThinkingContent,
ThinkingLevel,
Tool,
ToolCall,
ToolResultMessage,
@@ -622,14 +622,15 @@ function buildAdditionalModelRequestFields(
model: Model<"bedrock-converse-stream">,
options: BedrockOptions,
): Record<string, any> | undefined {
if (!options.reasoning || !model.reasoning) {
const reasoning = options.reasoning;
if (!reasoning || !model.reasoning || reasoning === "off") {
return undefined;
}
if (model.id.includes("anthropic.claude")) {
// Opus 4.6+ / Sonnet 4.6+ uses adaptive thinking with effort levels
if (supportsAdaptiveThinking(model.id)) {
let effort = mapThinkingLevelToEffort(options.reasoning);
let effort = mapThinkingLevelToEffort(reasoning);
// "max" effort is only supported on Opus 4.6; clamp to "high" for Sonnet 4.6
const supportsMax = model.id.includes("opus-4-6") || model.id.includes("opus-4.6");
if (effort === "max" && !supportsMax) {
@@ -642,7 +643,7 @@ function buildAdditionalModelRequestFields(
return result;
}
const defaultBudgets: Record<ThinkingLevel, number> = {
const defaultBudgets: Record<ThinkingEffort, number> = {
minimal: 1024,
low: 2048,
medium: 8192,
@@ -651,8 +652,8 @@ function buildAdditionalModelRequestFields(
};
// Custom budgets override defaults (xhigh not in ThinkingBudgets, use high)
const level = options.reasoning === "xhigh" ? "high" : options.reasoning;
const budget = options.thinkingBudgets?.[level] ?? defaultBudgets[options.reasoning];
const level = reasoning === "xhigh" ? "high" : reasoning;
const budget = options.thinkingBudgets?.[level] ?? defaultBudgets[level];
const result: Record<string, any> = {
thinking: {
+43 -58
View File
@@ -21,6 +21,7 @@ import { streamOpenAICodexResponses } from "./providers/openai-codex-responses";
import { type OpenAICompletionsOptions, streamOpenAICompletions } from "./providers/openai-completions";
import { streamOpenAIResponses } from "./providers/openai-responses";
import { isSyntheticModel, streamSynthetic } from "./providers/synthetic";
import { getAvailableThinkingEffort, type ThinkingEffort } from "./thinking";
import type {
Api,
AssistantMessage,
@@ -31,7 +32,6 @@ import type {
SimpleStreamOptions,
StreamOptions,
ThinkingBudgets,
ThinkingLevel,
ToolChoice,
} from "./types";
@@ -303,7 +303,7 @@ const MIN_OUTPUT_TOKENS = 1024;
export const OUTPUT_FALLBACK_BUFFER = 4000;
const ANTHROPIC_USE_INTERLEAVED_THINKING = true;
export const ANTHROPIC_THINKING: Record<ThinkingLevel, number> = {
export const ANTHROPIC_THINKING: Record<ThinkingEffort, number> = {
minimal: 1024,
low: 4096,
medium: 8192,
@@ -311,7 +311,7 @@ export const ANTHROPIC_THINKING: Record<ThinkingLevel, number> = {
xhigh: 32768,
};
const GOOGLE_THINKING: Record<ThinkingLevel, number> = {
const GOOGLE_THINKING: Record<ThinkingEffort, number> = {
minimal: 1024,
low: 4096,
medium: 8192,
@@ -319,7 +319,7 @@ const GOOGLE_THINKING: Record<ThinkingLevel, number> = {
xhigh: 24575,
};
const BEDROCK_CLAUDE_THINKING: Record<ThinkingLevel, number> = {
const BEDROCK_CLAUDE_THINKING: Record<ThinkingEffort, number> = {
minimal: 1024,
low: 2048,
medium: 8192,
@@ -330,8 +330,8 @@ const BEDROCK_CLAUDE_THINKING: Record<ThinkingLevel, number> = {
function resolveBedrockThinkingBudget(
model: Model<"bedrock-converse-stream">,
options?: SimpleStreamOptions,
): { budget: number; level: ThinkingLevel } | null {
if (!options?.reasoning || !model.reasoning) return null;
): { budget: number; level: ThinkingEffort } | null {
if (!options?.reasoning || !model.reasoning || options.reasoning === "off") return null;
if (!model.id.includes("anthropic.claude")) return null;
const level = options.reasoning === "xhigh" ? "high" : options.reasoning;
const budget = options.thinkingBudgets?.[level] ?? BEDROCK_CLAUDE_THINKING[level];
@@ -358,7 +358,7 @@ export function mapAnthropicToolChoice(choice?: ToolChoice): AnthropicOptions["t
/**
* Map ThinkingLevel to Anthropic effort levels for adaptive thinking (Opus 4.6+)
*/
function mapThinkingLevelToAnthropicEffort(level: ThinkingLevel): AnthropicOptions["effort"] {
function mapThinkingLevelToAnthropicEffort(level: ThinkingEffort, supportsXhigh: boolean): AnthropicOptions["effort"] {
switch (level) {
case "minimal":
return "low";
@@ -369,7 +369,7 @@ function mapThinkingLevelToAnthropicEffort(level: ThinkingLevel): AnthropicOptio
case "high":
return "high";
case "xhigh":
return "max";
return supportsXhigh ? "max" : "high";
default:
return "high";
}
@@ -429,14 +429,11 @@ function mapOptionsForApi<TApi extends Api>(
execHandlers: options?.execHandlers,
};
// Helper to clamp xhigh to high for providers that don't support it
const clampReasoning = (effort: ThinkingLevel | undefined) => (effort === "xhigh" ? "high" : effort);
switch (model.api) {
case "anthropic-messages": {
// Explicitly disable thinking when reasoning is not specified
const reasoning = options?.reasoning;
if (!reasoning) {
if (!reasoning || reasoning === "off") {
return {
...base,
thinkingEnabled: false,
@@ -462,9 +459,7 @@ function mapOptionsForApi<TApi extends Api>(
model.id.includes("sonnet-4.6")
) {
const supportsMaxEffort = model.id.includes("opus-4-6") || model.id.includes("opus-4.6");
const effort = mapThinkingLevelToAnthropicEffort(
supportsMaxEffort ? reasoning : (clampReasoning(reasoning) ?? reasoning),
);
const effort = mapThinkingLevelToAnthropicEffort(reasoning, supportsMaxEffort);
return {
...base,
thinkingEnabled: true,
@@ -535,28 +530,28 @@ function mapOptionsForApi<TApi extends Api>(
case "openai-completions":
return {
...base,
reasoningEffort: supportsXhigh(model) ? options?.reasoning : clampReasoning(options?.reasoning),
reasoningEffort: getAvailableThinkingEffort(supportsXhigh(model)),
toolChoice: mapOpenAiToolChoice(options?.toolChoice),
} as OptionsForApi<TApi>;
case "openai-responses":
return {
...base,
reasoningEffort: supportsXhigh(model) ? options?.reasoning : clampReasoning(options?.reasoning),
reasoningEffort: getAvailableThinkingEffort(supportsXhigh(model)),
toolChoice: mapOpenAiToolChoice(options?.toolChoice),
} as OptionsForApi<TApi>;
case "azure-openai-responses":
return {
...base,
reasoningEffort: supportsXhigh(model) ? options?.reasoning : clampReasoning(options?.reasoning),
reasoningEffort: getAvailableThinkingEffort(supportsXhigh(model)),
toolChoice: mapOpenAiToolChoice(options?.toolChoice),
} as OptionsForApi<TApi>;
case "openai-codex-responses":
return {
...base,
reasoningEffort: supportsXhigh(model) ? options?.reasoning : clampReasoning(options?.reasoning),
reasoningEffort: getAvailableThinkingEffort(supportsXhigh(model)),
toolChoice: mapOpenAiToolChoice(options?.toolChoice),
preferWebsockets: options?.preferWebsockets,
} as OptionsForApi<TApi>;
@@ -564,7 +559,8 @@ function mapOptionsForApi<TApi extends Api>(
case "google-generative-ai": {
// Explicitly disable thinking when reasoning is not specified
// This is needed because Gemini has "dynamic thinking" enabled by default
if (!options?.reasoning) {
const reasoning = options?.reasoning;
if (!reasoning || reasoning === "off") {
return {
...base,
thinking: { enabled: false },
@@ -573,7 +569,7 @@ function mapOptionsForApi<TApi extends Api>(
}
const googleModel = model as Model<"google-generative-ai">;
const effort = clampReasoning(options.reasoning)!;
const effort = reasoning === "xhigh" ? "high" : reasoning;
// Gemini 3+ models use thinkingLevel exclusively instead of thinkingBudget.
// https://ai.google.dev/gemini-api/docs/thinking#set-budget
@@ -599,7 +595,8 @@ function mapOptionsForApi<TApi extends Api>(
}
case "google-gemini-cli": {
if (!options?.reasoning) {
const reasoning = options?.reasoning;
if (!reasoning || reasoning === "off") {
return {
...base,
thinking: { enabled: false },
@@ -607,7 +604,7 @@ function mapOptionsForApi<TApi extends Api>(
} as OptionsForApi<TApi>;
}
const effort = clampReasoning(options.reasoning)!;
const effort = reasoning === "xhigh" ? "high" : reasoning;
// Gemini 3+ models use thinkingLevel instead of thinkingBudget
if (isGemini3ProModelId(model.id) || isGemini3FlashModelId(model.id)) {
@@ -650,7 +647,8 @@ function mapOptionsForApi<TApi extends Api>(
case "google-vertex": {
// Explicitly disable thinking when reasoning is not specified
if (!options?.reasoning) {
const reasoning = options?.reasoning;
if (!reasoning || reasoning === "off") {
return {
...base,
thinking: { enabled: false },
@@ -659,7 +657,7 @@ function mapOptionsForApi<TApi extends Api>(
}
const vertexModel = model as Model<"google-vertex">;
const effort = clampReasoning(options.reasoning)!;
const effort = reasoning === "xhigh" ? "high" : reasoning;
const geminiModel = vertexModel as unknown as Model<"google-generative-ai">;
if (isGemini3ProModel(geminiModel) || isGemini3FlashModel(geminiModel)) {
@@ -698,8 +696,6 @@ function mapOptionsForApi<TApi extends Api>(
}
}
type ClampedThinkingLevel = Exclude<ThinkingLevel, "xhigh">;
function isGemini3ProModelId(modelId: string): boolean {
return /3(?:\.\d+)?-pro/.test(modelId);
}
@@ -718,18 +714,14 @@ function isGemini3FlashModel(model: Model<"google-generative-ai">): boolean {
return isGemini3FlashModelId(model.id);
}
function getGemini3ThinkingLevel(
effort: ClampedThinkingLevel,
model: Model<"google-generative-ai">,
): GoogleThinkingLevel {
function getGemini3ThinkingLevel(effort: ThinkingEffort, model: Model<"google-generative-ai">): GoogleThinkingLevel {
if (isGemini3ProModel(model)) {
// Gemini 3 Pro only supports LOW/HIGH (for now)
switch (effort) {
case "minimal":
case "low":
return "LOW";
case "medium":
case "high":
default:
return "HIGH";
}
}
@@ -741,20 +733,19 @@ function getGemini3ThinkingLevel(
return "LOW";
case "medium":
return "MEDIUM";
case "high":
default:
return "HIGH";
}
}
function getGeminiCliThinkingLevel(effort: ClampedThinkingLevel, modelId: string): GoogleThinkingLevel {
function getGeminiCliThinkingLevel(effort: ThinkingEffort, modelId: string): GoogleThinkingLevel {
if (isGemini3ProModelId(modelId)) {
// Gemini 3 Pro only supports LOW/HIGH (for now)
switch (effort) {
case "minimal":
case "low":
return "LOW";
case "medium":
case "high":
default:
return "HIGH";
}
}
@@ -766,41 +757,35 @@ function getGeminiCliThinkingLevel(effort: ClampedThinkingLevel, modelId: string
return "LOW";
case "medium":
return "MEDIUM";
case "high":
default:
return "HIGH";
}
}
function getGoogleBudget(
model: Model<"google-generative-ai">,
effort: ClampedThinkingLevel,
effort: ThinkingEffort,
customBudgets?: ThinkingBudgets,
): number {
effort = effort === "xhigh" ? "high" : effort;
// Custom budgets take precedence if provided for this level
if (customBudgets?.[effort] !== undefined) {
return customBudgets[effort]!;
}
// See https://ai.google.dev/gemini-api/docs/thinking#set-budget
if (model.id.includes("2.5-pro")) {
const budgets: Record<ClampedThinkingLevel, number> = {
minimal: 128,
low: 2048,
medium: 8192,
high: 32768,
};
return budgets[effort];
}
if (model.id.includes("2.5-flash")) {
// Covers 2.5-flash-lite as well
const budgets: Record<ClampedThinkingLevel, number> = {
minimal: 128,
low: 2048,
medium: 8192,
high: 24576,
};
return budgets[effort];
if (model.id.includes("2.5-")) {
switch (effort) {
case "minimal":
return 128;
case "low":
return 2048;
case "medium":
return 8192;
default:
return model.id.includes("2.5-flash") ? 24576 : 32768;
}
}
// Unknown model - use dynamic
+117
View File
@@ -0,0 +1,117 @@
/** Provider-level thinking levels (no "off"), ordered least to most. */
export type ThinkingEffort = "minimal" | "low" | "medium" | "high" | "xhigh";
/**
* ThinkingLevel extended with "off" to disable reasoning entirely.
* Used in UI, config, session state, and CLI args.
* "off" is never sent to providers — callers strip it before streaming.
*/
export type ThinkingLevel = ThinkingEffort | "off";
/**
* ThinkingSelector extended with "inherit" to indicate the role should
* use the session-level default rather than an explicit choice.
* Used in per-role model assignment UI.
*/
export type ThinkingMode = ThinkingLevel | "inherit";
/** Provider-level thinking levels (no "off"), ordered least to most. */
export const THINKING_LEVELS = [
"minimal",
"low",
"medium",
"high",
"xhigh",
] as const satisfies readonly ThinkingEffort[];
/** All selectable thinking levels including "off", ordered none to maximum. */
export const ALL_THINKING_LEVELS = [
"off",
"minimal",
"low",
"medium",
"high",
"xhigh",
] as const satisfies readonly ThinkingLevel[];
/** All thinking modes including "inherit", ordered inherit → none → maximum. */
export const ALL_THINKING_MODES = [
"inherit",
"off",
"minimal",
"low",
"medium",
"high",
"xhigh",
] as const satisfies readonly ThinkingMode[];
/** Human-readable descriptions for every thinking mode. */
export const THINKING_MODE_DESCRIPTIONS: Record<ThinkingMode, string> = {
inherit: "Inherit session default",
off: "No reasoning",
minimal: "Very brief reasoning (~1k tokens)",
low: "Light reasoning (~2k tokens)",
medium: "Moderate reasoning (~8k tokens)",
high: "Deep reasoning (~16k tokens)",
xhigh: "Maximum reasoning (~32k tokens)",
};
/** Compact display labels for every thinking mode. */
export const THINKING_MODE_LABELS: Record<ThinkingMode, string> = {
inherit: "inherit",
off: "off",
minimal: "min",
low: "low",
medium: "medium",
high: "high",
xhigh: "xhigh",
};
const F_LEVEL = 3;
const F_SEL = 2;
const F_MODE = 1;
const F_THINKING: Record<string, number> = {
inherit: F_MODE,
off: F_SEL,
minimal: F_LEVEL,
low: F_LEVEL,
medium: F_LEVEL,
high: F_LEVEL,
xhigh: F_LEVEL,
};
// Parses an unknown value and returns a ThinkingLevel if valid, otherwise undefined.
export function parseThinkingEffort(level: string | null | undefined): ThinkingEffort | undefined {
return level && (F_THINKING[level] ?? 0) >= F_LEVEL ? (level as ThinkingEffort) : undefined;
}
// Parses an unknown value and returns a ThinkingSelector if valid, otherwise undefined.
export function parseThinkingLevel(level: string | null | undefined): ThinkingLevel | undefined {
return level && (F_THINKING[level] ?? 0) >= F_SEL ? (level as ThinkingLevel) : undefined;
}
// Parses an unknown value and returns a ThinkingMode if valid, otherwise undefined.
export function parseThinkingMode(level: string | null | undefined): ThinkingMode | undefined {
return level && (F_THINKING[level] ?? 0) >= F_MODE ? (level as ThinkingMode) : undefined;
}
/** Format a thinking mode as a compact display label. */
export function formatThinking(mode: ThinkingMode): string {
return THINKING_MODE_LABELS[mode];
}
const REG_LVL: readonly ThinkingLevel[] = ["off", "minimal", "low", "medium", "high"];
const XHI_LVL: readonly ThinkingLevel[] = ["off", "minimal", "low", "medium", "high", "xhigh"];
/** Returns the available thinking modes for a model based on whether it supports xhigh. */
export function getAvailableThinkingLevel(hasXhigh: boolean): ReadonlyArray<ThinkingLevel> {
return hasXhigh ? XHI_LVL : REG_LVL;
}
const REG_EFF: readonly ThinkingEffort[] = ["minimal", "low", "medium", "high"];
const XHI_EFF: readonly ThinkingEffort[] = ["minimal", "low", "medium", "high", "xhigh"];
export function getAvailableThinkingEffort(hasXhigh: boolean): ReadonlyArray<ThinkingEffort> {
return hasXhigh ? XHI_EFF : REG_EFF;
}
+2 -2
View File
@@ -109,10 +109,10 @@ export type KnownProvider =
| "lm-studio";
export type Provider = KnownProvider | string;
export type ThinkingLevel = "minimal" | "low" | "medium" | "high" | "xhigh";
import type { ThinkingEffort, ThinkingLevel } from "./thinking";
/** Token budgets for each thinking level (token-based providers only) */
export type ThinkingBudgets = { [key in ThinkingLevel]?: number };
export type ThinkingBudgets = { [key in ThinkingEffort]?: number };
export type MessageAttribution = "user" | "agent";
+16
View File
@@ -1,6 +1,22 @@
# Changelog
## [Unreleased]
### Changed
- Moved `ThinkingLevel` type from `@oh-my-pi/pi-agent-core` to `@oh-my-pi/pi-ai` for centralized thinking level definitions
- Replaced local thinking level validation with `parseThinkingLevel()` and `ALL_THINKING_LEVELS` from `@oh-my-pi/pi-ai`
- Updated thinking level option providers to use `THINKING_MODE_DESCRIPTIONS` from `@oh-my-pi/pi-ai` for consistent descriptions
- Renamed `RoleThinkingMode` type to `ThinkingMode` and changed default value from `'default'` to `'inherit'` for clarity
- Replaced `formatThinkingEffortLabel()` utility with `formatThinking()` from `@oh-my-pi/pi-ai`
- Renamed `extractExplicitThinkingLevel()` to `extractExplicitThinkingSelector()` in model resolver
- Updated thinking level clamping to use `getAvailableThinkingLevel()` from `@oh-my-pi/pi-ai`
### Removed
- Removed `thinking-effort-label.ts` utility file (functionality moved to `@oh-my-pi/pi-ai`)
- Removed local `VALID_THINKING_LEVELS` constant definitions across multiple files
- Removed `isValidThinkingLevel()` function (replaced by `parseThinkingLevel()` from `@oh-my-pi/pi-ai`)
- Removed `parseThinkingLevel()` helper from discovery module (now uses centralized version from `@oh-my-pi/pi-ai`)
## [13.8.0] - 2026-03-04
### Added
+6 -12
View File
@@ -1,7 +1,7 @@
/**
* CLI argument parsing and help display
*/
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import { ALL_THINKING_LEVELS, parseThinkingLevel, type ThinkingLevel } from "@oh-my-pi/pi-ai";
import { APP_NAME, CONFIG_DIR_NAME, logger } from "@oh-my-pi/pi-utils";
import chalk from "chalk";
import { BUILTIN_TOOLS } from "../tools";
@@ -48,12 +48,6 @@ export interface Args {
unknownFlags: Map<string, boolean | string>;
}
const VALID_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
export function isValidThinkingLevel(level: string): level is ThinkingLevel {
return VALID_THINKING_LEVELS.includes(level as ThinkingLevel);
}
export function parseArgs(args: string[], extensionFlags?: Map<string, { type: "boolean" | "string" }>): Args {
const result: Args = {
messages: [],
@@ -127,13 +121,13 @@ export function parseArgs(args: string[], extensionFlags?: Map<string, { type: "
}
result.tools = validTools;
} else if (arg === "--thinking" && i + 1 < args.length) {
const level = args[++i];
if (isValidThinkingLevel(level)) {
result.thinking = level;
const thinking = parseThinkingLevel(args[++i]);
if (thinking !== undefined) {
result.thinking = thinking;
} else {
logger.warn("Invalid thinking level passed to --thinking", {
level,
validThinkingLevels: [...VALID_THINKING_LEVELS],
level: thinking,
validThinkingLevels: [...ALL_THINKING_LEVELS],
});
}
} else if (arg === "--print" || arg === "-p") {
+2 -1
View File
@@ -2,6 +2,7 @@
* Root command for the coding agent CLI.
*/
import { ALL_THINKING_LEVELS } from "@oh-my-pi/pi-ai";
import { APP_NAME } from "@oh-my-pi/pi-utils";
import { Args, Command, Flags } from "@oh-my-pi/pi-utils/cli";
import { parseArgs } from "../cli/args";
@@ -86,7 +87,7 @@ export default class Index extends Command {
}),
thinking: Flags.string({
description: "Set thinking level: off, minimal, low, medium, high, xhigh",
options: ["off", "minimal", "low", "medium", "high", "xhigh"],
options: [...ALL_THINKING_LEVELS],
}),
hook: Flags.string({
description: "Load a hook/extension file (can be used multiple times)",
@@ -1,19 +1,21 @@
/**
* Model resolution, scoping, and initial selection
*/
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import { type Api, DEFAULT_MODEL_PER_PROVIDER, type KnownProvider, type Model, modelsAreEqual } from "@oh-my-pi/pi-ai";
import {
type Api,
DEFAULT_MODEL_PER_PROVIDER,
type KnownProvider,
type Model,
modelsAreEqual,
parseThinkingLevel,
type ThinkingLevel,
} from "@oh-my-pi/pi-ai";
import chalk from "chalk";
import MODEL_PRIO from "../priority.json" with { type: "json" };
import { fuzzyMatch } from "../utils/fuzzy";
import { MODEL_ROLE_IDS, type ModelRegistry, type ModelRole } from "./model-registry";
import type { Settings } from "./settings";
const VALID_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
function isValidThinkingLevel(level: string): level is ThinkingLevel {
return VALID_THINKING_LEVELS.includes(level as ThinkingLevel);
}
/** Default model IDs for each known provider */
export const defaultModelPerProvider: Record<KnownProvider, string> = DEFAULT_MODEL_PER_PROVIDER;
@@ -38,8 +40,9 @@ export function parseModelString(
const colonIdx = id.lastIndexOf(":");
if (colonIdx !== -1) {
const suffix = id.slice(colonIdx + 1);
if (isValidThinkingLevel(suffix)) {
return { provider, id: id.slice(0, colonIdx), thinkingLevel: suffix };
const thinkingLevel = parseThinkingLevel(suffix);
if (thinkingLevel) {
return { provider, id: id.slice(0, colonIdx), thinkingLevel };
}
}
return { provider, id };
@@ -246,7 +249,7 @@ function parseModelPatternWithContext(
pattern: string,
availableModels: Model<Api>[],
context: ModelPreferenceContext,
options?: { allowInvalidThinkingLevelFallback?: boolean },
options?: { allowInvalidThinkingSelectorFallback?: boolean },
): ParsedModelResult {
// Try exact match first
const exactMatch = tryMatchModel(pattern, availableModels, context);
@@ -264,7 +267,8 @@ function parseModelPatternWithContext(
const prefix = pattern.substring(0, lastColonIndex);
const suffix = pattern.substring(lastColonIndex + 1);
if (isValidThinkingLevel(suffix)) {
const parsedThinkingLevel = parseThinkingLevel(suffix);
if (parsedThinkingLevel) {
// Valid thinking level - recurse on prefix and use this level
const result = parseModelPatternWithContext(prefix, availableModels, context, options);
if (result.model) {
@@ -272,7 +276,7 @@ function parseModelPatternWithContext(
const explicitThinkingLevel = !result.warning;
return {
model: result.model,
thinkingLevel: explicitThinkingLevel ? suffix : undefined,
thinkingLevel: explicitThinkingLevel ? parsedThinkingLevel : undefined,
warning: result.warning,
explicitThinkingLevel,
};
@@ -280,7 +284,7 @@ function parseModelPatternWithContext(
return result;
}
const allowFallback = options?.allowInvalidThinkingLevelFallback ?? true;
const allowFallback = options?.allowInvalidThinkingSelectorFallback ?? true;
if (!allowFallback) {
return { model: undefined, thinkingLevel: undefined, warning: undefined, explicitThinkingLevel: false };
}
@@ -302,7 +306,7 @@ export function parseModelPattern(
pattern: string,
availableModels: Model<Api>[],
preferences?: ModelMatchPreferences,
options?: { allowInvalidThinkingLevelFallback?: boolean },
options?: { allowInvalidThinkingSelectorFallback?: boolean },
): ParsedModelResult {
const context = buildPreferenceContext(availableModels, preferences);
return parseModelPatternWithContext(pattern, availableModels, context, options);
@@ -361,7 +365,7 @@ export function resolveModelRoleValue(
const lastColonIndex = normalized.lastIndexOf(":");
const hasThinkingSuffix =
lastColonIndex > PREFIX_MODEL_ROLE.length && isValidThinkingLevel(normalized.slice(lastColonIndex + 1));
lastColonIndex > PREFIX_MODEL_ROLE.length && parseThinkingLevel(normalized.slice(lastColonIndex + 1));
const aliasCandidate = hasThinkingSuffix ? normalized.slice(0, lastColonIndex) : normalized;
const effectivePattern = expandRoleAlias(aliasCandidate, options?.settings);
const patternWithSuffix = hasThinkingSuffix
@@ -376,7 +380,7 @@ export function resolveModelRoleValue(
return { model, thinkingLevel, explicitThinkingLevel, warning };
}
export function extractExplicitThinkingLevel(
export function extractExplicitThinkingSelector(
value: string | undefined,
settings?: Settings,
): ThinkingLevel | undefined {
@@ -390,7 +394,7 @@ export function extractExplicitThinkingLevel(
visited.add(current);
const lastColonIndex = current.lastIndexOf(":");
const hasThinkingSuffix =
lastColonIndex > PREFIX_MODEL_ROLE.length && isValidThinkingLevel(current.slice(lastColonIndex + 1));
lastColonIndex > PREFIX_MODEL_ROLE.length && parseThinkingLevel(current.slice(lastColonIndex + 1));
if (hasThinkingSuffix) {
return current.slice(lastColonIndex + 1) as ThinkingLevel;
}
@@ -493,8 +497,9 @@ export async function resolveModelScope(
if (colonIdx !== -1) {
const suffix = pattern.substring(colonIdx + 1);
if (isValidThinkingLevel(suffix)) {
thinkingLevel = suffix;
const parsedThinkingLevel = parseThinkingLevel(suffix);
if (parsedThinkingLevel) {
thinkingLevel = parsedThinkingLevel;
explicitThinkingLevel = true;
globPattern = pattern.substring(0, colonIdx);
}
@@ -621,7 +626,7 @@ export function resolveCliModel(options: {
const candidates = provider ? availableModels.filter(model => model.provider === provider) : availableModels;
const { model, thinkingLevel, warning } = parseModelPattern(pattern, candidates, preferences, {
allowInvalidThinkingLevelFallback: false,
allowInvalidThinkingSelectorFallback: false,
});
if (!model) {
@@ -658,7 +663,7 @@ export async function findInitialModel(options: {
isContinuing: boolean;
defaultProvider?: string;
defaultModelId?: string;
defaultThinkingLevel?: ThinkingLevel;
defaultThinkingSelector?: ThinkingLevel;
modelRegistry: ModelRegistry;
}): Promise<InitialModelResult> {
const {
@@ -668,7 +673,7 @@ export async function findInitialModel(options: {
isContinuing,
defaultProvider,
defaultModelId,
defaultThinkingLevel,
defaultThinkingSelector,
modelRegistry,
} = options;
@@ -688,10 +693,10 @@ export async function findInitialModel(options: {
// 2. Use first model from scoped models (skip if continuing/resuming)
if (scopedModels.length > 0 && !isContinuing) {
const scoped = scopedModels[0];
const scopedThinkingLevel = scoped.thinkingLevel ?? defaultThinkingLevel ?? "off";
const scopedThinkingSelector = scoped.thinkingLevel ?? defaultThinkingSelector ?? "off";
return {
model: scoped.model,
thinkingLevel: scopedThinkingLevel,
thinkingLevel: scopedThinkingSelector,
fallbackMessage: undefined,
};
}
@@ -701,8 +706,8 @@ export async function findInitialModel(options: {
const found = modelRegistry.find(defaultProvider, defaultModelId);
if (found) {
model = found;
if (defaultThinkingLevel) {
thinkingLevel = defaultThinkingLevel;
if (defaultThinkingSelector) {
thinkingLevel = defaultThinkingSelector;
}
return { model, thinkingLevel, fallbackMessage: undefined };
}
@@ -1,4 +1,6 @@
/**
import { ALL_THINKING_LEVELS } from "@oh-my-pi/pi-ai";
/** Unified settings schema - single source of truth for all settings.
* Unified settings schema - single source of truth for all settings.
*
* Each setting is defined once here with:
@@ -190,7 +192,7 @@ export const SETTINGS_SCHEMA = {
},
defaultThinkingLevel: {
type: "enum",
values: ["off", "minimal", "low", "medium", "high", "xhigh"] as const,
values: ALL_THINKING_LEVELS,
default: "high",
ui: {
tab: "agent",
+4 -17
View File
@@ -1,6 +1,7 @@
import * as fs from "node:fs";
import * as path from "node:path";
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import type { ThinkingLevel } from "@oh-my-pi/pi-ai";
import { parseThinkingLevel } from "@oh-my-pi/pi-ai";
import { FileType, glob } from "@oh-my-pi/pi-natives";
import { CONFIG_DIR_NAME, tryParseJson } from "@oh-my-pi/pi-utils";
import { readFile } from "../capability/fs";
@@ -9,8 +10,6 @@ import type { Skill, SkillFrontmatter } from "../capability/skill";
import type { LoadContext, LoadResult, SourceMeta } from "../capability/types";
import { parseFrontmatter } from "../utils/frontmatter";
const VALID_THINKING_LEVELS: readonly string[] = ["off", "minimal", "low", "medium", "high", "xhigh"];
/**
* Standard paths for each config source.
*/
@@ -100,18 +99,6 @@ export function createSourceMeta(provider: string, filePath: string, level: "use
};
}
/**
* Parse thinking level from frontmatter.
* Supports keys: thinkingLevel, thinking-level, thinking
*/
export function parseThinkingLevel(frontmatter: Record<string, unknown>): ThinkingLevel | undefined {
const raw = frontmatter.thinkingLevel ?? frontmatter["thinking-level"] ?? frontmatter.thinking;
if (typeof raw === "string" && VALID_THINKING_LEVELS.includes(raw)) {
return raw as ThinkingLevel;
}
return undefined;
}
export function parseBoolean(value: unknown): boolean | undefined {
if (typeof value === "boolean") return value;
if (typeof value === "string") {
@@ -247,10 +234,10 @@ export function parseAgentFields(frontmatter: Record<string, unknown>): ParsedAg
}
const output = frontmatter.output !== undefined ? frontmatter.output : undefined;
const thinkingLevel =
typeof frontmatter.thinkingLevel === "string" ? parseThinkingLevel(frontmatter.thinkingLevel) : undefined;
const model = parseModelList(frontmatter.model);
const thinkingLevel = parseThinkingLevel(frontmatter);
const blocking = parseBoolean(frontmatter.blocking);
return { name, description, tools, spawns, model, output, thinkingLevel, blocking };
}
@@ -4,8 +4,7 @@
import type * as fs1 from "node:fs";
import * as fs from "node:fs/promises";
import * as path from "node:path";
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import type { ImageContent, Model, TextContent } from "@oh-my-pi/pi-ai";
import type { ImageContent, Model, TextContent, ThinkingLevel } from "@oh-my-pi/pi-ai";
import * as piCodingAgent from "@oh-my-pi/pi-coding-agent";
import type { KeyId } from "@oh-my-pi/pi-tui";
import { hasFsCode, isEacces, isEnoent, logger } from "@oh-my-pi/pi-utils";
@@ -7,7 +7,7 @@
* - Register commands, keyboard shortcuts, and CLI flags
* - Interact with the user via UI primitives
*/
import type { AgentMessage, AgentToolResult, AgentToolUpdateCallback, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import type { AgentMessage, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
import type {
Api,
AssistantMessageEvent,
@@ -19,6 +19,7 @@ import type {
OAuthLoginCallbacks,
SimpleStreamOptions,
TextContent,
ThinkingLevel,
ToolResultMessage,
} from "@oh-my-pi/pi-ai";
import type * as piCodingAgent from "@oh-my-pi/pi-coding-agent";
@@ -1,12 +1,17 @@
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import { type Model, modelsAreEqual, supportsXhigh } from "@oh-my-pi/pi-ai";
import {
formatThinking,
getAvailableThinkingLevel,
type Model,
modelsAreEqual,
supportsXhigh,
type ThinkingMode,
} from "@oh-my-pi/pi-ai";
import { Container, Input, matchesKey, Spacer, type Tab, TabBar, Text, type TUI, visibleWidth } from "@oh-my-pi/pi-tui";
import { MODEL_ROLE_IDS, MODEL_ROLES, type ModelRegistry, type ModelRole } from "../../config/model-registry";
import { resolveModelRoleValue } from "../../config/model-resolver";
import type { Settings } from "../../config/settings";
import { type ThemeColor, theme } from "../../modes/theme/theme";
import { fuzzyFilter } from "../../utils/fuzzy";
import { formatThinkingEffortLabel } from "../../utils/thinking-effort-label";
import { getTabBarTheme } from "../shared";
import { DynamicBorder } from "./dynamic-border";
@@ -16,11 +21,6 @@ function makeInvertedBadge(label: string, color: ThemeColor): string {
return `${bgAnsi}\x1b[30m ${label} \x1b[39m\x1b[49m`;
}
function formatRoleThinkingModeLabel(thinkingMode: RoleThinkingMode): string {
if (thinkingMode === "default") return "inherit";
return formatThinkingEffortLabel(thinkingMode);
}
interface ModelItem {
provider: string;
id: string;
@@ -32,14 +32,12 @@ interface ScopedModelItem {
thinkingLevel: string;
}
type RoleThinkingMode = "default" | ThinkingLevel;
interface RoleAssignment {
model: Model;
thinkingMode: RoleThinkingMode;
thinkingMode: ThinkingMode;
}
type RoleSelectCallback = (model: Model, role: ModelRole | null, thinkingMode?: RoleThinkingMode) => void;
type RoleSelectCallback = (model: Model, role: ModelRole | null, thinkingMode?: ThinkingMode) => void;
type CancelCallback = () => void;
interface MenuRoleAction {
label: string;
@@ -55,7 +53,6 @@ const MENU_ROLE_ACTIONS: MenuRoleAction[] = MODEL_ROLE_IDS.map(role => {
};
});
const THINKING_MODE_OPTIONS: RoleThinkingMode[] = ["default", "off", "minimal", "low", "medium", "high"];
const ALL_TAB = "ALL";
/**
@@ -100,7 +97,7 @@ export class ModelSelectorComponent extends Container {
settings: Settings,
modelRegistry: ModelRegistry,
scopedModels: ReadonlyArray<ScopedModelItem>,
onSelect: (model: Model, role: ModelRole | null, thinkingMode?: RoleThinkingMode) => void,
onSelect: (model: Model, role: ModelRole | null, thinkingMode?: ThinkingMode) => void,
onCancel: () => void,
options?: { temporaryOnly?: boolean; initialSearchInput?: string },
) {
@@ -195,7 +192,7 @@ export class ModelSelectorComponent extends Container {
if (model) {
this.#roles[role] = {
model,
thinkingMode: explicitThinkingLevel && thinkingLevel !== undefined ? thinkingLevel : "default",
thinkingMode: explicitThinkingLevel && thinkingLevel !== undefined ? thinkingLevel : "inherit",
};
}
}
@@ -412,7 +409,7 @@ export class ModelSelectorComponent extends Container {
if (!tag || !assigned || !modelsAreEqual(assigned.model, item.model)) continue;
const badge = makeInvertedBadge(tag, color ?? "success");
const thinkingLabel = formatRoleThinkingModeLabel(assigned.thinkingMode);
const thinkingLabel = formatThinking(assigned.thinkingMode);
roleBadgeTokens.push(`${badge} ${theme.fg("dim", `(${thinkingLabel})`)}`);
}
const badgeText = roleBadgeTokens.length > 0 ? ` ${roleBadgeTokens.join(" ")}` : "";
@@ -459,16 +456,12 @@ export class ModelSelectorComponent extends Container {
this.#listContainer.addChild(new Text(theme.fg("muted", ` Model Name: ${selected.model.name}`), 0, 0));
}
}
#getThinkingModesForModel(model: Model): RoleThinkingMode[] {
const thinkingModes = [...THINKING_MODE_OPTIONS];
if (supportsXhigh(model)) {
thinkingModes.push("xhigh");
}
return thinkingModes;
#getThinkingModesForModel(model: Model): ReadonlyArray<ThinkingMode> {
return ["inherit", ...getAvailableThinkingLevel(supportsXhigh(model))];
}
#getCurrentRoleThinkingMode(role: ModelRole): RoleThinkingMode {
return this.#roles[role]?.thinkingMode ?? "default";
#getCurrentRoleThinkingMode(role: ModelRole): ThinkingMode {
return this.#roles[role]?.thinkingMode ?? "inherit";
}
#getThinkingPreselectIndex(role: ModelRole, model: Model): number {
@@ -507,7 +500,7 @@ export class ModelSelectorComponent extends Container {
const optionLines = showingThinking
? thinkingOptions.map((thinkingMode, index) => {
const prefix = index === this.#menuSelectedIndex ? ` ${theme.nav.cursor} ` : " ";
const label = formatRoleThinkingModeLabel(thinkingMode);
const label = formatThinking(thinkingMode);
return `${prefix}${label}`;
})
: MENU_ROLE_ACTIONS.map((action, index) => {
@@ -664,12 +657,12 @@ export class ModelSelectorComponent extends Container {
}
}
#formatRoleModelValue(model: Model, thinkingMode: RoleThinkingMode): string {
#formatRoleModelValue(model: Model, thinkingMode: ThinkingMode): string {
const modelKey = `${model.provider}/${model.id}`;
if (thinkingMode === "default") return modelKey;
if (thinkingMode === "inherit") return modelKey;
return `${modelKey}:${thinkingMode}`;
}
#handleSelect(model: Model, role: ModelRole | null, thinkingMode?: RoleThinkingMode): void {
#handleSelect(model: Model, role: ModelRole | null, thinkingMode?: ThinkingMode): void {
// For temporary role, don't save to settings - just notify caller
if (role === null) {
this.#onSelectCallback(model, null);
@@ -6,6 +6,8 @@
* 1. Add it to settings-schema.ts with a `ui` field
* 2. That's it - it appears in the UI automatically
*/
import { ALL_THINKING_LEVELS, THINKING_MODE_DESCRIPTIONS } from "@oh-my-pi/pi-ai";
import { TERMINAL } from "@oh-my-pi/pi-tui";
import {
getDefault,
@@ -222,12 +224,11 @@ const OPTION_PROVIDERS: Partial<Record<SettingPath, OptionProvider>> = {
],
// Default thinking level
defaultThinkingLevel: [
{ value: "off", label: "off", description: "No reasoning" },
{ value: "minimal", label: "minimal", description: "Very brief (~1k tokens)" },
{ value: "low", label: "low", description: "Light (~2k tokens)" },
{ value: "medium", label: "medium", description: "Moderate (~8k tokens)" },
{ value: "high", label: "high", description: "Deep (~16k tokens)" },
{ value: "xhigh", label: "xhigh", description: "Maximum (~32k tokens)" },
...ALL_THINKING_LEVELS.map(level => ({
value: level,
label: level,
description: THINKING_MODE_DESCRIPTIONS[level],
})),
],
// Temperature
temperature: [
@@ -1,4 +1,4 @@
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import type { ThinkingLevel } from "@oh-my-pi/pi-ai";
import {
Container,
matchesKey,
@@ -1,17 +1,9 @@
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import { THINKING_MODE_DESCRIPTIONS, type ThinkingLevel } from "@oh-my-pi/pi-ai";
import { Container, type SelectItem, SelectList } from "@oh-my-pi/pi-tui";
import { getSelectListTheme } from "../../modes/theme/theme";
import { DynamicBorder } from "./dynamic-border";
const LEVEL_DESCRIPTIONS: Record<ThinkingLevel, string> = {
off: "No reasoning",
minimal: "Very brief reasoning (~1k tokens)",
low: "Light reasoning (~2k tokens)",
medium: "Moderate reasoning (~8k tokens)",
high: "Deep reasoning (~16k tokens)",
xhigh: "Maximum reasoning (~32k tokens)",
};
/**
* Component that renders a thinking level selector with borders
*/
@@ -29,7 +21,7 @@ export class ThinkingSelectorComponent extends Container {
const thinkingLevels: SelectItem[] = availableLevels.map(level => ({
value: level,
label: level,
description: LEVEL_DESCRIPTIONS[level],
description: THINKING_MODE_DESCRIPTIONS[level],
}));
// Add top border
@@ -1,5 +1,4 @@
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import { getOAuthProviders, type OAuthProvider } from "@oh-my-pi/pi-ai";
import { getOAuthProviders, type OAuthProvider, type ThinkingLevel } from "@oh-my-pi/pi-ai";
import type { Component } from "@oh-my-pi/pi-tui";
import { Input, Loader, Spacer, Text } from "@oh-my-pi/pi-tui";
import { getAgentDbPath, getProjectDir } from "@oh-my-pi/pi-utils";
@@ -3,8 +3,8 @@
*
* Spawns the agent in RPC mode and provides a typed API for all operations.
*/
import type { AgentEvent, AgentMessage, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import type { ImageContent } from "@oh-my-pi/pi-ai";
import type { AgentEvent, AgentMessage } from "@oh-my-pi/pi-agent-core";
import type { ImageContent, ThinkingLevel } from "@oh-my-pi/pi-ai";
import { isRecord, ptree, readJsonl } from "@oh-my-pi/pi-utils";
import type { BashResult } from "../../exec/bash-executor";
import type { SessionStats } from "../../session/agent-session";
@@ -4,8 +4,8 @@
* Commands are sent as JSON lines on stdin.
* Responses and events are emitted as JSON lines on stdout.
*/
import type { AgentMessage, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import type { ImageContent, Model } from "@oh-my-pi/pi-ai";
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
import type { ImageContent, Model, ThinkingLevel } from "@oh-my-pi/pi-ai";
import type { BashResult } from "../../exec/bash-executor";
import type { SessionStats } from "../../session/agent-session";
import type { CompactionResult } from "../../session/compaction";
@@ -1,5 +1,6 @@
import * as fs from "node:fs";
import * as path from "node:path";
import type { ThinkingLevel } from "@oh-my-pi/pi-ai";
import {
detectMacOSAppearance,
type HighlightColors as NativeHighlightColors,
@@ -1219,7 +1220,7 @@ export class Theme {
return this.mode;
}
getThinkingBorderColor(level: "off" | "minimal" | "low" | "medium" | "high" | "xhigh"): (str: string) => string {
getThinkingBorderColor(level: ThinkingLevel): (str: string) => string {
// Map thinking levels to dedicated theme colors
switch (level) {
case "off":
+6 -12
View File
@@ -1,12 +1,6 @@
import {
Agent,
type AgentEvent,
type AgentMessage,
type AgentTool,
INTENT_FIELD,
type ThinkingLevel,
} from "@oh-my-pi/pi-agent-core";
import { type Message, type Model, supportsXhigh } from "@oh-my-pi/pi-ai";
import { Agent, type AgentEvent, type AgentMessage, type AgentTool, INTENT_FIELD } from "@oh-my-pi/pi-agent-core";
import { type Message, type Model, supportsXhigh, type ThinkingSelector } from "@oh-my-pi/pi-ai";
import { prewarmOpenAICodexResponses } from "@oh-my-pi/pi-ai/providers/openai-codex-responses";
import type { Component } from "@oh-my-pi/pi-tui";
import { $env, getAgentDbPath, getAgentDir, getProjectDir, logger, postmortem } from "@oh-my-pi/pi-utils";
@@ -124,9 +118,9 @@ export interface CreateAgentSessionOptions {
* Used when model lookup is deferred because extension-provided models aren't registered yet. */
modelPattern?: string;
/** Thinking level. Default: from settings, else 'off' (clamped to model capabilities) */
thinkingLevel?: ThinkingLevel;
thinkingLevel?: ThinkingSelector;
/** Models available for cycling (Ctrl+P in interactive mode) */
scopedModels?: Array<{ model: Model; thinkingLevel: ThinkingLevel }>;
scopedModels?: Array<{ model: Model; thinkingLevel: ThinkingSelector }>;
/** System prompt. String replaces default, function receives default and returns final. */
systemPrompt?: string | ((defaultPrompt: string) => string);
@@ -702,7 +696,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
// If session has data and includes a thinking entry, restore it
if (thinkingLevel === undefined && hasExistingSession && hasThinkingEntry) {
thinkingLevel = existingSession.thinkingLevel as ThinkingLevel;
thinkingLevel = existingSession.thinkingLevel as ThinkingSelector;
}
if (thinkingLevel === undefined && !hasExplicitModel && !hasThinkingEntry && defaultRoleSpec.explicitThinkingLevel) {
@@ -24,7 +24,6 @@ import {
type AgentState,
type AgentTool,
INTENT_FIELD,
type ThinkingLevel,
} from "@oh-my-pi/pi-agent-core";
import type {
AssistantMessage,
@@ -33,6 +32,7 @@ import type {
Model,
ProviderSessionState,
TextContent,
ThinkingLevel,
ToolCall,
ToolChoice,
Usage,
@@ -40,6 +40,7 @@ import type {
} from "@oh-my-pi/pi-ai";
import {
calculateRateLimitBackoffMs,
getAvailableThinkingLevel,
isContextOverflow,
modelsAreEqual,
parseRateLimitReason,
@@ -49,7 +50,7 @@ import { abortableSleep, getAgentDbPath, isEnoent, logger } from "@oh-my-pi/pi-u
import type { AsyncJob, AsyncJobManager } from "../async";
import type { Rule } from "../capability/rule";
import { MODEL_ROLE_IDS, type ModelRegistry, type ModelRole } from "../config/model-registry";
import { extractExplicitThinkingLevel, parseModelString, resolveModelRoleValue } from "../config/model-resolver";
import { extractExplicitThinkingSelector, parseModelString, resolveModelRoleValue } from "../config/model-resolver";
import { expandPromptTemplate, type PromptTemplate, renderPromptTemplate } from "../config/prompt-templates";
import type { Settings, SkillsSettings } from "../config/settings";
import { type BashResult, executeBash as executeBashCommand } from "../exec/bash-executor";
@@ -252,10 +253,6 @@ export interface HandoffResult {
// ============================================================================
/** 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"];
const noOpUIContext: ExtensionUIContext = {
select: async (_title, _options, _dialogOptions) => undefined,
@@ -295,7 +292,6 @@ export class AgentSession {
readonly settings: Settings;
#asyncJobManager: AsyncJobManager | undefined = undefined;
#scopedModels: Array<{ model: Model; thinkingLevel: ThinkingLevel }>;
#promptTemplates: PromptTemplate[];
#slashCommands: FileSlashCommand[];
@@ -2864,9 +2860,9 @@ export class AgentSession {
* Get available thinking levels for current model.
* The provider will clamp to what the specific model supports internally.
*/
getAvailableThinkingLevels(): ThinkingLevel[] {
getAvailableThinkingLevels(): ReadonlyArray<ThinkingLevel> {
if (!this.supportsThinking()) return ["off"];
return this.supportsXhighThinking() ? THINKING_LEVELS_WITH_XHIGH : THINKING_LEVELS;
return getAvailableThinkingLevel(this.supportsXhighThinking());
}
/**
@@ -2883,8 +2879,8 @@ export class AgentSession {
return !!this.model?.reasoning;
}
#clampThinkingLevel(level: ThinkingLevel, availableLevels: ThinkingLevel[]): ThinkingLevel {
const ordered = THINKING_LEVELS_WITH_XHIGH;
#clampThinkingLevel(level: ThinkingLevel, availableLevels: ReadonlyArray<ThinkingLevel>): ThinkingLevel {
const ordered = getAvailableThinkingLevel(this.supportsXhighThinking());
const available = new Set(availableLevels);
const requestedIndex = ordered.indexOf(level);
if (requestedIndex === -1) {
@@ -3592,7 +3588,7 @@ Be thorough - include exact file paths, function names, error messages, and tech
const existingRoleValue = this.settings.getModelRole(role);
if (!existingRoleValue) return modelKey;
const thinkingLevel = extractExplicitThinkingLevel(existingRoleValue, this.settings);
const thinkingLevel = extractExplicitThinkingSelector(existingRoleValue, this.settings);
if (thinkingLevel === undefined) return modelKey;
return `${modelKey}:${thinkingLevel}`;
}
+3 -3
View File
@@ -4,8 +4,8 @@
* Runs each subagent on the main thread and forwards AgentEvents for progress tracking.
*/
import path from "node:path";
import type { AgentEvent, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import type { Api, Model, ToolChoice } from "@oh-my-pi/pi-ai";
import type { AgentEvent } from "@oh-my-pi/pi-agent-core";
import type { Api, Model, ThinkingSelector, ToolChoice } from "@oh-my-pi/pi-ai";
import { logger, untilAborted } from "@oh-my-pi/pi-utils";
import type { TSchema } from "@sinclair/typebox";
import Ajv, { type ValidateFunction } from "ajv";
@@ -143,7 +143,7 @@ export interface ExecutorOptions {
index: number;
id: string;
modelOverride?: string | string[];
thinkingLevel?: ThinkingLevel;
thinkingLevel?: ThinkingSelector;
outputSchema?: unknown;
/** Parent task recursion depth (0 = top-level, 1 = first child, etc.) */
taskDepth?: number;
+1 -2
View File
@@ -1,5 +1,4 @@
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import type { Usage } from "@oh-my-pi/pi-ai";
import type { ThinkingLevel, Usage } from "@oh-my-pi/pi-ai";
import { $env } from "@oh-my-pi/pi-utils";
import { type Static, Type } from "@sinclair/typebox";
import type { NestedRepoPatch } from "./worktree";
@@ -1,14 +0,0 @@
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
const COMPACT_THINKING_EFFORT_LABEL: Record<ThinkingLevel, string> = {
off: "off",
minimal: "min",
low: "low",
medium: "medium",
high: "high",
xhigh: "xhigh",
};
export function formatThinkingEffortLabel(level: ThinkingLevel): string {
return COMPACT_THINKING_EFFORT_LABEL[level];
}
@@ -12,8 +12,8 @@ import { afterEach, beforeEach, describe, expect, it } from "bun:test";
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { Agent, type ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import { getBundledModel, type Model } from "@oh-my-pi/pi-ai";
import { Agent } from "@oh-my-pi/pi-agent-core";
import { getBundledModel, type Model, type ThinkingLevel } from "@oh-my-pi/pi-ai";
import { ModelRegistry } from "@oh-my-pi/pi-coding-agent/config/model-registry";
import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings";
import { AgentSession } from "@oh-my-pi/pi-coding-agent/session/agent-session";
@@ -1,16 +0,0 @@
import { describe, expect, test } from "bun:test";
import { formatThinkingEffortLabel } from "@oh-my-pi/pi-coding-agent/utils/thinking-effort-label";
describe("formatThinkingEffortLabel", () => {
test("formats minimal as min", () => {
expect(formatThinkingEffortLabel("minimal")).toBe("min");
});
test("formats medium as medium", () => {
expect(formatThinkingEffortLabel("medium")).toBe("medium");
});
test("formats xhigh as xhigh", () => {
expect(formatThinkingEffortLabel("xhigh")).toBe("xhigh");
});
});
+5 -7
View File
@@ -11,15 +11,13 @@
import * as fs from "node:fs";
import * as path from "node:path";
import { parseArgs } from "node:util";
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import { ALL_THINKING_LEVELS, type ThinkingSelector } from "@oh-my-pi/pi-ai";
import { padding } from "@oh-my-pi/pi-tui";
import { TempDir } from "@oh-my-pi/pi-utils";
import { generateJsonReport, generateReport } from "./report";
import { type BenchmarkConfig, type ProgressEvent, runBenchmark } from "./runner";
import { type EditTask, loadTasksFromDir, validateFixturesFromDir } from "./tasks";
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
function generateReportFilename(config: BenchmarkConfig, format: "markdown" | "json"): string {
const modelName = config.model
.split("/")
@@ -209,14 +207,14 @@ async function main(): Promise<void> {
process.exit(0);
}
let thinkingLevel: ThinkingLevel = "low";
let thinkingLevel: ThinkingSelector = "low";
if (values.thinking) {
if (!THINKING_LEVELS.includes(values.thinking as ThinkingLevel)) {
if (!ALL_THINKING_LEVELS.includes(values.thinking as ThinkingSelector)) {
console.error(`Invalid thinking level: ${values.thinking}`);
console.error(`Valid levels: ${THINKING_LEVELS.join(", ")}`);
console.error(`Valid levels: ${ALL_THINKING_LEVELS.join(", ")}`);
process.exit(1);
}
thinkingLevel = values.thinking as ThinkingLevel;
thinkingLevel = values.thinking as ThinkingSelector;
}
const runsPerTask = parseInt(values.runs!, 10);
+3 -2
View File
@@ -7,7 +7,8 @@
/// <reference types="./bun-imports.d.ts" />
import * as fs from "node:fs";
import * as path from "node:path";
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import type { ThinkingSelector } from "@oh-my-pi/pi-ai";
import { computeLineHash, RpcClient, renderPromptTemplate } from "@oh-my-pi/pi-coding-agent";
import { Snowflake } from "@oh-my-pi/pi-utils";
import { diffLines } from "diff";
@@ -30,7 +31,7 @@ function makeTempDir(pre?: string): string {
export interface BenchmarkConfig {
provider: string;
model: string;
thinkingLevel?: ThinkingLevel;
thinkingLevel?: ThinkingSelector;
runsPerTask: number;
timeout: number;
maxTurns?: number;