From 7b02695b15be3cc23c98f0efe93a8480964837bb Mon Sep 17 00:00:00 2001 From: Kevin Codex Date: Tue, 5 May 2026 18:13:33 +0800 Subject: [PATCH] Feat/codex default provider (#1014) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: add .openclaude/ to gitignore The .openclaude/ directory contains auto-generated project-local files (wiki pages, convention cache, local settings) that should not be committed to the repository. Co-Authored-By: OpenClaude * feat: make Codex + GPT 5.5 the default provider and model Changes the default provider to Codex and default model to GPT 5.5: - package.json: dev script now uses provider-launch.ts codex - providerRecommendation.ts: getGoalDefaultOpenAIModel returns gpt-5.5 for coding and balanced goals (was gpt-4o) - providerConfig.ts: fallback model changed from gpt-4o to codexplan (resolves to gpt-5.5) - ProviderManager.tsx: Codex OAuth option now shows green "★ Recommended" badge in the provider picker Co-Authored-By: OpenClaude * fix: replace Box with nested Text in Codex label Ink's component cannot contain . The label is rendered inside a parent, so use nested elements instead. Co-Authored-By: OpenClaude * fix: default to Codex when no provider profile is saved When no persisted provider profile exists (fresh install / first run), buildStartupEnvFromProfile now injects Codex + GPT 5.5 env vars instead of returning process.env unchanged. Falls back gracefully — if Codex credentials are available (OAuth or existing), uses those; otherwise injects base URL and model defaults so the provider picker shows GPT 5.5 as the default. This closes the gap where node dist/cli.mjs (production start) would default to firstParty (Anthropic) when no profile or env vars were set. Co-Authored-By: OpenClaude * chore: resolve stash conflict markers from accidental stash pop Cleans up merge conflict artifacts left by a git stash pop from an unrelated branch (chore/add-atomic-chat-partner). Kept upstream (current branch) version in all cases. Co-Authored-By: OpenClaude * fix: restore memoize import and cleanup stash artifacts Restores the memoize import dropped during conflict resolution in modelSupportOverrides.ts. Removes duplicate originalEnv declaration and redundant delete statements in providerValidation.test.ts. Co-Authored-By: OpenClaude * revert change in package.json * fix broken test * fix color --------- Co-authored-by: OpenClaude --- .gitignore | 1 + src/components/ProviderManager.tsx | 7 +++++- src/components/StartupScreen.test.ts | 7 ++++++ src/components/StartupScreen.ts | 2 +- src/services/api/providerConfig.ts | 2 +- src/utils/providerProfile.test.ts | 10 ++++---- src/utils/providerProfile.ts | 21 +++++++++++++++- src/utils/providerProfiles.test.ts | 32 ++++++++++++++++++++++++ src/utils/providerRecommendation.test.ts | 4 +-- src/utils/providerRecommendation.ts | 4 +-- src/utils/providerValidation.ts | 14 ++++++++++- 11 files changed, 90 insertions(+), 14 deletions(-) diff --git a/.gitignore b/.gitignore index d941fa2eb..21811eae6 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ web/*.tsbuildinfo .env.* !.env.example .openclaude-profile.json +.openclaude/ reports/ GEMINI.md CLAUDE.md diff --git a/src/components/ProviderManager.tsx b/src/components/ProviderManager.tsx index d88a1cb3a..9b05632c8 100644 --- a/src/components/ProviderManager.tsx +++ b/src/components/ProviderManager.tsx @@ -1458,7 +1458,12 @@ export function ProviderManager({ mode, onDone }: Props): React.ReactNode { if (canUseCodexOAuth) { options.splice(6, 0, { value: 'codex-oauth', - label: 'Codex OAuth', + label: ( + + Codex OAuth + ★ Recommended + + ), description: 'Sign in with ChatGPT in your browser and store Codex credentials securely', }) diff --git a/src/components/StartupScreen.test.ts b/src/components/StartupScreen.test.ts index 11ba4d32e..3c4aef3b9 100644 --- a/src/components/StartupScreen.test.ts +++ b/src/components/StartupScreen.test.ts @@ -38,6 +38,11 @@ const ENV_KEYS = [ 'CLAUDE_MODEL', 'NVIDIA_NIM', 'MINIMAX_API_KEY', + 'XAI_API_KEY', + 'ANTHROPIC_DEFAULT_OPUS_MODEL', + 'ANTHROPIC_DEFAULT_SONNET_MODEL', + 'ANTHROPIC_DEFAULT_HAIKU_MODEL', + 'ANTHROPIC_BASE_URL', ] const originalEnv: Record = {} @@ -308,12 +313,14 @@ describe('detectProvider — modelOverride from --model flag', () => { }) test('undefined modelOverride preserves default behavior', () => { + process.env.ANTHROPIC_MODEL = 'claude-sonnet-4-6' const result = detectProvider(undefined) expect(result.name).toBe('Anthropic') expect(result.model).toContain('sonnet') }) test('no argument preserves default behavior', () => { + process.env.ANTHROPIC_MODEL = 'claude-sonnet-4-6' const result = detectProvider() expect(result.name).toBe('Anthropic') expect(result.model).toContain('sonnet') diff --git a/src/components/StartupScreen.ts b/src/components/StartupScreen.ts index acf280927..71692b2a0 100644 --- a/src/components/StartupScreen.ts +++ b/src/components/StartupScreen.ts @@ -171,7 +171,7 @@ export function detectProvider(modelOverride?: string): { name: string; model: s // Default: Anthropic - check settings.model first, then env vars const settings = getSettings_DEPRECATED() || {} - const modelSetting = modelOverride || settings.model || process.env.ANTHROPIC_MODEL || process.env.CLAUDE_MODEL || 'claude-sonnet-4-6' + const modelSetting = modelOverride || process.env.ANTHROPIC_MODEL || process.env.CLAUDE_MODEL || settings.model || 'claude-sonnet-4-6' const resolvedModel = parseUserSpecifiedModel(modelSetting) const baseUrl = process.env.ANTHROPIC_BASE_URL ?? 'https://api.anthropic.com' const isLocal = isLocalProviderUrl(baseUrl) diff --git a/src/services/api/providerConfig.ts b/src/services/api/providerConfig.ts index 6b4f7b825..694607c78 100644 --- a/src/services/api/providerConfig.ts +++ b/src/services/api/providerConfig.ts @@ -541,7 +541,7 @@ export function resolveProviderRequest(options?: { : process.env.OPENAI_MODEL?.trim()) || options?.fallbackModel?.trim() || (isGeminiMode ? DEFAULT_GEMINI_MODEL : undefined) || - (isGithubMode ? 'github:copilot' : 'gpt-4o') + (isGithubMode ? 'github:copilot' : 'codexplan') const descriptor = parseModelDescriptor(requestedModel) const explicitBaseUrl = asEnvUrl(options?.baseUrl) diff --git a/src/utils/providerProfile.test.ts b/src/utils/providerProfile.test.ts index 62e0cb564..fd885122b 100644 --- a/src/utils/providerProfile.test.ts +++ b/src/utils/providerProfile.test.ts @@ -209,7 +209,7 @@ test('openai launch ignores codex shell transport hints', async () => { }) assert.equal(env.OPENAI_BASE_URL, 'https://api.openai.com/v1') - assert.equal(env.OPENAI_MODEL, 'gpt-4o') + assert.equal(env.OPENAI_MODEL, 'gpt-5.5') assert.equal(env.OPENAI_API_KEY, 'sk-live') }) @@ -228,7 +228,7 @@ test('openai launch ignores codex persisted transport hints', async () => { }) assert.equal(env.OPENAI_BASE_URL, 'https://api.openai.com/v1') - assert.equal(env.OPENAI_MODEL, 'gpt-4o') + assert.equal(env.OPENAI_MODEL, 'gpt-5.5') assert.equal(env.OPENAI_API_KEY, 'sk-live') }) @@ -1220,7 +1220,7 @@ test('openai profiles ignore codex shell transport hints', () => { assert.deepEqual(env, { OPENAI_BASE_URL: 'https://api.openai.com/v1', - OPENAI_MODEL: 'gpt-4o', + OPENAI_MODEL: 'gpt-5.5', OPENAI_API_KEY: 'sk-live', }) }) @@ -1269,7 +1269,7 @@ test('openai profiles ignore poisoned shell model and base url values', () => { assert.deepEqual(env, { OPENAI_BASE_URL: 'https://api.openai.com/v1', - OPENAI_MODEL: 'gpt-4o', + OPENAI_MODEL: 'gpt-5.5', OPENAI_API_KEY: 'sk-live', }) }) @@ -1302,7 +1302,7 @@ test('startup env ignores poisoned persisted openai model and base url', async ( assert.equal(env.CLAUDE_CODE_USE_OPENAI, '1') assert.equal(env.OPENAI_API_KEY, 'sk-live') - assert.equal(env.OPENAI_MODEL, 'gpt-4o') + assert.equal(env.OPENAI_MODEL, 'gpt-5.5') assert.equal(env.OPENAI_BASE_URL, 'https://api.openai.com/v1') }) diff --git a/src/utils/providerProfile.ts b/src/utils/providerProfile.ts index 882acd95e..85460922d 100644 --- a/src/utils/providerProfile.ts +++ b/src/utils/providerProfile.ts @@ -1360,7 +1360,26 @@ export async function buildStartupEnvFromProfile(options?: { } if (!persisted) { - return processEnv + // No saved profile — default to Codex OAuth / GPT 5.5. + // If Codex credentials are available (OAuth or existing), use Codex. + // Otherwise inject the Codex env defaults so the provider picker + // shows GPT 5.5 as the default model when the user lands on it. + const codexEnv = buildCodexProfileEnv({}) + if (codexEnv) { + return buildCompatibilityProcessEnv({ + processEnv, + compatibilityMode: 'openai', + profileEnv: codexEnv, + }) + } + return buildCompatibilityProcessEnv({ + processEnv, + compatibilityMode: 'openai', + profileEnv: { + OPENAI_BASE_URL: DEFAULT_CODEX_BASE_URL, + OPENAI_MODEL: 'codexplan', + }, + }) } return buildLaunchEnv({ diff --git a/src/utils/providerProfiles.test.ts b/src/utils/providerProfiles.test.ts index e8f2d7a81..4fd2c9b37 100644 --- a/src/utils/providerProfiles.test.ts +++ b/src/utils/providerProfiles.test.ts @@ -656,6 +656,22 @@ describe('applyActiveProviderProfileFromConfig', () => { expect(process.env.OPENAI_MODEL).toBe('qwen2.5:3b') }) + beforeEach(() => { + delete process.env.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED + delete process.env.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED_ID + delete process.env.CLAUDE_CODE_USE_OPENAI + delete process.env.CLAUDE_CODE_USE_GEMINI + delete process.env.CLAUDE_CODE_USE_MISTRAL + delete process.env.CLAUDE_CODE_USE_GITHUB + delete process.env.CLAUDE_CODE_USE_BEDROCK + delete process.env.CLAUDE_CODE_USE_VERTEX + delete process.env.CLAUDE_CODE_USE_FOUNDRY + delete process.env.OPENAI_BASE_URL + delete process.env.OPENAI_API_BASE + delete process.env.OPENAI_MODEL + delete process.env.OPENAI_API_FORMAT + }) + test('applies active profile when a bare CLAUDE_CODE_USE_OPENAI flag is stale (no BASE_URL/MODEL)', async () => { // Regression: a leftover `CLAUDE_CODE_USE_OPENAI=1` in the shell with no // paired OPENAI_BASE_URL / OPENAI_MODEL is not a real explicit selection @@ -1434,6 +1450,22 @@ describe('setActiveProviderProfile', () => { }) describe('deleteProviderProfile', () => { + beforeEach(() => { + delete process.env.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED + delete process.env.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED_ID + delete process.env.CLAUDE_CODE_USE_OPENAI + delete process.env.CLAUDE_CODE_USE_GEMINI + delete process.env.CLAUDE_CODE_USE_MISTRAL + delete process.env.CLAUDE_CODE_USE_GITHUB + delete process.env.CLAUDE_CODE_USE_BEDROCK + delete process.env.CLAUDE_CODE_USE_VERTEX + delete process.env.CLAUDE_CODE_USE_FOUNDRY + delete process.env.OPENAI_BASE_URL + delete process.env.OPENAI_API_BASE + delete process.env.OPENAI_MODEL + delete process.env.OPENAI_API_FORMAT + }) + test('deleting final profile clears provider env when active profile applied it', async () => { const { applyProviderProfileToProcessEnv, diff --git a/src/utils/providerRecommendation.test.ts b/src/utils/providerRecommendation.test.ts index f85777bfa..442c5cd0d 100644 --- a/src/utils/providerRecommendation.test.ts +++ b/src/utils/providerRecommendation.test.ts @@ -189,6 +189,6 @@ test('coding goal recognizes codestral and devstral families', () => { test('goal defaults choose sensible openai models', () => { assert.equal(getGoalDefaultOpenAIModel('latency'), 'gpt-4o-mini') - assert.equal(getGoalDefaultOpenAIModel('balanced'), 'gpt-4o') - assert.equal(getGoalDefaultOpenAIModel('coding'), 'gpt-4o') + assert.equal(getGoalDefaultOpenAIModel('balanced'), 'gpt-5.5') + assert.equal(getGoalDefaultOpenAIModel('coding'), 'gpt-5.5') }) diff --git a/src/utils/providerRecommendation.ts b/src/utils/providerRecommendation.ts index 8bd1e2cd6..1c82ccba7 100644 --- a/src/utils/providerRecommendation.ts +++ b/src/utils/providerRecommendation.ts @@ -217,10 +217,10 @@ export function getGoalDefaultOpenAIModel(goal: RecommendationGoal): string { case 'latency': return 'gpt-4o-mini' case 'coding': - return 'gpt-4o' + return 'gpt-5.5' case 'balanced': default: - return 'gpt-4o' + return 'gpt-5.5' } } diff --git a/src/utils/providerValidation.ts b/src/utils/providerValidation.ts index b760168d7..831cfda83 100644 --- a/src/utils/providerValidation.ts +++ b/src/utils/providerValidation.ts @@ -14,6 +14,7 @@ import { getRouteCredentialEnvVars, getRouteCredentialValue, getRouteDescriptor, + getRouteDefaultModel, resolveActiveRouteIdFromEnv, resolveRouteIdFromBaseUrl, } from '../integrations/routeMetadata.js' @@ -22,6 +23,7 @@ import { isLocalProviderUrl, resolveCodexApiCredentials, resolveProviderRequest, + shouldUseCodexTransport, } from '../services/api/providerConfig.js' import { getGlobalClaudeFile } from './env.js' import { isBareMode } from './envUtils.js' @@ -203,6 +205,7 @@ function getRuntimeValidationTarget( const request = resolveProviderRequest({ model: env.OPENAI_MODEL, baseUrl: env.OPENAI_BASE_URL, + fallbackModel: getRouteDefaultModel('openai'), }) const baseUrlMatchedTarget = validationTargets.find(target => { @@ -391,6 +394,7 @@ export async function getProviderValidationError( const request = resolveProviderRequest({ model: env.OPENAI_MODEL, baseUrl: env.OPENAI_BASE_URL, + fallbackModel: getRouteDefaultModel('openai'), }) const genericRouteValidation = getGenericRouteCredentialValidationError( env, @@ -399,7 +403,14 @@ export async function getProviderValidationError( // Codex auth depends on transport resolution plus local auth/account state, // so it intentionally stays procedural instead of moving into descriptors. - if (request.transport === 'codex_responses') { + const explicitBaseUrl = + env.OPENAI_BASE_URL?.trim() || env.OPENAI_API_BASE?.trim() + const hasExplicitCodexIntent = + (env.OPENAI_MODEL?.trim() + ? shouldUseCodexTransport(env.OPENAI_MODEL, explicitBaseUrl) + : false) || Boolean(explicitBaseUrl && shouldUseCodexTransport('', explicitBaseUrl)) + + if (hasExplicitCodexIntent) { const credentials = resolveCodexApiCredentials(env) if (!credentials.apiKey) { const oauthHint = isBareMode() ? '' : ', choose Codex OAuth in /provider' @@ -407,6 +418,7 @@ export async function getProviderValidationError( ? `${oauthHint} or put auth.json at ${credentials.authPath}` : oauthHint const safeModel = + redactSecretValueForDisplay(env.OPENAI_MODEL, secretSource) ?? redactSecretValueForDisplay(request.requestedModel, secretSource) ?? 'the requested model' return `Codex auth is required for ${safeModel}. Set CODEX_API_KEY${authHint}.`