fix: codex thinking handling

This commit is contained in:
Ben Vargas 2026-01-05 11:40:44 -07:00 committed by Mario Zechner
parent 22870ae0c2
commit 02b72b49d5
23 changed files with 205 additions and 754 deletions

View file

@ -12,6 +12,10 @@
- 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.
### Fixed
- Managed binaries (`fd`, `rg`) now stored in `~/.pi/agent/bin/` instead of `tools/`, eliminating false deprecation warnings ([#470](https://github.com/badlogic/pi-mono/pull/470) by [@mcinteerj](https://github.com/mcinteerj))
@ -20,6 +24,7 @@
- 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

View file

@ -54,6 +54,7 @@
},
"devDependencies": {
"@types/diff": "^7.0.2",
"@types/ms": "^2.1.0",
"@types/node": "^24.3.0",
"@types/proper-lockfile": "^4.1.4",
"typescript": "^5.7.3",

View file

@ -978,16 +978,12 @@ export class AgentSession {
/**
* Set thinking level.
* Clamps to model capabilities: "off" if no reasoning, "high" if xhigh unsupported.
* Clamps to model capabilities based on available thinking levels.
* Saves to session and settings.
*/
setThinkingLevel(level: ThinkingLevel): void {
let effectiveLevel = level;
if (!this.supportsThinking()) {
effectiveLevel = "off";
} else if (level === "xhigh" && !this.supportsXhighThinking()) {
effectiveLevel = "high";
}
const availableLevels = this.getAvailableThinkingLevels();
const effectiveLevel = availableLevels.includes(level) ? level : this._clampThinkingLevel(level, availableLevels);
this.agent.setThinkingLevel(effectiveLevel);
this.sessionManager.appendThinkingLevelChange(effectiveLevel);
this.settingsManager.setDefaultThinkingLevel(effectiveLevel);
@ -1013,6 +1009,14 @@ export class AgentSession {
* Get available thinking levels for current model.
*/
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;
}
@ -1030,6 +1034,24 @@ export class AgentSession {
return !!this.model?.reasoning;
}
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";
}
// =========================================================================
// Queue Mode Management
// =========================================================================

View file

@ -18,6 +18,16 @@ 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()),
@ -40,6 +50,7 @@ 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(),
@ -107,6 +118,14 @@ 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.
*/
@ -330,6 +349,7 @@ 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,
@ -363,7 +383,15 @@ export class ModelRegistry {
* Find a model by provider and ID.
*/
find(provider: string, modelId: string): Model<Api> | undefined {
return this.models.find((m) => m.provider === provider && m.id === modelId) ?? 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;
}
/**

View file

@ -102,6 +102,18 @@ 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).
@ -122,6 +134,14 @@ export function parseModelPattern(pattern: string, availableModels: Model<Api>[]
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) {