From e1897ce013357f233aa2677843834ede7652e794 Mon Sep 17 00:00:00 2001 From: can1357 Date: Thu, 5 Mar 2026 00:03:40 +0100 Subject: [PATCH] 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). --- packages/agent/src/agent.ts | 5 +- packages/agent/src/types.ts | 7 +- packages/ai/CHANGELOG.md | 20 +++ packages/ai/src/index.ts | 1 + packages/ai/src/providers/amazon-bedrock.ts | 13 +- packages/ai/src/stream.ts | 101 +++++++-------- packages/ai/src/thinking.ts | 117 ++++++++++++++++++ packages/ai/src/types.ts | 4 +- packages/coding-agent/CHANGELOG.md | 16 +++ packages/coding-agent/src/cli/args.ts | 18 +-- packages/coding-agent/src/commands/launch.ts | 3 +- .../coding-agent/src/config/model-resolver.ts | 57 +++++---- .../src/config/settings-schema.ts | 6 +- .../coding-agent/src/discovery/helpers.ts | 21 +--- .../src/extensibility/extensions/loader.ts | 3 +- .../src/extensibility/extensions/types.ts | 3 +- .../src/modes/components/model-selector.ts | 49 ++++---- .../src/modes/components/settings-defs.ts | 13 +- .../src/modes/components/settings-selector.ts | 2 +- .../src/modes/components/thinking-selector.ts | 14 +-- .../modes/controllers/selector-controller.ts | 3 +- .../coding-agent/src/modes/rpc/rpc-client.ts | 4 +- .../coding-agent/src/modes/rpc/rpc-types.ts | 4 +- .../coding-agent/src/modes/theme/theme.ts | 3 +- packages/coding-agent/src/sdk.ts | 18 +-- .../coding-agent/src/session/agent-session.ts | 20 ++- packages/coding-agent/src/task/executor.ts | 6 +- packages/coding-agent/src/task/types.ts | 3 +- .../src/utils/thinking-effort-label.ts | 14 --- .../test/compaction-thinking-model.test.ts | 4 +- .../test/thinking-effort-label.test.ts | 16 --- packages/react-edit-benchmark/src/index.ts | 12 +- packages/react-edit-benchmark/src/runner.ts | 5 +- 33 files changed, 326 insertions(+), 259 deletions(-) create mode 100644 packages/ai/src/thinking.ts delete mode 100644 packages/coding-agent/src/utils/thinking-effort-label.ts delete mode 100644 packages/coding-agent/test/thinking-effort-label.test.ts diff --git a/packages/agent/src/agent.ts b/packages/agent/src/agent.ts index 73591c8fd..4a3459578 100644 --- a/packages/agent/src/agent.ts +++ b/packages/agent/src/agent.ts @@ -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"; diff --git a/packages/agent/src/types.ts b/packages/agent/src/types.ts index 04d5c1dd1..ec4615ef8 100644 --- a/packages/agent/src/types.ts +++ b/packages/agent/src/types.ts @@ -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: diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index f60d1b349..f8f47ca0e 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -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 diff --git a/packages/ai/src/index.ts b/packages/ai/src/index.ts index 17e685143..4c6a059b0 100644 --- a/packages/ai/src/index.ts +++ b/packages/ai/src/index.ts @@ -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"; diff --git a/packages/ai/src/providers/amazon-bedrock.ts b/packages/ai/src/providers/amazon-bedrock.ts index 77e1a735f..5578ceef0 100644 --- a/packages/ai/src/providers/amazon-bedrock.ts +++ b/packages/ai/src/providers/amazon-bedrock.ts @@ -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 | 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 = { + const defaultBudgets: Record = { 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 = { thinking: { diff --git a/packages/ai/src/stream.ts b/packages/ai/src/stream.ts index 53e091bfc..9d2a71ef0 100644 --- a/packages/ai/src/stream.ts +++ b/packages/ai/src/stream.ts @@ -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 = { +export const ANTHROPIC_THINKING: Record = { minimal: 1024, low: 4096, medium: 8192, @@ -311,7 +311,7 @@ export const ANTHROPIC_THINKING: Record = { xhigh: 32768, }; -const GOOGLE_THINKING: Record = { +const GOOGLE_THINKING: Record = { minimal: 1024, low: 4096, medium: 8192, @@ -319,7 +319,7 @@ const GOOGLE_THINKING: Record = { xhigh: 24575, }; -const BEDROCK_CLAUDE_THINKING: Record = { +const BEDROCK_CLAUDE_THINKING: Record = { minimal: 1024, low: 2048, medium: 8192, @@ -330,8 +330,8 @@ const BEDROCK_CLAUDE_THINKING: Record = { 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( 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( 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( case "openai-completions": return { ...base, - reasoningEffort: supportsXhigh(model) ? options?.reasoning : clampReasoning(options?.reasoning), + reasoningEffort: getAvailableThinkingEffort(supportsXhigh(model)), toolChoice: mapOpenAiToolChoice(options?.toolChoice), } as OptionsForApi; case "openai-responses": return { ...base, - reasoningEffort: supportsXhigh(model) ? options?.reasoning : clampReasoning(options?.reasoning), + reasoningEffort: getAvailableThinkingEffort(supportsXhigh(model)), toolChoice: mapOpenAiToolChoice(options?.toolChoice), } as OptionsForApi; case "azure-openai-responses": return { ...base, - reasoningEffort: supportsXhigh(model) ? options?.reasoning : clampReasoning(options?.reasoning), + reasoningEffort: getAvailableThinkingEffort(supportsXhigh(model)), toolChoice: mapOpenAiToolChoice(options?.toolChoice), } as OptionsForApi; 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; @@ -564,7 +559,8 @@ function mapOptionsForApi( 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( } 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( } 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( } as OptionsForApi; } - 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( 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( } 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( } } -type ClampedThinkingLevel = Exclude; - 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 = { - 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 = { - 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 diff --git a/packages/ai/src/thinking.ts b/packages/ai/src/thinking.ts new file mode 100644 index 000000000..d70f69d06 --- /dev/null +++ b/packages/ai/src/thinking.ts @@ -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 = { + 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 = { + 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 = { + 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 { + 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 { + return hasXhigh ? XHI_EFF : REG_EFF; +} diff --git a/packages/ai/src/types.ts b/packages/ai/src/types.ts index 3b61a51dc..03ea5157a 100644 --- a/packages/ai/src/types.ts +++ b/packages/ai/src/types.ts @@ -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"; diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 866b16d1b..f415f8f65 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -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 diff --git a/packages/coding-agent/src/cli/args.ts b/packages/coding-agent/src/cli/args.ts index 70cb27b5f..693153404 100644 --- a/packages/coding-agent/src/cli/args.ts +++ b/packages/coding-agent/src/cli/args.ts @@ -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; } -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): Args { const result: Args = { messages: [], @@ -127,13 +121,13 @@ export function parseArgs(args: string[], extensionFlags?: Map = 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[], 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[], 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 { 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 }; } diff --git a/packages/coding-agent/src/config/settings-schema.ts b/packages/coding-agent/src/config/settings-schema.ts index 74caa27ed..87ae8c58d 100644 --- a/packages/coding-agent/src/config/settings-schema.ts +++ b/packages/coding-agent/src/config/settings-schema.ts @@ -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", diff --git a/packages/coding-agent/src/discovery/helpers.ts b/packages/coding-agent/src/discovery/helpers.ts index 0992cd803..ae1204e55 100644 --- a/packages/coding-agent/src/discovery/helpers.ts +++ b/packages/coding-agent/src/discovery/helpers.ts @@ -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): 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): 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 }; } diff --git a/packages/coding-agent/src/extensibility/extensions/loader.ts b/packages/coding-agent/src/extensibility/extensions/loader.ts index a525d821e..c3ba5b096 100644 --- a/packages/coding-agent/src/extensibility/extensions/loader.ts +++ b/packages/coding-agent/src/extensibility/extensions/loader.ts @@ -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"; diff --git a/packages/coding-agent/src/extensibility/extensions/types.ts b/packages/coding-agent/src/extensibility/extensions/types.ts index bef867640..8e2f21611 100644 --- a/packages/coding-agent/src/extensibility/extensions/types.ts +++ b/packages/coding-agent/src/extensibility/extensions/types.ts @@ -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"; diff --git a/packages/coding-agent/src/modes/components/model-selector.ts b/packages/coding-agent/src/modes/components/model-selector.ts index cbe27eddb..e0f7fc993 100644 --- a/packages/coding-agent/src/modes/components/model-selector.ts +++ b/packages/coding-agent/src/modes/components/model-selector.ts @@ -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, - 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 { + 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); diff --git a/packages/coding-agent/src/modes/components/settings-defs.ts b/packages/coding-agent/src/modes/components/settings-defs.ts index f8f870a35..9ec5eb2c1 100644 --- a/packages/coding-agent/src/modes/components/settings-defs.ts +++ b/packages/coding-agent/src/modes/components/settings-defs.ts @@ -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> = { ], // 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: [ diff --git a/packages/coding-agent/src/modes/components/settings-selector.ts b/packages/coding-agent/src/modes/components/settings-selector.ts index 2c372ff5a..738564c1b 100644 --- a/packages/coding-agent/src/modes/components/settings-selector.ts +++ b/packages/coding-agent/src/modes/components/settings-selector.ts @@ -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, diff --git a/packages/coding-agent/src/modes/components/thinking-selector.ts b/packages/coding-agent/src/modes/components/thinking-selector.ts index bcf86bc48..f99534d8c 100644 --- a/packages/coding-agent/src/modes/components/thinking-selector.ts +++ b/packages/coding-agent/src/modes/components/thinking-selector.ts @@ -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 = { - 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 diff --git a/packages/coding-agent/src/modes/controllers/selector-controller.ts b/packages/coding-agent/src/modes/controllers/selector-controller.ts index ad1e406f6..672341aa8 100644 --- a/packages/coding-agent/src/modes/controllers/selector-controller.ts +++ b/packages/coding-agent/src/modes/controllers/selector-controller.ts @@ -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"; diff --git a/packages/coding-agent/src/modes/rpc/rpc-client.ts b/packages/coding-agent/src/modes/rpc/rpc-client.ts index 916f49a9e..cf18e0406 100644 --- a/packages/coding-agent/src/modes/rpc/rpc-client.ts +++ b/packages/coding-agent/src/modes/rpc/rpc-client.ts @@ -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"; diff --git a/packages/coding-agent/src/modes/rpc/rpc-types.ts b/packages/coding-agent/src/modes/rpc/rpc-types.ts index a6e5a1747..bbf2dc7ae 100644 --- a/packages/coding-agent/src/modes/rpc/rpc-types.ts +++ b/packages/coding-agent/src/modes/rpc/rpc-types.ts @@ -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"; diff --git a/packages/coding-agent/src/modes/theme/theme.ts b/packages/coding-agent/src/modes/theme/theme.ts index 40c6de54f..1bcd06e74 100644 --- a/packages/coding-agent/src/modes/theme/theme.ts +++ b/packages/coding-agent/src/modes/theme/theme.ts @@ -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": diff --git a/packages/coding-agent/src/sdk.ts b/packages/coding-agent/src/sdk.ts index 53796695d..53b90de66 100644 --- a/packages/coding-agent/src/sdk.ts +++ b/packages/coding-agent/src/sdk.ts @@ -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) { diff --git a/packages/coding-agent/src/session/agent-session.ts b/packages/coding-agent/src/session/agent-session.ts index 8e13a5c43..d22c1256c 100644 --- a/packages/coding-agent/src/session/agent-session.ts +++ b/packages/coding-agent/src/session/agent-session.ts @@ -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 { 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 { + 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}`; } diff --git a/packages/coding-agent/src/task/executor.ts b/packages/coding-agent/src/task/executor.ts index 45454c5e0..6d59c901d 100644 --- a/packages/coding-agent/src/task/executor.ts +++ b/packages/coding-agent/src/task/executor.ts @@ -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; diff --git a/packages/coding-agent/src/task/types.ts b/packages/coding-agent/src/task/types.ts index d0a42a492..a04415350 100644 --- a/packages/coding-agent/src/task/types.ts +++ b/packages/coding-agent/src/task/types.ts @@ -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"; diff --git a/packages/coding-agent/src/utils/thinking-effort-label.ts b/packages/coding-agent/src/utils/thinking-effort-label.ts deleted file mode 100644 index 4c9fec086..000000000 --- a/packages/coding-agent/src/utils/thinking-effort-label.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core"; - -const COMPACT_THINKING_EFFORT_LABEL: Record = { - off: "off", - minimal: "min", - low: "low", - medium: "medium", - high: "high", - xhigh: "xhigh", -}; - -export function formatThinkingEffortLabel(level: ThinkingLevel): string { - return COMPACT_THINKING_EFFORT_LABEL[level]; -} diff --git a/packages/coding-agent/test/compaction-thinking-model.test.ts b/packages/coding-agent/test/compaction-thinking-model.test.ts index b4855caed..4c906b6d0 100644 --- a/packages/coding-agent/test/compaction-thinking-model.test.ts +++ b/packages/coding-agent/test/compaction-thinking-model.test.ts @@ -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"; diff --git a/packages/coding-agent/test/thinking-effort-label.test.ts b/packages/coding-agent/test/thinking-effort-label.test.ts deleted file mode 100644 index 570a4fa54..000000000 --- a/packages/coding-agent/test/thinking-effort-label.test.ts +++ /dev/null @@ -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"); - }); -}); diff --git a/packages/react-edit-benchmark/src/index.ts b/packages/react-edit-benchmark/src/index.ts index af2550160..06b262352 100644 --- a/packages/react-edit-benchmark/src/index.ts +++ b/packages/react-edit-benchmark/src/index.ts @@ -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 { 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); diff --git a/packages/react-edit-benchmark/src/runner.ts b/packages/react-edit-benchmark/src/runner.ts index 2845a2a9d..747633707 100644 --- a/packages/react-edit-benchmark/src/runner.ts +++ b/packages/react-edit-benchmark/src/runner.ts @@ -7,7 +7,8 @@ /// 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;