From 23216ca01cfa42f170b08d2d063e9b01f34f2723 Mon Sep 17 00:00:00 2001 From: Meet Patel Date: Thu, 2 Apr 2026 17:17:14 +0530 Subject: [PATCH 1/2] feat: Refactor model handling & reasoning effort across navigation, typeahead, OpenAI/Codex providers, API shim, configs, and UI (adds EffortPicker, new mappings/options, unique suggestion IDs, effort utilities; removes deprecated aliases; defaults Codex to gpt-5.4; improves selection logic and status display) --- src/commands/effort/effort.tsx | 52 +++++- .../CustomSelect/use-select-navigation.ts | 111 +++++++------ src/components/EffortPicker.tsx | 152 ++++++++++++++++++ src/components/StartupScreen.ts | 34 +++- src/hooks/useTypeahead.tsx | 16 +- src/services/api/openaiShim.ts | 15 +- src/services/api/providerConfig.ts | 50 +++++- src/utils/effort.ts | 52 +++++- src/utils/model/aliases.ts | 2 - src/utils/model/model.ts | 43 +++-- src/utils/model/modelOptions.ts | 82 ++++++++-- src/utils/model/modelStrings.ts | 5 +- src/utils/model/providers.ts | 18 ++- src/utils/status.tsx | 41 ++++- src/utils/suggestions/commandSuggestions.ts | 30 +++- 15 files changed, 587 insertions(+), 116 deletions(-) create mode 100644 src/components/EffortPicker.tsx diff --git a/src/commands/effort/effort.tsx b/src/commands/effort/effort.tsx index 0dadd606..1cbc83d1 100644 --- a/src/commands/effort/effort.tsx +++ b/src/commands/effort/effort.tsx @@ -4,7 +4,8 @@ import { useMainLoopModel } from '../../hooks/useMainLoopModel.js'; import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../services/analytics/index.js'; import { useAppState, useSetAppState } from '../../state/AppState.js'; import type { LocalJSXCommandOnDone } from '../../types/command.js'; -import { type EffortValue, getDisplayedEffortLevel, getEffortEnvOverride, getEffortValueDescription, isEffortLevel, toPersistableEffort } from '../../utils/effort.js'; +import { type EffortValue, getDisplayedEffortLevel, getEffortEnvOverride, getEffortValueDescription, isEffortLevel, isOpenAIEffortLevel, modelUsesOpenAIEffort, toPersistableEffort } from '../../utils/effort.js'; +import { EffortPicker } from '../../components/EffortPicker.js'; import { updateSettingsForSource } from '../../utils/settings/settings.js'; const COMMON_HELP_ARGS = ['help', '-h', '--help']; type EffortCommandResult = { @@ -109,12 +110,15 @@ export function executeEffort(args: string): EffortCommandResult { if (normalized === 'auto' || normalized === 'unset') { return unsetEffortLevel(); } - if (!isEffortLevel(normalized)) { - return { - message: `Invalid argument: ${args}. Valid options are: low, medium, high, max, auto` - }; + if (isEffortLevel(normalized)) { + return setEffortValue(normalized); } - return setEffortValue(normalized); + if (isOpenAIEffortLevel(normalized)) { + return setEffortValue(normalized); + } + return { + message: `Invalid argument: ${args}. Valid options are: low, medium, high, max, xhigh, auto` + }; } function ShowCurrentEffort(t0) { const { @@ -174,10 +178,44 @@ export async function call(onDone: LocalJSXCommandOnDone, _context: unknown, arg onDone('Usage: /effort [low|medium|high|max|auto]\n\nEffort levels:\n- low: Quick, straightforward implementation\n- medium: Balanced approach with standard testing\n- high: Comprehensive implementation with extensive testing\n- max: Maximum capability with deepest reasoning (Opus 4.6 only)\n- auto: Use the default effort level for your model'); return; } - if (!args || args === 'current' || args === 'status') { + if (args === 'current' || args === 'status') { return ; } + if (!args) { + return ; + } const result = executeEffort(args); return ; } + +function EffortPickerWrapper({ onDone }: { onDone: LocalJSXCommandOnDone }) { + const setAppState = useSetAppState(); + const model = useMainLoopModel(); + const usesOpenAIEffort = modelUsesOpenAIEffort(model); + + function handleSelect(effort: EffortValue | undefined) { + const persistable = toPersistableEffort(effort); + if (persistable !== undefined) { + updateSettingsForSource('userSettings', { + effortLevel: persistable + }); + } + logEvent('tengu_effort_command', { + effort: (effort ?? 'auto') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS + }); + setAppState(prev => ({ + ...prev, + effortValue: effort + })); + const description = effort ? getEffortValueDescription(effort) : 'Use default effort level for your model'; + const suffix = persistable !== undefined ? '' : ' (this session only)'; + onDone(`Set effort level to ${effort ?? 'auto'}${suffix}: ${description}`); + } + + function handleCancel() { + onDone('Cancelled'); + } + + return ; +} //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsInVzZU1haW5Mb29wTW9kZWwiLCJBbmFseXRpY3NNZXRhZGF0YV9JX1ZFUklGSUVEX1RISVNfSVNfTk9UX0NPREVfT1JfRklMRVBBVEhTIiwibG9nRXZlbnQiLCJ1c2VBcHBTdGF0ZSIsInVzZVNldEFwcFN0YXRlIiwiTG9jYWxKU1hDb21tYW5kT25Eb25lIiwiRWZmb3J0VmFsdWUiLCJnZXREaXNwbGF5ZWRFZmZvcnRMZXZlbCIsImdldEVmZm9ydEVudk92ZXJyaWRlIiwiZ2V0RWZmb3J0VmFsdWVEZXNjcmlwdGlvbiIsImlzRWZmb3J0TGV2ZWwiLCJ0b1BlcnNpc3RhYmxlRWZmb3J0IiwidXBkYXRlU2V0dGluZ3NGb3JTb3VyY2UiLCJDT01NT05fSEVMUF9BUkdTIiwiRWZmb3J0Q29tbWFuZFJlc3VsdCIsIm1lc3NhZ2UiLCJlZmZvcnRVcGRhdGUiLCJ2YWx1ZSIsInNldEVmZm9ydFZhbHVlIiwiZWZmb3J0VmFsdWUiLCJwZXJzaXN0YWJsZSIsInVuZGVmaW5lZCIsInJlc3VsdCIsImVmZm9ydExldmVsIiwiZXJyb3IiLCJlZmZvcnQiLCJlbnZPdmVycmlkZSIsImVudlJhdyIsInByb2Nlc3MiLCJlbnYiLCJDTEFVREVfQ09ERV9FRkZPUlRfTEVWRUwiLCJkZXNjcmlwdGlvbiIsInN1ZmZpeCIsInNob3dDdXJyZW50RWZmb3J0IiwiYXBwU3RhdGVFZmZvcnQiLCJtb2RlbCIsImVmZmVjdGl2ZVZhbHVlIiwibGV2ZWwiLCJ1bnNldEVmZm9ydExldmVsIiwiZXhlY3V0ZUVmZm9ydCIsImFyZ3MiLCJub3JtYWxpemVkIiwidG9Mb3dlckNhc2UiLCJTaG93Q3VycmVudEVmZm9ydCIsInQwIiwib25Eb25lIiwiX3RlbXAiLCJzIiwiQXBwbHlFZmZvcnRBbmRDbG9zZSIsIiQiLCJfYyIsInNldEFwcFN0YXRlIiwidDEiLCJ0MiIsInByZXYiLCJ1c2VFZmZlY3QiLCJjYWxsIiwiX2NvbnRleHQiLCJQcm9taXNlIiwiUmVhY3ROb2RlIiwidHJpbSIsImluY2x1ZGVzIl0sInNvdXJjZXMiOlsiZWZmb3J0LnRzeCJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgKiBhcyBSZWFjdCBmcm9tICdyZWFjdCdcbmltcG9ydCB7IHVzZU1haW5Mb29wTW9kZWwgfSBmcm9tICcuLi8uLi9ob29rcy91c2VNYWluTG9vcE1vZGVsLmpzJ1xuaW1wb3J0IHtcbiAgdHlwZSBBbmFseXRpY3NNZXRhZGF0YV9JX1ZFUklGSUVEX1RISVNfSVNfTk9UX0NPREVfT1JfRklMRVBBVEhTLFxuICBsb2dFdmVudCxcbn0gZnJvbSAnLi4vLi4vc2VydmljZXMvYW5hbHl0aWNzL2luZGV4LmpzJ1xuaW1wb3J0IHsgdXNlQXBwU3RhdGUsIHVzZVNldEFwcFN0YXRlIH0gZnJvbSAnLi4vLi4vc3RhdGUvQXBwU3RhdGUuanMnXG5pbXBvcnQgdHlwZSB7IExvY2FsSlNYQ29tbWFuZE9uRG9uZSB9IGZyb20gJy4uLy4uL3R5cGVzL2NvbW1hbmQuanMnXG5pbXBvcnQge1xuICB0eXBlIEVmZm9ydFZhbHVlLFxuICBnZXREaXNwbGF5ZWRFZmZvcnRMZXZlbCxcbiAgZ2V0RWZmb3J0RW52T3ZlcnJpZGUsXG4gIGdldEVmZm9ydFZhbHVlRGVzY3JpcHRpb24sXG4gIGlzRWZmb3J0TGV2ZWwsXG4gIHRvUGVyc2lzdGFibGVFZmZvcnQsXG59IGZyb20gJy4uLy4uL3V0aWxzL2VmZm9ydC5qcydcbmltcG9ydCB7IHVwZGF0ZVNldHRpbmdzRm9yU291cmNlIH0gZnJvbSAnLi4vLi4vdXRpbHMvc2V0dGluZ3Mvc2V0dGluZ3MuanMnXG5cbmNvbnN0IENPTU1PTl9IRUxQX0FSR1MgPSBbJ2hlbHAnLCAnLWgnLCAnLS1oZWxwJ11cblxudHlwZSBFZmZvcnRDb21tYW5kUmVzdWx0ID0ge1xuICBtZXNzYWdlOiBzdHJpbmdcbiAgZWZmb3J0VXBkYXRlPzogeyB2YWx1ZTogRWZmb3J0VmFsdWUgfCB1bmRlZmluZWQgfVxufVxuXG5mdW5jdGlvbiBzZXRFZmZvcnRWYWx1ZShlZmZvcnRWYWx1ZTogRWZmb3J0VmFsdWUpOiBFZmZvcnRDb21tYW5kUmVzdWx0IHtcbiAgY29uc3QgcGVyc2lzdGFibGUgPSB0b1BlcnNpc3RhYmxlRWZmb3J0KGVmZm9ydFZhbHVlKVxuICBpZiAocGVyc2lzdGFibGUgIT09IHVuZGVmaW5lZCkge1xuICAgIGNvbnN0IHJlc3VsdCA9IHVwZGF0ZVNldHRpbmdzRm9yU291cmNlKCd1c2VyU2V0dGluZ3MnLCB7XG4gICAgICBlZmZvcnRMZXZlbDogcGVyc2lzdGFibGUsXG4gICAgfSlcbiAgICBpZiAocmVzdWx0LmVycm9yKSB7XG4gICAgICByZXR1cm4ge1xuICAgICAgICBtZXNzYWdlOiBgRmFpbGVkIHRvIHNldCBlZmZvcnQgbGV2ZWw6ICR7cmVzdWx0LmVycm9yLm1lc3NhZ2V9YCxcbiAgICAgIH1cbiAgICB9XG4gIH1cbiAgbG9nRXZlbnQoJ3Rlbmd1X2VmZm9ydF9jb21tYW5kJywge1xuICAgIGVmZm9ydDpcbiAgICAgIGVmZm9ydFZhbHVlIGFzIEFuYWx5dGljc01ldGFkYXRhX0lfVkVSSUZJRURfVEhJU19JU19OT1RfQ09ERV9PUl9GSUxFUEFUSFMsXG4gIH0pXG5cbiAgLy8gRW52IHZhciB3aW5zIGF0IHJlc29sdmVBcHBsaWVkRWZmb3J0IHRpbWUuIE9ubHkgZmxhZyBpdCB3aGVuIGl0IGFjdHVhbGx5XG4gIC8vIGNvbmZsaWN0cyDigJQgaWYgZW52IG1hdGNoZXMgd2hhdCB0aGUgdXNlciBqdXN0IGFza2VkIGZvciwgdGhlIG91dGNvbWUgaXNcbiAgLy8gdGhlIHNhbWUsIHNvIFwiU2V0IGVmZm9ydCB0byBYXCIgaXMgdHJ1ZSBhbmQgdGhlIG5vdGUgaXMgbm9pc2UuXG4gIGNvbnN0IGVudk92ZXJyaWRlID0gZ2V0RWZmb3J0RW52T3ZlcnJpZGUoKVxuICBpZiAoZW52T3ZlcnJpZGUgIT09IHVuZGVmaW5lZCAmJiBlbnZPdmVycmlkZSAhPT0gZWZmb3J0VmFsdWUpIHtcbiAgICBjb25zdCBlbnZSYXcgPSBwcm9jZXNzLmVudi5DTEFVREVfQ09ERV9FRkZPUlRfTEVWRUxcbiAgICBpZiAocGVyc2lzdGFibGUgPT09IHVuZGVmaW5lZCkge1xuICAgICAgcmV0dXJuIHtcbiAgICAgICAgbWVzc2FnZTogYE5vdCBhcHBsaWVkOiBDTEFVREVfQ09ERV9FRkZPUlRfTEVWRUw9JHtlbnZSYXd9IG92ZXJyaWRlcyBlZmZvcnQgdGhpcyBzZXNzaW9uLCBhbmQgJHtlZmZvcnRWYWx1ZX0gaXMgc2Vzc2lvbi1vbmx5IChub3RoaW5nIHNhdmVkKWAsXG4gICAgICAgIGVmZm9ydFVwZGF0ZTogeyB2YWx1ZTogZWZmb3J0VmFsdWUgfSxcbiAgICAgIH1cbiAgICB9XG4gICAgcmV0dXJuIHtcbiAgICAgIG1lc3NhZ2U6IGBDTEFVREVfQ09ERV9FRkZPUlRfTEVWRUw9JHtlbnZSYXd9IG92ZXJyaWRlcyB0aGlzIHNlc3Npb24g4oCUIGNsZWFyIGl0IGFuZCAke2VmZm9ydFZhbHVlfSB0YWtlcyBvdmVyYCxcbiAgICAgIGVmZm9ydFVwZGF0ZTogeyB2YWx1ZTogZWZmb3J0VmFsdWUgfSxcbiAgICB9XG4gIH1cblxuICBjb25zdCBkZXNjcmlwdGlvbiA9IGdldEVmZm9ydFZhbHVlRGVzY3JpcHRpb24oZWZmb3J0VmFsdWUpXG4gIGNvbnN0IHN1ZmZpeCA9IHBlcnNpc3RhYmxlICE9PSB1bmRlZmluZWQgPyAnJyA6ICcgKHRoaXMgc2Vzc2lvbiBvbmx5KSdcbiAgcmV0dXJuIHtcbiAgICBtZXNzYWdlOiBgU2V0IGVmZm9ydCBsZXZlbCB0byAke2VmZm9ydFZhbHVlfSR7c3VmZml4fTogJHtkZXNjcmlwdGlvbn1gLFxuICAgIGVmZm9ydFVwZGF0ZTogeyB2YWx1ZTogZWZmb3J0VmFsdWUgfSxcbiAgfVxufVxuXG5leHBvcnQgZnVuY3Rpb24gc2hvd0N1cnJlbnRFZmZvcnQoXG4gIGFwcFN0YXRlRWZmb3J0OiBFZmZvcnRWYWx1ZSB8IHVuZGVmaW5lZCxcbiAgbW9kZWw6IHN0cmluZyxcbik6IEVmZm9ydENvbW1hbmRSZXN1bHQge1xuICBjb25zdCBlbnZPdmVycmlkZSA9IGdldEVmZm9ydEVudk92ZXJyaWRlKClcbiAgY29uc3QgZWZmZWN0aXZlVmFsdWUgPVxuICAgIGVudk92ZXJyaWRlID09PSBudWxsID8gdW5kZWZpbmVkIDogKGVudk92ZXJyaWRlID8/IGFwcFN0YXRlRWZmb3J0KVxuICBpZiAoZWZmZWN0aXZlVmFsdWUgPT09IHVuZGVmaW5lZCkge1xuICAgIGNvbnN0IGxldmVsID0gZ2V0RGlzcGxheWVkRWZmb3J0TGV2ZWwobW9kZWwsIGFwcFN0YXRlRWZmb3J0KVxuICAgIHJldHVybiB7IG1lc3NhZ2U6IGBFZmZvcnQgbGV2ZWw6IGF1dG8gKGN1cnJlbnRseSAke2xldmVsfSlgIH1cbiAgfVxuICBjb25zdCBkZXNjcmlwdGlvbiA9IGdldEVmZm9ydFZhbHVlRGVzY3JpcHRpb24oZWZmZWN0aXZlVmFsdWUpXG4gIHJldHVybiB7XG4gICAgbWVzc2FnZTogYEN1cnJlbnQgZWZmb3J0IGxldmVsOiAke2VmZmVjdGl2ZVZhbHVlfSAoJHtkZXNjcmlwdGlvbn0pYCxcbiAgfVxufVxuXG5mdW5jdGlvbiB1bnNldEVmZm9ydExldmVsKCk6IEVmZm9ydENvbW1hbmRSZXN1bHQge1xuICBjb25zdCByZXN1bHQgPSB1cGRhdGVTZXR0aW5nc0ZvclNvdXJjZSgndXNlclNldHRpbmdzJywge1xuICAgIGVmZm9ydExldmVsOiB1bmRlZmluZWQsXG4gIH0pXG4gIGlmIChyZXN1bHQuZXJyb3IpIHtcbiAgICByZXR1cm4ge1xuICAgICAgbWVzc2FnZTogYEZhaWxlZCB0byBzZXQgZWZmb3J0IGxldmVsOiAke3Jlc3VsdC5lcnJvci5tZXNzYWdlfWAsXG4gICAgfVxuICB9XG4gIGxvZ0V2ZW50KCd0ZW5ndV9lZmZvcnRfY29tbWFuZCcsIHtcbiAgICBlZmZvcnQ6XG4gICAgICAnYXV0bycgYXMgQW5hbHl0aWNzTWV0YWRhdGFfSV9WRVJJRklFRF9USElTX0lTX05PVF9DT0RFX09SX0ZJTEVQQVRIUyxcbiAgfSlcbiAgLy8gZW52PWF1dG8vdW5zZXQgKG51bGwpIG1hdGNoZXMgd2hhdCAvZWZmb3J0IGF1dG8gYXNrcyBmb3IsIHNvIG9ubHkgd2FyblxuICAvLyB3aGVuIGVudiBpcyBwaW5uaW5nIGEgc3BlY2lmaWMgbGV2ZWwgdGhhdCB3aWxsIGtlZXAgb3ZlcnJpZGluZy5cbiAgY29uc3QgZW52T3ZlcnJpZGUgPSBnZXRFZmZvcnRFbnZPdmVycmlkZSgpXG4gIGlmIChlbnZPdmVycmlkZSAhPT0gdW5kZWZpbmVkICYmIGVudk92ZXJyaWRlICE9PSBudWxsKSB7XG4gICAgY29uc3QgZW52UmF3ID0gcHJvY2Vzcy5lbnYuQ0xBVURFX0NPREVfRUZGT1JUX0xFVkVMXG4gICAgcmV0dXJuIHtcbiAgICAgIG1lc3NhZ2U6IGBDbGVhcmVkIGVmZm9ydCBmcm9tIHNldHRpbmdzLCBidXQgQ0xBVURFX0NPREVfRUZGT1JUX0xFVkVMPSR7ZW52UmF3fSBzdGlsbCBjb250cm9scyB0aGlzIHNlc3Npb25gLFxuICAgICAgZWZmb3J0VXBkYXRlOiB7IHZhbHVlOiB1bmRlZmluZWQgfSxcbiAgICB9XG4gIH1cbiAgcmV0dXJuIHtcbiAgICBtZXNzYWdlOiAnRWZmb3J0IGxldmVsIHNldCB0byBhdXRvJyxcbiAgICBlZmZvcnRVcGRhdGU6IHsgdmFsdWU6IHVuZGVmaW5lZCB9LFxuICB9XG59XG5cbmV4cG9ydCBmdW5jdGlvbiBleGVjdXRlRWZmb3J0KGFyZ3M6IHN0cmluZyk6IEVmZm9ydENvbW1hbmRSZXN1bHQge1xuICBjb25zdCBub3JtYWxpemVkID0gYXJncy50b0xvd2VyQ2FzZSgpXG4gIGlmIChub3JtYWxpemVkID09PSAnYXV0bycgfHwgbm9ybWFsaXplZCA9PT0gJ3Vuc2V0Jykge1xuICAgIHJldHVybiB1bnNldEVmZm9ydExldmVsKClcbiAgfVxuXG4gIGlmICghaXNFZmZvcnRMZXZlbChub3JtYWxpemVkKSkge1xuICAgIHJldHVybiB7XG4gICAgICBtZXNzYWdlOiBgSW52YWxpZCBhcmd1bWVudDogJHthcmdzfS4gVmFsaWQgb3B0aW9ucyBhcmU6IGxvdywgbWVkaXVtLCBoaWdoLCBtYXgsIGF1dG9gLFxuICAgIH1cbiAgfVxuXG4gIHJldHVybiBzZXRFZmZvcnRWYWx1ZShub3JtYWxpemVkKVxufVxuXG5mdW5jdGlvbiBTaG93Q3VycmVudEVmZm9ydCh7XG4gIG9uRG9uZSxcbn06IHtcbiAgb25Eb25lOiAocmVzdWx0OiBzdHJpbmcpID0+IHZvaWRcbn0pOiBSZWFjdC5SZWFjdE5vZGUge1xuICBjb25zdCBlZmZvcnRWYWx1ZSA9IHVzZUFwcFN0YXRlKHMgPT4gcy5lZmZvcnRWYWx1ZSlcbiAgY29uc3QgbW9kZWwgPSB1c2VNYWluTG9vcE1vZGVsKClcbiAgY29uc3QgeyBtZXNzYWdlIH0gPSBzaG93Q3VycmVudEVmZm9ydChlZmZvcnRWYWx1ZSwgbW9kZWwpXG4gIG9uRG9uZShtZXNzYWdlKVxuICByZXR1cm4gbnVsbFxufVxuXG5mdW5jdGlvbiBBcHBseUVmZm9ydEFuZENsb3NlKHtcbiAgcmVzdWx0LFxuICBvbkRvbmUsXG59OiB7XG4gIHJlc3VsdDogRWZmb3J0Q29tbWFuZFJlc3VsdFxuICBvbkRvbmU6IChyZXN1bHQ6IHN0cmluZykgPT4gdm9pZFxufSk6IFJlYWN0LlJlYWN0Tm9kZSB7XG4gIGNvbnN0IHNldEFwcFN0YXRlID0gdXNlU2V0QXBwU3RhdGUoKVxuICBjb25zdCB7IGVmZm9ydFVwZGF0ZSwgbWVzc2FnZSB9ID0gcmVzdWx0XG4gIFJlYWN0LnVzZUVmZmVjdCgoKSA9PiB7XG4gICAgaWYgKGVmZm9ydFVwZGF0ZSkge1xuICAgICAgc2V0QXBwU3RhdGUocHJldiA9PiAoe1xuICAgICAgICAuLi5wcmV2LFxuICAgICAgICBlZmZvcnRWYWx1ZTogZWZmb3J0VXBkYXRlLnZhbHVlLFxuICAgICAgfSkpXG4gICAgfVxuICAgIG9uRG9uZShtZXNzYWdlKVxuICB9LCBbc2V0QXBwU3RhdGUsIGVmZm9ydFVwZGF0ZSwgbWVzc2FnZSwgb25Eb25lXSlcbiAgcmV0dXJuIG51bGxcbn1cblxuZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIGNhbGwoXG4gIG9uRG9uZTogTG9jYWxKU1hDb21tYW5kT25Eb25lLFxuICBfY29udGV4dDogdW5rbm93bixcbiAgYXJncz86IHN0cmluZyxcbik6IFByb21pc2U8UmVhY3QuUmVhY3ROb2RlPiB7XG4gIGFyZ3MgPSBhcmdzPy50cmltKCkgfHwgJydcblxuICBpZiAoQ09NTU9OX0hFTFBfQVJHUy5pbmNsdWRlcyhhcmdzKSkge1xuICAgIG9uRG9uZShcbiAgICAgICdVc2FnZTogL2VmZm9ydCBbbG93fG1lZGl1bXxoaWdofG1heHxhdXRvXVxcblxcbkVmZm9ydCBsZXZlbHM6XFxuLSBsb3c6IFF1aWNrLCBzdHJhaWdodGZvcndhcmQgaW1wbGVtZW50YXRpb25cXG4tIG1lZGl1bTogQmFsYW5jZWQgYXBwcm9hY2ggd2l0aCBzdGFuZGFyZCB0ZXN0aW5nXFxuLSBoaWdoOiBDb21wcmVoZW5zaXZlIGltcGxlbWVudGF0aW9uIHdpdGggZXh0ZW5zaXZlIHRlc3RpbmdcXG4tIG1heDogTWF4aW11bSBjYXBhYmlsaXR5IHdpdGggZGVlcGVzdCByZWFzb25pbmcgKE9wdXMgNC42IG9ubHkpXFxuLSBhdXRvOiBVc2UgdGhlIGRlZmF1bHQgZWZmb3J0IGxldmVsIGZvciB5b3VyIG1vZGVsJyxcbiAgICApXG4gICAgcmV0dXJuXG4gIH1cblxuICBpZiAoIWFyZ3MgfHwgYXJncyA9PT0gJ2N1cnJlbnQnIHx8IGFyZ3MgPT09ICdzdGF0dXMnKSB7XG4gICAgcmV0dXJuIDxTaG93Q3VycmVudEVmZm9ydCBvbkRvbmU9e29uRG9uZX0gLz5cbiAgfVxuXG4gIGNvbnN0IHJlc3VsdCA9IGV4ZWN1dGVFZmZvcnQoYXJncylcbiAgcmV0dXJuIDxBcHBseUVmZm9ydEFuZENsb3NlIHJlc3VsdD17cmVzdWx0fSBvbkRvbmU9e29uRG9uZX0gLz5cbn1cbiJdLCJtYXBwaW5ncyI6IjtBQUFBLE9BQU8sS0FBS0EsS0FBSyxNQUFNLE9BQU87QUFDOUIsU0FBU0MsZ0JBQWdCLFFBQVEsaUNBQWlDO0FBQ2xFLFNBQ0UsS0FBS0MsMERBQTBELEVBQy9EQyxRQUFRLFFBQ0gsbUNBQW1DO0FBQzFDLFNBQVNDLFdBQVcs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 \ No newline at end of file diff --git a/src/components/CustomSelect/use-select-navigation.ts b/src/components/CustomSelect/use-select-navigation.ts index 7ecb4e71..544bbfa7 100644 --- a/src/components/CustomSelect/use-select-navigation.ts +++ b/src/components/CustomSelect/use-select-navigation.ts @@ -84,44 +84,44 @@ const reducer = (state: State, action: Action): State => { return state } - // Wrap to first item if at the end - const next = item.next || state.optionMap.first + // If there's a next item in the list, go to it + if (item.next) { + const needsToScroll = item.next.index >= state.visibleToIndex - if (!next) { + if (!needsToScroll) { + return { + ...state, + focusedValue: item.next.value, + } + } + + const nextVisibleToIndex = Math.min( + state.optionMap.size, + state.visibleToIndex + 1, + ) + + const nextVisibleFromIndex = nextVisibleToIndex - state.visibleOptionCount + + return { + ...state, + focusedValue: item.next.value, + visibleFromIndex: nextVisibleFromIndex, + visibleToIndex: nextVisibleToIndex, + } + } + + // No next item - wrap to first item + const firstItem = state.optionMap.first + if (!firstItem) { return state } // When wrapping to first, reset viewport to start - if (!item.next && next === state.optionMap.first) { - return { - ...state, - focusedValue: next.value, - visibleFromIndex: 0, - visibleToIndex: state.visibleOptionCount, - } - } - - const needsToScroll = next.index >= state.visibleToIndex - - if (!needsToScroll) { - return { - ...state, - focusedValue: next.value, - } - } - - const nextVisibleToIndex = Math.min( - state.optionMap.size, - state.visibleToIndex + 1, - ) - - const nextVisibleFromIndex = nextVisibleToIndex - state.visibleOptionCount - return { ...state, - focusedValue: next.value, - visibleFromIndex: nextVisibleFromIndex, - visibleToIndex: nextVisibleToIndex, + focusedValue: firstItem.value, + visibleFromIndex: 0, + visibleToIndex: state.visibleOptionCount, } } @@ -136,44 +136,43 @@ const reducer = (state: State, action: Action): State => { return state } - // Wrap to last item if at the beginning - const previous = item.previous || state.optionMap.last + // If there's a previous item in the list, go to it + if (item.previous) { + const needsToScroll = item.previous.index < state.visibleFromIndex - if (!previous) { - return state - } + if (!needsToScroll) { + return { + ...state, + focusedValue: item.previous.value, + } + } + + const nextVisibleFromIndex = Math.max(0, state.visibleFromIndex - 1) + const nextVisibleToIndex = nextVisibleFromIndex + state.visibleOptionCount - // When wrapping to last, reset viewport to end - if (!item.previous && previous === state.optionMap.last) { - const nextVisibleToIndex = state.optionMap.size - const nextVisibleFromIndex = Math.max( - 0, - nextVisibleToIndex - state.visibleOptionCount, - ) return { ...state, - focusedValue: previous.value, + focusedValue: item.previous.value, visibleFromIndex: nextVisibleFromIndex, visibleToIndex: nextVisibleToIndex, } } - const needsToScroll = previous.index <= state.visibleFromIndex - - if (!needsToScroll) { - return { - ...state, - focusedValue: previous.value, - } + // No previous item - wrap to last item + const lastItem = state.optionMap.last + if (!lastItem) { + return state } - const nextVisibleFromIndex = Math.max(0, state.visibleFromIndex - 1) - - const nextVisibleToIndex = nextVisibleFromIndex + state.visibleOptionCount - + // When wrapping to last, reset viewport to end + const nextVisibleToIndex = state.optionMap.size + const nextVisibleFromIndex = Math.max( + 0, + nextVisibleToIndex - state.visibleOptionCount, + ) return { ...state, - focusedValue: previous.value, + focusedValue: lastItem.value, visibleFromIndex: nextVisibleFromIndex, visibleToIndex: nextVisibleToIndex, } diff --git a/src/components/EffortPicker.tsx b/src/components/EffortPicker.tsx new file mode 100644 index 00000000..2e86509e --- /dev/null +++ b/src/components/EffortPicker.tsx @@ -0,0 +1,152 @@ +import React, { useState } from 'react' +import { Box, Text } from '../ink.js' +import { useMainLoopModel } from '../hooks/useMainLoopModel.js' +import { useAppState, useSetAppState } from '../state/AppState.js' +import type { EffortLevel, OpenAIEffortLevel } from '../utils/effort.js' +import { + getAvailableEffortLevels, + getDisplayedEffortLevel, + getEffortLevelDescription, + getEffortLevelLabel, + getEffortValueDescription, + modelSupportsEffort, + modelUsesOpenAIEffort, + standardEffortToOpenAI, + isOpenAIEffortLevel, +} from '../utils/effort.js' +import { getAPIProvider } from '../utils/model/providers.js' +import { getReasoningEffortForModel } from '../services/api/providerConfig.js' +import { Select } from './CustomSelect/select.js' +import { effortLevelToSymbol } from './EffortIndicator.js' +import { KeyboardShortcutHint } from './design-system/KeyboardShortcutHint.js' +import { Byline } from './design-system/Byline.js' + +type EffortOption = { + label: React.ReactNode + value: string + description: string + isAvailable: boolean +} + +type Props = { + onSelect: (effort: EffortLevel | undefined) => void + onCancel?: () => void +} + +export function EffortPicker({ onSelect, onCancel }: Props) { + const model = useMainLoopModel() + const appStateEffort = useAppState((s: any) => s.effortValue) + const setAppState = useSetAppState() + const provider = getAPIProvider() + const usesOpenAIEffort = modelUsesOpenAIEffort(model) + const availableLevels = getAvailableEffortLevels(model) + const currentDisplayedLevel = getDisplayedEffortLevel(model, appStateEffort) + + // For OpenAI/Codex, get the model's default reasoning effort + const modelReasoningEffort = usesOpenAIEffort ? getReasoningEffortForModel(model) : undefined + const defaultEffortForModel = modelReasoningEffort || currentDisplayedLevel + + const options: EffortOption[] = [ + { + label: , + value: 'auto', + description: 'Use the default effort level for your model', + isAvailable: true, + }, + ...availableLevels.map(level => { + const displayLevel = usesOpenAIEffort + ? (level === 'xhigh' ? 'max' : level) + : level + const isCurrent = currentDisplayedLevel === displayLevel + return { + label: ( + + ), + value: level, + description: getEffortLevelDescription(level as EffortLevel), + isAvailable: true, + } + }), + ] + + function handleSelect(value: string) { + if (value === 'auto') { + setAppState(prev => ({ + ...prev, + effortValue: undefined, + })) + onSelect(undefined) + } else { + const effortLevel = value as EffortLevel + setAppState(prev => ({ + ...prev, + effortValue: effortLevel, + })) + onSelect(effortLevel) + } + } + + function handleCancel() { + onCancel?.() + } + + const supportsEffort = modelSupportsEffort(model) + // For OpenAI/Codex, use the model's default reasoning effort as initial focus + // For Claude, use the displayed effort level or 'auto' + const initialFocus = usesOpenAIEffort + ? (modelReasoningEffort || 'auto') + : (appStateEffort ? String(appStateEffort) : 'auto') + + return ( + + + Set effort level + + {usesOpenAIEffort + ? `OpenAI/Codex provider (${provider})` + : supportsEffort + ? `Claude model · ${provider} provider` + : `Effort not supported for this model` + } + + + + + + + + + + + + + + + + + ) +} + +function EffortOptionLabel({ level, text, isCurrent }: { level: EffortLevel | 'auto', text: string, isCurrent: boolean }) { + const symbol = level === 'auto' ? '⊘' : effortLevelToSymbol(level as EffortLevel) + const color = isCurrent ? 'remember' : level === 'auto' ? 'subtle' : 'suggestion' + + return ( + <> + {symbol} + {text} + {isCurrent && (current)} + + ) +} diff --git a/src/components/StartupScreen.ts b/src/components/StartupScreen.ts index b20d26c1..e38a4111 100644 --- a/src/components/StartupScreen.ts +++ b/src/components/StartupScreen.ts @@ -97,21 +97,45 @@ function detectProvider(): { name: string; model: string; baseUrl: string; isLoc } if (useOpenAI) { - const model = process.env.OPENAI_MODEL || 'gpt-4o' + const rawModel = process.env.OPENAI_MODEL || 'gpt-4o' const baseUrl = process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1' const isLocal = /localhost|127\.0\.0\.1|0\.0\.0\.0/.test(baseUrl) let name = 'OpenAI' - if (/deepseek/i.test(baseUrl) || /deepseek/i.test(model)) name = 'DeepSeek' + if (/deepseek/i.test(baseUrl) || /deepseek/i.test(rawModel)) name = 'DeepSeek' else if (/openrouter/i.test(baseUrl)) name = 'OpenRouter' else if (/together/i.test(baseUrl)) name = 'Together AI' else if (/groq/i.test(baseUrl)) name = 'Groq' - else if (/mistral/i.test(baseUrl) || /mistral/i.test(model)) name = 'Mistral' + else if (/mistral/i.test(baseUrl) || /mistral/i.test(rawModel)) name = 'Mistral' else if (/azure/i.test(baseUrl)) name = 'Azure OpenAI' else if (/localhost:11434/i.test(baseUrl)) name = 'Ollama' else if (/localhost:1234/i.test(baseUrl)) name = 'LM Studio' - else if (/llama/i.test(model)) name = 'Meta Llama' + else if (/llama/i.test(rawModel)) name = 'Meta Llama' else if (isLocal) name = 'Local' - return { name, model, baseUrl, isLocal } + + // Resolve model alias to actual model name + reasoning effort + let displayModel = rawModel + const codexAliases: Record = { + codexplan: { model: 'gpt-5.4', reasoningEffort: 'high' }, + 'gpt-5.4': { model: 'gpt-5.4', reasoningEffort: 'high' }, + 'gpt-5.3-codex': { model: 'gpt-5.3-codex', reasoningEffort: 'high' }, + 'gpt-5.3-codex-spark': { model: 'gpt-5.3-codex-spark' }, + codexspark: { model: 'gpt-5.3-codex-spark' }, + 'gpt-5.2-codex': { model: 'gpt-5.2-codex', reasoningEffort: 'high' }, + 'gpt-5.1-codex-max': { model: 'gpt-5.1-codex-max', reasoningEffort: 'high' }, + 'gpt-5.1-codex-mini': { model: 'gpt-5.1-codex-mini' }, + 'gpt-5.4-mini': { model: 'gpt-5.4-mini', reasoningEffort: 'medium' }, + 'gpt-5.2': { model: 'gpt-5.2', reasoningEffort: 'medium' }, + } + const alias = rawModel.toLowerCase() + if (alias in codexAliases) { + const resolved = codexAliases[alias] + displayModel = resolved.model + if (resolved.reasoningEffort) { + displayModel = `${displayModel} (${resolved.reasoningEffort})` + } + } + + return { name, model: displayModel, baseUrl, isLocal } } // Default: Anthropic diff --git a/src/hooks/useTypeahead.tsx b/src/hooks/useTypeahead.tsx index a269902b..8183a011 100644 --- a/src/hooks/useTypeahead.tsx +++ b/src/hooks/useTypeahead.tsx @@ -1242,17 +1242,25 @@ export function useTypeahead({ const handleAutocompletePrevious = useCallback(() => { setSuggestionsState(prev => ({ ...prev, - selectedSuggestion: prev.selectedSuggestion <= 0 ? suggestions.length - 1 : prev.selectedSuggestion - 1 + selectedSuggestion: prev.suggestions.length === 0 + ? -1 + : prev.selectedSuggestion <= 0 + ? prev.suggestions.length - 1 + : Math.min(prev.selectedSuggestion - 1, prev.suggestions.length - 1) })); - }, [suggestions.length, setSuggestionsState]); + }, [setSuggestionsState]); // Handler for autocomplete:next - selects next suggestion const handleAutocompleteNext = useCallback(() => { setSuggestionsState(prev => ({ ...prev, - selectedSuggestion: prev.selectedSuggestion >= suggestions.length - 1 ? 0 : prev.selectedSuggestion + 1 + selectedSuggestion: prev.suggestions.length === 0 + ? -1 + : prev.selectedSuggestion >= prev.suggestions.length - 1 + ? 0 + : Math.max(0, prev.selectedSuggestion + 1) })); - }, [suggestions.length, setSuggestionsState]); + }, [setSuggestionsState]); // Autocomplete context keybindings - only active when suggestions are visible const autocompleteHandlers = useMemo(() => ({ diff --git a/src/services/api/openaiShim.ts b/src/services/api/openaiShim.ts index 8ab97ac3..6b28805e 100644 --- a/src/services/api/openaiShim.ts +++ b/src/services/api/openaiShim.ts @@ -665,9 +665,11 @@ class OpenAIShimStream { class OpenAIShimMessages { private defaultHeaders: Record + private reasoningEffort?: 'low' | 'medium' | 'high' | 'xhigh' - constructor(defaultHeaders: Record) { + constructor(defaultHeaders: Record, reasoningEffort?: 'low' | 'medium' | 'high' | 'xhigh') { this.defaultHeaders = defaultHeaders + this.reasoningEffort = reasoningEffort } create( @@ -679,7 +681,7 @@ class OpenAIShimMessages { let httpResponse: Response | undefined const promise = (async () => { - const request = resolveProviderRequest({ model: params.model }) + const request = resolveProviderRequest({ model: params.model, reasoningEffortOverride: self.reasoningEffort }) const response = await self._doRequest(request, params, options) httpResponse = response @@ -1018,9 +1020,11 @@ class OpenAIShimMessages { class OpenAIShimBeta { messages: OpenAIShimMessages + reasoningEffort?: 'low' | 'medium' | 'high' | 'xhigh' - constructor(defaultHeaders: Record) { - this.messages = new OpenAIShimMessages(defaultHeaders) + constructor(defaultHeaders: Record, reasoningEffort?: 'low' | 'medium' | 'high' | 'xhigh') { + this.messages = new OpenAIShimMessages(defaultHeaders, reasoningEffort) + this.reasoningEffort = reasoningEffort } } @@ -1028,6 +1032,7 @@ export function createOpenAIShimClient(options: { defaultHeaders?: Record maxRetries?: number timeout?: number + reasoningEffort?: 'low' | 'medium' | 'high' | 'xhigh' }): unknown { hydrateGithubModelsTokenFromSecureStorage() @@ -1050,7 +1055,7 @@ export function createOpenAIShimClient(options: { const beta = new OpenAIShimBeta({ ...(options.defaultHeaders ?? {}), - }) + }, options.reasoningEffort) return { beta, diff --git a/src/services/api/providerConfig.ts b/src/services/api/providerConfig.ts index 90643aa1..1c3097db 100644 --- a/src/services/api/providerConfig.ts +++ b/src/services/api/providerConfig.ts @@ -20,13 +20,43 @@ const CODEX_ALIAS_MODELS: Record< model: 'gpt-5.4', reasoningEffort: 'high', }, + 'gpt-5.4': { + model: 'gpt-5.4', + reasoningEffort: 'high', + }, + 'gpt-5.3-codex': { + model: 'gpt-5.3-codex', + reasoningEffort: 'high', + }, + 'gpt-5.3-codex-spark': { + model: 'gpt-5.3-codex-spark', + }, codexspark: { model: 'gpt-5.3-codex-spark', }, + 'gpt-5.2-codex': { + model: 'gpt-5.2-codex', + reasoningEffort: 'high', + }, + 'gpt-5.1-codex-max': { + model: 'gpt-5.1-codex-max', + reasoningEffort: 'high', + }, + 'gpt-5.1-codex-mini': { + model: 'gpt-5.1-codex-mini', + }, + 'gpt-5.4-mini': { + model: 'gpt-5.4-mini', + reasoningEffort: 'medium', + }, + 'gpt-5.2': { + model: 'gpt-5.2', + reasoningEffort: 'medium', + }, } as const type CodexAlias = keyof typeof CODEX_ALIAS_MODELS -type ReasoningEffort = 'low' | 'medium' | 'high' +type ReasoningEffort = 'low' | 'medium' | 'high' | 'xhigh' export type ProviderTransport = 'chat_completions' | 'codex_responses' @@ -102,7 +132,7 @@ function decodeJwtPayload(token: string): Record | undefined { function parseReasoningEffort(value: string | undefined): ReasoningEffort | undefined { if (!value) return undefined const normalized = value.trim().toLowerCase() - if (normalized === 'low' || normalized === 'medium' || normalized === 'high') { + if (normalized === 'low' || normalized === 'medium' || normalized === 'high' || normalized === 'xhigh') { return normalized } return undefined @@ -193,6 +223,7 @@ export function resolveProviderRequest(options?: { model?: string baseUrl?: string fallbackModel?: string + reasoningEffortOverride?: ReasoningEffort }): ResolvedProviderRequest { const isGithubMode = isEnvTruthy(process.env.CLAUDE_CODE_USE_GITHUB) const requestedModel = @@ -217,6 +248,11 @@ export function resolveProviderRequest(options?: { ? normalizeGithubModelsApiModel(requestedModel) : descriptor.baseModel + const reasoning = options?.reasoningEffortOverride + ? { effort: options.reasoningEffortOverride } + : descriptor.reasoning + + return { transport, requestedModel, @@ -227,7 +263,7 @@ export function resolveProviderRequest(options?: { ? DEFAULT_CODEX_BASE_URL : DEFAULT_OPENAI_BASE_URL) ).replace(/\/+$/, ''), - reasoning: descriptor.reasoning, + reasoning, } } @@ -336,3 +372,11 @@ export function resolveCodexApiCredentials( source: 'auth.json', } } + +export function getReasoningEffortForModel(model: string): ReasoningEffort | undefined { + const normalized = model.trim().toLowerCase() + const base = normalized.split('?', 1)[0] ?? normalized + const alias = base as CodexAlias + const aliasConfig = CODEX_ALIAS_MODELS[alias] + return aliasConfig?.reasoningEffort +} diff --git a/src/utils/effort.ts b/src/utils/effort.ts index cafcf3de..2a391ee6 100644 --- a/src/utils/effort.ts +++ b/src/utils/effort.ts @@ -17,6 +17,14 @@ export const EFFORT_LEVELS = [ 'max', ] as const satisfies readonly EffortLevel[] +export const OPENAI_EFFORT_LEVELS = [ + 'low', + 'medium', + 'high', + 'xhigh', +] as const + +export type OpenAIEffortLevel = typeof OPENAI_EFFORT_LEVELS[number] export type EffortValue = EffortLevel | number // @[MODEL LAUNCH]: Add the new model to the allowlist if it supports the effort parameter. @@ -68,6 +76,46 @@ export function isEffortLevel(value: string): value is EffortLevel { return (EFFORT_LEVELS as readonly string[]).includes(value) } +export function isOpenAIEffortLevel(value: string): value is OpenAIEffortLevel { + return (OPENAI_EFFORT_LEVELS as readonly string[]).includes(value) +} + +export function modelUsesOpenAIEffort(model: string): boolean { + const provider = getAPIProvider() + return provider === 'openai' || provider === 'codex' +} + +export function getAvailableEffortLevels(model: string): EffortLevel[] | OpenAIEffortLevel[] { + if (modelUsesOpenAIEffort(model)) { + return [...OPENAI_EFFORT_LEVELS] as OpenAIEffortLevel[] + } + const levels: EffortLevel[] = ['low', 'medium', 'high'] + if (modelSupportsMaxEffort(model)) { + levels.push('max') + } + return levels +} + +export function getEffortLevelLabel(level: EffortLevel | OpenAIEffortLevel): string { + if (level === 'xhigh') return 'Extra High' + if (level === 'max') return 'Max' + return capitalize(level) +} + +export function openAIEffortToStandard(level: OpenAIEffortLevel): EffortLevel { + if (level === 'xhigh') return 'max' + return level +} + +export function standardEffortToOpenAI(level: EffortLevel): OpenAIEffortLevel { + if (level === 'max') return 'xhigh' + return level as OpenAIEffortLevel +} + +function capitalize(s: string): string { + return s.charAt(0).toUpperCase() + s.slice(1) +} + export function parseEffortValue(value: unknown): EffortValue | undefined { if (value === undefined || value === null || value === '') { return undefined @@ -221,7 +269,7 @@ export function convertEffortValueToLevel(value: EffortValue): EffortLevel { * @param level The effort level to describe * @returns Human-readable description */ -export function getEffortLevelDescription(level: EffortLevel): string { +export function getEffortLevelDescription(level: EffortLevel | OpenAIEffortLevel): string { switch (level) { case 'low': return 'Quick, straightforward implementation with minimal overhead' @@ -231,6 +279,8 @@ export function getEffortLevelDescription(level: EffortLevel): string { return 'Comprehensive implementation with extensive testing and documentation' case 'max': return 'Maximum capability with deepest reasoning (Opus 4.6 only)' + case 'xhigh': + return 'Extra high reasoning effort for complex tasks (OpenAI/Codex)' } } diff --git a/src/utils/model/aliases.ts b/src/utils/model/aliases.ts index 91514da1..75ae388c 100644 --- a/src/utils/model/aliases.ts +++ b/src/utils/model/aliases.ts @@ -6,8 +6,6 @@ export const MODEL_ALIASES = [ 'sonnet[1m]', 'opus[1m]', 'opusplan', - 'codexplan', - 'codexspark', ] as const export type ModelAlias = (typeof MODEL_ALIASES)[number] diff --git a/src/utils/model/model.ts b/src/utils/model/model.ts index 6c81a8ef..97a74d95 100644 --- a/src/utils/model/model.ts +++ b/src/utils/model/model.ts @@ -123,6 +123,10 @@ export function getDefaultOpusModel(): ModelName { if (getAPIProvider() === 'openai') { return process.env.OPENAI_MODEL || 'gpt-4o' } + // Codex provider: use user-specified model or default to gpt-5.4 + if (getAPIProvider() === 'codex') { + return process.env.OPENAI_MODEL || 'gpt-5.4' + } // 3P providers (Bedrock, Vertex, Foundry) — kept as a separate branch // even when values match, since 3P availability lags firstParty and // these will diverge again at the next model launch. @@ -145,6 +149,10 @@ export function getDefaultSonnetModel(): ModelName { if (getAPIProvider() === 'openai') { return process.env.OPENAI_MODEL || 'gpt-4o' } + // Codex provider + if (getAPIProvider() === 'codex') { + return process.env.OPENAI_MODEL || 'gpt-5.4' + } // Default to Sonnet 4.5 for 3P since they may not have 4.6 yet if (getAPIProvider() !== 'firstParty') { return getModelStrings().sonnet45 @@ -165,6 +173,10 @@ export function getDefaultHaikuModel(): ModelName { if (getAPIProvider() === 'openai') { return process.env.OPENAI_MODEL || 'gpt-4o-mini' } + // Codex provider + if (getAPIProvider() === 'codex') { + return process.env.OPENAI_MODEL || 'gpt-5.4' + } // Haiku 4.5 is available on all platforms (first-party, Foundry, Bedrock, Vertex) return getModelStrings().haiku45 @@ -217,6 +229,10 @@ export function getDefaultMainLoopModelSetting(): ModelName | ModelAlias { if (getAPIProvider() === 'openai') { return process.env.OPENAI_MODEL || 'gpt-4o' } + // Codex provider: always use the configured Codex model (default gpt-5.4) + if (getAPIProvider() === 'codex') { + return process.env.OPENAI_MODEL || 'gpt-5.4' + } // Ants default to defaultModel from flag config, or Opus 1M if not configured if (process.env.USER_TYPE === 'ant') { @@ -343,12 +359,6 @@ export function renderDefaultModelSetting( if (setting === 'opusplan') { return 'Opus 4.6 in plan mode, else Sonnet 4.6' } - if (setting === 'codexplan') { - return 'Codex Plan (GPT-5.4 high reasoning)' - } - if (setting === 'codexspark') { - return 'Codex Spark (GPT-5.3 Codex Spark)' - } return renderModelName(parseUserSpecifiedModel(setting)) } @@ -383,11 +393,12 @@ export function renderModelSetting(setting: ModelName | ModelAlias): string { if (setting === 'opusplan') { return 'Opus Plan' } + // Handle Codex models - show actual model name + resolved model if (setting === 'codexplan') { - return 'Codex Plan' + return 'codexplan (gpt-5.4)' } if (setting === 'codexspark') { - return 'Codex Spark' + return 'codexspark (gpt-5.3-codex-spark)' } if (isModelAlias(setting)) { return capitalize(setting) @@ -401,8 +412,8 @@ export function renderModelSetting(setting: ModelName | ModelAlias): string { * if the model is not recognized as a public model. */ export function getPublicModelDisplayName(model: ModelName): string | null { - // For OpenAI/Gemini providers, show the actual model name not a Claude alias - if (getAPIProvider() === 'openai' || getAPIProvider() === 'gemini') { + // For OpenAI/Gemini/Codex providers, show the actual model name not a Claude alias + if (getAPIProvider() === 'openai' || getAPIProvider() === 'gemini' || getAPIProvider() === 'codex') { return null } switch (model) { @@ -517,10 +528,6 @@ export function parseUserSpecifiedModel( if (isModelAlias(modelString)) { switch (modelString) { - case 'codexplan': - return modelInputTrimmed - case 'codexspark': - return modelInputTrimmed case 'opusplan': return getDefaultSonnetModel() + (has1mTag ? '[1m]' : '') // Sonnet is default, Opus in plan mode case 'sonnet': @@ -535,6 +542,14 @@ export function parseUserSpecifiedModel( } } + // Handle Codex aliases - map to actual model names + if (modelString === 'codexplan') { + return 'gpt-5.4' + } + if (modelString === 'codexspark') { + return 'gpt-5.3-codex-spark' + } + // Opus 4/4.1 are no longer available on the first-party API (same as // Claude.ai) — silently remap to the current Opus default. The 'opus' // alias already resolves to 4.6, so the only users on these explicit diff --git a/src/utils/model/modelOptions.ts b/src/utils/model/modelOptions.ts index 0c464d6a..84371c84 100644 --- a/src/utils/model/modelOptions.ts +++ b/src/utils/model/modelOptions.ts @@ -268,20 +268,65 @@ function getOpusPlanOption(): ModelOption { function getCodexPlanOption(): ModelOption { return { - value: 'codexplan', - label: 'Codex Plan', + value: 'gpt-5.4', + label: 'gpt-5.4', description: 'GPT-5.4 on the Codex backend with high reasoning', } } function getCodexSparkOption(): ModelOption { return { - value: 'codexspark', - label: 'Codex Spark', + value: 'gpt-5.3-codex-spark', + label: 'gpt-5.3-codex-spark', description: 'GPT-5.3 Codex Spark on the Codex backend for fast tool loops', } } +function getCodexModelOptions(): ModelOption[] { + return [ + { + value: 'gpt-5.4', + label: 'gpt-5.4', + description: 'GPT-5.4 with high reasoning', + }, + { + value: 'gpt-5.3-codex', + label: 'gpt-5.3-codex', + description: 'GPT-5.3 Codex with high reasoning', + }, + { + value: 'gpt-5.3-codex-spark', + label: 'gpt-5.3-codex-spark', + description: 'GPT-5.3 Codex Spark for fast tool loops', + }, + { + value: 'codexspark', + label: 'codexspark', + description: 'GPT-5.3 Codex Spark alias for fast tool loops', + }, + { + value: 'gpt-5.2-codex', + label: 'gpt-5.2-codex', + description: 'GPT-5.2 Codex with high reasoning', + }, + { + value: 'gpt-5.1-codex-max', + label: 'gpt-5.1-codex-max', + description: 'GPT-5.1 Codex Max for deep reasoning', + }, + { + value: 'gpt-5.1-codex-mini', + label: 'gpt-5.1-codex-mini', + description: 'GPT-5.1 Codex Mini - faster, cheaper', + }, + { + value: 'gpt-5.4-mini', + label: 'gpt-5.4-mini', + description: 'GPT-5.4 Mini - faster, cheaper', + }, + ] +} + // @[MODEL LAUNCH]: Update the model picker lists below to include/reorder options for the new model. // Each user tier (ant, Max/Team Premium, Pro/Team Standard/Enterprise, PAYG 1P, PAYG 3P) has its own list. function getModelOptionsBase(fastMode = false): ModelOption[] { @@ -360,8 +405,9 @@ function getModelOptionsBase(fastMode = false): ModelOption[] { // PAYG 3P: Default (Sonnet 4.5) + Sonnet (3P custom) or Sonnet 4.6/1M + Opus (3P custom) or Opus 4.1/Opus 4.6/Opus1M + Haiku + Opus 4.1 const payg3pOptions = [getDefaultOptionForUser(fastMode)] - if (getAPIProvider() === 'openai') { - payg3pOptions.push(getCodexPlanOption(), getCodexSparkOption()) + // Add Codex models for openai and codex providers + if (getAPIProvider() === 'openai' || getAPIProvider() === 'codex') { + payg3pOptions.push(...getCodexModelOptions()) } const customSonnet = getCustomSonnetOption() @@ -517,9 +563,9 @@ export function getModelOptions(fastMode = false): ModelOption[] { return filterModelOptionsByAllowlist(options) } else if (customModel === 'opusplan') { return filterModelOptionsByAllowlist([...options, getOpusPlanOption()]) - } else if (customModel === 'codexplan') { + } else if (customModel === 'gpt-5.4') { return filterModelOptionsByAllowlist([...options, getCodexPlanOption()]) - } else if (customModel === 'codexspark') { + } else if (customModel === 'gpt-5.3-codex-spark') { return filterModelOptionsByAllowlist([...options, getCodexSparkOption()]) } else if (customModel === 'opus' && getAPIProvider() === 'firstParty') { return filterModelOptionsByAllowlist([ @@ -554,11 +600,23 @@ export function getModelOptions(fastMode = false): ModelOption[] { */ function filterModelOptionsByAllowlist(options: ModelOption[]): ModelOption[] { const settings = getSettings_DEPRECATED() || {} - if (!settings.availableModels) { - return options // No restrictions - } - return options.filter( + const filtered = !settings.availableModels + ? options // No restrictions + : options.filter( opt => opt.value === null || (opt.value !== null && isModelAllowed(opt.value)), ) + + // Select state uses option values as identity keys. If two entries share the + // same value (e.g. provider-specific aliases collapsing to one model ID), + // navigation/focus can become inconsistent and appear as duplicate rendering. + const seen = new Set() + return filtered.filter(opt => { + const key = String(opt.value) + if (seen.has(key)) { + return false + } + seen.add(key) + return true + }) } diff --git a/src/utils/model/modelStrings.ts b/src/utils/model/modelStrings.ts index 5b7be104..4d8399d1 100644 --- a/src/utils/model/modelStrings.ts +++ b/src/utils/model/modelStrings.ts @@ -23,9 +23,12 @@ export type ModelStrings = Record const MODEL_KEYS = Object.keys(ALL_MODEL_CONFIGS) as ModelKey[] function getBuiltinModelStrings(provider: APIProvider): ModelStrings { + // Codex piggybacks on the OpenAI provider transport for Anthropic tier aliases. + // Reuse OpenAI mappings so model string lookups never return undefined. + const providerKey = provider === 'codex' ? 'openai' : provider const out = {} as ModelStrings for (const key of MODEL_KEYS) { - out[key] = ALL_MODEL_CONFIGS[key][provider] + out[key] = ALL_MODEL_CONFIGS[key][providerKey] } return out } diff --git a/src/utils/model/providers.ts b/src/utils/model/providers.ts index 30a1f1c9..6b6d627e 100644 --- a/src/utils/model/providers.ts +++ b/src/utils/model/providers.ts @@ -9,6 +9,7 @@ export type APIProvider = | 'openai' | 'gemini' | 'github' + | 'codex' export function getAPIProvider(): APIProvider { return isEnvTruthy(process.env.CLAUDE_CODE_USE_GEMINI) @@ -16,7 +17,9 @@ export function getAPIProvider(): APIProvider { : isEnvTruthy(process.env.CLAUDE_CODE_USE_GITHUB) ? 'github' : isEnvTruthy(process.env.CLAUDE_CODE_USE_OPENAI) - ? 'openai' + ? isCodexModel() + ? 'codex' + : 'openai' : isEnvTruthy(process.env.CLAUDE_CODE_USE_BEDROCK) ? 'bedrock' : isEnvTruthy(process.env.CLAUDE_CODE_USE_VERTEX) @@ -29,6 +32,19 @@ export function getAPIProvider(): APIProvider { export function usesAnthropicAccountFlow(): boolean { return getAPIProvider() === 'firstParty' } +function isCodexModel(): boolean { + const model = (process.env.OPENAI_MODEL || '').toLowerCase() + return ( + model === 'codexplan' || + model === 'codexspark' || + model === 'gpt-5.4' || + model === 'gpt-5.3-codex' || + model === 'gpt-5.3-codex-spark' || + model === 'gpt-5.2-codex' || + model === 'gpt-5.1-codex-max' || + model === 'gpt-5.1-codex-mini' + ) +} export function getAPIProviderForStatsig(): AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS { return getAPIProvider() as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS diff --git a/src/utils/status.tsx b/src/utils/status.tsx index 4e7c6c4b..a712034d 100644 --- a/src/utils/status.tsx +++ b/src/utils/status.tsx @@ -12,6 +12,7 @@ import { formatNumber } from './format.js'; import { getIdeClientName, type IDEExtensionInstallationStatus, isJetBrainsIde, toIDEDisplayName } from './ide.js'; import { getClaudeAiUserDefaultModelDescription, modelDisplayString } from './model/model.js'; import { getAPIProvider } from './model/providers.js'; +import { resolveProviderRequest } from '../services/api/providerConfig.js'; import { getMTLSConfig } from './mtls.js'; import { checkInstall } from './nativeInstaller/index.js'; import { getProxyUrl } from './proxy.js'; @@ -247,6 +248,7 @@ export function buildAPIProviderProperties(): Property[] { vertex: 'Google Vertex AI', foundry: 'Microsoft Foundry', openai: 'OpenAI-compatible', + codex: 'Codex', gemini: 'Google Gemini', }[apiProvider]; properties.push({ @@ -325,34 +327,73 @@ export function buildAPIProviderProperties(): Property[] { } } else if (apiProvider === 'openai') { const openaiBaseUrl = process.env.OPENAI_BASE_URL; - if (openaiBaseUrl) { - properties.push({ - label: 'OpenAI base URL', - value: redactSecretValueForDisplay(openaiBaseUrl, process.env) ?? openaiBaseUrl - }); + if (openaiBaseUrl) { + properties.push({ + label: 'OpenAI base URL', + value: redactSecretValueForDisplay(openaiBaseUrl, process.env) ?? openaiBaseUrl + }); + } + const openaiModel = process.env.OPENAI_MODEL; + if (openaiModel) { + // Build display model string with resolved model + reasoning effort + let modelDisplay = openaiModel; + const resolved = resolveProviderRequest({ model: openaiModel }); + const resolvedModel = resolved.resolvedModel; + const reasoningEffort = resolved.reasoning?.effort; + if (resolvedModel && resolvedModel !== openaiModel.toLowerCase()) { + // Show resolved model name + modelDisplay = resolvedModel; } - const openaiModel = process.env.OPENAI_MODEL; - if (openaiModel) { - properties.push({ - label: 'Model', - value: redactSecretValueForDisplay(openaiModel, process.env) ?? openaiModel - }); + if (reasoningEffort) { + modelDisplay = `${modelDisplay} (${reasoningEffort})`; } - } else if (apiProvider === 'gemini') { - const geminiBaseUrl = process.env.GEMINI_BASE_URL; - if (geminiBaseUrl) { - properties.push({ - label: 'Gemini base URL', - value: redactSecretValueForDisplay(geminiBaseUrl, process.env) ?? geminiBaseUrl - }); + properties.push({ + label: 'Model', + value: redactSecretValueForDisplay(modelDisplay, process.env) ?? modelDisplay + }); + } + } else if (apiProvider === 'codex') { + const codexBaseUrl = process.env.OPENAI_BASE_URL; + if (codexBaseUrl) { + properties.push({ + label: 'Codex base URL', + value: redactSecretValueForDisplay(codexBaseUrl, process.env) ?? codexBaseUrl + }); + } + const openaiModel = process.env.OPENAI_MODEL; + if (openaiModel) { + // Build display model string with resolved model + reasoning effort + let modelDisplay = openaiModel; + const resolved = resolveProviderRequest({ model: openaiModel }); + const resolvedModel = resolved.resolvedModel; + const reasoningEffort = resolved.reasoning?.effort; + if (resolvedModel && resolvedModel !== openaiModel.toLowerCase()) { + // Show resolved model name + modelDisplay = resolvedModel; } - const geminiModel = process.env.GEMINI_MODEL; - if (geminiModel) { - properties.push({ - label: 'Model', - value: redactSecretValueForDisplay(geminiModel, process.env) ?? geminiModel - }); + if (reasoningEffort) { + modelDisplay = `${modelDisplay} (${reasoningEffort})`; } + properties.push({ + label: 'Model', + value: redactSecretValueForDisplay(modelDisplay, process.env) ?? modelDisplay + }); + } + } else if (apiProvider === 'gemini') { + const geminiBaseUrl = process.env.GEMINI_BASE_URL; + if (geminiBaseUrl) { + properties.push({ + label: 'Gemini base URL', + value: redactSecretValueForDisplay(geminiBaseUrl, process.env) ?? geminiBaseUrl + }); + } + const geminiModel = process.env.GEMINI_MODEL; + if (geminiModel) { + properties.push({ + label: 'Model', + value: redactSecretValueForDisplay(geminiModel, process.env) ?? geminiModel + }); + } } const proxyUrl = getProxyUrl(); if (proxyUrl) { diff --git a/src/utils/suggestions/commandSuggestions.ts b/src/utils/suggestions/commandSuggestions.ts index 4a90db55..2f83ae6f 100644 --- a/src/utils/suggestions/commandSuggestions.ts +++ b/src/utils/suggestions/commandSuggestions.ts @@ -286,6 +286,25 @@ function createCommandSuggestionItem( } } +/** + * Ensure suggestion IDs are unique for React keys and selection logic. + * If duplicates exist, append a stable numeric suffix to subsequent entries. + */ +function ensureUniqueSuggestionIds(items: SuggestionItem[]): SuggestionItem[] { + const counts = new Map() + return items.map(item => { + const seen = counts.get(item.id) ?? 0 + counts.set(item.id, seen + 1) + if (seen === 0) { + return item + } + return { + ...item, + id: `${item.id}#${seen + 1}`, + } + }) +} + /** * Generate command suggestions based on input */ @@ -369,14 +388,14 @@ export function generateCommandSuggestions( // Combine with built-in commands prioritized after recently used, // so they remain visible even when many skills are installed - return [ + return ensureUniqueSuggestionIds([ ...recentlyUsed, ...builtinCommands, ...userCommands, ...projectCommands, ...policyCommands, ...otherCommands, - ].map(cmd => createCommandSuggestionItem(cmd)) + ].map(cmd => createCommandSuggestionItem(cmd))) } // The Fuse index filters isHidden at build time and is keyed on the @@ -491,10 +510,13 @@ export function generateCommandSuggestions( if (hiddenExact) { const hiddenId = getCommandId(hiddenExact) if (!fuseSuggestions.some(s => s.id === hiddenId)) { - return [createCommandSuggestionItem(hiddenExact), ...fuseSuggestions] + return ensureUniqueSuggestionIds([ + createCommandSuggestionItem(hiddenExact), + ...fuseSuggestions, + ]) } } - return fuseSuggestions + return ensureUniqueSuggestionIds(fuseSuggestions) } /**