import { readFile } from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; import TOML from '@iarna/toml'; import type { IProviderModels } from '@/shared/interfaces.js'; import type { ProviderChangeActiveModelInput, ProviderCurrentActiveModel, ProviderModelOption, ProviderModelsDefinition, ProviderSessionActiveModelChange, } from '@/shared/types.js'; import { buildDefaultProviderCurrentActiveModel, readObjectRecord, readOptionalString, writeProviderSessionActiveModelChange, } from '@/shared/utils.js'; export const CODEX_FALLBACK_MODELS: ProviderModelsDefinition = { OPTIONS: [ { value: 'gpt-5.5', label: 'gpt-5.5', effort: { default: 'medium', values: [{ value: 'low' }, { value: 'medium' }, { value: 'high' }, { value: 'xhigh' }], }, }, { value: 'gpt-5.4', label: 'gpt-5.4', effort: { default: 'medium', values: [{ value: 'low' }, { value: 'medium' }, { value: 'high' }, { value: 'xhigh' }], }, }, { value: 'gpt-5.4-mini', label: 'gpt-5.4-mini', effort: { default: 'medium', values: [{ value: 'low' }, { value: 'medium' }, { value: 'high' }, { value: 'xhigh' }], }, }, { value: 'gpt-5.3-codex', label: 'gpt-5.3-codex', effort: { default: 'medium', values: [{ value: 'low' }, { value: 'medium' }, { value: 'high' }, { value: 'xhigh' }], }, }, { value: 'gpt-5.2', label: 'gpt-5.2', effort: { default: 'medium', values: [{ value: 'low' }, { value: 'medium' }, { value: 'high' }, { value: 'xhigh' }], }, }, ], DEFAULT: 'gpt-5.4', }; type CodexCachedModel = { slug?: string; display_name?: string; description?: string; priority?: number; visibility?: string; supported_in_api?: boolean; default_reasoning_level?: string; supported_reasoning_levels?: Array<{ effort?: string; description?: string; }>; }; const CODEX_MODELS_CACHE_PATH = path.join(os.homedir(), '.codex', 'models_cache.json'); const CODEX_CONFIG_PATH = path.join(os.homedir(), '.codex', 'config.toml'); const isCodexCachedModel = (value: unknown): value is CodexCachedModel => { const record = readObjectRecord(value); return Boolean(record && readOptionalString(record.slug)); }; const readCodexPriority = (value: unknown): number => ( typeof value === 'number' && Number.isFinite(value) ? value : Number.MAX_SAFE_INTEGER ); const mapCodexModel = (model: CodexCachedModel): ProviderModelOption => { const effortValues = Array.isArray(model.supported_reasoning_levels) ? model.supported_reasoning_levels .map((level) => { const value = readOptionalString(level?.effort); if (!value) { return null; } return { value, description: readOptionalString(level?.description), }; }) .filter((level): level is NonNullable => Boolean(level)) : []; return { value: model.slug as string, label: readOptionalString(model.display_name) ?? (model.slug as string), description: readOptionalString(model.description), effort: effortValues.length > 0 ? { default: readOptionalString(model.default_reasoning_level) ?? undefined, values: effortValues, } : undefined, }; }; const buildCodexModelsDefinition = (models: CodexCachedModel[]): ProviderModelsDefinition => { const sortedModels = [...models] .filter((model) => model.visibility === 'list' && model.supported_in_api !== false) .sort((left, right) => readCodexPriority(left.priority) - readCodexPriority(right.priority)); const options: ProviderModelOption[] = []; const seenValues = new Set(); for (const model of sortedModels) { const mappedModel = mapCodexModel(model); if (seenValues.has(mappedModel.value)) { continue; } seenValues.add(mappedModel.value); options.push(mappedModel); } if (options.length === 0) { return CODEX_FALLBACK_MODELS; } return { OPTIONS: options, DEFAULT: options[0]?.value ?? CODEX_FALLBACK_MODELS.DEFAULT, }; }; export class CodexProviderModels implements IProviderModels { async getSupportedModels(): Promise { try { const raw = await readFile(CODEX_MODELS_CACHE_PATH, 'utf8'); const parsed = readObjectRecord(JSON.parse(raw)); const models = Array.isArray(parsed?.models) ? parsed.models.filter(isCodexCachedModel) : []; return buildCodexModelsDefinition(models); } catch { return CODEX_FALLBACK_MODELS; } } async getCurrentActiveModel(): Promise { try { const raw = await readFile(CODEX_CONFIG_PATH, 'utf8'); const parsed = readObjectRecord(TOML.parse(raw)); const model = readOptionalString(parsed?.model); if (!model) { return buildDefaultProviderCurrentActiveModel(await this.getSupportedModels()); } return { model, }; } catch { return buildDefaultProviderCurrentActiveModel(await this.getSupportedModels()); } } async changeActiveModel( input: ProviderChangeActiveModelInput, ): Promise { return writeProviderSessionActiveModelChange('codex', input); } }