diff --git a/packages/agent/src/agent.ts b/packages/agent/src/agent.ts index a8c3eb2d..4ec7910c 100644 --- a/packages/agent/src/agent.ts +++ b/packages/agent/src/agent.ts @@ -8,7 +8,6 @@ import { type ImageContent, type Message, type Model, - type ReasoningEffort, streamSimple, type TextContent, } from "@mariozechner/pi-ai"; @@ -276,8 +275,7 @@ export class Agent { this._state.streamMessage = null; this._state.error = undefined; - const reasoning: ReasoningEffort | undefined = - this._state.thinkingLevel === "off" ? undefined : (this._state.thinkingLevel as ReasoningEffort); + const reasoning = this._state.thinkingLevel === "off" ? undefined : this._state.thinkingLevel; const context: AgentContext = { systemPrompt: this._state.systemPrompt, diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 8ea497c7..ca577ea4 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,11 +2,19 @@ ## [Unreleased] +### Breaking Changes + +- OpenAI Codex models no longer have per-thinking-level variants (e.g., `gpt-5.2-codex-high`). Use the base model ID and set thinking level separately. The Codex provider clamps reasoning effort to what each model supports internally. (initial implementation by [@ben-vargas](https://github.com/ben-vargas) in [#472](https://github.com/badlogic/pi-mono/pull/472)) + ### Added - Headless OAuth support for all callback-server providers (Google Gemini CLI, Antigravity, OpenAI Codex): paste redirect URL when browser callback is unreachable ([#428](https://github.com/badlogic/pi-mono/pull/428) by [@ben-vargas](https://github.com/ben-vargas), [#468](https://github.com/badlogic/pi-mono/pull/468) by [@crcatala](https://github.com/crcatala)) - Cancellable GitHub Copilot device code polling via AbortSignal +### Fixed + +- Codex requests now omit the `reasoning` field entirely when thinking is off, letting the backend use its default instead of forcing a value. ([#472](https://github.com/badlogic/pi-mono/pull/472)) + ## [0.36.0] - 2026-01-05 ### Added diff --git a/packages/ai/scripts/generate-models.ts b/packages/ai/scripts/generate-models.ts index 6152b4db..bd3fa2fc 100644 --- a/packages/ai/scripts/generate-models.ts +++ b/packages/ai/scripts/generate-models.ts @@ -454,7 +454,6 @@ async function generateModels() { provider: "openai-codex", baseUrl: CODEX_BASE_URL, reasoning: true, - thinkingLevels: ["low", "medium", "high", "xhigh"], input: ["text", "image"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: CODEX_CONTEXT, @@ -479,7 +478,6 @@ async function generateModels() { provider: "openai-codex", baseUrl: CODEX_BASE_URL, reasoning: true, - thinkingLevels: ["low", "medium", "high", "xhigh"], input: ["text", "image"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: CODEX_CONTEXT, @@ -492,7 +490,6 @@ async function generateModels() { provider: "openai-codex", baseUrl: CODEX_BASE_URL, reasoning: true, - thinkingLevels: ["low", "medium", "high"], input: ["text", "image"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: CODEX_CONTEXT, @@ -505,7 +502,6 @@ async function generateModels() { provider: "openai-codex", baseUrl: CODEX_BASE_URL, reasoning: true, - thinkingLevels: ["medium", "high"], input: ["text", "image"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: CODEX_CONTEXT, @@ -518,7 +514,6 @@ async function generateModels() { provider: "openai-codex", baseUrl: CODEX_BASE_URL, reasoning: true, - thinkingLevels: ["medium", "high"], input: ["text", "image"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: CODEX_CONTEXT, @@ -531,7 +526,6 @@ async function generateModels() { provider: "openai-codex", baseUrl: CODEX_BASE_URL, reasoning: true, - thinkingLevels: ["medium", "high"], input: ["text", "image"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: CODEX_CONTEXT, @@ -544,7 +538,6 @@ async function generateModels() { provider: "openai-codex", baseUrl: CODEX_BASE_URL, reasoning: true, - thinkingLevels: ["low", "medium", "high"], input: ["text", "image"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: CODEX_CONTEXT, @@ -999,9 +992,6 @@ export const MODELS = { `; } output += `\t\t\treasoning: ${model.reasoning},\n`; - if (model.thinkingLevels) { - output += `\t\t\tthinkingLevels: ${JSON.stringify(model.thinkingLevels)},\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 bc288087..a67594b6 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -2781,7 +2781,6 @@ export const MODELS = { provider: "openai-codex", baseUrl: "https://chatgpt.com/backend-api", reasoning: true, - thinkingLevels: ["medium","high"], input: ["text", "image"], cost: { input: 0, @@ -2816,7 +2815,6 @@ export const MODELS = { provider: "openai-codex", baseUrl: "https://chatgpt.com/backend-api", reasoning: true, - thinkingLevels: ["low","medium","high"], input: ["text", "image"], cost: { input: 0, @@ -2834,7 +2832,6 @@ export const MODELS = { provider: "openai-codex", baseUrl: "https://chatgpt.com/backend-api", reasoning: true, - thinkingLevels: ["medium","high"], input: ["text", "image"], cost: { input: 0, @@ -2920,7 +2917,6 @@ export const MODELS = { provider: "openai-codex", baseUrl: "https://chatgpt.com/backend-api", reasoning: true, - thinkingLevels: ["low","medium","high"], input: ["text", "image"], cost: { input: 0, @@ -2938,7 +2934,6 @@ export const MODELS = { provider: "openai-codex", baseUrl: "https://chatgpt.com/backend-api", reasoning: true, - thinkingLevels: ["low","medium","high","xhigh"], input: ["text", "image"], cost: { input: 0, @@ -2956,7 +2951,6 @@ export const MODELS = { provider: "openai-codex", baseUrl: "https://chatgpt.com/backend-api", reasoning: true, - thinkingLevels: ["medium","high"], input: ["text", "image"], cost: { input: 0, @@ -2991,7 +2985,6 @@ export const MODELS = { provider: "openai-codex", baseUrl: "https://chatgpt.com/backend-api", reasoning: true, - thinkingLevels: ["low","medium","high","xhigh"], input: ["text", "image"], cost: { input: 0, @@ -4056,7 +4049,7 @@ export const MODELS = { cacheWrite: 0, }, contextWindow: 256000, - maxTokens: 32768, + maxTokens: 128000, } satisfies Model<"openai-completions">, "meta-llama/llama-3-70b-instruct": { id: "meta-llama/llama-3-70b-instruct", @@ -6676,6 +6669,23 @@ export const MODELS = { contextWindow: 163840, maxTokens: 65536, } satisfies Model<"openai-completions">, + "tngtech/tng-r1t-chimera:free": { + id: "tngtech/tng-r1t-chimera:free", + name: "TNG: R1T Chimera (free)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 163840, + maxTokens: 65536, + } satisfies Model<"openai-completions">, "x-ai/grok-3": { id: "x-ai/grok-3", name: "xAI: Grok 3", diff --git a/packages/ai/src/models.ts b/packages/ai/src/models.ts index 1c6b2f51..b6c91565 100644 --- a/packages/ai/src/models.ts +++ b/packages/ai/src/models.ts @@ -1,50 +1,13 @@ import { MODELS } from "./models.generated.js"; -import type { Api, KnownProvider, Model, ReasoningEffort, Usage } from "./types.js"; +import type { Api, KnownProvider, Model, Usage } from "./types.js"; const modelRegistry: Map>> = new Map(); -const CODEX_THINKING_SUFFIXES = ["-none", "-minimal", "-low", "-medium", "-high", "-xhigh"]; -const CODEX_THINKING_LEVELS: Record = { - "gpt-5.2-codex": ["low", "medium", "high", "xhigh"], - "gpt-5.1-codex-max": ["low", "medium", "high", "xhigh"], - "gpt-5.1-codex": ["low", "medium", "high"], - "gpt-5.1-codex-mini": ["medium", "high"], - "codex-mini-latest": ["medium", "high"], - "gpt-5-codex-mini": ["medium", "high"], - "gpt-5-codex": ["low", "medium", "high"], -}; - -function isCodexThinkingVariant(modelId: string): boolean { - const normalized = modelId.toLowerCase(); - return CODEX_THINKING_SUFFIXES.some((suffix) => normalized.endsWith(suffix)); -} - -function normalizeCodexModelId(modelId: string): string { - const normalized = modelId.toLowerCase(); - for (const suffix of CODEX_THINKING_SUFFIXES) { - if (normalized.endsWith(suffix)) { - return modelId.slice(0, modelId.length - suffix.length); - } - } - return modelId; -} - -function applyCodexThinkingLevels(model: Model): Model { - if (model.provider !== "openai-codex") return model; - const thinkingLevels = CODEX_THINKING_LEVELS[model.id]; - if (!thinkingLevels) return model; - return { ...model, thinkingLevels }; -} - // Initialize registry from MODELS on module load for (const [provider, models] of Object.entries(MODELS)) { const providerModels = new Map>(); for (const [id, model] of Object.entries(models)) { - const typedModel = model as Model; - if (provider === "openai-codex" && isCodexThinkingVariant(typedModel.id)) { - continue; - } - providerModels.set(id, applyCodexThinkingLevels(typedModel)); + providerModels.set(id, model as Model); } modelRegistry.set(provider, providerModels); } @@ -59,16 +22,7 @@ export function getModel> { const providerModels = modelRegistry.get(provider); - const direct = providerModels?.get(modelId as string); - if (direct) return direct as Model>; - if (provider === "openai-codex") { - const normalized = normalizeCodexModelId(modelId as string); - const normalizedModel = providerModels?.get(normalized); - if (normalizedModel) { - return normalizedModel as Model>; - } - } - return direct as unknown as Model>; + return providerModels?.get(modelId as string) as Model>; } export function getProviders(): KnownProvider[] { @@ -96,12 +50,9 @@ const XHIGH_MODELS = new Set(["gpt-5.1-codex-max", "gpt-5.2", "gpt-5.2-codex"]); /** * Check if a model supports xhigh thinking level. - * Currently only certain OpenAI models support this. + * Currently only certain OpenAI Codex models support this. */ export function supportsXhigh(model: Model): boolean { - if (model.thinkingLevels) { - return model.thinkingLevels.includes("xhigh"); - } return XHIGH_MODELS.has(model.id); } diff --git a/packages/ai/src/stream.ts b/packages/ai/src/stream.ts index ed9d1102..09453d5f 100644 --- a/packages/ai/src/stream.ts +++ b/packages/ai/src/stream.ts @@ -21,8 +21,8 @@ import type { KnownProvider, Model, OptionsForApi, - ReasoningEffort, SimpleStreamOptions, + ThinkingLevel, } from "./types.js"; const VERTEX_ADC_CREDENTIALS_PATH = join(homedir(), ".config", "gcloud", "application_default_credentials.json"); @@ -180,7 +180,7 @@ function mapOptionsForApi( }; // Helper to clamp xhigh to high for providers that don't support it - const clampReasoning = (effort: ReasoningEffort | undefined) => (effort === "xhigh" ? "high" : effort); + const clampReasoning = (effort: ThinkingLevel | undefined) => (effort === "xhigh" ? "high" : effort); switch (model.api) { case "anthropic-messages": { @@ -286,7 +286,7 @@ function mapOptionsForApi( // Models using thinkingBudget (Gemini 2.x, Claude via Antigravity) // Claude requires max_tokens > thinking.budget_tokens // So we need to ensure maxTokens accounts for both thinking and output - const budgets: Record = { + const budgets: Record = { minimal: 1024, low: 2048, medium: 8192, @@ -350,7 +350,7 @@ function mapOptionsForApi( } } -type ClampedReasoningEffort = Exclude; +type ClampedThinkingLevel = Exclude; function isGemini3ProModel(model: Model<"google-generative-ai">): boolean { // Covers gemini-3-pro, gemini-3-pro-preview, and possible other prefixed ids in the future @@ -363,7 +363,7 @@ function isGemini3FlashModel(model: Model<"google-generative-ai">): boolean { } function getGemini3ThinkingLevel( - effort: ClampedReasoningEffort, + effort: ClampedThinkingLevel, model: Model<"google-generative-ai">, ): GoogleThinkingLevel { if (isGemini3ProModel(model)) { @@ -390,7 +390,7 @@ function getGemini3ThinkingLevel( } } -function getGeminiCliThinkingLevel(effort: ClampedReasoningEffort, modelId: string): GoogleThinkingLevel { +function getGeminiCliThinkingLevel(effort: ClampedThinkingLevel, modelId: string): GoogleThinkingLevel { if (modelId.includes("3-pro")) { // Gemini 3 Pro only supports LOW/HIGH (for now) switch (effort) { @@ -415,10 +415,10 @@ function getGeminiCliThinkingLevel(effort: ClampedReasoningEffort, modelId: stri } } -function getGoogleBudget(model: Model<"google-generative-ai">, effort: ClampedReasoningEffort): number { +function getGoogleBudget(model: Model<"google-generative-ai">, effort: ClampedThinkingLevel): number { // See https://ai.google.dev/gemini-api/docs/thinking#set-budget if (model.id.includes("2.5-pro")) { - const budgets: Record = { + const budgets: Record = { minimal: 128, low: 2048, medium: 8192, @@ -429,7 +429,7 @@ function getGoogleBudget(model: Model<"google-generative-ai">, effort: ClampedRe if (model.id.includes("2.5-flash")) { // Covers 2.5-flash-lite as well - const budgets: Record = { + const budgets: Record = { minimal: 128, low: 2048, medium: 8192, diff --git a/packages/ai/src/types.ts b/packages/ai/src/types.ts index f578d256..e0221c68 100644 --- a/packages/ai/src/types.ts +++ b/packages/ai/src/types.ts @@ -56,7 +56,7 @@ export type KnownProvider = | "mistral"; export type Provider = KnownProvider | string; -export type ReasoningEffort = "minimal" | "low" | "medium" | "high" | "xhigh"; +export type ThinkingLevel = "minimal" | "low" | "medium" | "high" | "xhigh"; // Base options all providers share export interface StreamOptions { @@ -68,7 +68,7 @@ export interface StreamOptions { // Unified options with reasoning passed to streamSimple() and completeSimple() export interface SimpleStreamOptions extends StreamOptions { - reasoning?: ReasoningEffort; + reasoning?: ThinkingLevel; } // Generic StreamFunction with typed options @@ -210,8 +210,6 @@ export interface Model { provider: Provider; baseUrl: string; reasoning: boolean; - /** Supported reasoning levels for this model (excluding "off"). */ - thinkingLevels?: ReasoningEffort[]; input: ("text" | "image")[]; cost: { input: number; // $/million tokens diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 89abcddb..8d1a6d52 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -11,10 +11,7 @@ - OAuth login UI now uses dedicated dialog component with consistent borders - Assume truecolor support for all terminals except `dumb`, empty, or `linux` (fixes colors over SSH) - -### Changed - -- Thinking level availability now reflects per-model supported reasoning levels. +- OpenAI Codex clean-up: removed per-thinking-level model variants, thinking level is now set separately and the provider clamps to what each model supports internally (initial implementation in [#472](https://github.com/badlogic/pi-mono/pull/472) by [@ben-vargas](https://github.com/ben-vargas)) ### Fixed @@ -24,7 +21,6 @@ - Migration warnings now ignore `fd.exe` and `rg.exe` in `tools/` on Windows ([#458](https://github.com/badlogic/pi-mono/pull/458) by [@carlosgtrz](https://github.com/carlosgtrz)) - CI: add `examples/extensions/with-deps` to workspaces to fix typecheck ([#467](https://github.com/badlogic/pi-mono/pull/467) by [@aliou](https://github.com/aliou)) - SDK: passing `extensions: []` now disables extension discovery as documented ([#465](https://github.com/badlogic/pi-mono/pull/465) by [@aliou](https://github.com/aliou)) -- Legacy Codex model IDs with thinking suffixes resolve to their base models. ## [0.36.0] - 2026-01-05 diff --git a/packages/coding-agent/src/core/agent-session.ts b/packages/coding-agent/src/core/agent-session.ts index e5459e0b..92194eaa 100644 --- a/packages/coding-agent/src/core/agent-session.ts +++ b/packages/coding-agent/src/core/agent-session.ts @@ -1007,16 +1007,10 @@ export class AgentSession { /** * Get available thinking levels for current model. + * The provider will clamp to what the specific model supports internally. */ getAvailableThinkingLevels(): ThinkingLevel[] { if (!this.supportsThinking()) return ["off"]; - - const modelLevels = this.model?.thinkingLevels; - if (modelLevels && modelLevels.length > 0) { - const withOff: ThinkingLevel[] = ["off", ...modelLevels]; - return THINKING_LEVELS_WITH_XHIGH.filter((level) => withOff.includes(level)); - } - return this.supportsXhighThinking() ? THINKING_LEVELS_WITH_XHIGH : THINKING_LEVELS; } diff --git a/packages/coding-agent/src/core/model-registry.ts b/packages/coding-agent/src/core/model-registry.ts index cab368c6..3208e1e5 100644 --- a/packages/coding-agent/src/core/model-registry.ts +++ b/packages/coding-agent/src/core/model-registry.ts @@ -18,16 +18,6 @@ import type { AuthStorage } from "./auth-storage.js"; const Ajv = (AjvModule as any).default || AjvModule; -const ThinkingLevelsSchema = Type.Array( - Type.Union([ - Type.Literal("minimal"), - Type.Literal("low"), - Type.Literal("medium"), - Type.Literal("high"), - Type.Literal("xhigh"), - ]), -); - // Schema for OpenAI compatibility settings const OpenAICompatSchema = Type.Object({ supportsStore: Type.Optional(Type.Boolean()), @@ -50,7 +40,6 @@ const ModelDefinitionSchema = Type.Object({ ]), ), reasoning: Type.Boolean(), - thinkingLevels: Type.Optional(ThinkingLevelsSchema), input: Type.Array(Type.Union([Type.Literal("text"), Type.Literal("image")])), cost: Type.Object({ input: Type.Number(), @@ -118,14 +107,6 @@ function resolveApiKeyConfig(keyConfig: string): string | undefined { return keyConfig; } -function normalizeCodexModelId(modelId: string): string { - const suffixes = ["-none", "-minimal", "-low", "-medium", "-high", "-xhigh"]; - const normalized = modelId.toLowerCase(); - const matchedSuffix = suffixes.find((suffix) => normalized.endsWith(suffix)); - if (!matchedSuffix) return modelId; - return modelId.slice(0, modelId.length - matchedSuffix.length); -} - /** * Model registry - loads and manages models, resolves API keys via AuthStorage. */ @@ -349,7 +330,6 @@ export class ModelRegistry { provider: providerName, baseUrl: providerConfig.baseUrl!, reasoning: modelDef.reasoning, - thinkingLevels: modelDef.thinkingLevels, input: modelDef.input as ("text" | "image")[], cost: modelDef.cost, contextWindow: modelDef.contextWindow, @@ -383,15 +363,7 @@ export class ModelRegistry { * Find a model by provider and ID. */ find(provider: string, modelId: string): Model | undefined { - const exact = this.models.find((m) => m.provider === provider && m.id === modelId); - if (exact) return exact; - if (provider === "openai-codex") { - const normalized = normalizeCodexModelId(modelId); - if (normalized !== modelId) { - return this.models.find((m) => m.provider === provider && m.id === normalized) ?? undefined; - } - } - return undefined; + return this.models.find((m) => m.provider === provider && m.id === modelId); } /** diff --git a/packages/coding-agent/src/core/model-resolver.ts b/packages/coding-agent/src/core/model-resolver.ts index c6ed7ef3..98c8fd60 100644 --- a/packages/coding-agent/src/core/model-resolver.ts +++ b/packages/coding-agent/src/core/model-resolver.ts @@ -102,18 +102,6 @@ export interface ParsedModelResult { warning: string | undefined; } -const THINKING_SUFFIXES = ["-none", "-minimal", "-low", "-medium", "-high", "-xhigh"]; - -function stripThinkingSuffix(pattern: string): string { - const normalized = pattern.toLowerCase(); - for (const suffix of THINKING_SUFFIXES) { - if (normalized.endsWith(suffix)) { - return pattern.slice(0, pattern.length - suffix.length); - } - } - return pattern; -} - /** * Parse a pattern to extract model and thinking level. * Handles models with colons in their IDs (e.g., OpenRouter's :exacto suffix). @@ -134,14 +122,6 @@ export function parseModelPattern(pattern: string, availableModels: Model[] return { model: exactMatch, thinkingLevel: "off", warning: undefined }; } - const normalizedPattern = stripThinkingSuffix(pattern); - if (normalizedPattern !== pattern) { - const normalizedMatch = tryMatchModel(normalizedPattern, availableModels); - if (normalizedMatch) { - return { model: normalizedMatch, thinkingLevel: "off", warning: undefined }; - } - } - // No match - try splitting on last colon if present const lastColonIndex = pattern.lastIndexOf(":"); if (lastColonIndex === -1) {