feat: 修复 Codex 模型映射并添加登录后模型配置面板

- configs.ts: 将 codex 字段从 Anthropic 模型名改为实际 OpenAI 模型名
  (opus→gpt-5.4, sonnet→gpt-5.4-mini, haiku→gpt-5.4-mini, opus47→gpt-5.5)
- modelMapping.ts: 移除不存在的 gpt-5.4-nano,修复 haiku 映射,添加 opus47
- ConsoleOAuthFlow.tsx: OAuth 成功后显示模型配置面板,可编辑三种模型名称
- 已登录用户再次选择 Codex 时跳过 OAuth 直接进入模型配置
- Ctrl+R 快捷键清除登录状态并重新 OAuth
- modelOptions.ts: codex provider 支持 CODEX_DEFAULT_*_MODEL 环境变量

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
claude-code-best
2026-04-26 23:38:56 +08:00
parent d091dd8bae
commit 1058b7e643
7 changed files with 330 additions and 52 deletions

View File

@@ -45,7 +45,7 @@ describe('resolveCodexModel', () => {
})
test('maps known haiku model via DEFAULT_MODEL_MAP', () => {
expect(resolveCodexModel('claude-haiku-4-5-20251001')).toBe('gpt-5.4-nano')
expect(resolveCodexModel('claude-haiku-4-5-20251001')).toBe('gpt-5.4-mini')
})
test('maps known opus model via DEFAULT_MODEL_MAP', () => {
@@ -58,7 +58,7 @@ describe('resolveCodexModel', () => {
})
test('maps legacy haiku models', () => {
expect(resolveCodexModel('claude-3-5-haiku-20241022')).toBe('gpt-5.4-nano')
expect(resolveCodexModel('claude-3-5-haiku-20241022')).toBe('gpt-5.4-mini')
})
test('maps legacy opus models', () => {
@@ -67,7 +67,7 @@ describe('resolveCodexModel', () => {
})
test('uses family default for unrecognized haiku model', () => {
expect(resolveCodexModel('claude-haiku-99')).toBe('gpt-5.4-nano')
expect(resolveCodexModel('claude-haiku-99')).toBe('gpt-5.4-mini')
})
test('uses family default for unrecognized sonnet model', () => {

View File

@@ -12,15 +12,16 @@ const DEFAULT_MODEL_MAP: Record<string, string> = {
'claude-opus-4-1-20250805': 'gpt-5.4',
'claude-opus-4-5-20251101': 'gpt-5.4',
'claude-opus-4-6': 'gpt-5.4',
'claude-haiku-4-5-20251001': 'gpt-5.4-nano',
'claude-3-5-haiku-20241022': 'gpt-5.4-nano',
'claude-opus-4-7': 'gpt-5.5',
'claude-haiku-4-5-20251001': 'gpt-5.4-mini',
'claude-3-5-haiku-20241022': 'gpt-5.4-mini',
}
/**
* Default model for each family when an exact match is not in DEFAULT_MODEL_MAP.
*/
const DEFAULT_FAMILY_MAP: Record<string, string> = {
haiku: 'gpt-5.4-nano',
haiku: 'gpt-5.4-mini',
sonnet: 'gpt-5.4-mini',
opus: 'gpt-5.4',
}

View File

@@ -58,6 +58,18 @@ type OAuthStatus =
} // Gemini Generate Content API platform
| { state: 'codex_oauth_waiting'; url: string } // ChatGPT OAuth browser login in progress
| { state: 'codex_oauth_start' } // Trigger ChatGPT OAuth flow
| {
state: 'codex_models'
haikuModel: string
sonnetModel: string
opusModel: string
activeField: 'haiku_model' | 'sonnet_model' | 'opus_model'
codexResult: {
apiKey: string | null
accessToken: string
refreshToken: string
}
} // Codex model name configuration after OAuth success
| { state: 'ready_to_start' } // Flow started, waiting for browser to open
| { state: 'waiting_for_login'; url: string } // Browser opened, waiting for user to login
| { state: 'creating_api_key' } // Got access token, creating API key
@@ -365,31 +377,19 @@ export function ConsoleOAuthFlow({
manualCode: manualCodePromise,
})
const env: Record<string, string | undefined> = {
CODEX_API_KEY: result.apiKey ?? undefined,
CODEX_ACCESS_TOKEN: result.accessToken,
CODEX_REFRESH_TOKEN: result.refreshToken,
CODEX_LOGIN_METHOD: 'chatgpt_subscription',
}
updateSettingsForSource('userSettings', {
modelType: 'codex',
env,
} as any)
for (const [key, value] of Object.entries(env)) {
if (value !== undefined) {
process.env[key] = value
}
}
setOAuthStatus({ state: 'success' })
void sendNotification(
{
message: 'OpenAI Codex (ChatGPT) login successful',
notificationType: 'auth_success',
// Transition to model configuration panel with defaults
setOAuthStatus({
state: 'codex_models',
haikuModel: process.env.CODEX_DEFAULT_HAIKU_MODEL || 'gpt-5.4-mini',
sonnetModel: process.env.CODEX_DEFAULT_SONNET_MODEL || 'gpt-5.4-mini',
opusModel: process.env.CODEX_DEFAULT_OPUS_MODEL || 'gpt-5.5',
activeField: 'haiku_model',
codexResult: {
apiKey: result.apiKey,
accessToken: result.accessToken,
refreshToken: result.refreshToken,
},
terminal,
)
onDone()
})
} catch (err) {
logError(err as Error)
setOAuthStatus({
@@ -714,7 +714,37 @@ function OAuthStatusMessage({
})
} else if (value === 'codex_chatgpt') {
logEvent('tengu_codex_chatgpt_selected', {})
// Skip OAuth if already authenticated — go straight to model config
const settings = getSettings_DEPRECATED()
const hasToken = !!(
process.env.CODEX_ACCESS_TOKEN ||
settings?.env?.CODEX_ACCESS_TOKEN
)
if (hasToken) {
setOAuthStatus({
state: 'codex_models',
haikuModel:
process.env.CODEX_DEFAULT_HAIKU_MODEL ||
settings?.env?.CODEX_DEFAULT_HAIKU_MODEL ||
'gpt-5.4-mini',
sonnetModel:
process.env.CODEX_DEFAULT_SONNET_MODEL ||
settings?.env?.CODEX_DEFAULT_SONNET_MODEL ||
'gpt-5.4-mini',
opusModel:
process.env.CODEX_DEFAULT_OPUS_MODEL ||
settings?.env?.CODEX_DEFAULT_OPUS_MODEL ||
'gpt-5.5',
activeField: 'haiku_model',
codexResult: {
apiKey: process.env.CODEX_API_KEY || null,
accessToken: process.env.CODEX_ACCESS_TOKEN || '',
refreshToken: process.env.CODEX_REFRESH_TOKEN || '',
},
})
} else {
setOAuthStatus({ state: 'codex_oauth_start' })
}
} else if (value === 'gemini_api') {
logEvent('tengu_gemini_api_selected', {})
setOAuthStatus({
@@ -1489,6 +1519,231 @@ function OAuthStatusMessage({
)
}
case 'codex_models': {
type CodexField = 'haiku_model' | 'sonnet_model' | 'opus_model'
const CODEX_FIELDS: CodexField[] = ['haiku_model', 'sonnet_model', 'opus_model']
const cm = oauthStatus as {
state: 'codex_models'
activeField: CodexField
haikuModel: string
sonnetModel: string
opusModel: string
codexResult: { apiKey: string | null; accessToken: string; refreshToken: string }
}
const { activeField, haikuModel, sonnetModel, opusModel, codexResult } = cm
const codexDisplayValues: Record<CodexField, string> = {
haiku_model: haikuModel,
sonnet_model: sonnetModel,
opus_model: opusModel,
}
const [codexModelInput, setCodexModelInput] = useState(
() => codexDisplayValues[activeField],
)
const [codexModelCursor, setCodexModelCursor] = useState(
() => codexDisplayValues[activeField].length,
)
const buildCodexModelState = useCallback(
(field: CodexField, value: string, newActive?: CodexField) => {
const s = {
state: 'codex_models' as const,
activeField: newActive ?? activeField,
haikuModel,
sonnetModel,
opusModel,
codexResult,
}
switch (field) {
case 'haiku_model':
return { ...s, haikuModel: value }
case 'sonnet_model':
return { ...s, sonnetModel: value }
case 'opus_model':
return { ...s, opusModel: value }
}
},
[activeField, haikuModel, sonnetModel, opusModel, codexResult],
)
const doCodexModelSave = useCallback(() => {
const finalVals = { ...codexDisplayValues, [activeField]: codexModelInput }
const env: Record<string, string | undefined> = {
CODEX_API_KEY: codexResult.apiKey ?? undefined,
CODEX_ACCESS_TOKEN: codexResult.accessToken,
CODEX_REFRESH_TOKEN: codexResult.refreshToken,
CODEX_LOGIN_METHOD: 'chatgpt_subscription',
CODEX_DEFAULT_HAIKU_MODEL: finalVals.haiku_model,
CODEX_DEFAULT_SONNET_MODEL: finalVals.sonnet_model,
CODEX_DEFAULT_OPUS_MODEL: finalVals.opus_model,
}
const { error } = updateSettingsForSource('userSettings', {
modelType: 'codex' as any,
env,
} as any)
if (error) {
setOAuthStatus({
state: 'error',
message: 'Failed to save settings. Please try again.',
toRetry: {
state: 'codex_models',
haikuModel: finalVals.haiku_model,
sonnetModel: finalVals.sonnet_model,
opusModel: finalVals.opus_model,
activeField: 'haiku_model',
codexResult,
},
})
} else {
for (const [k, v] of Object.entries(env)) {
if (v !== undefined) {
process.env[k] = v
}
}
setOAuthStatus({ state: 'success' })
void onDone()
}
}, [activeField, codexModelInput, codexDisplayValues, codexResult, setOAuthStatus, onDone])
const handleCodexModelEnter = useCallback(() => {
const idx = CODEX_FIELDS.indexOf(activeField)
if (idx === CODEX_FIELDS.length - 1) {
setOAuthStatus(buildCodexModelState(activeField, codexModelInput))
doCodexModelSave()
} else {
const next = CODEX_FIELDS[idx + 1]!
setOAuthStatus(buildCodexModelState(activeField, codexModelInput, next))
setCodexModelInput(codexDisplayValues[next] ?? '')
setCodexModelCursor((codexDisplayValues[next] ?? '').length)
}
}, [
activeField,
codexModelInput,
buildCodexModelState,
doCodexModelSave,
codexDisplayValues,
setOAuthStatus,
])
useKeybinding(
'tabs:next',
() => {
const idx = CODEX_FIELDS.indexOf(activeField)
if (idx < CODEX_FIELDS.length - 1) {
setOAuthStatus(
buildCodexModelState(activeField, codexModelInput, CODEX_FIELDS[idx + 1]),
)
setCodexModelInput(codexDisplayValues[CODEX_FIELDS[idx + 1]!] ?? '')
setCodexModelCursor((codexDisplayValues[CODEX_FIELDS[idx + 1]!] ?? '').length)
}
},
{ context: 'FormField' },
)
useKeybinding(
'tabs:previous',
() => {
const idx = CODEX_FIELDS.indexOf(activeField)
if (idx > 0) {
setOAuthStatus(
buildCodexModelState(activeField, codexModelInput, CODEX_FIELDS[idx - 1]),
)
setCodexModelInput(codexDisplayValues[CODEX_FIELDS[idx - 1]!] ?? '')
setCodexModelCursor((codexDisplayValues[CODEX_FIELDS[idx - 1]!] ?? '').length)
}
},
{ context: 'FormField' },
)
useKeybinding(
'confirm:no',
() => {
setOAuthStatus({ state: 'idle' })
},
{ context: 'Confirmation' },
)
// Ctrl+D: clear codex login state and re-login
useKeybinding(
'oauth:codex-relogin',
() => {
// Clear codex credentials from process.env
delete process.env.CODEX_ACCESS_TOKEN
delete process.env.CODEX_REFRESH_TOKEN
delete process.env.CODEX_API_KEY
delete process.env.CODEX_LOGIN_METHOD
delete process.env.CODEX_DEFAULT_HAIKU_MODEL
delete process.env.CODEX_DEFAULT_SONNET_MODEL
delete process.env.CODEX_DEFAULT_OPUS_MODEL
// Clear from settings.json
updateSettingsForSource('userSettings', {
modelType: undefined,
env: {
CODEX_ACCESS_TOKEN: undefined,
CODEX_REFRESH_TOKEN: undefined,
CODEX_API_KEY: undefined,
CODEX_LOGIN_METHOD: undefined,
CODEX_DEFAULT_HAIKU_MODEL: undefined,
CODEX_DEFAULT_SONNET_MODEL: undefined,
CODEX_DEFAULT_OPUS_MODEL: undefined,
},
} as any)
// Restart OAuth flow
setOAuthStatus({ state: 'codex_oauth_start' })
},
{ context: 'FormField' },
)
const codexModelColumns = useTerminalSize().columns - 20
const renderCodexModelRow = (
field: CodexField,
label: string,
) => {
const active = activeField === field
const val = codexDisplayValues[field]
return (
<Box>
<Text
backgroundColor={active ? 'suggestion' : undefined}
color={active ? 'inverseText' : undefined}
>
{` ${label} `}
</Text>
<Text> </Text>
{active ? (
<TextInput
value={codexModelInput}
onChange={setCodexModelInput}
onSubmit={handleCodexModelEnter}
cursorOffset={codexModelCursor}
onChangeCursorOffset={setCodexModelCursor}
columns={codexModelColumns}
focus={true}
/>
) : val ? (
<Text color="success">{val}</Text>
) : null}
</Box>
)
}
return (
<Box flexDirection="column" gap={1}>
<Text bold>Codex Model Configuration</Text>
<Text dimColor>
ChatGPT login successful. Configure model names (press Enter on last field to save).
</Text>
<Box flexDirection="column" gap={1}>
{renderCodexModelRow('haiku_model', 'Haiku ')}
{renderCodexModelRow('sonnet_model', 'Sonnet ')}
{renderCodexModelRow('opus_model', 'Opus ')}
</Box>
<Text dimColor>
/Tab to switch · Enter on last field to save · Ctrl+R to re-login · Esc to go back
</Text>
</Box>
)
}
case 'platform_setup':
return (
<Box flexDirection="column" gap={1} marginTop={1}>

View File

@@ -156,6 +156,8 @@ export const DEFAULT_BINDINGS: KeybindingBlock[] = [
'shift+tab': 'tabs:previous',
up: 'tabs:previous',
down: 'tabs:next',
// Re-login: clear codex credentials and restart OAuth
'ctrl+r': 'oauth:codex-relogin',
},
},
{

View File

@@ -109,6 +109,8 @@ export const KEYBINDING_ACTIONS = [
// Tabs navigation actions
'tabs:next',
'tabs:previous',
// OAuth re-login action (codex model config panel)
'oauth:codex-relogin',
// Transcript viewer actions
'transcript:toggleShowAll',
'transcript:exit',

View File

@@ -13,7 +13,7 @@ export const CLAUDE_3_7_SONNET_CONFIG = {
foundry: 'claude-3-7-sonnet',
openai: 'claude-3-7-sonnet-20250219',
gemini: 'claude-3-7-sonnet-20250219',
codex: 'claude-3-7-sonnet-20250219',
codex: 'gpt-5.4-mini',
grok: 'claude-3-7-sonnet-20250219',
} as const satisfies ModelConfig
@@ -24,7 +24,7 @@ export const CLAUDE_3_5_V2_SONNET_CONFIG = {
foundry: 'claude-3-5-sonnet',
openai: 'claude-3-5-sonnet-20241022',
gemini: 'claude-3-5-sonnet-20241022',
codex: 'claude-3-5-sonnet-20241022',
codex: 'gpt-5.4-mini',
grok: 'claude-3-5-sonnet-20241022',
} as const satisfies ModelConfig
@@ -35,7 +35,7 @@ export const CLAUDE_3_5_HAIKU_CONFIG = {
foundry: 'claude-3-5-haiku',
openai: 'claude-3-5-haiku-20241022',
gemini: 'claude-3-5-haiku-20241022',
codex: 'claude-3-5-haiku-20241022',
codex: 'gpt-5.4-mini',
grok: 'claude-3-5-haiku-20241022',
} as const satisfies ModelConfig
@@ -46,7 +46,7 @@ export const CLAUDE_HAIKU_4_5_CONFIG = {
foundry: 'claude-haiku-4-5',
openai: 'claude-haiku-4-5-20251001',
gemini: 'claude-haiku-4-5-20251001',
codex: 'claude-haiku-4-5-20251001',
codex: 'gpt-5.4-mini',
grok: 'claude-haiku-4-5-20251001',
} as const satisfies ModelConfig
@@ -57,7 +57,7 @@ export const CLAUDE_SONNET_4_CONFIG = {
foundry: 'claude-sonnet-4',
openai: 'claude-sonnet-4-20250514',
gemini: 'claude-sonnet-4-20250514',
codex: 'claude-sonnet-4-20250514',
codex: 'gpt-5.4-mini',
grok: 'claude-sonnet-4-20250514',
} as const satisfies ModelConfig
@@ -68,7 +68,7 @@ export const CLAUDE_SONNET_4_5_CONFIG = {
foundry: 'claude-sonnet-4-5',
openai: 'claude-sonnet-4-5-20250929',
gemini: 'claude-sonnet-4-5-20250929',
codex: 'claude-sonnet-4-5-20250929',
codex: 'gpt-5.4-mini',
grok: 'claude-sonnet-4-5-20250929',
} as const satisfies ModelConfig
@@ -79,7 +79,7 @@ export const CLAUDE_OPUS_4_CONFIG = {
foundry: 'claude-opus-4',
openai: 'claude-opus-4-20250514',
gemini: 'claude-opus-4-20250514',
codex: 'claude-opus-4-20250514',
codex: 'gpt-5.4',
grok: 'claude-opus-4-20250514',
} as const satisfies ModelConfig
@@ -90,7 +90,7 @@ export const CLAUDE_OPUS_4_1_CONFIG = {
foundry: 'claude-opus-4-1',
openai: 'claude-opus-4-1-20250805',
gemini: 'claude-opus-4-1-20250805',
codex: 'claude-opus-4-1-20250805',
codex: 'gpt-5.4',
grok: 'claude-opus-4-1-20250805',
} as const satisfies ModelConfig
@@ -101,7 +101,7 @@ export const CLAUDE_OPUS_4_5_CONFIG = {
foundry: 'claude-opus-4-5',
openai: 'claude-opus-4-5-20251101',
gemini: 'claude-opus-4-5-20251101',
codex: 'claude-opus-4-5-20251101',
codex: 'gpt-5.4',
grok: 'claude-opus-4-5-20251101',
} as const satisfies ModelConfig
@@ -112,7 +112,7 @@ export const CLAUDE_OPUS_4_6_CONFIG = {
foundry: 'claude-opus-4-6',
openai: 'claude-opus-4-6',
gemini: 'claude-opus-4-6',
codex: 'claude-opus-4-6',
codex: 'gpt-5.4',
grok: 'claude-opus-4-6',
} as const satisfies ModelConfig
@@ -123,7 +123,7 @@ export const CLAUDE_OPUS_4_7_CONFIG = {
foundry: 'claude-opus-4-7',
openai: 'claude-opus-4-7',
gemini: 'claude-opus-4-7',
codex: 'claude-opus-4-7',
codex: 'gpt-5.5',
grok: 'claude-opus-4-7',
} as const satisfies ModelConfig
@@ -134,7 +134,7 @@ export const CLAUDE_SONNET_4_6_CONFIG = {
foundry: 'claude-sonnet-4-6',
openai: 'claude-sonnet-4-6',
gemini: 'claude-sonnet-4-6',
codex: 'claude-sonnet-4-6',
codex: 'gpt-5.4-mini',
grok: 'claude-sonnet-4-6',
} as const satisfies ModelConfig

View File

@@ -83,6 +83,8 @@ function getCustomSonnetOption(): ModelOption | undefined {
? process.env.OPENAI_DEFAULT_SONNET_MODEL
: provider === 'gemini'
? process.env.GEMINI_DEFAULT_SONNET_MODEL
: provider === 'codex'
? process.env.CODEX_DEFAULT_SONNET_MODEL
: process.env.ANTHROPIC_DEFAULT_SONNET_MODEL
// When a 3P user has a custom sonnet model string, show it directly
if (is3P && customSonnetModel) {
@@ -93,12 +95,16 @@ function getCustomSonnetOption(): ModelOption | undefined {
? process.env.OPENAI_DEFAULT_SONNET_MODEL_NAME
: provider === 'gemini'
? process.env.GEMINI_DEFAULT_SONNET_MODEL_NAME
: provider === 'codex'
? process.env.CODEX_DEFAULT_SONNET_MODEL_NAME
: process.env.ANTHROPIC_DEFAULT_SONNET_MODEL_NAME
const descEnv =
provider === 'openai'
? process.env.OPENAI_DEFAULT_SONNET_MODEL_DESCRIPTION
: provider === 'gemini'
? process.env.GEMINI_DEFAULT_SONNET_MODEL_DESCRIPTION
: provider === 'codex'
? process.env.CODEX_DEFAULT_SONNET_MODEL_DESCRIPTION
: process.env.ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION
return {
value: 'sonnet',
@@ -132,6 +138,8 @@ function getCustomOpusOption(): ModelOption | undefined {
? process.env.OPENAI_DEFAULT_OPUS_MODEL
: provider === 'gemini'
? process.env.GEMINI_DEFAULT_OPUS_MODEL
: provider === 'codex'
? process.env.CODEX_DEFAULT_OPUS_MODEL
: process.env.ANTHROPIC_DEFAULT_OPUS_MODEL
// When a 3P user has a custom opus model string, show it directly
if (is3P && customOpusModel) {
@@ -142,12 +150,16 @@ function getCustomOpusOption(): ModelOption | undefined {
? process.env.OPENAI_DEFAULT_OPUS_MODEL_NAME
: provider === 'gemini'
? process.env.GEMINI_DEFAULT_OPUS_MODEL_NAME
: provider === 'codex'
? process.env.CODEX_DEFAULT_OPUS_MODEL_NAME
: process.env.ANTHROPIC_DEFAULT_OPUS_MODEL_NAME
const descEnv =
provider === 'openai'
? process.env.OPENAI_DEFAULT_OPUS_MODEL_DESCRIPTION
: provider === 'gemini'
? process.env.GEMINI_DEFAULT_OPUS_MODEL_DESCRIPTION
: provider === 'codex'
? process.env.CODEX_DEFAULT_OPUS_MODEL_DESCRIPTION
: process.env.ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION
return {
value: 'opus',
@@ -232,6 +244,8 @@ function getCustomHaikuOption(): ModelOption | undefined {
? process.env.OPENAI_DEFAULT_HAIKU_MODEL
: provider === 'gemini'
? process.env.GEMINI_DEFAULT_HAIKU_MODEL
: provider === 'codex'
? process.env.CODEX_DEFAULT_HAIKU_MODEL
: process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL
// When a 3P user has a custom haiku model string, show it directly
if (is3P && customHaikuModel) {
@@ -241,12 +255,16 @@ function getCustomHaikuOption(): ModelOption | undefined {
? process.env.OPENAI_DEFAULT_HAIKU_MODEL_NAME
: provider === 'gemini'
? process.env.GEMINI_DEFAULT_HAIKU_MODEL_NAME
: provider === 'codex'
? process.env.CODEX_DEFAULT_HAIKU_MODEL_NAME
: process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME
const descEnv =
provider === 'openai'
? process.env.OPENAI_DEFAULT_HAIKU_MODEL_DESCRIPTION
: provider === 'gemini'
? process.env.GEMINI_DEFAULT_HAIKU_MODEL_DESCRIPTION
: provider === 'codex'
? process.env.CODEX_DEFAULT_HAIKU_MODEL_DESCRIPTION
: process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION
return {
value: 'haiku',