fix: clean up Codex thinking level handling

- Remove per-thinking-level model variants (gpt-5.2-codex-high, etc.)
- Remove thinkingLevels from Model type
- Provider clamps reasoning effort internally
- Omit reasoning field when thinking is off

fixes #472
This commit is contained in:
Mario Zechner 2026-01-05 21:58:26 +01:00
parent 02b72b49d5
commit 0b9e3ada0c
11 changed files with 45 additions and 148 deletions

View file

@ -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

View file

@ -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`;

View file

@ -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",

View file

@ -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<string, Map<string, Model<Api>>> = new Map();
const CODEX_THINKING_SUFFIXES = ["-none", "-minimal", "-low", "-medium", "-high", "-xhigh"];
const CODEX_THINKING_LEVELS: Record<string, ReasoningEffort[]> = {
"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<TApi extends Api>(model: Model<TApi>): Model<TApi> {
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<string, Model<Api>>();
for (const [id, model] of Object.entries(models)) {
const typedModel = model as Model<Api>;
if (provider === "openai-codex" && isCodexThinkingVariant(typedModel.id)) {
continue;
}
providerModels.set(id, applyCodexThinkingLevels(typedModel));
providerModels.set(id, model as Model<Api>);
}
modelRegistry.set(provider, providerModels);
}
@ -59,16 +22,7 @@ export function getModel<TProvider extends KnownProvider, TModelId extends keyof
modelId: TModelId,
): Model<ModelApi<TProvider, TModelId>> {
const providerModels = modelRegistry.get(provider);
const direct = providerModels?.get(modelId as string);
if (direct) return direct as Model<ModelApi<TProvider, TModelId>>;
if (provider === "openai-codex") {
const normalized = normalizeCodexModelId(modelId as string);
const normalizedModel = providerModels?.get(normalized);
if (normalizedModel) {
return normalizedModel as Model<ModelApi<TProvider, TModelId>>;
}
}
return direct as unknown as Model<ModelApi<TProvider, TModelId>>;
return providerModels?.get(modelId as string) as Model<ModelApi<TProvider, TModelId>>;
}
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<TApi extends Api>(model: Model<TApi>): boolean {
if (model.thinkingLevels) {
return model.thinkingLevels.includes("xhigh");
}
return XHIGH_MODELS.has(model.id);
}

View file

@ -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<TApi extends Api>(
};
// 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<TApi extends Api>(
// 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<ClampedReasoningEffort, number> = {
const budgets: Record<ClampedThinkingLevel, number> = {
minimal: 1024,
low: 2048,
medium: 8192,
@ -350,7 +350,7 @@ function mapOptionsForApi<TApi extends Api>(
}
}
type ClampedReasoningEffort = Exclude<ReasoningEffort, "xhigh">;
type ClampedThinkingLevel = Exclude<ThinkingLevel, "xhigh">;
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<ClampedReasoningEffort, number> = {
const budgets: Record<ClampedThinkingLevel, number> = {
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<ClampedReasoningEffort, number> = {
const budgets: Record<ClampedThinkingLevel, number> = {
minimal: 128,
low: 2048,
medium: 8192,

View file

@ -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<TApi extends Api> {
provider: Provider;
baseUrl: string;
reasoning: boolean;
/** Supported reasoning levels for this model (excluding "off"). */
thinkingLevels?: ReasoningEffort[];
input: ("text" | "image")[];
cost: {
input: number; // $/million tokens