diff --git a/packages/agent/src/types.ts b/packages/agent/src/types.ts index 803b9566..f7704b10 100644 --- a/packages/agent/src/types.ts +++ b/packages/agent/src/types.ts @@ -249,8 +249,8 @@ export interface AgentLoopConfig extends SimpleStreamOptions { /** * Thinking/reasoning level for models that support it. - * Note: "xhigh" is only supported by selected model families. Use supportsXhigh() from @mariozechner/pi-ai - * to detect support for a concrete model. + * Note: "xhigh" is only supported by selected model families. Use model thinking-level metadata + * from @mariozechner/pi-ai to detect support for a concrete model. */ export type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh"; diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 080d5031..86d997f4 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,17 +2,28 @@ ## [Unreleased] -<<<<<<< feat/add-xiaomi-provider +### Breaking Changes + +- Replaced `OpenAICompletionsCompat.reasoningEffortMap` with top-level `Model.thinkingLevelMap` for model-specific thinking controls ([#3208](https://github.com/badlogic/pi-mono/issues/3208)). Migration: move mappings from `model.compat.reasoningEffortMap` to `model.thinkingLevelMap`. See `packages/ai/README.md#custom-models` and `packages/coding-agent/docs/models.md#thinking-level-map`. Map values keep the same provider-specific string semantics, and `null` marks a pi thinking level unsupported. Example: + ```ts + // Before + compat: { reasoningEffortMap: { high: "high", xhigh: "max" } } + + // After + thinkingLevelMap: { minimal: null, low: null, medium: null, high: "high", xhigh: "max" } + ``` +- Removed `supportsXhigh()`. Migration: use `getSupportedThinkingLevels(model).includes("xhigh")` or `clampThinkingLevel(model, requestedLevel)` instead ([#3208](https://github.com/badlogic/pi-mono/issues/3208)). + ### Added - Added Xiaomi MiMo provider (openai-completions compatible) with `XIAOMI_API_KEY` authentication. -======= +- Added `Model.thinkingLevelMap`, `getSupportedThinkingLevels()`, and `clampThinkingLevel()` so model metadata can describe supported thinking levels and provider-specific level values ([#3208](https://github.com/badlogic/pi-mono/issues/3208)). + ## [0.71.1] - 2026-05-01 ### Added - Added `websocket-cached` transport support for OpenAI Codex Responses used with ChatGPT subscription auth. This keeps the same WebSocket open for a session and, after the first request, sends only new conversation items instead of resending the full chat history when possible. ->>>>>>> main ## [0.71.0] - 2026-04-30 diff --git a/packages/ai/README.md b/packages/ai/README.md index 37f7b9d9..2d12ea36 100644 --- a/packages/ai/README.md +++ b/packages/ai/README.md @@ -446,7 +446,7 @@ if (model.reasoning) { const response = await completeSimple(model, { messages: [{ role: 'user', content: 'Solve: 2x + 5 = 13' }] }, { - reasoning: 'medium' // 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' (xhigh maps to high on non-OpenAI providers) + reasoning: 'medium' // 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' }); // Access thinking and text blocks @@ -821,6 +821,8 @@ const response = await stream(ollamaModel, context, { Some OpenAI-compatible servers do not understand the `developer` role used for reasoning-capable models. For those providers, set `compat.supportsDeveloperRole` to `false` so the system prompt is sent as a `system` message instead. If the server also does not support `reasoning_effort`, set `compat.supportsReasoningEffort` to `false` too. +Use model-level `thinkingLevelMap` to describe model-specific thinking controls. Keys are pi thinking levels (`off`, `minimal`, `low`, `medium`, `high`, `xhigh`). Missing keys use provider defaults, string values are sent to the provider, and `null` marks a level unsupported. + This commonly applies to Ollama, vLLM, SGLang, and similar OpenAI-compatible servers. You can set `compat` at the provider level or per model. ```typescript @@ -835,6 +837,13 @@ const ollamaReasoningModel: Model<'openai-completions'> = { cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 131072, maxTokens: 32000, + thinkingLevelMap: { + minimal: null, + low: null, + medium: null, + high: 'high', + xhigh: null, + }, compat: { supportsDeveloperRole: false, supportsReasoningEffort: false, diff --git a/packages/ai/scripts/generate-models.ts b/packages/ai/scripts/generate-models.ts index c94259a3..69b6cce8 100644 --- a/packages/ai/scripts/generate-models.ts +++ b/packages/ai/scripts/generate-models.ts @@ -78,6 +78,82 @@ const EAGER_TOOL_INPUT_STREAMING_UNSUPPORTED_ANTHROPIC_MODELS = new Set([ "github-copilot:claude-sonnet-4.5", ]); +const DEEPSEEK_V4_THINKING_LEVEL_MAP = { + minimal: null, + low: null, + medium: null, + high: "high", + xhigh: "max", +} as const; + +function mergeThinkingLevelMap(model: Model, map: NonNullable["thinkingLevelMap"]>): void { + model.thinkingLevelMap = { ...model.thinkingLevelMap, ...map }; +} + +function supportsOpenAiXhigh(modelId: string): boolean { + return ( + modelId.includes("gpt-5.2") || + modelId.includes("gpt-5.3") || + modelId.includes("gpt-5.4") || + modelId.includes("gpt-5.5") + ); +} + +function isGoogleThinkingApi(model: Model): boolean { + return model.api === "google-generative-ai" || model.api === "google-vertex"; +} + +function isGemini3ProModel(modelId: string): boolean { + return /gemini-3(?:\.\d+)?-pro/.test(modelId.toLowerCase()); +} + +function isGemini3FlashModel(modelId: string): boolean { + return /gemini-3(?:\.\d+)?-flash/.test(modelId.toLowerCase()); +} + +function isGemma4Model(modelId: string): boolean { + return /gemma-?4/.test(modelId.toLowerCase()); +} + +function applyThinkingLevelMetadata(model: Model): void { + if ( + (model.api === "openai-responses" || model.api === "azure-openai-responses") && + model.id.startsWith("gpt-5") + ) { + mergeThinkingLevelMap(model, { off: null }); + } + if (supportsOpenAiXhigh(model.id)) { + mergeThinkingLevelMap(model, { xhigh: "xhigh" }); + } + if (model.id.includes("opus-4-6") || model.id.includes("opus-4.6")) { + mergeThinkingLevelMap(model, { xhigh: "max" }); + } + if (model.id.includes("opus-4-7") || model.id.includes("opus-4.7")) { + mergeThinkingLevelMap(model, { xhigh: "xhigh" }); + } + if (model.api === "openai-completions" && model.id.includes("deepseek-v4")) { + mergeThinkingLevelMap(model, DEEPSEEK_V4_THINKING_LEVEL_MAP); + } + if (isGoogleThinkingApi(model) && isGemini3ProModel(model.id)) { + mergeThinkingLevelMap(model, { off: null, minimal: null, low: "LOW", medium: null, high: "HIGH" }); + } + if (isGoogleThinkingApi(model) && isGemini3FlashModel(model.id)) { + mergeThinkingLevelMap(model, { off: null }); + } + if (isGoogleThinkingApi(model) && isGemma4Model(model.id)) { + mergeThinkingLevelMap(model, { off: null, minimal: "MINIMAL", low: null, medium: null, high: "HIGH" }); + } + if (model.provider === "groq" && model.id === "qwen/qwen3-32b") { + mergeThinkingLevelMap(model, { minimal: null, low: null, medium: null, high: "default" }); + } + if (model.provider === "openai-codex" && supportsOpenAiXhigh(model.id)) { + mergeThinkingLevelMap(model, { minimal: "low" }); + } + if (model.provider === "openai-codex" && model.id === "gpt-5.1-codex-mini") { + mergeThinkingLevelMap(model, { minimal: "medium", low: "medium", medium: "medium", high: "high" }); + } +} + function getAnthropicMessagesCompat(provider: string, modelId: string): AnthropicMessagesCompat | undefined { return EAGER_TOOL_INPUT_STREAMING_UNSUPPORTED_ANTHROPIC_MODELS.has(`${provider}:${modelId}`) ? { supportsEagerToolInputStreaming: false } @@ -1164,13 +1240,6 @@ async function generateModels() { const deepseekCompat: OpenAICompletionsCompat = { requiresReasoningContentOnAssistantMessages: true, thinkingFormat: "deepseek", - reasoningEffortMap: { - minimal: "high", - low: "high", - medium: "high", - high: "high", - xhigh: "max", - }, }; const deepseekV4Models: Model<"openai-completions">[] = [ { @@ -1224,6 +1293,7 @@ async function generateModels() { } : deepseekCompat), }; + mergeThinkingLevelMap(candidate, DEEPSEEK_V4_THINKING_LEVEL_MAP); } } @@ -1615,6 +1685,10 @@ async function generateModels() { })); allModels.push(...azureOpenAiModels); + for (const model of allModels) { + applyThinkingLevelMetadata(model); + } + // Group by provider and deduplicate by model ID const providers: Record>> = {}; for (const model of allModels) { @@ -1662,6 +1736,9 @@ export const MODELS = { `; } output += `\t\t\treasoning: ${model.reasoning},\n`; + if (model.thinkingLevelMap) { + output += `\t\t\tthinkingLevelMap: ${JSON.stringify(model.thinkingLevelMap)},\n`; + } output += `\t\t\tinput: [${model.input.map(i => `"${i}"`).join(", ")}],\n`; output += `\t\t\tcost: {\n`; output += `\t\t\t\tinput: ${model.cost.input},\n`; diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index ae2ffe06..b35c287f 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -250,6 +250,7 @@ export const MODELS = { provider: "amazon-bedrock", baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", reasoning: true, + thinkingLevelMap: {"xhigh":"max"}, input: ["text", "image"], cost: { input: 5, @@ -267,6 +268,7 @@ export const MODELS = { provider: "amazon-bedrock", baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 5, @@ -335,6 +337,7 @@ export const MODELS = { provider: "amazon-bedrock", baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", reasoning: true, + thinkingLevelMap: {"xhigh":"max"}, input: ["text", "image"], cost: { input: 16.5, @@ -454,6 +457,7 @@ export const MODELS = { provider: "amazon-bedrock", baseUrl: "https://bedrock-runtime.eu-central-1.amazonaws.com", reasoning: true, + thinkingLevelMap: {"xhigh":"max"}, input: ["text", "image"], cost: { input: 5, @@ -471,6 +475,7 @@ export const MODELS = { provider: "amazon-bedrock", baseUrl: "https://bedrock-runtime.eu-central-1.amazonaws.com", reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 5, @@ -573,6 +578,7 @@ export const MODELS = { provider: "amazon-bedrock", baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", reasoning: true, + thinkingLevelMap: {"xhigh":"max"}, input: ["text", "image"], cost: { input: 5, @@ -590,6 +596,7 @@ export const MODELS = { provider: "amazon-bedrock", baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 5, @@ -1423,6 +1430,7 @@ export const MODELS = { provider: "amazon-bedrock", baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", reasoning: true, + thinkingLevelMap: {"xhigh":"max"}, input: ["text", "image"], cost: { input: 5, @@ -1440,6 +1448,7 @@ export const MODELS = { provider: "amazon-bedrock", baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 5, @@ -1867,6 +1876,7 @@ export const MODELS = { provider: "anthropic", baseUrl: "https://api.anthropic.com", reasoning: true, + thinkingLevelMap: {"xhigh":"max"}, input: ["text", "image"], cost: { input: 5, @@ -1884,6 +1894,7 @@ export const MODELS = { provider: "anthropic", baseUrl: "https://api.anthropic.com", reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 5, @@ -2158,6 +2169,7 @@ export const MODELS = { provider: "azure-openai-responses", baseUrl: "", reasoning: true, + thinkingLevelMap: {"off":null}, input: ["text", "image"], cost: { input: 1.25, @@ -2175,6 +2187,7 @@ export const MODELS = { provider: "azure-openai-responses", baseUrl: "", reasoning: false, + thinkingLevelMap: {"off":null}, input: ["text", "image"], cost: { input: 1.25, @@ -2192,6 +2205,7 @@ export const MODELS = { provider: "azure-openai-responses", baseUrl: "", reasoning: true, + thinkingLevelMap: {"off":null}, input: ["text", "image"], cost: { input: 1.25, @@ -2209,6 +2223,7 @@ export const MODELS = { provider: "azure-openai-responses", baseUrl: "", reasoning: true, + thinkingLevelMap: {"off":null}, input: ["text", "image"], cost: { input: 0.25, @@ -2226,6 +2241,7 @@ export const MODELS = { provider: "azure-openai-responses", baseUrl: "", reasoning: true, + thinkingLevelMap: {"off":null}, input: ["text", "image"], cost: { input: 0.05, @@ -2243,6 +2259,7 @@ export const MODELS = { provider: "azure-openai-responses", baseUrl: "", reasoning: true, + thinkingLevelMap: {"off":null}, input: ["text", "image"], cost: { input: 15, @@ -2260,6 +2277,7 @@ export const MODELS = { provider: "azure-openai-responses", baseUrl: "", reasoning: true, + thinkingLevelMap: {"off":null}, input: ["text", "image"], cost: { input: 1.25, @@ -2277,6 +2295,7 @@ export const MODELS = { provider: "azure-openai-responses", baseUrl: "", reasoning: true, + thinkingLevelMap: {"off":null}, input: ["text", "image"], cost: { input: 1.25, @@ -2294,6 +2313,7 @@ export const MODELS = { provider: "azure-openai-responses", baseUrl: "", reasoning: true, + thinkingLevelMap: {"off":null}, input: ["text", "image"], cost: { input: 1.25, @@ -2311,6 +2331,7 @@ export const MODELS = { provider: "azure-openai-responses", baseUrl: "", reasoning: true, + thinkingLevelMap: {"off":null}, input: ["text", "image"], cost: { input: 1.25, @@ -2328,6 +2349,7 @@ export const MODELS = { provider: "azure-openai-responses", baseUrl: "", reasoning: true, + thinkingLevelMap: {"off":null}, input: ["text", "image"], cost: { input: 0.25, @@ -2345,6 +2367,7 @@ export const MODELS = { provider: "azure-openai-responses", baseUrl: "", reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 1.75, @@ -2362,6 +2385,7 @@ export const MODELS = { provider: "azure-openai-responses", baseUrl: "", reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 1.75, @@ -2379,6 +2403,7 @@ export const MODELS = { provider: "azure-openai-responses", baseUrl: "", reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 1.75, @@ -2396,6 +2421,7 @@ export const MODELS = { provider: "azure-openai-responses", baseUrl: "", reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 21, @@ -2413,6 +2439,7 @@ export const MODELS = { provider: "azure-openai-responses", baseUrl: "", reasoning: false, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 1.75, @@ -2430,6 +2457,7 @@ export const MODELS = { provider: "azure-openai-responses", baseUrl: "", reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 1.75, @@ -2447,6 +2475,7 @@ export const MODELS = { provider: "azure-openai-responses", baseUrl: "", reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 1.75, @@ -2464,6 +2493,7 @@ export const MODELS = { provider: "azure-openai-responses", baseUrl: "", reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 2.5, @@ -2481,6 +2511,7 @@ export const MODELS = { provider: "azure-openai-responses", baseUrl: "", reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 0.75, @@ -2498,6 +2529,7 @@ export const MODELS = { provider: "azure-openai-responses", baseUrl: "", reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 0.2, @@ -2515,6 +2547,7 @@ export const MODELS = { provider: "azure-openai-responses", baseUrl: "", reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 30, @@ -2532,6 +2565,7 @@ export const MODELS = { provider: "azure-openai-responses", baseUrl: "", reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 5, @@ -2549,6 +2583,7 @@ export const MODELS = { provider: "azure-openai-responses", baseUrl: "", reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 30, @@ -2944,6 +2979,7 @@ export const MODELS = { provider: "cloudflare-ai-gateway", baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", reasoning: true, + thinkingLevelMap: {"xhigh":"max"}, input: ["text", "image"], cost: { input: 5, @@ -2961,6 +2997,7 @@ export const MODELS = { provider: "cloudflare-ai-gateway", baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 5, @@ -3097,6 +3134,7 @@ export const MODELS = { provider: "cloudflare-ai-gateway", baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", reasoning: true, + thinkingLevelMap: {"off":null}, input: ["text", "image"], cost: { input: 1.25, @@ -3114,6 +3152,7 @@ export const MODELS = { provider: "cloudflare-ai-gateway", baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", reasoning: true, + thinkingLevelMap: {"off":null}, input: ["text", "image"], cost: { input: 1.25, @@ -3131,6 +3170,7 @@ export const MODELS = { provider: "cloudflare-ai-gateway", baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 1.75, @@ -3148,6 +3188,7 @@ export const MODELS = { provider: "cloudflare-ai-gateway", baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 1.75, @@ -3165,6 +3206,7 @@ export const MODELS = { provider: "cloudflare-ai-gateway", baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 1.75, @@ -3182,6 +3224,7 @@ export const MODELS = { provider: "cloudflare-ai-gateway", baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 2.5, @@ -3199,6 +3242,7 @@ export const MODELS = { provider: "cloudflare-ai-gateway", baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 5, @@ -3520,8 +3564,9 @@ export const MODELS = { api: "openai-completions", provider: "deepseek", baseUrl: "https://api.deepseek.com", - compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek","reasoningEffortMap":{"minimal":"high","low":"high","medium":"high","high":"high","xhigh":"max"}}, + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, input: ["text"], cost: { input: 0.14, @@ -3538,8 +3583,9 @@ export const MODELS = { api: "openai-completions", provider: "deepseek", baseUrl: "https://api.deepseek.com", - compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek","reasoningEffortMap":{"minimal":"high","low":"high","medium":"high","high":"high","xhigh":"max"}}, + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, input: ["text"], cost: { input: 0.435, @@ -3922,6 +3968,7 @@ export const MODELS = { baseUrl: "https://api.individual.githubcopilot.com", headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, reasoning: true, + thinkingLevelMap: {"xhigh":"max"}, input: ["text", "image"], cost: { input: 0, @@ -3940,6 +3987,7 @@ export const MODELS = { baseUrl: "https://api.individual.githubcopilot.com", headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 0, @@ -4128,6 +4176,7 @@ export const MODELS = { baseUrl: "https://api.individual.githubcopilot.com", headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, reasoning: true, + thinkingLevelMap: {"off":null}, input: ["text", "image"], cost: { input: 0, @@ -4146,6 +4195,7 @@ export const MODELS = { baseUrl: "https://api.individual.githubcopilot.com", headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, reasoning: true, + thinkingLevelMap: {"off":null}, input: ["text", "image"], cost: { input: 0, @@ -4164,6 +4214,7 @@ export const MODELS = { baseUrl: "https://api.individual.githubcopilot.com", headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, reasoning: true, + thinkingLevelMap: {"off":null}, input: ["text", "image"], cost: { input: 0, @@ -4182,6 +4233,7 @@ export const MODELS = { baseUrl: "https://api.individual.githubcopilot.com", headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, reasoning: true, + thinkingLevelMap: {"off":null}, input: ["text", "image"], cost: { input: 0, @@ -4200,6 +4252,7 @@ export const MODELS = { baseUrl: "https://api.individual.githubcopilot.com", headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, reasoning: true, + thinkingLevelMap: {"off":null}, input: ["text", "image"], cost: { input: 0, @@ -4218,6 +4271,7 @@ export const MODELS = { baseUrl: "https://api.individual.githubcopilot.com", headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, reasoning: true, + thinkingLevelMap: {"off":null}, input: ["text", "image"], cost: { input: 0, @@ -4236,6 +4290,7 @@ export const MODELS = { baseUrl: "https://api.individual.githubcopilot.com", headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 0, @@ -4254,6 +4309,7 @@ export const MODELS = { baseUrl: "https://api.individual.githubcopilot.com", headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 0, @@ -4272,6 +4328,7 @@ export const MODELS = { baseUrl: "https://api.individual.githubcopilot.com", headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 0, @@ -4290,6 +4347,7 @@ export const MODELS = { baseUrl: "https://api.individual.githubcopilot.com", headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 0, @@ -4308,6 +4366,7 @@ export const MODELS = { baseUrl: "https://api.individual.githubcopilot.com", headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 0, @@ -4326,6 +4385,7 @@ export const MODELS = { baseUrl: "https://api.individual.githubcopilot.com", headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 0, @@ -4619,6 +4679,7 @@ export const MODELS = { provider: "google", baseUrl: "https://generativelanguage.googleapis.com/v1beta", reasoning: true, + thinkingLevelMap: {"off":null}, input: ["text", "image"], cost: { input: 0.5, @@ -4636,6 +4697,7 @@ export const MODELS = { provider: "google", baseUrl: "https://generativelanguage.googleapis.com/v1beta", reasoning: true, + thinkingLevelMap: {"off":null,"minimal":null,"low":"LOW","medium":null,"high":"HIGH"}, input: ["text", "image"], cost: { input: 2, @@ -4653,6 +4715,7 @@ export const MODELS = { provider: "google", baseUrl: "https://generativelanguage.googleapis.com/v1beta", reasoning: true, + thinkingLevelMap: {"off":null}, input: ["text", "image"], cost: { input: 0.25, @@ -4670,6 +4733,7 @@ export const MODELS = { provider: "google", baseUrl: "https://generativelanguage.googleapis.com/v1beta", reasoning: true, + thinkingLevelMap: {"off":null,"minimal":null,"low":"LOW","medium":null,"high":"HIGH"}, input: ["text", "image"], cost: { input: 2, @@ -4687,6 +4751,7 @@ export const MODELS = { provider: "google", baseUrl: "https://generativelanguage.googleapis.com/v1beta", reasoning: true, + thinkingLevelMap: {"off":null,"minimal":null,"low":"LOW","medium":null,"high":"HIGH"}, input: ["text", "image"], cost: { input: 2, @@ -4789,6 +4854,7 @@ export const MODELS = { provider: "google", baseUrl: "https://generativelanguage.googleapis.com/v1beta", reasoning: true, + thinkingLevelMap: {"off":null,"minimal":"MINIMAL","low":null,"medium":null,"high":"HIGH"}, input: ["text", "image"], cost: { input: 0, @@ -4806,6 +4872,7 @@ export const MODELS = { provider: "google", baseUrl: "https://generativelanguage.googleapis.com/v1beta", reasoning: true, + thinkingLevelMap: {"off":null,"minimal":"MINIMAL","low":null,"medium":null,"high":"HIGH"}, input: ["text", "image"], cost: { input: 0, @@ -4978,6 +5045,7 @@ export const MODELS = { provider: "google-vertex", baseUrl: "https://{location}-aiplatform.googleapis.com", reasoning: true, + thinkingLevelMap: {"off":null}, input: ["text", "image"], cost: { input: 0.5, @@ -4995,6 +5063,7 @@ export const MODELS = { provider: "google-vertex", baseUrl: "https://{location}-aiplatform.googleapis.com", reasoning: true, + thinkingLevelMap: {"off":null,"minimal":null,"low":"LOW","medium":null,"high":"HIGH"}, input: ["text", "image"], cost: { input: 2, @@ -5012,6 +5081,7 @@ export const MODELS = { provider: "google-vertex", baseUrl: "https://{location}-aiplatform.googleapis.com", reasoning: true, + thinkingLevelMap: {"off":null,"minimal":null,"low":"LOW","medium":null,"high":"HIGH"}, input: ["text", "image"], cost: { input: 2, @@ -5029,6 +5099,7 @@ export const MODELS = { provider: "google-vertex", baseUrl: "https://{location}-aiplatform.googleapis.com", reasoning: true, + thinkingLevelMap: {"off":null,"minimal":null,"low":"LOW","medium":null,"high":"HIGH"}, input: ["text", "image"], cost: { input: 2, @@ -5337,6 +5408,7 @@ export const MODELS = { provider: "groq", baseUrl: "https://api.groq.com/openai/v1", reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"default"}, input: ["text"], cost: { input: 0.29, @@ -6786,6 +6858,7 @@ export const MODELS = { provider: "openai", baseUrl: "https://api.openai.com/v1", reasoning: true, + thinkingLevelMap: {"off":null}, input: ["text", "image"], cost: { input: 1.25, @@ -6803,6 +6876,7 @@ export const MODELS = { provider: "openai", baseUrl: "https://api.openai.com/v1", reasoning: false, + thinkingLevelMap: {"off":null}, input: ["text", "image"], cost: { input: 1.25, @@ -6820,6 +6894,7 @@ export const MODELS = { provider: "openai", baseUrl: "https://api.openai.com/v1", reasoning: true, + thinkingLevelMap: {"off":null}, input: ["text", "image"], cost: { input: 1.25, @@ -6837,6 +6912,7 @@ export const MODELS = { provider: "openai", baseUrl: "https://api.openai.com/v1", reasoning: true, + thinkingLevelMap: {"off":null}, input: ["text", "image"], cost: { input: 0.25, @@ -6854,6 +6930,7 @@ export const MODELS = { provider: "openai", baseUrl: "https://api.openai.com/v1", reasoning: true, + thinkingLevelMap: {"off":null}, input: ["text", "image"], cost: { input: 0.05, @@ -6871,6 +6948,7 @@ export const MODELS = { provider: "openai", baseUrl: "https://api.openai.com/v1", reasoning: true, + thinkingLevelMap: {"off":null}, input: ["text", "image"], cost: { input: 15, @@ -6888,6 +6966,7 @@ export const MODELS = { provider: "openai", baseUrl: "https://api.openai.com/v1", reasoning: true, + thinkingLevelMap: {"off":null}, input: ["text", "image"], cost: { input: 1.25, @@ -6905,6 +6984,7 @@ export const MODELS = { provider: "openai", baseUrl: "https://api.openai.com/v1", reasoning: true, + thinkingLevelMap: {"off":null}, input: ["text", "image"], cost: { input: 1.25, @@ -6922,6 +7002,7 @@ export const MODELS = { provider: "openai", baseUrl: "https://api.openai.com/v1", reasoning: true, + thinkingLevelMap: {"off":null}, input: ["text", "image"], cost: { input: 1.25, @@ -6939,6 +7020,7 @@ export const MODELS = { provider: "openai", baseUrl: "https://api.openai.com/v1", reasoning: true, + thinkingLevelMap: {"off":null}, input: ["text", "image"], cost: { input: 1.25, @@ -6956,6 +7038,7 @@ export const MODELS = { provider: "openai", baseUrl: "https://api.openai.com/v1", reasoning: true, + thinkingLevelMap: {"off":null}, input: ["text", "image"], cost: { input: 0.25, @@ -6973,6 +7056,7 @@ export const MODELS = { provider: "openai", baseUrl: "https://api.openai.com/v1", reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 1.75, @@ -6990,6 +7074,7 @@ export const MODELS = { provider: "openai", baseUrl: "https://api.openai.com/v1", reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 1.75, @@ -7007,6 +7092,7 @@ export const MODELS = { provider: "openai", baseUrl: "https://api.openai.com/v1", reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 1.75, @@ -7024,6 +7110,7 @@ export const MODELS = { provider: "openai", baseUrl: "https://api.openai.com/v1", reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 21, @@ -7041,6 +7128,7 @@ export const MODELS = { provider: "openai", baseUrl: "https://api.openai.com/v1", reasoning: false, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 1.75, @@ -7058,6 +7146,7 @@ export const MODELS = { provider: "openai", baseUrl: "https://api.openai.com/v1", reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 1.75, @@ -7075,6 +7164,7 @@ export const MODELS = { provider: "openai", baseUrl: "https://api.openai.com/v1", reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 1.75, @@ -7092,6 +7182,7 @@ export const MODELS = { provider: "openai", baseUrl: "https://api.openai.com/v1", reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 2.5, @@ -7109,6 +7200,7 @@ export const MODELS = { provider: "openai", baseUrl: "https://api.openai.com/v1", reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 0.75, @@ -7126,6 +7218,7 @@ export const MODELS = { provider: "openai", baseUrl: "https://api.openai.com/v1", reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 0.2, @@ -7143,6 +7236,7 @@ export const MODELS = { provider: "openai", baseUrl: "https://api.openai.com/v1", reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 30, @@ -7160,6 +7254,7 @@ export const MODELS = { provider: "openai", baseUrl: "https://api.openai.com/v1", reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 5, @@ -7177,6 +7272,7 @@ export const MODELS = { provider: "openai", baseUrl: "https://api.openai.com/v1", reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 30, @@ -7366,6 +7462,7 @@ export const MODELS = { provider: "openai-codex", baseUrl: "https://chatgpt.com/backend-api", reasoning: true, + thinkingLevelMap: {"minimal":"medium","low":"medium","medium":"medium","high":"high"}, input: ["text", "image"], cost: { input: 0.25, @@ -7383,6 +7480,7 @@ export const MODELS = { provider: "openai-codex", baseUrl: "https://chatgpt.com/backend-api", reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh","minimal":"low"}, input: ["text", "image"], cost: { input: 1.75, @@ -7400,6 +7498,7 @@ export const MODELS = { provider: "openai-codex", baseUrl: "https://chatgpt.com/backend-api", reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh","minimal":"low"}, input: ["text", "image"], cost: { input: 1.75, @@ -7417,6 +7516,7 @@ export const MODELS = { provider: "openai-codex", baseUrl: "https://chatgpt.com/backend-api", reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh","minimal":"low"}, input: ["text", "image"], cost: { input: 1.75, @@ -7434,6 +7534,7 @@ export const MODELS = { provider: "openai-codex", baseUrl: "https://chatgpt.com/backend-api", reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh","minimal":"low"}, input: ["text"], cost: { input: 0, @@ -7451,6 +7552,7 @@ export const MODELS = { provider: "openai-codex", baseUrl: "https://chatgpt.com/backend-api", reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh","minimal":"low"}, input: ["text", "image"], cost: { input: 2.5, @@ -7468,6 +7570,7 @@ export const MODELS = { provider: "openai-codex", baseUrl: "https://chatgpt.com/backend-api", reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh","minimal":"low"}, input: ["text", "image"], cost: { input: 0.75, @@ -7485,6 +7588,7 @@ export const MODELS = { provider: "openai-codex", baseUrl: "https://chatgpt.com/backend-api", reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh","minimal":"low"}, input: ["text", "image"], cost: { input: 5, @@ -7572,6 +7676,7 @@ export const MODELS = { provider: "opencode", baseUrl: "https://opencode.ai/zen", reasoning: true, + thinkingLevelMap: {"xhigh":"max"}, input: ["text", "image"], cost: { input: 5, @@ -7589,6 +7694,7 @@ export const MODELS = { provider: "opencode", baseUrl: "https://opencode.ai/zen", reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 5, @@ -7657,6 +7763,7 @@ export const MODELS = { provider: "opencode", baseUrl: "https://opencode.ai/zen/v1", reasoning: true, + thinkingLevelMap: {"off":null}, input: ["text", "image"], cost: { input: 0.5, @@ -7674,6 +7781,7 @@ export const MODELS = { provider: "opencode", baseUrl: "https://opencode.ai/zen/v1", reasoning: true, + thinkingLevelMap: {"off":null,"minimal":null,"low":"LOW","medium":null,"high":"HIGH"}, input: ["text", "image"], cost: { input: 2, @@ -7725,6 +7833,7 @@ export const MODELS = { provider: "opencode", baseUrl: "https://opencode.ai/zen/v1", reasoning: true, + thinkingLevelMap: {"off":null}, input: ["text", "image"], cost: { input: 1.07, @@ -7742,6 +7851,7 @@ export const MODELS = { provider: "opencode", baseUrl: "https://opencode.ai/zen/v1", reasoning: true, + thinkingLevelMap: {"off":null}, input: ["text", "image"], cost: { input: 1.07, @@ -7759,6 +7869,7 @@ export const MODELS = { provider: "opencode", baseUrl: "https://opencode.ai/zen/v1", reasoning: true, + thinkingLevelMap: {"off":null}, input: ["text", "image"], cost: { input: 0, @@ -7776,6 +7887,7 @@ export const MODELS = { provider: "opencode", baseUrl: "https://opencode.ai/zen/v1", reasoning: true, + thinkingLevelMap: {"off":null}, input: ["text", "image"], cost: { input: 1.07, @@ -7793,6 +7905,7 @@ export const MODELS = { provider: "opencode", baseUrl: "https://opencode.ai/zen/v1", reasoning: true, + thinkingLevelMap: {"off":null}, input: ["text", "image"], cost: { input: 1.07, @@ -7810,6 +7923,7 @@ export const MODELS = { provider: "opencode", baseUrl: "https://opencode.ai/zen/v1", reasoning: true, + thinkingLevelMap: {"off":null}, input: ["text", "image"], cost: { input: 1.25, @@ -7827,6 +7941,7 @@ export const MODELS = { provider: "opencode", baseUrl: "https://opencode.ai/zen/v1", reasoning: true, + thinkingLevelMap: {"off":null}, input: ["text", "image"], cost: { input: 0.25, @@ -7844,6 +7959,7 @@ export const MODELS = { provider: "opencode", baseUrl: "https://opencode.ai/zen/v1", reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 1.75, @@ -7861,6 +7977,7 @@ export const MODELS = { provider: "opencode", baseUrl: "https://opencode.ai/zen/v1", reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 1.75, @@ -7878,6 +7995,7 @@ export const MODELS = { provider: "opencode", baseUrl: "https://opencode.ai/zen/v1", reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 1.75, @@ -7895,6 +8013,7 @@ export const MODELS = { provider: "opencode", baseUrl: "https://opencode.ai/zen/v1", reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 2.5, @@ -7912,6 +8031,7 @@ export const MODELS = { provider: "opencode", baseUrl: "https://opencode.ai/zen/v1", reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 0.75, @@ -7929,6 +8049,7 @@ export const MODELS = { provider: "opencode", baseUrl: "https://opencode.ai/zen/v1", reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 0.2, @@ -7946,6 +8067,7 @@ export const MODELS = { provider: "opencode", baseUrl: "https://opencode.ai/zen/v1", reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 30, @@ -7963,6 +8085,7 @@ export const MODELS = { provider: "opencode", baseUrl: "https://opencode.ai/zen/v1", reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 5, @@ -7980,6 +8103,7 @@ export const MODELS = { provider: "opencode", baseUrl: "https://opencode.ai/zen/v1", reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 30, @@ -8151,8 +8275,9 @@ export const MODELS = { api: "openai-completions", provider: "opencode-go", baseUrl: "https://opencode.ai/zen/go/v1", - compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek","reasoningEffortMap":{"minimal":"high","low":"high","medium":"high","high":"high","xhigh":"max"}}, + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, input: ["text"], cost: { input: 0.14, @@ -8169,8 +8294,9 @@ export const MODELS = { api: "openai-completions", provider: "opencode-go", baseUrl: "https://opencode.ai/zen/go/v1", - compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek","reasoningEffortMap":{"minimal":"high","low":"high","medium":"high","high":"high","xhigh":"max"}}, + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, input: ["text"], cost: { input: 1.74, @@ -8666,6 +8792,7 @@ export const MODELS = { provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", reasoning: true, + thinkingLevelMap: {"xhigh":"max"}, input: ["text", "image"], cost: { input: 5, @@ -8683,6 +8810,7 @@ export const MODELS = { provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", reasoning: true, + thinkingLevelMap: {"xhigh":"max"}, input: ["text", "image"], cost: { input: 30, @@ -8700,6 +8828,7 @@ export const MODELS = { provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 5, @@ -9126,6 +9255,7 @@ export const MODELS = { baseUrl: "https://openrouter.ai/api/v1", compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, input: ["text"], cost: { input: 0.14, @@ -9144,6 +9274,7 @@ export const MODELS = { baseUrl: "https://openrouter.ai/api/v1", compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, input: ["text"], cost: { input: 0.435, @@ -9185,7 +9316,7 @@ export const MODELS = { cacheRead: 0.024999999999999998, cacheWrite: 0.08333333333333334, }, - contextWindow: 1048576, + contextWindow: 1000000, maxTokens: 8192, } satisfies Model<"openai-completions">, "google/gemini-2.0-flash-lite-001": { @@ -10878,6 +11009,7 @@ export const MODELS = { provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 1.75, @@ -10895,6 +11027,7 @@ export const MODELS = { provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", reasoning: false, + thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 1.75, @@ -10912,6 +11045,7 @@ export const MODELS = { provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 1.75, @@ -10929,6 +11063,7 @@ export const MODELS = { provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 21, @@ -10946,6 +11081,7 @@ export const MODELS = { provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", reasoning: false, + thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 1.75, @@ -10963,6 +11099,7 @@ export const MODELS = { provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 1.75, @@ -10980,6 +11117,7 @@ export const MODELS = { provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 2.5, @@ -10997,6 +11135,7 @@ export const MODELS = { provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 0.75, @@ -11014,6 +11153,7 @@ export const MODELS = { provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 0.19999999999999998, @@ -11031,6 +11171,7 @@ export const MODELS = { provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 30, @@ -11048,6 +11189,7 @@ export const MODELS = { provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 5, @@ -11065,6 +11207,7 @@ export const MODELS = { provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 30, @@ -13464,6 +13607,7 @@ export const MODELS = { provider: "vercel-ai-gateway", baseUrl: "https://ai-gateway.vercel.sh", reasoning: true, + thinkingLevelMap: {"xhigh":"max"}, input: ["text", "image"], cost: { input: 5, @@ -13481,6 +13625,7 @@ export const MODELS = { provider: "vercel-ai-gateway", baseUrl: "https://ai-gateway.vercel.sh", reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 5, @@ -14858,6 +15003,7 @@ export const MODELS = { provider: "vercel-ai-gateway", baseUrl: "https://ai-gateway.vercel.sh", reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 1.75, @@ -14875,6 +15021,7 @@ export const MODELS = { provider: "vercel-ai-gateway", baseUrl: "https://ai-gateway.vercel.sh", reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 1.75, @@ -14892,6 +15039,7 @@ export const MODELS = { provider: "vercel-ai-gateway", baseUrl: "https://ai-gateway.vercel.sh", reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 1.75, @@ -14909,6 +15057,7 @@ export const MODELS = { provider: "vercel-ai-gateway", baseUrl: "https://ai-gateway.vercel.sh", reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 21, @@ -14926,6 +15075,7 @@ export const MODELS = { provider: "vercel-ai-gateway", baseUrl: "https://ai-gateway.vercel.sh", reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 1.75, @@ -14943,6 +15093,7 @@ export const MODELS = { provider: "vercel-ai-gateway", baseUrl: "https://ai-gateway.vercel.sh", reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 1.75, @@ -14960,6 +15111,7 @@ export const MODELS = { provider: "vercel-ai-gateway", baseUrl: "https://ai-gateway.vercel.sh", reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 2.5, @@ -14977,6 +15129,7 @@ export const MODELS = { provider: "vercel-ai-gateway", baseUrl: "https://ai-gateway.vercel.sh", reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 0.75, @@ -14994,6 +15147,7 @@ export const MODELS = { provider: "vercel-ai-gateway", baseUrl: "https://ai-gateway.vercel.sh", reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 0.19999999999999998, @@ -15011,6 +15165,7 @@ export const MODELS = { provider: "vercel-ai-gateway", baseUrl: "https://ai-gateway.vercel.sh", reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 30, @@ -15028,6 +15183,7 @@ export const MODELS = { provider: "vercel-ai-gateway", baseUrl: "https://ai-gateway.vercel.sh", reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 5, @@ -15045,6 +15201,7 @@ export const MODELS = { provider: "vercel-ai-gateway", baseUrl: "https://ai-gateway.vercel.sh", reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 30, diff --git a/packages/ai/src/models.ts b/packages/ai/src/models.ts index 47fdc3c5..2125e685 100644 --- a/packages/ai/src/models.ts +++ b/packages/ai/src/models.ts @@ -1,5 +1,5 @@ import { MODELS } from "./models.generated.js"; -import type { Api, KnownProvider, Model, Usage } from "./types.js"; +import type { Api, KnownProvider, Model, ModelThinkingLevel, Usage } from "./types.js"; const modelRegistry: Map>> = new Map(); @@ -45,36 +45,38 @@ export function calculateCost(model: Model, usage: Usage return usage.cost; } -/** - * Check if a model supports xhigh thinking level. - * - * Supported today: - * - GPT-5.2 / GPT-5.3 / GPT-5.4 / GPT-5.5 model families - * - DeepSeek V4 Pro and Flash - * - Opus 4.6+ models (xhigh maps to adaptive effort "max" on Anthropic-compatible providers) - */ -export function supportsXhigh(model: Model): boolean { - if ( - model.id.includes("gpt-5.2") || - model.id.includes("gpt-5.3") || - model.id.includes("gpt-5.4") || - model.id.includes("gpt-5.5") || - model.id.includes("deepseek-v4-pro") || - model.id.includes("deepseek-v4-flash") - ) { - return true; - } +const EXTENDED_THINKING_LEVELS: ModelThinkingLevel[] = ["off", "minimal", "low", "medium", "high", "xhigh"]; - if ( - model.id.includes("opus-4-6") || - model.id.includes("opus-4.6") || - model.id.includes("opus-4-7") || - model.id.includes("opus-4.7") - ) { - return true; - } +export function getSupportedThinkingLevels(model: Model): ModelThinkingLevel[] { + if (!model.reasoning) return ["off"]; - return false; + return EXTENDED_THINKING_LEVELS.filter((level) => { + const mapped = model.thinkingLevelMap?.[level]; + if (mapped === null) return false; + if (level === "xhigh") return mapped !== undefined; + return true; + }); +} + +export function clampThinkingLevel( + model: Model, + level: ModelThinkingLevel, +): ModelThinkingLevel { + const availableLevels = getSupportedThinkingLevels(model); + if (availableLevels.includes(level)) return level; + + const requestedIndex = EXTENDED_THINKING_LEVELS.indexOf(level); + if (requestedIndex === -1) return availableLevels[0] ?? "off"; + + for (let i = requestedIndex; i < EXTENDED_THINKING_LEVELS.length; i++) { + const candidate = EXTENDED_THINKING_LEVELS[i]; + if (availableLevels.includes(candidate)) return candidate; + } + for (let i = requestedIndex - 1; i >= 0; i--) { + const candidate = EXTENDED_THINKING_LEVELS[i]; + if (availableLevels.includes(candidate)) return candidate; + } + return availableLevels[0] ?? "off"; } /** diff --git a/packages/ai/src/providers/amazon-bedrock.ts b/packages/ai/src/providers/amazon-bedrock.ts index f23dd5d0..e4200e9d 100644 --- a/packages/ai/src/providers/amazon-bedrock.ts +++ b/packages/ai/src/providers/amazon-bedrock.ts @@ -495,11 +495,12 @@ function supportsAdaptiveThinking(modelId: string, modelName?: string): boolean } function mapThinkingLevelToEffort( + model: Model<"bedrock-converse-stream">, level: SimpleStreamOptions["reasoning"], - modelId: string, - modelName?: string, ): "low" | "medium" | "high" | "xhigh" | "max" { - const candidates = getModelMatchCandidates(modelId, modelName); + const mapped = level ? model.thinkingLevelMap?.[level] : undefined; + if (typeof mapped === "string") return mapped as "low" | "medium" | "high" | "xhigh" | "max"; + switch (level) { case "minimal": case "low": @@ -508,14 +509,6 @@ function mapThinkingLevelToEffort( return "medium"; case "high": return "high"; - case "xhigh": - if (candidates.some((s) => s.includes("opus-4-6"))) { - return "max"; - } - if (candidates.some((s) => s.includes("opus-4-7"))) { - return "xhigh"; - } - return "high"; default: return "high"; } @@ -892,7 +885,7 @@ function buildAdditionalModelRequestFields( const result: Record = supportsAdaptiveThinking(model.id, model.name) ? { thinking: { type: "adaptive", ...(display !== undefined ? { display } : {}) }, - output_config: { effort: mapThinkingLevelToEffort(options.reasoning, model.id, model.name) }, + output_config: { effort: mapThinkingLevelToEffort(model, options.reasoning) }, } : (() => { const defaultBudgets: Record = { diff --git a/packages/ai/src/providers/anthropic.ts b/packages/ai/src/providers/anthropic.ts index 13d82b85..62febfc3 100644 --- a/packages/ai/src/providers/anthropic.ts +++ b/packages/ai/src/providers/anthropic.ts @@ -694,24 +694,21 @@ function supportsAdaptiveThinking(modelId: string): boolean { * Map ThinkingLevel to Anthropic effort levels for adaptive thinking. * Note: effort "max" is only valid on Opus 4.6, while Opus 4.7 supports "xhigh". */ -function mapThinkingLevelToEffort(level: SimpleStreamOptions["reasoning"], modelId: string): AnthropicEffort { +function mapThinkingLevelToEffort( + model: Model<"anthropic-messages">, + level: SimpleStreamOptions["reasoning"], +): AnthropicEffort { + const mapped = level ? model.thinkingLevelMap?.[level] : undefined; + if (typeof mapped === "string") return mapped as AnthropicEffort; + switch (level) { case "minimal": - return "low"; case "low": return "low"; case "medium": return "medium"; case "high": return "high"; - case "xhigh": - if (modelId.includes("opus-4-6") || modelId.includes("opus-4.6")) { - return "max"; - } - if (modelId.includes("opus-4-7") || modelId.includes("opus-4.7")) { - return "xhigh"; - } - return "high"; default: return "high"; } @@ -735,7 +732,7 @@ export const streamSimpleAnthropic: StreamFunction<"anthropic-messages", SimpleS // For Opus 4.6 and Sonnet 4.6: use adaptive thinking with effort level // For older models: use budget-based thinking if (supportsAdaptiveThinking(model.id)) { - const effort = mapThinkingLevelToEffort(options.reasoning, model.id); + const effort = mapThinkingLevelToEffort(model, options.reasoning); return streamAnthropic(model, context, { ...base, thinkingEnabled: true, diff --git a/packages/ai/src/providers/azure-openai-responses.ts b/packages/ai/src/providers/azure-openai-responses.ts index 587b75cc..5d4bc9c6 100644 --- a/packages/ai/src/providers/azure-openai-responses.ts +++ b/packages/ai/src/providers/azure-openai-responses.ts @@ -1,7 +1,7 @@ import { AzureOpenAI } from "openai"; import type { ResponseCreateParamsStreaming } from "openai/resources/responses/responses.js"; import { getEnvApiKey } from "../env-api-keys.js"; -import { supportsXhigh } from "../models.js"; +import { clampThinkingLevel } from "../models.js"; import type { Api, AssistantMessage, @@ -14,7 +14,7 @@ import type { import { AssistantMessageEventStream } from "../utils/event-stream.js"; import { headersToRecord } from "../utils/headers.js"; import { convertResponsesMessages, convertResponsesTools, processResponsesStream } from "./openai-responses-shared.js"; -import { buildBaseOptions, clampReasoning } from "./simple-options.js"; +import { buildBaseOptions } from "./simple-options.js"; const DEFAULT_AZURE_API_VERSION = "v1"; const AZURE_TOOL_CALL_PROVIDERS = new Set(["openai", "openai-codex", "opencode", "azure-openai-responses"]); @@ -139,7 +139,8 @@ export const streamSimpleAzureOpenAIResponses: StreamFunction<"azure-openai-resp } const base = buildBaseOptions(model, options, apiKey); - const reasoningEffort = supportsXhigh(model) ? options?.reasoning : clampReasoning(options?.reasoning); + const clampedReasoning = options?.reasoning ? clampThinkingLevel(model, options.reasoning) : undefined; + const reasoningEffort = clampedReasoning === "off" ? undefined : clampedReasoning; return streamAzureOpenAIResponses(model, context, { ...base, @@ -261,13 +262,18 @@ function buildParams( if (model.reasoning) { if (options?.reasoningEffort || options?.reasoningSummary) { + const effort = options?.reasoningEffort + ? (model.thinkingLevelMap?.[options.reasoningEffort] ?? options.reasoningEffort) + : "medium"; params.reasoning = { - effort: options?.reasoningEffort || "medium", + effort: effort as NonNullable["effort"], summary: options?.reasoningSummary || "auto", }; params.include = ["reasoning.encrypted_content"]; - } else { - params.reasoning = { effort: "none" }; + } else if (model.thinkingLevelMap?.off !== null) { + params.reasoning = { + effort: (model.thinkingLevelMap?.off ?? "none") as NonNullable["effort"], + }; } } diff --git a/packages/ai/src/providers/google-vertex.ts b/packages/ai/src/providers/google-vertex.ts index b8bc1ded..e29adcb1 100644 --- a/packages/ai/src/providers/google-vertex.ts +++ b/packages/ai/src/providers/google-vertex.ts @@ -7,7 +7,7 @@ import { type ThinkingConfig, ThinkingLevel, } from "@google/genai"; -import { calculateCost } from "../models.js"; +import { calculateCost, clampThinkingLevel } from "../models.js"; import type { Api, AssistantMessage, @@ -33,7 +33,7 @@ import { mapToolChoice, retainThoughtSignature, } from "./google-shared.js"; -import { buildBaseOptions, clampReasoning } from "./simple-options.js"; +import { buildBaseOptions } from "./simple-options.js"; export interface GoogleVertexOptions extends StreamOptions { toolChoice?: "auto" | "none" | "any"; @@ -305,7 +305,8 @@ export const streamSimpleGoogleVertex: StreamFunction<"google-vertex", SimpleStr } satisfies GoogleVertexOptions); } - const effort = clampReasoning(options.reasoning)!; + const clampedReasoning = clampThinkingLevel(model, options.reasoning); + const effort = (clampedReasoning === "off" ? "high" : clampedReasoning) as ClampedThinkingLevel; const geminiModel = model as unknown as Model<"google-generative-ai">; if (isGemini3ProModel(geminiModel) || isGemini3FlashModel(geminiModel)) { diff --git a/packages/ai/src/providers/google.ts b/packages/ai/src/providers/google.ts index 9d69e65d..fd1adfdd 100644 --- a/packages/ai/src/providers/google.ts +++ b/packages/ai/src/providers/google.ts @@ -5,7 +5,7 @@ import { type ThinkingConfig, } from "@google/genai"; import { getEnvApiKey } from "../env-api-keys.js"; -import { calculateCost } from "../models.js"; +import { calculateCost, clampThinkingLevel } from "../models.js"; import type { Api, AssistantMessage, @@ -31,7 +31,7 @@ import { mapToolChoice, retainThoughtSignature, } from "./google-shared.js"; -import { buildBaseOptions, clampReasoning } from "./simple-options.js"; +import { buildBaseOptions } from "./simple-options.js"; export interface GoogleOptions extends StreamOptions { toolChoice?: "auto" | "none" | "any"; @@ -290,7 +290,8 @@ export const streamSimpleGoogle: StreamFunction<"google-generative-ai", SimpleSt return streamGoogle(model, context, { ...base, thinking: { enabled: false } } satisfies GoogleOptions); } - const effort = clampReasoning(options.reasoning)!; + const clampedReasoning = clampThinkingLevel(model, options.reasoning); + const effort = (clampedReasoning === "off" ? "high" : clampedReasoning) as ClampedThinkingLevel; const googleModel = model as Model<"google-generative-ai">; if (isGemini3ProModel(googleModel) || isGemini3FlashModel(googleModel) || isGemma4Model(googleModel)) { diff --git a/packages/ai/src/providers/mistral.ts b/packages/ai/src/providers/mistral.ts index df3ef53b..c1500dbf 100644 --- a/packages/ai/src/providers/mistral.ts +++ b/packages/ai/src/providers/mistral.ts @@ -7,7 +7,7 @@ import type { FunctionTool, } from "@mistralai/mistralai/models/components"; import { getEnvApiKey } from "../env-api-keys.js"; -import { calculateCost } from "../models.js"; +import { calculateCost, clampThinkingLevel } from "../models.js"; import type { AssistantMessage, Context, @@ -26,7 +26,7 @@ import { AssistantMessageEventStream } from "../utils/event-stream.js"; import { shortHash } from "../utils/hash.js"; import { parseStreamingJson } from "../utils/json-parse.js"; import { sanitizeSurrogates } from "../utils/sanitize-unicode.js"; -import { buildBaseOptions, clampReasoning } from "./simple-options.js"; +import { buildBaseOptions } from "./simple-options.js"; import { transformMessages } from "./transform-messages.js"; const MISTRAL_TOOL_CALL_ID_LENGTH = 9; @@ -119,13 +119,15 @@ export const streamSimpleMistral: StreamFunction<"mistral-conversations", Simple } const base = buildBaseOptions(model, options, apiKey); - const reasoning = clampReasoning(options?.reasoning); + const clampedReasoning = options?.reasoning ? clampThinkingLevel(model, options.reasoning) : undefined; + const reasoning = clampedReasoning === "off" ? undefined : clampedReasoning; const shouldUseReasoning = model.reasoning && reasoning !== undefined; return streamMistral(model, context, { ...base, promptMode: shouldUseReasoning && usesPromptModeReasoning(model) ? "reasoning" : undefined, - reasoningEffort: shouldUseReasoning && usesReasoningEffort(model) ? mapReasoningEffort(reasoning) : undefined, + reasoningEffort: + shouldUseReasoning && usesReasoningEffort(model) ? mapReasoningEffort(model, reasoning) : undefined, } satisfies MistralOptions); }; @@ -594,8 +596,11 @@ function usesPromptModeReasoning(model: Model<"mistral-conversations">): boolean return model.reasoning && !usesReasoningEffort(model); } -function mapReasoningEffort(_level: Exclude): MistralReasoningEffort { - return "high"; +function mapReasoningEffort( + model: Model<"mistral-conversations">, + level: Exclude, +): MistralReasoningEffort { + return (model.thinkingLevelMap?.[level] ?? "high") as MistralReasoningEffort; } function mapToolChoice( diff --git a/packages/ai/src/providers/openai-codex-responses.ts b/packages/ai/src/providers/openai-codex-responses.ts index 6469e741..d57411f3 100644 --- a/packages/ai/src/providers/openai-codex-responses.ts +++ b/packages/ai/src/providers/openai-codex-responses.ts @@ -21,7 +21,7 @@ if (typeof process !== "undefined" && (process.versions?.node || process.version } import { getEnvApiKey } from "../env-api-keys.js"; -import { supportsXhigh } from "../models.js"; +import { clampThinkingLevel } from "../models.js"; import type { Api, AssistantMessage, @@ -35,7 +35,7 @@ import type { import { AssistantMessageEventStream } from "../utils/event-stream.js"; import { headersToRecord } from "../utils/headers.js"; import { convertResponsesMessages, convertResponsesTools, processResponsesStream } from "./openai-responses-shared.js"; -import { buildBaseOptions, clampReasoning } from "./simple-options.js"; +import { buildBaseOptions } from "./simple-options.js"; // ============================================================================ // Configuration @@ -299,7 +299,8 @@ export const streamSimpleOpenAICodexResponses: StreamFunction<"openai-codex-resp } const base = buildBaseOptions(model, options, apiKey); - const reasoningEffort = supportsXhigh(model) ? options?.reasoning : clampReasoning(options?.reasoning); + const clampedReasoning = options?.reasoning ? clampThinkingLevel(model, options.reasoning) : undefined; + const reasoningEffort = clampedReasoning === "off" ? undefined : clampedReasoning; return streamOpenAICodexResponses(model, context, { ...base, @@ -346,27 +347,21 @@ function buildRequestBody( } if (options?.reasoningEffort !== undefined) { - body.reasoning = { - effort: clampReasoningEffort(model.id, options.reasoningEffort), - summary: options.reasoningSummary ?? "auto", - }; + const effort = + options.reasoningEffort === "none" + ? (model.thinkingLevelMap?.off ?? "none") + : (model.thinkingLevelMap?.[options.reasoningEffort] ?? options.reasoningEffort); + if (effort !== null) { + body.reasoning = { + effort, + summary: options.reasoningSummary ?? "auto", + }; + } } return body; } -function clampReasoningEffort(modelId: string, effort: string): string { - const id = modelId.includes("/") ? modelId.split("/").pop()! : modelId; - if ( - (id.startsWith("gpt-5.2") || id.startsWith("gpt-5.3") || id.startsWith("gpt-5.4") || id.startsWith("gpt-5.5")) && - effort === "minimal" - ) - return "low"; - if (id === "gpt-5.1" && effort === "xhigh") return "high"; - if (id === "gpt-5.1-codex-mini") return effort === "high" || effort === "xhigh" ? "high" : "medium"; - return effort; -} - function getServiceTierCostMultiplier( model: Pick, "id">, serviceTier: ResponseCreateParamsStreaming["service_tier"] | undefined, diff --git a/packages/ai/src/providers/openai-completions.ts b/packages/ai/src/providers/openai-completions.ts index 6c7ef0d0..b4e19e5f 100644 --- a/packages/ai/src/providers/openai-completions.ts +++ b/packages/ai/src/providers/openai-completions.ts @@ -11,7 +11,7 @@ import type { ChatCompletionToolMessageParam, } from "openai/resources/chat/completions.js"; import { getEnvApiKey } from "../env-api-keys.js"; -import { calculateCost, supportsXhigh } from "../models.js"; +import { calculateCost, clampThinkingLevel } from "../models.js"; import type { AssistantMessage, CacheRetention, @@ -36,7 +36,7 @@ import { parseStreamingJson } from "../utils/json-parse.js"; import { sanitizeSurrogates } from "../utils/sanitize-unicode.js"; import { isCloudflareProvider, resolveCloudflareBaseUrl } from "./cloudflare.js"; import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./github-copilot-headers.js"; -import { buildBaseOptions, clampReasoning } from "./simple-options.js"; +import { buildBaseOptions } from "./simple-options.js"; import { transformMessages } from "./transform-messages.js"; /** @@ -410,7 +410,8 @@ export const streamSimpleOpenAICompletions: StreamFunction<"openai-completions", } const base = buildBaseOptions(model, options, apiKey); - const reasoningEffort = supportsXhigh(model) ? options?.reasoning : clampReasoning(options?.reasoning); + const clampedReasoning = options?.reasoning ? clampThinkingLevel(model, options.reasoning) : undefined; + const reasoningEffort = clampedReasoning === "off" ? undefined : clampedReasoning; const toolChoice = (options as OpenAICompletionsOptions | undefined)?.toolChoice; return streamOpenAICompletions(model, context, { @@ -547,21 +548,27 @@ function buildParams( } else if (compat.thinkingFormat === "deepseek" && model.reasoning) { (params as any).thinking = { type: options?.reasoningEffort ? "enabled" : "disabled" }; if (options?.reasoningEffort) { - (params as any).reasoning_effort = mapReasoningEffort(options.reasoningEffort, compat.reasoningEffortMap); + (params as any).reasoning_effort = + model.thinkingLevelMap?.[options.reasoningEffort] ?? options.reasoningEffort; } } else if (compat.thinkingFormat === "openrouter" && model.reasoning) { // OpenRouter normalizes reasoning across providers via a nested reasoning object. const openRouterParams = params as typeof params & { reasoning?: { effort?: string } }; if (options?.reasoningEffort) { openRouterParams.reasoning = { - effort: mapReasoningEffort(options.reasoningEffort, compat.reasoningEffortMap), + effort: model.thinkingLevelMap?.[options.reasoningEffort] ?? options.reasoningEffort, }; - } else { - openRouterParams.reasoning = { effort: "none" }; + } else if (model.thinkingLevelMap?.off !== null) { + openRouterParams.reasoning = { effort: model.thinkingLevelMap?.off ?? "none" }; } } else if (options?.reasoningEffort && model.reasoning && compat.supportsReasoningEffort) { // OpenAI-style reasoning_effort - (params as any).reasoning_effort = mapReasoningEffort(options.reasoningEffort, compat.reasoningEffortMap); + (params as any).reasoning_effort = model.thinkingLevelMap?.[options.reasoningEffort] ?? options.reasoningEffort; + } else if (!options?.reasoningEffort && model.reasoning && compat.supportsReasoningEffort) { + const offValue = model.thinkingLevelMap?.off; + if (typeof offValue === "string") { + (params as any).reasoning_effort = offValue; + } } // OpenRouter provider routing preferences @@ -583,13 +590,6 @@ function buildParams( return params; } -function mapReasoningEffort( - effort: NonNullable, - reasoningEffortMap: Partial, string>>, -): string { - return reasoningEffortMap[effort] ?? effort; -} - function getCompatCacheControl( compat: ResolvedOpenAICompletionsCompat, cacheRetention: CacheRetention, @@ -1057,32 +1057,13 @@ function detectCompat(model: Model<"openai-completions">): ResolvedOpenAIComplet const useMaxTokens = baseUrl.includes("chutes.ai") || isMoonshot || isCloudflareAiGateway; const isGrok = provider === "xai" || baseUrl.includes("api.x.ai"); - const isGroq = provider === "groq" || baseUrl.includes("groq.com"); const isDeepSeek = provider === "deepseek" || baseUrl.includes("deepseek.com"); const cacheControlFormat = provider === "openrouter" && model.id.startsWith("anthropic/") ? "anthropic" : undefined; - const reasoningEffortMap = isDeepSeek - ? { - minimal: "high", - low: "high", - medium: "high", - high: "high", - xhigh: "max", - } - : isGroq && model.id === "qwen/qwen3-32b" - ? { - minimal: "default", - low: "default", - medium: "default", - high: "default", - xhigh: "default", - } - : {}; return { supportsStore: !isNonStandard, supportsDeveloperRole: !isNonStandard, supportsReasoningEffort: !isGrok && !isZai && !isMoonshot && !isCloudflareAiGateway, - reasoningEffortMap, supportsUsageInStreaming: true, maxTokensField: useMaxTokens ? "max_tokens" : "max_completion_tokens", requiresToolResultName: false, @@ -1118,7 +1099,6 @@ function getCompat(model: Model<"openai-completions">): ResolvedOpenAICompletion supportsStore: model.compat.supportsStore ?? detected.supportsStore, supportsDeveloperRole: model.compat.supportsDeveloperRole ?? detected.supportsDeveloperRole, supportsReasoningEffort: model.compat.supportsReasoningEffort ?? detected.supportsReasoningEffort, - reasoningEffortMap: model.compat.reasoningEffortMap ?? detected.reasoningEffortMap, supportsUsageInStreaming: model.compat.supportsUsageInStreaming ?? detected.supportsUsageInStreaming, maxTokensField: model.compat.maxTokensField ?? detected.maxTokensField, requiresToolResultName: model.compat.requiresToolResultName ?? detected.requiresToolResultName, diff --git a/packages/ai/src/providers/openai-responses.ts b/packages/ai/src/providers/openai-responses.ts index 7d327468..8cc446d3 100644 --- a/packages/ai/src/providers/openai-responses.ts +++ b/packages/ai/src/providers/openai-responses.ts @@ -1,7 +1,7 @@ import OpenAI from "openai"; import type { ResponseCreateParamsStreaming } from "openai/resources/responses/responses.js"; import { getEnvApiKey } from "../env-api-keys.js"; -import { supportsXhigh } from "../models.js"; +import { clampThinkingLevel } from "../models.js"; import type { Api, AssistantMessage, @@ -19,7 +19,7 @@ import { headersToRecord } from "../utils/headers.js"; import { isCloudflareProvider, resolveCloudflareBaseUrl } from "./cloudflare.js"; import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./github-copilot-headers.js"; import { convertResponsesMessages, convertResponsesTools, processResponsesStream } from "./openai-responses-shared.js"; -import { buildBaseOptions, clampReasoning } from "./simple-options.js"; +import { buildBaseOptions } from "./simple-options.js"; const OPENAI_TOOL_CALL_PROVIDERS = new Set(["openai", "openai-codex", "opencode"]); @@ -150,7 +150,8 @@ export const streamSimpleOpenAIResponses: StreamFunction<"openai-responses", Sim } const base = buildBaseOptions(model, options, apiKey); - const reasoningEffort = supportsXhigh(model) ? options?.reasoning : clampReasoning(options?.reasoning); + const clampedReasoning = options?.reasoning ? clampThinkingLevel(model, options.reasoning) : undefined; + const reasoningEffort = clampedReasoning === "off" ? undefined : clampedReasoning; return streamOpenAIResponses(model, context, { ...base, @@ -246,13 +247,18 @@ function buildParams(model: Model<"openai-responses">, context: Context, options if (model.reasoning) { if (options?.reasoningEffort || options?.reasoningSummary) { + const effort = options?.reasoningEffort + ? (model.thinkingLevelMap?.[options.reasoningEffort] ?? options.reasoningEffort) + : "medium"; params.reasoning = { - effort: options?.reasoningEffort || "medium", + effort: effort as NonNullable["effort"], summary: options?.reasoningSummary || "auto", }; params.include = ["reasoning.encrypted_content"]; - } else if (model.provider !== "github-copilot") { - params.reasoning = { effort: "none" }; + } else if (model.provider !== "github-copilot" && model.thinkingLevelMap?.off !== null) { + params.reasoning = { + effort: (model.thinkingLevelMap?.off ?? "none") as NonNullable["effort"], + }; } } diff --git a/packages/ai/src/types.ts b/packages/ai/src/types.ts index 6a74c6c3..60502150 100644 --- a/packages/ai/src/types.ts +++ b/packages/ai/src/types.ts @@ -47,6 +47,8 @@ export type KnownProvider = export type Provider = KnownProvider | string; export type ThinkingLevel = "minimal" | "low" | "medium" | "high" | "xhigh"; +export type ModelThinkingLevel = "off" | ThinkingLevel; +export type ThinkingLevelMap = Partial>; /** Token budgets for each thinking level (token-based providers only) */ export interface ThinkingBudgets { @@ -284,8 +286,6 @@ export interface OpenAICompletionsCompat { supportsDeveloperRole?: boolean; /** Whether the provider supports `reasoning_effort`. Default: auto-detected from URL. */ supportsReasoningEffort?: boolean; - /** Optional mapping from pi-ai reasoning levels to provider/model-specific `reasoning_effort` values. */ - reasoningEffortMap?: Partial>; /** Whether the provider supports `stream_options: { include_usage: true }` for token usage in streaming responses. Default: true. */ supportsUsageInStreaming?: boolean; /** Which field to use for max tokens. Default: auto-detected from URL. */ @@ -433,6 +433,11 @@ export interface Model { provider: Provider; baseUrl: string; reasoning: boolean; + /** + * Maps pi thinking levels to provider/model-specific values. + * Missing keys use provider defaults. null marks a level as unsupported. + */ + thinkingLevelMap?: ThinkingLevelMap; input: ("text" | "image")[]; cost: { input: number; // $/million tokens diff --git a/packages/ai/test/openai-completions-thinking-as-text.test.ts b/packages/ai/test/openai-completions-thinking-as-text.test.ts index 24425f3a..a0bb6519 100644 --- a/packages/ai/test/openai-completions-thinking-as-text.test.ts +++ b/packages/ai/test/openai-completions-thinking-as-text.test.ts @@ -25,7 +25,6 @@ const compat = { supportsStore: true, supportsDeveloperRole: true, supportsReasoningEffort: true, - reasoningEffortMap: {}, supportsUsageInStreaming: true, maxTokensField: "max_completion_tokens", requiresToolResultName: false, diff --git a/packages/ai/test/openai-completions-tool-result-images.test.ts b/packages/ai/test/openai-completions-tool-result-images.test.ts index ac1eb28a..cd4d6436 100644 --- a/packages/ai/test/openai-completions-tool-result-images.test.ts +++ b/packages/ai/test/openai-completions-tool-result-images.test.ts @@ -23,7 +23,6 @@ const compat: Required = { supportsStore: true, supportsDeveloperRole: true, supportsReasoningEffort: true, - reasoningEffortMap: {}, supportsUsageInStreaming: true, maxTokensField: "max_completion_tokens", requiresToolResultName: false, diff --git a/packages/ai/test/supports-xhigh.test.ts b/packages/ai/test/supports-xhigh.test.ts index 8474d7ef..6a66ee94 100644 --- a/packages/ai/test/supports-xhigh.test.ts +++ b/packages/ai/test/supports-xhigh.test.ts @@ -1,52 +1,52 @@ import { describe, expect, it } from "vitest"; -import { getModel, supportsXhigh } from "../src/models.js"; +import { getModel, getSupportedThinkingLevels } from "../src/models.js"; -describe("supportsXhigh", () => { - it("returns true for Anthropic Opus 4.6 on anthropic-messages API", () => { +describe("getSupportedThinkingLevels", () => { + it("includes xhigh for Anthropic Opus 4.6 on anthropic-messages API", () => { const model = getModel("anthropic", "claude-opus-4-6"); expect(model).toBeDefined(); - expect(supportsXhigh(model!)).toBe(true); + expect(getSupportedThinkingLevels(model!)).toContain("xhigh"); }); - it("returns true for Anthropic Opus 4.7 on anthropic-messages API", () => { + it("includes xhigh for Anthropic Opus 4.7 on anthropic-messages API", () => { const model = getModel("anthropic", "claude-opus-4-7"); expect(model).toBeDefined(); - expect(supportsXhigh(model!)).toBe(true); + expect(getSupportedThinkingLevels(model!)).toContain("xhigh"); }); - it("returns false for non-Opus Anthropic models", () => { + it("does not include xhigh for non-Opus Anthropic models", () => { const model = getModel("anthropic", "claude-sonnet-4-5"); expect(model).toBeDefined(); - expect(supportsXhigh(model!)).toBe(false); + expect(getSupportedThinkingLevels(model!)).not.toContain("xhigh"); }); - it.each(["gpt-5.4", "gpt-5.5"] as const)("returns true for %s models", (modelId) => { + it.each(["gpt-5.4", "gpt-5.5"] as const)("includes xhigh for %s models", (modelId) => { const model = getModel("openai-codex", modelId); expect(model).toBeDefined(); - expect(supportsXhigh(model!)).toBe(true); + expect(getSupportedThinkingLevels(model!)).toContain("xhigh"); }); - it("returns true for DeepSeek V4 Flash on the DeepSeek provider", () => { + it("includes only high/xhigh plus off for DeepSeek V4 Flash on the DeepSeek provider", () => { const model = getModel("deepseek", "deepseek-v4-flash"); expect(model).toBeDefined(); - expect(supportsXhigh(model!)).toBe(true); + expect(getSupportedThinkingLevels(model!)).toEqual(["off", "high", "xhigh"]); }); - it("returns true for DeepSeek V4 Flash on opencode-go", () => { + it("includes only high/xhigh plus off for DeepSeek V4 Flash on opencode-go", () => { const model = getModel("opencode-go", "deepseek-v4-flash"); expect(model).toBeDefined(); - expect(supportsXhigh(model!)).toBe(true); + expect(getSupportedThinkingLevels(model!)).toEqual(["off", "high", "xhigh"]); }); - it("returns true for DeepSeek V4 Flash on OpenRouter", () => { + it("includes only high/xhigh plus off for DeepSeek V4 Flash on OpenRouter", () => { const model = getModel("openrouter", "deepseek/deepseek-v4-flash"); expect(model).toBeDefined(); - expect(supportsXhigh(model!)).toBe(true); + expect(getSupportedThinkingLevels(model!)).toEqual(["off", "high", "xhigh"]); }); - it("returns true for OpenRouter Opus 4.6 (openai-completions API)", () => { + it("includes xhigh for OpenRouter Opus 4.6 (openai-completions API)", () => { const model = getModel("openrouter", "anthropic/claude-opus-4.6"); expect(model).toBeDefined(); - expect(supportsXhigh(model!)).toBe(true); + expect(getSupportedThinkingLevels(model!)).toContain("xhigh"); }); }); diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 29c32c11..39f93180 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,14 @@ ## [Unreleased] +### Breaking Changes + +- Replaced `compat.reasoningEffortMap` in `models.json` and `pi.registerProvider()` model definitions with model-level `thinkingLevelMap` ([#3208](https://github.com/badlogic/pi-mono/issues/3208)). Migration: move old mappings from `compat.reasoningEffortMap` to `thinkingLevelMap`. Use string values for provider-specific thinking values and `null` for unsupported pi levels that should be hidden and skipped by cycling. See `docs/models.md#thinking-level-map` and `docs/custom-provider.md`. + +### Added + +- Added model-level `thinkingLevelMap` support in `models.json` and `pi.registerProvider()`, allowing models to expose only the thinking levels they actually support ([#3208](https://github.com/badlogic/pi-mono/issues/3208)). + ### Fixed - Fixed `pi.registerProvider()` to honor per-model `baseUrl` overrides ([#4063](https://github.com/badlogic/pi-mono/issues/4063)). diff --git a/packages/coding-agent/docs/custom-provider.md b/packages/coding-agent/docs/custom-provider.md index 783f9ff8..3e0b326f 100644 --- a/packages/coding-agent/docs/custom-provider.md +++ b/packages/coding-agent/docs/custom-provider.md @@ -201,28 +201,29 @@ The `api` field determines which streaming implementation is used: | `google-vertex` | Google Vertex AI API | | `bedrock-converse-stream` | Amazon Bedrock Converse API | -Most OpenAI-compatible providers work with `openai-completions`. Use `compat` for quirks: +Most OpenAI-compatible providers work with `openai-completions`. Use model-level `thinkingLevelMap` for model-specific thinking levels, and `compat` for provider quirks: ```typescript models: [{ id: "custom-model", // ... + reasoning: true, + thinkingLevelMap: { // map pi levels to provider values; null hides unsupported levels + minimal: null, + low: null, + medium: null, + high: "default", + xhigh: "max" + }, compat: { - supportsDeveloperRole: false, // use "system" instead of "developer" + supportsDeveloperRole: false, // use "system" instead of "developer" supportsReasoningEffort: true, - reasoningEffortMap: { // map pi-ai levels to provider values - minimal: "default", - low: "default", - medium: "default", - high: "default", - xhigh: "default" - }, - maxTokensField: "max_tokens", // instead of "max_completion_tokens" - requiresToolResultName: true, // tool results need name field - thinkingFormat: "qwen", // top-level enable_thinking: true - cacheControlFormat: "anthropic" // Anthropic-style cache_control markers - } - }] + maxTokensField: "max_tokens", // instead of "max_completion_tokens" + requiresToolResultName: true, // tool results need name field + thinkingFormat: "qwen", // top-level enable_thinking: true + cacheControlFormat: "anthropic" // Anthropic-style cache_control markers + } +}] ``` Use `qwen-chat-template` instead for local Qwen-compatible servers that read `chat_template_kwargs.enable_thinking`. @@ -601,6 +602,9 @@ interface ProviderModelConfig { /** Whether the model supports extended thinking. */ reasoning: boolean; + /** Maps pi thinking levels to provider/model-specific values; null marks a level unsupported. */ + thinkingLevelMap?: Partial>; + /** Supported input types. */ input: ("text" | "image")[]; @@ -626,7 +630,6 @@ interface ProviderModelConfig { supportsStore?: boolean; supportsDeveloperRole?: boolean; supportsReasoningEffort?: boolean; - reasoningEffortMap?: Partial>; supportsUsageInStreaming?: boolean; maxTokensField?: "max_completion_tokens" | "max_tokens"; requiresToolResultName?: boolean; diff --git a/packages/coding-agent/docs/models.md b/packages/coding-agent/docs/models.md index e2c4748d..0bc41643 100644 --- a/packages/coding-agent/docs/models.md +++ b/packages/coding-agent/docs/models.md @@ -192,6 +192,7 @@ If your command is slow, expensive, rate-limited, or should keep using a previou | `name` | No | `id` | Human-readable model label. Used for matching (`--model` patterns) and shown in model details/status text. | | `api` | No | provider's `api` | Override provider's API for this model | | `reasoning` | No | `false` | Supports extended thinking | +| `thinkingLevelMap` | No | omitted | Maps pi thinking levels to provider values and marks unsupported levels (see below) | | `input` | No | `["text"]` | Input types: `["text"]` or `["text", "image"]` | | `contextWindow` | No | `128000` | Context window size in tokens | | `maxTokens` | No | `16384` | Maximum output tokens | @@ -202,6 +203,48 @@ Current behavior: - `/model` and `--list-models` list entries by model `id`. - The configured `name` is used for model matching and detail/status text. +### Thinking Level Map + +Use `thinkingLevelMap` on a model to describe model-specific thinking controls. Keys are pi thinking levels: `off`, `minimal`, `low`, `medium`, `high`, `xhigh`. + +Values are tristate: + +| Value | Meaning | +|-------|---------| +| omitted | Level is supported and uses the provider's default mapping | +| string | Level is supported and this value is sent to the provider | +| `null` | Level is unsupported and hidden/skipped/clamped away | + +Example for a model that only supports off, high, and max reasoning: + +```json +{ + "id": "deepseek-v4-pro", + "reasoning": true, + "thinkingLevelMap": { + "minimal": null, + "low": null, + "medium": null, + "high": "high", + "xhigh": "max" + } +} +``` + +Example for a model where thinking cannot be disabled: + +```json +{ + "id": "always-thinking-model", + "reasoning": true, + "thinkingLevelMap": { + "off": null + } +} +``` + +Migration: older configs that used `compat.reasoningEffortMap` should move that mapping to model-level `thinkingLevelMap`. Use `null` for levels that should not appear in the UI. + ## Overriding Built-in Providers Route a built-in provider through a proxy without redefining models: @@ -332,7 +375,6 @@ For providers with partial OpenAI compatibility, use the `compat` field. | `supportsStore` | Provider supports `store` field | | `supportsDeveloperRole` | Use `developer` vs `system` role | | `supportsReasoningEffort` | Support for `reasoning_effort` parameter | -| `reasoningEffortMap` | Map pi thinking levels to provider-specific `reasoning_effort` values | | `supportsUsageInStreaming` | Supports `stream_options: { include_usage: true }` (default: `true`) | | `maxTokensField` | Use `max_completion_tokens` or `max_tokens` | | `requiresToolResultName` | Include `name` on tool result messages | diff --git a/packages/coding-agent/src/core/agent-session.ts b/packages/coding-agent/src/core/agent-session.ts index eb430a3c..15476744 100644 --- a/packages/coding-agent/src/core/agent-session.ts +++ b/packages/coding-agent/src/core/agent-session.ts @@ -24,7 +24,13 @@ import type { ThinkingLevel, } from "@mariozechner/pi-agent-core"; import type { AssistantMessage, ImageContent, Message, Model, TextContent } from "@mariozechner/pi-ai"; -import { isContextOverflow, modelsAreEqual, resetApiProviders, supportsXhigh } from "@mariozechner/pi-ai"; +import { + clampThinkingLevel, + getSupportedThinkingLevels, + isContextOverflow, + modelsAreEqual, + resetApiProviders, +} from "@mariozechner/pi-ai"; import { theme } from "../modes/interactive/theme/theme.js"; import { stripFrontmatter } from "../utils/frontmatter.js"; import { sleep } from "../utils/sleep.js"; @@ -230,9 +236,6 @@ interface ToolDefinitionEntry { /** Standard thinking levels */ const THINKING_LEVELS: ThinkingLevel[] = ["off", "minimal", "low", "medium", "high"]; -/** Thinking levels including xhigh (for supported models) */ -const THINKING_LEVELS_WITH_XHIGH: ThinkingLevel[] = ["off", "minimal", "low", "medium", "high", "xhigh"]; - // ============================================================================ // AgentSession Class // ============================================================================ @@ -1546,15 +1549,8 @@ export class AgentSession { * The provider will clamp to what the specific model supports internally. */ getAvailableThinkingLevels(): ThinkingLevel[] { - if (!this.supportsThinking()) return ["off"]; - return this.supportsXhighThinking() ? THINKING_LEVELS_WITH_XHIGH : THINKING_LEVELS; - } - - /** - * Check if current model supports xhigh thinking level. - */ - supportsXhighThinking(): boolean { - return this.model ? supportsXhigh(this.model) : false; + if (!this.model) return THINKING_LEVELS; + return getSupportedThinkingLevels(this.model) as ThinkingLevel[]; } /** @@ -1574,22 +1570,8 @@ export class AgentSession { return this.thinkingLevel; } - private _clampThinkingLevel(level: ThinkingLevel, availableLevels: ThinkingLevel[]): ThinkingLevel { - const ordered = THINKING_LEVELS_WITH_XHIGH; - const available = new Set(availableLevels); - const requestedIndex = ordered.indexOf(level); - if (requestedIndex === -1) { - return availableLevels[0] ?? "off"; - } - for (let i = requestedIndex; i < ordered.length; i++) { - const candidate = ordered[i]; - if (available.has(candidate)) return candidate; - } - for (let i = requestedIndex - 1; i >= 0; i--) { - const candidate = ordered[i]; - if (available.has(candidate)) return candidate; - } - return availableLevels[0] ?? "off"; + private _clampThinkingLevel(level: ThinkingLevel, _availableLevels: ThinkingLevel[]): ThinkingLevel { + return this.model ? (clampThinkingLevel(this.model, level) as ThinkingLevel) : "off"; } // ========================================================================= diff --git a/packages/coding-agent/src/core/extensions/types.ts b/packages/coding-agent/src/core/extensions/types.ts index 919603c9..50a102cb 100644 --- a/packages/coding-agent/src/core/extensions/types.ts +++ b/packages/coding-agent/src/core/extensions/types.ts @@ -1359,6 +1359,8 @@ export interface ProviderModelConfig { baseUrl?: string; /** Whether the model supports extended thinking. */ reasoning: boolean; + /** Maps pi thinking levels to provider/model-specific values; null marks a level unsupported. */ + thinkingLevelMap?: Model["thinkingLevelMap"]; /** Supported input types. */ input: ("text" | "image")[]; /** Cost per token (for tracking, can be 0). */ diff --git a/packages/coding-agent/src/core/model-registry.ts b/packages/coding-agent/src/core/model-registry.ts index 1a496668..798b8f8c 100644 --- a/packages/coding-agent/src/core/model-registry.ts +++ b/packages/coding-agent/src/core/model-registry.ts @@ -80,20 +80,21 @@ const VercelGatewayRoutingSchema = Type.Object({ order: Type.Optional(Type.Array(Type.String())), }); -// Schema for OpenAI compatibility settings -const ReasoningEffortMapSchema = Type.Object({ - minimal: Type.Optional(Type.String()), - low: Type.Optional(Type.String()), - medium: Type.Optional(Type.String()), - high: Type.Optional(Type.String()), - xhigh: Type.Optional(Type.String()), +// Schema for thinking level support and provider-specific values +const ThinkingLevelMapValueSchema = Type.Union([Type.String(), Type.Null()]); +const ThinkingLevelMapSchema = Type.Object({ + off: Type.Optional(ThinkingLevelMapValueSchema), + minimal: Type.Optional(ThinkingLevelMapValueSchema), + low: Type.Optional(ThinkingLevelMapValueSchema), + medium: Type.Optional(ThinkingLevelMapValueSchema), + high: Type.Optional(ThinkingLevelMapValueSchema), + xhigh: Type.Optional(ThinkingLevelMapValueSchema), }); const OpenAICompletionsCompatSchema = Type.Object({ supportsStore: Type.Optional(Type.Boolean()), supportsDeveloperRole: Type.Optional(Type.Boolean()), supportsReasoningEffort: Type.Optional(Type.Boolean()), - reasoningEffortMap: Type.Optional(ReasoningEffortMapSchema), supportsUsageInStreaming: Type.Optional(Type.Boolean()), maxTokensField: Type.Optional(Type.Union([Type.Literal("max_completion_tokens"), Type.Literal("max_tokens")])), requiresToolResultName: Type.Optional(Type.Boolean()), @@ -141,6 +142,7 @@ const ModelDefinitionSchema = Type.Object({ api: Type.Optional(Type.String({ minLength: 1 })), baseUrl: Type.Optional(Type.String({ minLength: 1 })), reasoning: Type.Optional(Type.Boolean()), + thinkingLevelMap: Type.Optional(ThinkingLevelMapSchema), input: Type.Optional(Type.Array(Type.Union([Type.Literal("text"), Type.Literal("image")]))), cost: Type.Optional( Type.Object({ @@ -160,6 +162,7 @@ const ModelDefinitionSchema = Type.Object({ const ModelOverrideSchema = Type.Object({ name: Type.Optional(Type.String({ minLength: 1 })), reasoning: Type.Optional(Type.Boolean()), + thinkingLevelMap: Type.Optional(ThinkingLevelMapSchema), input: Type.Optional(Type.Array(Type.Union([Type.Literal("text"), Type.Literal("image")]))), cost: Type.Optional( Type.Object({ @@ -288,6 +291,9 @@ function applyModelOverride(model: Model, override: ModelOverride): Model["thinkingLevelMap"]; input: ("text" | "image")[]; cost: { input: number; output: number; cacheRead: number; cacheWrite: number }; contextWindow: number; diff --git a/packages/coding-agent/src/core/sdk.ts b/packages/coding-agent/src/core/sdk.ts index 53c42583..e59c5ee0 100644 --- a/packages/coding-agent/src/core/sdk.ts +++ b/packages/coding-agent/src/core/sdk.ts @@ -1,6 +1,6 @@ import { join } from "node:path"; import { Agent, type AgentMessage, type ThinkingLevel } from "@mariozechner/pi-agent-core"; -import { type Message, type Model, streamSimple } from "@mariozechner/pi-ai"; +import { clampThinkingLevel, type Message, type Model, streamSimple } from "@mariozechner/pi-ai"; import { getAgentDir } from "../config.js"; import { AgentSession } from "./agent-session.js"; import { formatNoModelsAvailableMessage } from "./auth-guidance.js"; @@ -262,8 +262,10 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {} } // Clamp to model capabilities - if (!model || !model.reasoning) { + if (!model) { thinkingLevel = "off"; + } else { + thinkingLevel = clampThinkingLevel(model, thinkingLevel) as ThinkingLevel; } const defaultActiveToolNames: ToolName[] = ["read", "bash", "edit", "write"]; diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index c68f5cb1..d4cd1339 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -7,7 +7,7 @@ import { resolve } from "node:path"; import { createInterface } from "node:readline"; -import { type ImageContent, modelsAreEqual, supportsXhigh } from "@mariozechner/pi-ai"; +import { type ImageContent, modelsAreEqual } from "@mariozechner/pi-ai"; import { ProcessTerminal, setKeybindings, TUI } from "@mariozechner/pi-tui"; import chalk from "chalk"; import { type Args, type Mode, parseArgs, printHelp } from "./cli/args.js"; @@ -595,15 +595,7 @@ export async function main(args: string[], options?: MainOptions) { }); const cliThinkingOverride = parsed.thinking !== undefined || cliThinkingFromModel; if (created.session.model && cliThinkingOverride) { - let effectiveThinking = created.session.thinkingLevel; - if (!created.session.model.reasoning) { - effectiveThinking = "off"; - } else if (effectiveThinking === "xhigh" && !supportsXhigh(created.session.model)) { - effectiveThinking = "high"; - } - if (effectiveThinking !== created.session.thinkingLevel) { - created.session.setThinkingLevel(effectiveThinking); - } + created.session.setThinkingLevel(created.session.thinkingLevel); } return { diff --git a/packages/coding-agent/test/model-registry.test.ts b/packages/coding-agent/test/model-registry.test.ts index 23fdf6fd..cd15b4df 100644 --- a/packages/coding-agent/test/model-registry.test.ts +++ b/packages/coding-agent/test/model-registry.test.ts @@ -396,7 +396,7 @@ describe("ModelRegistry", () => { } }); - test("compat schema accepts reasoningEffortMap, supportsStrictMode, and cacheControlFormat", () => { + test("model schema accepts thinkingLevelMap and compat schema accepts supportsStrictMode and cacheControlFormat", () => { writeRawModelsJson({ demo: { baseUrl: "https://example.com/v1", @@ -410,11 +410,11 @@ describe("ModelRegistry", () => { cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 1000, maxTokens: 100, + thinkingLevelMap: { + minimal: null, + high: "max", + }, compat: { - reasoningEffortMap: { - minimal: "default", - high: "max", - }, supportsStrictMode: false, cacheControlFormat: "anthropic", }, @@ -424,10 +424,11 @@ describe("ModelRegistry", () => { }); const registry = ModelRegistry.create(authStorage, modelsJsonPath); - const compat = registry.find("demo", "demo-model")?.compat as OpenAICompletionsCompat | undefined; + const model = registry.find("demo", "demo-model"); + const compat = model?.compat as OpenAICompletionsCompat | undefined; expect(registry.getError()).toBeUndefined(); - expect(compat?.reasoningEffortMap).toEqual({ minimal: "default", high: "max" }); + expect(model?.thinkingLevelMap).toEqual({ minimal: null, high: "max" }); expect(compat?.supportsStrictMode).toBe(false); expect(compat?.cacheControlFormat).toBe("anthropic"); });