refactor(runtime): remove legacy codex lanes

This commit is contained in:
777genius 2026-04-19 22:22:13 +03:00
parent e90bdc5b7f
commit 1f7dd2100f
33 changed files with 816 additions and 1726 deletions

View file

@ -18,6 +18,32 @@ Record the chosen direction for improving Codex integration in the multimodel ru
- Assessment: `🎯 9 🛡️ 9 🧠 7`
- Estimated first serious wave: `2200-4500` lines across `agent_teams_orchestrator`, `claude_team`, and `plugin-kit-ai`
## Current Status As Of 2026-04-19
The staged cutover is now complete through Phase 4.
- Phase 0 - implementation-complete and evidence-backed
- Phase 1 - rollout-state preparation complete
- Phase 2 - limited internal unlock completed
- Phase 3 - native-first default switch completed
- Phase 4 - legacy Codex lane removal completed
Current product truth:
- Codex now runs only through the `codex-native` lane in normal product flows
- legacy `adapter` and `api` Codex runtime lanes have been removed from active runtime selection and launch paths
- runtime status now exposes a single native Codex backend option
- stored legacy Codex backend values normalize forward to `codex-native`
- the remaining supported credential surface for native Codex is:
- `CODEX_API_KEY`
- `OPENAI_API_KEY`
Repo-visible evidence:
- [codex-native-runtime-phase-0-signoff-evidence.md](./codex-native-runtime-phase-0-signoff-evidence.md)
- [codex-native-runtime-phase-1-signoff-evidence.md](./codex-native-runtime-phase-1-signoff-evidence.md)
- [codex-native-runtime-phase-4-signoff-evidence.md](./codex-native-runtime-phase-4-signoff-evidence.md)
## One-Page Summary
We are **not** doing a one-shot swap from the current Codex backend to `@openai/codex-sdk / codex exec`.

View file

@ -0,0 +1,199 @@
# Codex Native Runtime - Phase 4 Sign-off Evidence
Captured on 2026-04-19.
This file records the repo-visible evidence package for the final native-only Codex cutover.
Related documents:
- [codex-native-runtime-integration-decision.md](./codex-native-runtime-integration-decision.md)
- [codex-native-runtime-phase-1-signoff-evidence.md](./codex-native-runtime-phase-1-signoff-evidence.md)
## Verdict
Phase 4 legacy removal is now complete.
What this proves:
- `codex-native` is now the only Codex runtime lane
- old `adapter` and `api` Codex lanes are no longer launchable through active runtime code paths
- Codex runtime status now exposes a single native option instead of a mixed legacy/native selector
- stored legacy backend values normalize forward to `codex-native`
- UI-facing Codex status, model availability, launch identity, replay parsing, and provisioning all remain truthful after legacy removal
What this does **not** mean:
- plugin execution parity is now guaranteed for multimodel Codex sessions
- broader app-server or interactive-request parity has been added
- Codex runtime failures silently fall back to another hidden Codex implementation
## Command Package
### `agent_teams_orchestrator`
Executed:
```bash
bun test src/services/runtimeBackends/codexBackendResolver.test.ts \
src/services/runtimeBackends/registry.codexNativeStates.test.ts \
src/services/runtimeBackends/registry.agentTeams.test.ts \
src/utils/swarm/spawnUtils.test.ts
```
Observed result:
- `23 pass`
- `0 fail`
Executed:
```bash
bun run signoff:codex-native-phase4
```
Observed result:
- exit code `0`
- four live CLI native-only scenarios verified:
- `ready`
- `authentication-required`
- `runtime-missing`
- `openai-api-key-also-works`
### `claude_team`
Executed:
```bash
pnpm exec vitest run \
test/main/services/runtime/ClaudeMultimodelBridgeService.test.ts \
test/main/services/runtime/providerAwareCliEnv.test.ts \
test/main/services/runtime/ProviderConnectionService.test.ts \
test/main/ipc/configValidation.test.ts \
test/main/services/team/TeamProvisioningService.test.ts \
test/main/services/parsing/CodexNativePhase0Smoke.test.ts \
test/main/services/parsing/SessionParser.test.ts \
test/main/services/team/BoardTaskExactLogStrictParser.test.ts \
test/renderer/components/runtime/providerConnectionUi.test.ts \
test/renderer/components/runtime/ProviderRuntimeBackendSelector.test.ts \
test/renderer/components/runtime/ProviderRuntimeSettingsDialog.test.ts \
test/renderer/components/cli/CliStatusVisibility.test.ts \
test/renderer/components/team/dialogs/ProvisioningProviderStatusList.test.ts \
test/renderer/components/team/dialogs/launchDialogPrefill.test.ts \
test/renderer/utils/memberRuntimeSummary.test.ts \
test/renderer/utils/teamModelAvailability.test.ts
```
Observed result:
- `16` files passed
- `180` tests passed
- `0` failures
## Live Native-only Status Evidence
Runner:
```bash
runtime status --provider codex --json
```
Observed live scenarios:
### Ready
- selected backend: `codex-native`
- resolved backend: `codex-native`
- provider status: `Codex native runtime ready`
- native option:
- `selectable=true`
- `available=true`
- `state=ready`
- `audience=general`
- `statusMessage=Ready`
### Authentication required
- selected backend: `codex-native`
- resolved backend: `null`
- provider status: `Codex native runtime unavailable`
- native option:
- `selectable=false`
- `available=false`
- `state=authentication-required`
- `audience=general`
- `statusMessage=Authentication required`
### Runtime missing
- selected backend: `codex-native`
- resolved backend: `null`
- provider status: `Codex native runtime unavailable`
- native option:
- `selectable=false`
- `available=false`
- `state=runtime-missing`
- `audience=general`
- `statusMessage=Codex CLI not found`
### `OPENAI_API_KEY` also works
- selected backend: `codex-native`
- resolved backend: `codex-native`
- provider status: `Codex native runtime ready`
- explicit proof that the native lane still accepts:
- `CODEX_API_KEY`
- or `OPENAI_API_KEY`
This is the explicit proof that the final cutover no longer depends on a legacy adapter/API runtime seam while still preserving the supported credential surface.
## App-facing Native-only Truth Proof
Covered by green targeted tests:
- `test/main/services/runtime/ClaudeMultimodelBridgeService.test.ts`
- `test/main/services/runtime/providerAwareCliEnv.test.ts`
- `test/main/services/runtime/ProviderConnectionService.test.ts`
- `test/main/ipc/configValidation.test.ts`
- `test/main/services/team/TeamProvisioningService.test.ts`
- `test/main/services/parsing/CodexNativePhase0Smoke.test.ts`
- `test/main/services/parsing/SessionParser.test.ts`
- `test/main/services/team/BoardTaskExactLogStrictParser.test.ts`
- `test/renderer/components/runtime/providerConnectionUi.test.ts`
- `test/renderer/components/runtime/ProviderRuntimeBackendSelector.test.ts`
- `test/renderer/components/runtime/ProviderRuntimeSettingsDialog.test.ts`
- `test/renderer/components/cli/CliStatusVisibility.test.ts`
- `test/renderer/components/team/dialogs/ProvisioningProviderStatusList.test.ts`
- `test/renderer/components/team/dialogs/launchDialogPrefill.test.ts`
- `test/renderer/utils/memberRuntimeSummary.test.ts`
- `test/renderer/utils/teamModelAvailability.test.ts`
These tests prove:
- legacy Codex backend values normalize forward to `codex-native`
- settings and dashboard now describe Codex as native-first, not adapter/API-first
- provider backend identity survives team launch, relaunch, and launch-prefill flows
- parser and exact-log readers stay truthful for native transcript authority rows
- provisioning summaries and member runtime summaries no longer flatten native truth into old Codex copy
- team model availability is keyed to the native runtime path instead of old ChatGPT-subscription heuristics
## Legacy Removal Proof
Covered by green targeted tests and runtime sign-off:
- orchestrator runtime backend resolver now exposes only `codex-native`
- runtime registry now exposes a single Codex backend option
- no active runtime branch launches Codex through:
- `adapter`
- `api`
- old transport-only smoke/signoff scripts tied to legacy Codex runtime were removed
This is the explicit proof that Phase 4 is a real cutover, not just a UI relabeling.
## Sign-off Conclusion
✅ The Phase 4 exit gate is satisfied.
Codex inside the multimodel runtime is now native-only.
There is no longer a product-supported legacy Codex runtime lane to roll back to inside normal UI flows.

View file

@ -5,6 +5,8 @@
import * as path from 'path';
import { migrateProviderBackendId } from '@shared/utils/providerBackend';
import type {
AppConfig,
DisplayConfig,
@ -450,11 +452,13 @@ function validateRuntimeSection(data: unknown): ValidationSuccess<'runtime'> | V
) {
return {
valid: false,
error:
'runtime.providerBackends.codex must be one of: auto, adapter, api, codex-native',
error: 'runtime.providerBackends.codex must be one of: codex-native',
};
}
providerBackends.codex = backendId;
providerBackends.codex = migrateProviderBackendId(
'codex',
backendId
) as RuntimeConfig['providerBackends']['codex'];
continue;
}

View file

@ -91,6 +91,7 @@ import {
PROTECTED_CLI_FLAGS,
} from '@shared/utils/cliArgsParser';
import { createLogger } from '@shared/utils/logger';
import { migrateProviderBackendId } from '@shared/utils/providerBackend';
import { isRateLimitMessage } from '@shared/utils/rateLimitDetector';
import {
buildStandaloneSlashCommandMeta,
@ -1434,6 +1435,17 @@ async function handleLaunchTeam(
const membersMeta = await membersStore.getMeta(tn);
const members = membersMeta?.members ?? [];
const resolvedProviderId =
payload.providerId === 'codex'
? 'codex'
: payload.providerId === 'gemini'
? 'gemini'
: meta?.providerId === 'codex'
? 'codex'
: meta?.providerId === 'gemini'
? 'gemini'
: 'anthropic';
const createRequest: TeamCreateRequest = {
teamName: tn,
displayName: meta?.displayName,
@ -1441,20 +1453,11 @@ async function handleLaunchTeam(
color: meta?.color,
cwd,
prompt: typeof payload.prompt === 'string' ? payload.prompt.trim() || undefined : undefined,
providerId:
payload.providerId === 'codex'
? 'codex'
: payload.providerId === 'gemini'
? 'gemini'
: meta?.providerId === 'codex'
? 'codex'
: meta?.providerId === 'gemini'
? 'gemini'
: 'anthropic',
providerBackendId:
providerBackendValidation.value ??
meta?.providerBackendId ??
membersMeta?.providerBackendId,
providerId: resolvedProviderId,
providerBackendId: migrateProviderBackendId(
resolvedProviderId,
providerBackendValidation.value ?? meta?.providerBackendId ?? membersMeta?.providerBackendId
),
model: typeof payload.model === 'string' ? payload.model.trim() || undefined : undefined,
effort: isValidEffort(payload.effort) ? payload.effort : undefined,
limitContext: typeof payload.limitContext === 'boolean' ? payload.limitContext : undefined,
@ -3926,6 +3929,8 @@ async function handleGetSavedRequest(
const membersMeta = await membersStore.getMeta(tn);
const members = membersMeta?.members ?? [];
const resolvedProviderId = meta.providerId ?? 'anthropic';
return {
success: true,
data: {
@ -3935,8 +3940,11 @@ async function handleGetSavedRequest(
color: meta.color,
cwd: meta.cwd,
prompt: meta.prompt,
providerId: meta.providerId ?? 'anthropic',
providerBackendId: meta.providerBackendId ?? membersMeta?.providerBackendId,
providerId: resolvedProviderId,
providerBackendId: migrateProviderBackendId(
resolvedProviderId,
meta.providerBackendId ?? membersMeta?.providerBackendId
),
model: meta.model,
effort: meta.effort as TeamCreateRequest['effort'],
skipPermissions: meta.skipPermissions,

View file

@ -11,6 +11,7 @@
import { getClaudeBasePath, setClaudeBasePathOverride } from '@main/utils/pathDecoder';
import { validateRegexPattern } from '@main/utils/regexValidation';
import { migrateProviderBackendId } from '@shared/utils/providerBackend';
import { createLogger } from '@shared/utils/logger';
import * as fs from 'fs';
import * as fsp from 'fs/promises';
@ -226,7 +227,7 @@ export interface GeneralConfig {
export interface RuntimeConfig {
providerBackends: {
gemini: 'auto' | 'api' | 'cli-sdk';
codex: 'auto' | 'adapter' | 'api' | 'codex-native';
codex: 'codex-native';
};
}
@ -575,6 +576,10 @@ export class ConfigManager {
providerBackends: {
...DEFAULT_CONFIG.runtime.providerBackends,
...(loaded.runtime?.providerBackends ?? {}),
codex: migrateProviderBackendId(
'codex',
loaded.runtime?.providerBackends?.codex
) as RuntimeConfig['providerBackends']['codex'],
},
},
display: {

View file

@ -25,7 +25,7 @@ const PROVIDER_CAPABILITIES: Record<
configurableAuthModes: ['auto', 'oauth', 'api_key'],
},
codex: {
supportsOAuth: true,
supportsOAuth: false,
supportsApiKey: true,
configurableAuthModes: [],
},
@ -42,7 +42,6 @@ const PROVIDER_API_KEY_ENV_VARS: Partial<Record<CliProviderId, string>> = {
gemini: 'GEMINI_API_KEY',
};
const CODEX_API_KEY_BETA_ENV_VAR = 'CLAUDE_CODE_CODEX_API_KEY_BETA';
const CODEX_NATIVE_API_KEY_ENV_VAR = 'CODEX_API_KEY';
const CODEX_NATIVE_BACKEND_ID = 'codex-native';
@ -65,8 +64,7 @@ export class ProviderConnectionService {
}
if (providerId === 'codex') {
const codexConnection = this.configManager.getConfig().providerConnections.codex;
return this.shouldExposeCodexConnectionModes() ? codexConnection.authMode : null;
return null;
}
return null;
@ -109,27 +107,7 @@ export class ProviderConnectionService {
return env;
}
const codexConnection = this.configManager.getConfig().providerConnections.codex;
const codexRuntimeBackend = this.getConfiguredCodexRuntimeBackend(runtimeBackendOverride);
if (!this.shouldExposeCodexConnectionModes(runtimeBackendOverride)) {
delete env[CODEX_API_KEY_BETA_ENV_VAR];
delete env.OPENAI_API_KEY;
delete env[CODEX_NATIVE_API_KEY_ENV_VAR];
return env;
}
if (codexConnection.apiKeyBetaEnabled) {
env[CODEX_API_KEY_BETA_ENV_VAR] = '1';
} else {
delete env[CODEX_API_KEY_BETA_ENV_VAR];
}
if (codexConnection.authMode === 'oauth') {
delete env.OPENAI_API_KEY;
delete env[CODEX_NATIVE_API_KEY_ENV_VAR];
return env;
}
const storedKey = await this.apiKeyService.lookupPreferred('OPENAI_API_KEY');
const existingOpenAiKey =
typeof env.OPENAI_API_KEY === 'string' && env.OPENAI_API_KEY.trim()
@ -147,11 +125,7 @@ export class ProviderConnectionService {
if (resolvedApiKey) {
env.OPENAI_API_KEY = resolvedApiKey;
if (codexRuntimeBackend === CODEX_NATIVE_BACKEND_ID) {
env[CODEX_NATIVE_API_KEY_ENV_VAR] = resolvedApiKey;
} else {
delete env[CODEX_NATIVE_API_KEY_ENV_VAR];
}
env[CODEX_NATIVE_API_KEY_ENV_VAR] = resolvedApiKey;
return env;
}
@ -192,22 +166,7 @@ export class ProviderConnectionService {
return env;
}
const codexConnection = this.configManager.getConfig().providerConnections.codex;
const codexRuntimeBackend = this.getConfiguredCodexRuntimeBackend(runtimeBackendOverride);
if (!this.shouldExposeCodexConnectionModes(runtimeBackendOverride)) {
return env;
}
if (codexConnection.apiKeyBetaEnabled) {
env[CODEX_API_KEY_BETA_ENV_VAR] = '1';
} else {
delete env[CODEX_API_KEY_BETA_ENV_VAR];
}
if (codexConnection.authMode !== 'api_key') {
return env;
}
const storedKey = await this.apiKeyService.lookupPreferred('OPENAI_API_KEY');
const existingOpenAiKey =
typeof env.OPENAI_API_KEY === 'string' && env.OPENAI_API_KEY.trim()
@ -272,30 +231,18 @@ export class ProviderConnectionService {
return null;
}
const codexConnection = this.configManager.getConfig().providerConnections.codex;
const codexRuntimeBackend = this.getConfiguredCodexRuntimeBackend(runtimeBackendOverride);
if (
!this.shouldExposeCodexConnectionModes(runtimeBackendOverride) ||
codexConnection.authMode !== 'api_key'
) {
return null;
}
if (typeof env.OPENAI_API_KEY === 'string' && env.OPENAI_API_KEY.trim()) {
return null;
}
if (
codexRuntimeBackend === CODEX_NATIVE_BACKEND_ID &&
typeof env[CODEX_NATIVE_API_KEY_ENV_VAR] === 'string' &&
env[CODEX_NATIVE_API_KEY_ENV_VAR]?.trim()
(typeof env.OPENAI_API_KEY === 'string' && env.OPENAI_API_KEY.trim()) ||
(typeof env[CODEX_NATIVE_API_KEY_ENV_VAR] === 'string' &&
env[CODEX_NATIVE_API_KEY_ENV_VAR]?.trim())
) {
return null;
}
return codexRuntimeBackend === CODEX_NATIVE_BACKEND_ID
? 'Codex API key mode is enabled for codex-native, but no OPENAI_API_KEY or CODEX_API_KEY is configured. Add a stored/environment API key or switch Codex auth mode back to OAuth.'
: 'Codex API key mode is enabled, but no OPENAI_API_KEY is configured. Add a stored/environment API key or switch Codex auth mode back to OAuth.';
? 'Codex native requires OPENAI_API_KEY or CODEX_API_KEY. Add a stored or environment API key before launching Codex.'
: 'Codex requires OPENAI_API_KEY or CODEX_API_KEY. Add a stored or environment API key before launching Codex.';
}
async getConfiguredConnectionIssues(
@ -336,27 +283,17 @@ export class ProviderConnectionService {
const codexRuntimeBackend =
providerId === 'codex' ? this.getConfiguredCodexRuntimeBackend() : null;
const externalCredential = this.getExternalCredential(providerId, codexRuntimeBackend);
const codexBetaEnabled =
providerId === 'codex'
? this.configManager.getConfig().providerConnections.codex.apiKeyBetaEnabled
: undefined;
const configurableAuthModes =
providerId === 'codex' &&
(codexBetaEnabled || codexRuntimeBackend === CODEX_NATIVE_BACKEND_ID)
? (['oauth', 'api_key'] as CliProviderAuthMode[])
: capabilities.configurableAuthModes;
providerId === 'codex' ? ([] as CliProviderAuthMode[]) : capabilities.configurableAuthModes;
const configuredAuthMode =
providerId === 'codex' &&
!(codexBetaEnabled || codexRuntimeBackend === CODEX_NATIVE_BACKEND_ID)
? null
: this.getConfiguredAuthMode(providerId);
providerId === 'codex' ? null : this.getConfiguredAuthMode(providerId);
return {
...capabilities,
configurableAuthModes,
configuredAuthMode,
apiKeyBetaAvailable: providerId === 'codex' ? true : undefined,
apiKeyBetaEnabled: codexBetaEnabled,
apiKeyBetaAvailable: providerId === 'codex' ? undefined : undefined,
apiKeyBetaEnabled: providerId === 'codex' ? undefined : undefined,
apiKeyConfigured: Boolean(storedApiKey?.value.trim() || externalCredential?.value.trim()),
apiKeySource: storedApiKey?.value.trim()
? 'stored'
@ -380,31 +317,16 @@ export class ProviderConnectionService {
return this.apiKeyService.lookupPreferred(envVarName);
}
private getConfiguredCodexRuntimeBackend(
runtimeBackendOverride?: string | null
): 'auto' | 'adapter' | 'api' | 'codex-native' {
if (
runtimeBackendOverride === 'auto' ||
runtimeBackendOverride === 'adapter' ||
runtimeBackendOverride === 'api' ||
runtimeBackendOverride === CODEX_NATIVE_BACKEND_ID
) {
private getConfiguredCodexRuntimeBackend(runtimeBackendOverride?: string | null): 'codex-native' {
if (runtimeBackendOverride === CODEX_NATIVE_BACKEND_ID) {
return runtimeBackendOverride;
}
return this.configManager.getConfig().runtime.providerBackends.codex;
}
private shouldExposeCodexConnectionModes(runtimeBackendOverride?: string | null): boolean {
const config = this.configManager.getConfig();
return (
config.providerConnections.codex.apiKeyBetaEnabled ||
this.getConfiguredCodexRuntimeBackend(runtimeBackendOverride) === CODEX_NATIVE_BACKEND_ID
);
return CODEX_NATIVE_BACKEND_ID;
}
private getExternalCredential(
providerId: CliProviderId,
codexRuntimeBackend: 'auto' | 'adapter' | 'api' | 'codex-native' | null = null
codexRuntimeBackend: 'codex-native' | null = null
): ExternalCredential {
const shellEnv = getCachedShellEnv() ?? {};
const sources = [shellEnv, process.env];
@ -440,14 +362,12 @@ export class ProviderConnectionService {
}
if (providerId === 'codex') {
if (codexRuntimeBackend === CODEX_NATIVE_BACKEND_ID) {
const nativeApiKey = findEnvValue(CODEX_NATIVE_API_KEY_ENV_VAR);
if (nativeApiKey) {
return {
label: `Detected from ${CODEX_NATIVE_API_KEY_ENV_VAR}`,
value: nativeApiKey,
};
}
const nativeApiKey = findEnvValue(CODEX_NATIVE_API_KEY_ENV_VAR);
if (nativeApiKey) {
return {
label: `Detected from ${CODEX_NATIVE_API_KEY_ENV_VAR}`,
value: nativeApiKey,
};
}
const apiKey = findEnvValue('OPENAI_API_KEY');

View file

@ -1,4 +1,5 @@
import { FileReadTimeoutError, readFileUtf8WithTimeout } from '@main/utils/fsRead';
import { migrateProviderBackendId } from '@shared/utils/providerBackend';
import { getTeamsBasePath } from '@main/utils/pathDecoder';
import * as fs from 'fs';
import * as path from 'path';
@ -83,6 +84,11 @@ export class TeamMetaStore {
return null;
}
const providerId =
file.providerId === 'anthropic' || file.providerId === 'codex' || file.providerId === 'gemini'
? file.providerId
: undefined;
return {
version: 1,
displayName:
@ -92,13 +98,11 @@ export class TeamMetaStore {
color: typeof file.color === 'string' ? file.color.trim() || undefined : undefined,
cwd: file.cwd.trim(),
prompt: typeof file.prompt === 'string' ? file.prompt.trim() || undefined : undefined,
providerId:
file.providerId === 'anthropic' ||
file.providerId === 'codex' ||
file.providerId === 'gemini'
? file.providerId
: undefined,
providerBackendId: normalizeOptionalBackendId(file.providerBackendId),
providerId,
providerBackendId: migrateProviderBackendId(
providerId,
normalizeOptionalBackendId(file.providerBackendId)
),
model: typeof file.model === 'string' ? file.model.trim() || undefined : undefined,
effort: typeof file.effort === 'string' ? file.effort.trim() || undefined : undefined,
skipPermissions: typeof file.skipPermissions === 'boolean' ? file.skipPermissions : undefined,
@ -119,7 +123,10 @@ export class TeamMetaStore {
cwd: data.cwd.trim(),
prompt: data.prompt?.trim() || undefined,
providerId: data.providerId,
providerBackendId: normalizeOptionalBackendId(data.providerBackendId),
providerBackendId: migrateProviderBackendId(
data.providerId,
normalizeOptionalBackendId(data.providerBackendId)
),
model: data.model?.trim() || undefined,
effort: data.effort?.trim() || undefined,
skipPermissions: data.skipPermissions,

View file

@ -48,6 +48,7 @@ import {
} from '@shared/utils/inboxNoise';
import { isLeadAgentType, isLeadMember } from '@shared/utils/leadDetection';
import { createLogger } from '@shared/utils/logger';
import { migrateProviderBackendId } from '@shared/utils/providerBackend';
import { isDefaultProviderModelSelection } from '@shared/utils/providerModelSelection';
import { formatTaskDisplayLabel } from '@shared/utils/taskIdentity';
import {
@ -390,7 +391,7 @@ function getConfiguredRuntimeBackend(providerId: TeamProviderId): string | null
case 'gemini':
return runtimeConfig.gemini;
case 'codex':
return runtimeConfig.codex;
return migrateProviderBackendId('codex', runtimeConfig.codex) ?? 'codex-native';
case 'anthropic':
default:
return null;
@ -420,7 +421,9 @@ function buildRuntimeLaunchWarning(
const providerLabel = getTeamProviderLabel(providerId);
const modelLabel = request.model?.trim() || 'default';
const effortLabel = request.effort ?? 'default';
const backend = request.providerBackendId?.trim() || getConfiguredRuntimeBackend(providerId);
const backend =
migrateProviderBackendId(providerId, request.providerBackendId?.trim()) ||
getConfiguredRuntimeBackend(providerId);
const flags: string[] = [];
if (env.CLAUDE_CODE_USE_GEMINI === '1') flags.push('USE_GEMINI');
if (env.CLAUDE_CODE_USE_OPENAI === '1') flags.push('USE_OPENAI');
@ -466,10 +469,12 @@ function logRuntimeLaunchSnapshot(
const providerId = resolveTeamProviderId(request.providerId);
const snapshot = {
providerId,
providerBackendId: request.providerBackendId ?? null,
providerBackendId: migrateProviderBackendId(providerId, request.providerBackendId) ?? null,
model: request.model ?? null,
effort: request.effort ?? null,
configuredBackend: request.providerBackendId?.trim() || getConfiguredRuntimeBackend(providerId),
configuredBackend:
migrateProviderBackendId(providerId, request.providerBackendId?.trim()) ||
getConfiguredRuntimeBackend(providerId),
promptSize: options?.promptSize ?? null,
expectedMembersCount: options?.expectedMembersCount ?? null,
geminiRuntimeAuth:
@ -1159,8 +1164,10 @@ function shouldSkipResumeForProviderRuntimeChange(
return { skip: false };
}
const requestedBackendId = request.providerBackendId?.trim() || null;
const previousBackendId = persistedProviderBackendId?.trim() || null;
const requestedBackendId =
migrateProviderBackendId(providerId, request.providerBackendId?.trim()) || null;
const previousBackendId =
migrateProviderBackendId(providerId, persistedProviderBackendId?.trim()) || null;
if (requestedBackendId && previousBackendId && requestedBackendId !== previousBackendId) {
return {
skip: true,

View file

@ -244,7 +244,7 @@ function getProviderTerminalCommand(provider: CliProviderStatus): {
return {
args: ['auth', 'login', '--provider', provider.providerId],
env: {
CLAUDE_CODE_CODEX_BACKEND: provider.selectedBackendId ?? 'auto',
CLAUDE_CODE_CODEX_BACKEND: provider.selectedBackendId ?? 'codex-native',
},
};
}
@ -272,7 +272,7 @@ function getProviderTerminalLogoutCommand(provider: CliProviderStatus): {
return {
args: ['auth', 'logout', '--provider', provider.providerId],
env: {
CLAUDE_CODE_CODEX_BACKEND: provider.selectedBackendId ?? 'auto',
CLAUDE_CODE_CODEX_BACKEND: provider.selectedBackendId ?? 'codex-native',
},
};
}

View file

@ -11,10 +11,7 @@ import {
TooltipProvider,
TooltipTrigger,
} from '@renderer/components/ui/tooltip';
import {
formatProviderBackendLabel,
isLegacyCodexProviderBackendId,
} from '@renderer/utils/providerBackendIdentity';
import { formatProviderBackendLabel } from '@renderer/utils/providerBackendIdentity';
import type { CliProviderStatus } from '@shared/types';
@ -60,15 +57,7 @@ export function getProviderRuntimeBackendAudienceLabel(
export function getVisibleProviderRuntimeBackendOptions(
provider: CliProviderStatus
): NonNullable<CliProviderStatus['availableBackends']> {
const options = provider.availableBackends ?? [];
if (provider.providerId !== 'codex') {
return options;
}
const selectedBackendId = provider.selectedBackendId ?? null;
return options.filter(
(option) => !isLegacyCodexProviderBackendId(option.id) || option.id === selectedBackendId
);
return provider.availableBackends ?? [];
}
export function getOptionDisplayLabel(
@ -77,17 +66,6 @@ export function getOptionDisplayLabel(
resolvedOption: NonNullable<CliProviderStatus['availableBackends']>[number] | null
): string {
if (provider.providerId === 'codex') {
if (option.id === 'auto') {
const currentLabel =
resolvedOption && resolvedOption.id !== 'auto'
? (formatProviderBackendLabel(provider.providerId, resolvedOption.id) ??
resolvedOption.label)
: null;
return currentLabel
? `Legacy auto fallback (currently: ${currentLabel})`
: 'Legacy auto fallback';
}
const legacyLabel = formatProviderBackendLabel(provider.providerId, option.id);
if (legacyLabel) {
return legacyLabel;

View file

@ -30,6 +30,7 @@ import {
isConnectionManagedRuntimeProvider,
} from './providerConnectionUi';
import {
getVisibleProviderRuntimeBackendOptions,
getProviderRuntimeBackendSummary,
ProviderRuntimeBackendSelector,
} from './ProviderRuntimeBackendSelector';
@ -38,13 +39,7 @@ import type { CliProviderAuthMode, CliProviderId, CliProviderStatus } from '@sha
import type { ApiKeyEntry } from '@shared/types/extensions';
type ApiKeyProviderId = 'anthropic' | 'codex' | 'gemini';
type PendingConnectionAction =
| 'auto'
| 'oauth'
| 'api_key'
| 'codex-beta-on'
| 'codex-beta-off'
| null;
type PendingConnectionAction = 'auto' | 'oauth' | 'api_key' | null;
interface ConnectionMethodCardOption {
readonly authMode: CliProviderAuthMode;
readonly title: string;
@ -86,7 +81,7 @@ const API_KEY_PROVIDER_CONFIG: Record<
name: 'OpenAI API Key',
title: 'API key',
description:
'Use `OPENAI_API_KEY` for Codex runs that need API-key billing. Codex native stays the primary runtime path while your subscription session remains available when you switch back.',
'Codex native requires API-key credentials. Save OPENAI_API_KEY here and the app will mirror it into the native CODEX_API_KEY environment when launching Codex.',
placeholder: 'sk-proj-...',
},
gemini: {
@ -103,10 +98,6 @@ function isApiKeyProviderId(providerId: CliProviderId): providerId is ApiKeyProv
return providerId === 'anthropic' || providerId === 'codex' || providerId === 'gemini';
}
function hasExplicitRuntimeBackends(provider: CliProviderStatus): boolean {
return (provider.availableBackends?.length ?? 0) > 0;
}
function isCodexNativeLane(provider: CliProviderStatus): boolean {
return (
provider.providerId === 'codex' &&
@ -124,11 +115,7 @@ function getConnectionDescription(provider: CliProviderStatus): string {
case 'anthropic':
return 'Choose how app-launched Anthropic sessions authenticate.';
case 'codex':
return hasExplicitRuntimeBackends(provider)
? 'Choose which credentials app-launched Codex sessions should use. Codex native remains the primary runtime path unless you intentionally keep a legacy fallback selected.'
: provider.connection?.apiKeyBetaEnabled
? 'Choose whether app-launched Codex sessions use your Codex subscription or API-key billing.'
: 'Codex native uses your subscription session by default. Enable API key mode only if you want native Codex launches to consume API-key credentials.';
return 'Codex launches always use the native runtime now. Manage API-key credentials here before launching teams or one-shot Codex runs.';
case 'gemini':
return 'Configure optional API access. CLI SDK and ADC are still discovered automatically.';
}
@ -139,9 +126,7 @@ function getRuntimeDescription(provider: CliProviderStatus): string {
case 'anthropic':
return 'Anthropic currently has no separate runtime backend selector.';
case 'codex':
return hasExplicitRuntimeBackends(provider)
? 'Choose which Codex runtime backend multimodel should use. Codex native is the default. Legacy fallbacks stay hidden unless they are already selected.'
: 'Codex native is the default runtime path. Connection method only controls which credentials the runtime can consume.';
return 'Codex now runs only through the native runtime path.';
case 'gemini':
return 'Choose which Gemini runtime backend multimodel should use.';
}
@ -160,9 +145,7 @@ function getAuthModeDescription(providerId: CliProviderId, authMode: CliProvider
}
if (providerId === 'codex') {
return authMode === 'api_key'
? 'Use API-key credentials for app-launched Codex sessions. Codex native remains the primary runtime path and will consume those credentials when needed.'
: 'Use your Codex subscription session. API-key-only fallback paths remain unavailable until you switch this credential mode.';
return 'Codex always launches through the native runtime and requires API-key credentials.';
}
return '';
@ -172,7 +155,6 @@ function getConnectionAlert(provider: CliProviderStatus): string | null {
const authMode = provider.connection?.configuredAuthMode;
const hasAnthropicSubscriptionSession =
provider.authMethod === 'oauth_token' || provider.authMethod === 'claude.ai';
const hasCodexSubscriptionSession = provider.authMethod === 'oauth_token';
if (
provider.providerId === 'anthropic' &&
@ -198,26 +180,10 @@ function getConnectionAlert(provider: CliProviderStatus): string | null {
return 'A saved API key is available, but app-launched Anthropic sessions use it only after you switch to API key mode.';
}
if (
provider.providerId === 'codex' &&
authMode === 'api_key' &&
!provider.connection?.apiKeyConfigured
) {
if (provider.providerId === 'codex' && !provider.connection?.apiKeyConfigured) {
return isCodexNativeLane(provider)
? 'API key mode is selected, but no OPENAI_API_KEY or CODEX_API_KEY credential is available yet.'
: 'API key mode is selected, but no OPENAI_API_KEY credential is available yet.';
}
if (provider.providerId === 'codex' && authMode === 'oauth' && !hasCodexSubscriptionSession) {
return 'Codex subscription mode is selected. Sign in with Codex to use this provider.';
}
if (
provider.providerId === 'codex' &&
authMode === 'oauth' &&
provider.connection?.apiKeySource === 'stored'
) {
return 'A saved OPENAI_API_KEY is available, but Codex uses it only after you switch to API key mode.';
? 'No OPENAI_API_KEY or CODEX_API_KEY credential is available yet.'
: 'No OPENAI_API_KEY credential is available yet.';
}
if (
@ -253,22 +219,7 @@ function getConnectionMethodCardOptions(
},
];
case 'codex':
if (!provider.connection?.apiKeyBetaEnabled) {
return null;
}
return [
{
authMode: 'oauth',
title: 'Codex subscription',
description: 'Use your Codex sign-in session and subscription access.',
},
{
authMode: 'api_key',
title: 'OpenAI API key',
description: 'Use OPENAI_API_KEY and OpenAI API billing.',
},
];
return null;
default:
return null;
}
@ -276,9 +227,7 @@ function getConnectionMethodCardOptions(
function getConnectionMethodCardsHint(provider: CliProviderStatus): string | null {
if (provider.providerId === 'codex') {
return hasExplicitRuntimeBackends(provider)
? 'Connection method controls credentials only. Runtime backend selection is independent.'
: 'Runtime follows your connection method automatically.';
return 'Codex uses saved or environment API-key credentials for the native runtime.';
}
if (provider.providerId === 'anthropic') {
@ -461,15 +410,6 @@ export const ProviderRuntimeSettingsDialog = ({
statusSelectedProvider.connection.configuredAuthMode;
}
if (statusSelectedProvider.providerId === 'codex') {
nextConnection.configuredAuthMode =
appConfig?.providerConnections?.codex.authMode ??
statusSelectedProvider.connection.configuredAuthMode;
nextConnection.apiKeyBetaEnabled =
appConfig?.providerConnections?.codex.apiKeyBetaEnabled ??
statusSelectedProvider.connection.apiKeyBetaEnabled;
}
if (statusApiKeyConfig) {
if (nextConnection.apiKeySource === 'stored') {
nextConnection.apiKeyConfigured = Boolean(selectedApiKey);
@ -488,8 +428,6 @@ export const ProviderRuntimeSettingsDialog = ({
};
}, [
appConfig?.providerConnections?.anthropic.authMode,
appConfig?.providerConnections?.codex.apiKeyBetaEnabled,
appConfig?.providerConnections?.codex.authMode,
selectedApiKey,
statusApiKeyConfig,
statusSelectedProvider,
@ -517,7 +455,10 @@ export const ProviderRuntimeSettingsDialog = ({
: false;
const hideConnectionMethodMeta = showConnectionMethodCards;
const canConfigureRuntime =
!connectionManagedRuntime && (selectedProvider?.availableBackends?.length ?? 0) > 0;
!connectionManagedRuntime &&
(selectedProvider
? getVisibleProviderRuntimeBackendOptions(selectedProvider).length > 1
: false);
const apiKeyConfig =
selectedProvider && isApiKeyProviderId(selectedProvider.providerId)
@ -527,9 +468,9 @@ export const ProviderRuntimeSettingsDialog = ({
selectedProvider &&
isApiKeyProviderId(selectedProvider.providerId) &&
activeApiKeyFormProviderId === selectedProvider.providerId;
const codexApiKeyBetaEnabled = selectedProvider?.connection?.apiKeyBetaEnabled === true;
const showApiKeySection = Boolean(
apiKeyConfig && (selectedProvider?.providerId !== 'codex' || codexApiKeyBetaEnabled)
apiKeyConfig &&
(selectedProvider?.providerId !== 'codex' || !selectedProvider.connection?.supportsOAuth)
);
const connectionAlert = selectedProvider ? getConnectionAlert(selectedProvider) : null;
const connectionLoading = selectedProviderLoading || connectionSaving;
@ -541,9 +482,7 @@ export const ProviderRuntimeSettingsDialog = ({
const hasSubscriptionSession =
selectedProvider?.providerId === 'anthropic'
? selectedProvider.authMethod === 'oauth_token' || selectedProvider.authMethod === 'claude.ai'
: selectedProvider?.providerId === 'codex'
? selectedProvider.authMethod === 'oauth_token'
: false;
: false;
const canRequestSubscriptionLogin =
Boolean(selectedProvider?.connection?.supportsOAuth && onRequestLogin) &&
configuredAuthMode !== 'api_key' &&
@ -567,21 +506,6 @@ export const ProviderRuntimeSettingsDialog = ({
}
if (connectionSaving) {
if (selectedProvider.providerId === 'codex') {
switch (pendingConnectionAction) {
case 'codex-beta-on':
return 'Enabling API key mode...';
case 'codex-beta-off':
return 'Disabling API key mode...';
case 'api_key':
return 'Switching to OpenAI API key...';
case 'oauth':
return 'Switching to Codex subscription...';
default:
return 'Applying connection changes...';
}
}
if (selectedProvider.providerId === 'anthropic') {
switch (pendingConnectionAction) {
case 'api_key':
@ -679,7 +603,7 @@ export const ProviderRuntimeSettingsDialog = ({
};
const handleAuthModeChange = async (authMode: string): Promise<void> => {
if (selectedProvider?.providerId !== 'anthropic' && selectedProvider?.providerId !== 'codex') {
if (selectedProvider?.providerId !== 'anthropic') {
return;
}
@ -692,54 +616,10 @@ export const ProviderRuntimeSettingsDialog = ({
setPendingConnectionAction(nextAuthMode);
setConnectionError(null);
let updateSucceeded = false;
try {
if (selectedProvider.providerId === 'anthropic') {
await updateConfig('providerConnections', {
anthropic: {
authMode: nextAuthMode,
},
});
} else {
await updateConfig('providerConnections', {
codex: {
authMode: nextAuthMode === 'api_key' ? 'api_key' : 'oauth',
},
});
}
updateSucceeded = true;
} catch (error) {
setConnectionError(error instanceof Error ? error.message : 'Failed to update connection');
} finally {
if (updateSucceeded) {
try {
await onRefreshProvider?.(selectedProvider.providerId);
} catch {
setConnectionError('Connection updated, but failed to refresh provider status.');
}
}
setConnectionSaving(false);
setPendingConnectionAction(null);
}
};
const handleCodexBetaToggle = async (enabled: boolean): Promise<void> => {
const fallbackApiKeyScope = selectedApiKey?.scope ?? 'user';
const shouldOpenApiKeyForm =
enabled &&
selectedProvider?.providerId === 'codex' &&
!selectedProvider.connection?.apiKeyConfigured &&
!selectedApiKey;
setConnectionSaving(true);
setPendingConnectionAction(enabled ? 'codex-beta-on' : 'codex-beta-off');
setConnectionError(null);
let updateSucceeded = false;
try {
await updateConfig('providerConnections', {
codex: {
apiKeyBetaEnabled: enabled,
authMode: enabled ? 'api_key' : 'oauth',
anthropic: {
authMode: nextAuthMode,
},
});
updateSucceeded = true;
@ -747,15 +627,8 @@ export const ProviderRuntimeSettingsDialog = ({
setConnectionError(error instanceof Error ? error.message : 'Failed to update connection');
} finally {
if (updateSucceeded) {
if (shouldOpenApiKeyForm) {
setActiveApiKeyFormProviderId('codex');
setApiKeyScope(fallbackApiKeyScope);
setApiKeyValue('');
setApiKeyError(null);
}
try {
await onRefreshProvider?.('codex');
await onRefreshProvider?.(selectedProvider.providerId);
} catch {
setConnectionError('Connection updated, but failed to refresh provider status.');
}
@ -901,78 +774,12 @@ export const ProviderRuntimeSettingsDialog = ({
{selectedProvider.authenticated &&
(selectedProvider.authMethod === 'oauth_token' ||
selectedProvider.authMethod === 'claude.ai')
? selectedProvider.providerId === 'codex'
? 'Reconnect Codex'
: 'Reconnect Anthropic'
? 'Reconnect Anthropic'
: getProviderConnectLabel(selectedProvider)}
</Button>
) : null}
</div>
{selectedProvider.providerId === 'codex' &&
selectedProvider.connection?.apiKeyBetaAvailable &&
!selectedProvider.connection.apiKeyBetaEnabled &&
!showConnectionMethodCards ? (
<div
className="space-y-3 rounded-md border p-3"
style={{ borderColor: 'var(--color-border-subtle)' }}
>
<div className="grid gap-2 sm:grid-cols-2">
<div
className="rounded-md border p-3"
style={{
borderColor: 'rgba(74, 222, 128, 0.3)',
backgroundColor: 'rgba(74, 222, 128, 0.08)',
}}
>
<div className="text-sm font-medium" style={{ color: 'var(--color-text)' }}>
Codex subscription
</div>
<div className="mt-1 text-xs" style={{ color: 'var(--color-text-muted)' }}>
Use your Codex sign-in session and subscription access.
</div>
<div
className="mt-3 inline-flex rounded-full px-2 py-0.5 text-[11px]"
style={{
color: '#86efac',
backgroundColor: 'rgba(74, 222, 128, 0.14)',
}}
>
Current
</div>
</div>
<div
className="rounded-md border p-3"
style={{ borderColor: 'var(--color-border-subtle)' }}
>
<div className="text-sm font-medium" style={{ color: 'var(--color-text)' }}>
OpenAI API key (Beta)
</div>
<div className="mt-1 text-xs" style={{ color: 'var(--color-text-muted)' }}>
Use OPENAI_API_KEY and OpenAI API billing for Codex.
</div>
<div className="mt-3">
<Button
size="sm"
variant="outline"
disabled={connectionBusy}
onClick={() => void handleCodexBetaToggle(true)}
>
{pendingConnectionAction === 'codex-beta-on' ? (
<>
<Loader2 className="mr-1 size-3.5 animate-spin" />
Enabling...
</>
) : (
'Enable API key mode'
)}
</Button>
</div>
</div>
</div>
</div>
) : null}
{showConnectionMethodCards ? (
<div className="space-y-2">
<Label className="text-xs">Connection method</Label>
@ -1056,20 +863,6 @@ export const ProviderRuntimeSettingsDialog = ({
{selectedProvider.connection.apiKeySourceLabel}
</span>
) : null}
{selectedProvider.providerId === 'codex' &&
selectedProvider.connection?.apiKeyBetaEnabled ? (
<button
type="button"
onClick={() => void handleCodexBetaToggle(false)}
className="text-xs underline-offset-2 hover:underline"
style={{ color: 'var(--color-text-muted)' }}
disabled={connectionBusy}
>
{pendingConnectionAction === 'codex-beta-off'
? 'Disabling...'
: 'Disable API key mode'}
</button>
) : null}
</div>
{showApiKeySection && apiKeyConfig ? (

View file

@ -1,7 +1,6 @@
import type { CliProviderAuthMode, CliProviderStatus } from '@shared/types';
const CODEX_SUBSCRIPTION_LABEL = 'Codex subscription';
const CODEX_API_KEY_LABEL = 'OpenAI API key';
const CODEX_NATIVE_LABEL = 'Codex native';
const ANTHROPIC_SUBSCRIPTION_LABEL = 'Anthropic subscription';
const AUTH_MODE_LABELS: Record<CliProviderAuthMode, string> = {
@ -23,7 +22,7 @@ export function formatProviderAuthModeLabelForProvider(
}
if (providerId === 'codex' && authMode === 'oauth') {
return CODEX_SUBSCRIPTION_LABEL;
return CODEX_NATIVE_LABEL;
}
if (providerId === 'anthropic' && authMode === 'oauth') {
@ -59,7 +58,7 @@ export function formatProviderAuthMethodLabelForProvider(
authMethod: string | null
): string {
if (providerId === 'codex' && authMethod === 'oauth_token') {
return CODEX_SUBSCRIPTION_LABEL;
return CODEX_NATIVE_LABEL;
}
if (providerId === 'anthropic' && (authMethod === 'oauth_token' || authMethod === 'claude.ai')) {
@ -95,23 +94,11 @@ function getSelectedRuntimeBackendOption(
}
export function isConnectionManagedRuntimeProvider(provider: CliProviderStatus): boolean {
return provider.providerId === 'codex' && (provider.availableBackends?.length ?? 0) === 0;
return false;
}
function getCodexCurrentRuntimeLabel(provider: CliProviderStatus): string {
if (isCodexNativeLane(provider) && provider.backend?.label) {
return provider.backend.label;
}
if (provider.authenticated) {
return provider.authMethod === 'api_key' ? CODEX_API_KEY_LABEL : CODEX_SUBSCRIPTION_LABEL;
}
if (provider.connection?.configuredAuthMode === 'api_key') {
return CODEX_API_KEY_LABEL;
}
return CODEX_SUBSCRIPTION_LABEL;
return provider.backend?.label ?? CODEX_NATIVE_LABEL;
}
export function getProviderCurrentRuntimeSummary(provider: CliProviderStatus): string | null {
@ -126,6 +113,15 @@ export function getProviderCurrentRuntimeSummary(provider: CliProviderStatus): s
export function formatProviderStatusText(provider: CliProviderStatus): string {
const selectedBackendOption = getSelectedRuntimeBackendOption(provider);
if (provider.providerId === 'codex') {
if (selectedBackendOption?.statusMessage) {
return selectedBackendOption.statusMessage;
}
return (
provider.statusMessage ?? (provider.authenticated ? 'Codex native ready' : 'Not connected')
);
}
if (
isCodexNativeLane(provider) &&
selectedBackendOption &&
@ -168,11 +164,7 @@ export function getProviderConnectionModeSummary(provider: CliProviderStatus): s
return null;
}
if (provider.providerId === 'codex') {
return null;
}
if (provider.providerId === 'anthropic' && provider.authenticated) {
if (provider.authenticated) {
return null;
}
@ -212,22 +204,10 @@ export function getProviderCredentialSummary(provider: CliProviderStatus): strin
: (provider.connection.apiKeySourceLabel ?? 'API key is configured');
}
if (provider.providerId === 'codex' && provider.connection?.apiKeyBetaEnabled !== true) {
if (isCodexNativeLane(provider)) {
return provider.connection.apiKeySource === 'stored'
? 'Saved API key available in Manage'
: (provider.connection.apiKeySourceLabel ?? 'API key is configured');
}
if (provider.providerId === 'codex') {
return provider.connection.apiKeySource === 'stored'
? 'OpenAI API key is saved in Manage. Enable API key mode to use it.'
: 'OpenAI API key detected. Enable API key mode in Manage to use it.';
}
if (provider.authMethod !== 'api_key' && provider.providerId === 'codex') {
return provider.connection.apiKeySource === 'stored'
? 'OpenAI API key is also configured in Manage'
: (provider.connection.apiKeySourceLabel ?? 'OpenAI API key is configured');
? 'Saved API key available in Manage'
: (provider.connection.apiKeySourceLabel ?? 'API key is configured');
}
return provider.connection.apiKeySourceLabel ?? null;
@ -258,17 +238,6 @@ export function getProviderDisconnectAction(provider: CliProviderStatus): {
};
}
if (provider.providerId === 'codex' && provider.authMethod === 'oauth_token') {
return {
label: 'Disconnect',
confirmLabel: 'Disconnect',
title: 'Disconnect Codex subscription?',
message: provider.connection?.apiKeyConfigured
? 'This removes the local Codex subscription session from the Claude CLI runtime. Saved OPENAI_API_KEY credentials in Manage stay available.'
: 'This removes the local Codex subscription session from the Claude CLI runtime.',
};
}
if (provider.providerId === 'gemini' && provider.authMethod === 'cli_oauth_personal') {
return {
label: 'Disconnect',
@ -288,7 +257,7 @@ export function getProviderConnectLabel(provider: CliProviderStatus): string {
}
if (provider.providerId === 'codex') {
return 'Connect Codex';
return 'Configure API key';
}
if (provider.providerId === 'gemini') {
@ -299,6 +268,10 @@ export function getProviderConnectLabel(provider: CliProviderStatus): string {
}
export function shouldShowProviderConnectAction(provider: CliProviderStatus): boolean {
if (provider.providerId === 'codex') {
return false;
}
if (!provider.canLoginFromUi || provider.authenticated) {
return false;
}

View file

@ -109,7 +109,7 @@ function getProviderTerminalCommand(provider: CliProviderStatus): {
return {
args: ['auth', 'login', '--provider', provider.providerId],
env: {
CLAUDE_CODE_CODEX_BACKEND: provider.selectedBackendId ?? 'auto',
CLAUDE_CODE_CODEX_BACKEND: provider.selectedBackendId ?? 'codex-native',
},
};
}
@ -137,7 +137,7 @@ function getProviderTerminalLogoutCommand(provider: CliProviderStatus): {
return {
args: ['auth', 'logout', '--provider', provider.providerId],
env: {
CLAUDE_CODE_CODEX_BACKEND: provider.selectedBackendId ?? 'auto',
CLAUDE_CODE_CODEX_BACKEND: provider.selectedBackendId ?? 'codex-native',
},
};
}

View file

@ -50,9 +50,7 @@ export function getProvisioningProviderBackendSummary(
const effectiveOption = options.find((option) => option.id === effectiveBackendId) ?? null;
const inferredProviderId =
provider.providerId ??
(effectiveBackendId === 'codex-native' ||
effectiveBackendId === 'adapter' ||
options.some((option) => option.id === 'codex-native' || option.id === 'adapter')
(effectiveBackendId === 'codex-native' || options.some((option) => option.id === 'codex-native')
? 'codex'
: undefined);
const normalizedLabel =

View file

@ -1,3 +1,9 @@
import {
formatProviderBackendLabel,
getDefaultProviderBackendId,
migrateProviderBackendId,
} from '@shared/utils/providerBackend';
import type { CliProviderStatus, TeamProviderId } from '@shared/types';
function normalizeOptionalBackendId(value: string | null | undefined): string | undefined {
@ -5,22 +11,7 @@ function normalizeOptionalBackendId(value: string | null | undefined): string |
return trimmed ? trimmed : undefined;
}
export function getDefaultProviderBackendId(
providerId: TeamProviderId | CliProviderStatus['providerId'] | undefined
): string | undefined {
return providerId === 'codex' ? 'codex-native' : undefined;
}
export function isLegacyCodexProviderBackendId(
providerBackendId: string | null | undefined
): boolean {
const normalizedBackendId = normalizeOptionalBackendId(providerBackendId);
return (
normalizedBackendId === 'auto' ||
normalizedBackendId === 'adapter' ||
normalizedBackendId === 'api'
);
}
export { formatProviderBackendLabel, getDefaultProviderBackendId };
export function resolveEffectiveProviderBackendId(
provider: Pick<CliProviderStatus, 'selectedBackendId' | 'resolvedBackendId'> | null | undefined
@ -32,57 +23,10 @@ export function resolveUiOwnedProviderBackendId(
providerId: TeamProviderId | CliProviderStatus['providerId'] | undefined,
provider: Pick<CliProviderStatus, 'selectedBackendId' | 'resolvedBackendId'> | null | undefined
): string | undefined {
const normalizedProviderId = providerId ?? undefined;
if (normalizedProviderId === 'codex') {
const selectedBackendId = normalizeOptionalBackendId(provider?.selectedBackendId);
if (!selectedBackendId || selectedBackendId === 'auto') {
return 'codex-native';
}
return selectedBackendId;
}
return resolveEffectiveProviderBackendId(provider);
}
export function formatProviderBackendLabel(
providerId: TeamProviderId | undefined,
providerBackendId: string | undefined
): string | undefined {
const normalizedProviderId = providerId ?? 'anthropic';
const normalizedBackendId = normalizeOptionalBackendId(providerBackendId);
if (!normalizedBackendId) {
return undefined;
}
if (normalizedProviderId === 'codex') {
switch (normalizedBackendId) {
case 'codex-native':
return 'Codex native';
case 'adapter':
return 'Legacy adapter fallback';
case 'api':
return 'Legacy OpenAI fallback';
case 'auto':
return 'Legacy auto fallback';
default:
return normalizedBackendId;
}
}
if (normalizedProviderId === 'gemini') {
switch (normalizedBackendId) {
case 'cli-sdk':
return 'CLI SDK';
case 'api':
return 'API';
case 'auto':
return undefined;
default:
return normalizedBackendId;
}
}
return normalizedBackendId;
return migrateProviderBackendId(
providerId,
provider?.selectedBackendId ?? provider?.resolvedBackendId
);
}
export function formatTeamProviderBackendLabel(

View file

@ -323,16 +323,8 @@ export function sortTeamProviderModels(
export function isCodexChatGptSubscriptionProviderStatus(
providerStatus?: RuntimeAwareProviderStatus | null
): boolean {
if (providerStatus?.providerId !== 'codex') {
return false;
}
const endpointLabel = providerStatus.backend?.endpointLabel?.toLowerCase() ?? '';
return (
providerStatus.authMethod === 'oauth_token' &&
(providerStatus.backend?.kind === 'adapter' ||
endpointLabel.includes('chatgpt.com/backend-api/codex/responses'))
);
void providerStatus;
return false;
}
function isRuntimeHiddenTeamModel(

View file

@ -337,7 +337,7 @@ export interface AppConfig {
runtime: {
providerBackends: {
gemini: 'auto' | 'api' | 'cli-sdk';
codex: 'auto' | 'adapter' | 'api' | 'codex-native';
codex: 'codex-native';
};
};
/** Display and UI settings */

View file

@ -0,0 +1,76 @@
import type { TeamProviderId } from '@shared/types';
type RuntimeProviderId = TeamProviderId;
function normalizeOptionalBackendId(value: unknown): string | undefined {
if (typeof value !== 'string') {
return undefined;
}
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : undefined;
}
export function getDefaultProviderBackendId(
providerId: TeamProviderId | RuntimeProviderId | undefined
): string | undefined {
return providerId === 'codex' ? 'codex-native' : undefined;
}
export function isLegacyCodexProviderBackendId(
providerBackendId: string | null | undefined
): boolean {
const normalizedBackendId = normalizeOptionalBackendId(providerBackendId);
return (
normalizedBackendId === 'auto' ||
normalizedBackendId === 'adapter' ||
normalizedBackendId === 'api'
);
}
export function migrateProviderBackendId(
providerId: TeamProviderId | RuntimeProviderId | undefined,
providerBackendId: string | null | undefined
): string | undefined {
const normalizedBackendId = normalizeOptionalBackendId(providerBackendId);
if (providerId !== 'codex') {
return normalizedBackendId;
}
if (!normalizedBackendId || isLegacyCodexProviderBackendId(normalizedBackendId)) {
return 'codex-native';
}
return normalizedBackendId;
}
export function formatProviderBackendLabel(
providerId: TeamProviderId | undefined,
providerBackendId: string | undefined
): string | undefined {
const normalizedBackendId = migrateProviderBackendId(providerId, providerBackendId);
if (!normalizedBackendId) {
return undefined;
}
if ((providerId ?? 'anthropic') === 'codex') {
if (normalizedBackendId === 'codex-native') {
return 'Codex native';
}
return normalizedBackendId;
}
if ((providerId ?? 'anthropic') === 'gemini') {
switch (normalizedBackendId) {
case 'cli-sdk':
return 'CLI SDK';
case 'api':
return 'API';
case 'auto':
return undefined;
default:
return normalizedBackendId;
}
}
return normalizedBackendId;
}

View file

@ -240,7 +240,7 @@ describe('configValidation', () => {
}
});
it('accepts Codex runtime backend updates for api and codex-native', () => {
it('normalizes legacy Codex runtime backend updates to codex-native', () => {
const apiResult = validateConfigUpdatePayload('runtime', {
providerBackends: {
codex: 'api',
@ -251,7 +251,7 @@ describe('configValidation', () => {
if (apiResult.valid) {
expect(apiResult.data).toEqual({
providerBackends: {
codex: 'api',
codex: 'codex-native',
},
});
}
@ -281,7 +281,7 @@ describe('configValidation', () => {
expect(result.valid).toBe(false);
if (!result.valid) {
expect(result.error).toContain('auto, adapter, api, codex-native');
expect(result.error).toContain('runtime.providerBackends.codex must be one of: codex-native');
}
});
});

View file

@ -325,31 +325,24 @@ describe('ClaudeMultimodelBridgeService', () => {
codex: {
supported: true,
authenticated: true,
authMethod: 'oauth_token',
authMethod: 'api_key',
verificationState: 'verified',
canLoginFromUi: true,
statusMessage: 'Codex native lane is wired but remains locked for normal selection.',
detailMessage: 'Use the fallback adapter/API lane unless the experimental native lane is explicitly enabled.',
canLoginFromUi: false,
statusMessage: 'Codex native runtime ready',
detailMessage: 'Codex native runtime is ready through the local codex exec seam.',
selectedBackendId: 'codex-native',
resolvedBackendId: 'codex-native',
availableBackends: [
{
id: 'auto',
label: 'Auto',
id: 'codex-native',
label: 'Codex native',
selectable: true,
recommended: true,
available: true,
},
{
id: 'codex-native',
label: 'Codex native',
selectable: false,
recommended: false,
available: true,
state: 'locked',
audience: 'internal',
statusMessage: 'Experimental native lane',
detailMessage: 'Phase 0 keeps the lane locked behind rollout policy.',
state: 'ready',
audience: 'general',
statusMessage: 'Ready',
detailMessage: 'Codex native runtime is ready through the local codex exec seam.',
},
],
externalRuntimeDiagnostics: [
@ -368,7 +361,7 @@ describe('ClaudeMultimodelBridgeService', () => {
plugins: {
status: 'unsupported',
ownership: 'shared',
reason: 'codex-native phase 0 keeps plugin execution disabled.',
reason: 'Plugins are not currently guaranteed for codex-native sessions in the multimodel runtime.',
},
mcp: {
status: 'unsupported',
@ -416,17 +409,13 @@ describe('ClaudeMultimodelBridgeService', () => {
label: 'Codex native',
},
availableBackends: [
expect.objectContaining({
id: 'auto',
selectable: true,
}),
expect.objectContaining({
id: 'codex-native',
selectable: false,
selectable: true,
available: true,
state: 'locked',
audience: 'internal',
statusMessage: 'Experimental native lane',
state: 'ready',
audience: 'general',
statusMessage: 'Ready',
}),
],
externalRuntimeDiagnostics: [
@ -444,7 +433,7 @@ describe('ClaudeMultimodelBridgeService', () => {
expect(getProviderCurrentRuntimeSummary(codex!)).toBeNull();
});
it('preserves codex-native internal unlock readiness from runtime status payloads', async () => {
it('preserves codex-native ready truth from runtime status payloads', async () => {
execCliMock.mockResolvedValue({
stdout: JSON.stringify({
providers: {
@ -461,12 +450,12 @@ describe('ClaudeMultimodelBridgeService', () => {
id: 'codex-native',
label: 'Codex native',
selectable: true,
recommended: false,
recommended: true,
available: true,
state: 'ready',
audience: 'internal',
statusMessage: 'Ready for internal use',
detailMessage: 'Internal rollout only.',
audience: 'general',
statusMessage: 'Ready',
detailMessage: 'Codex native runtime is ready through the local codex exec seam.',
},
],
capabilities: {
@ -502,8 +491,8 @@ describe('ClaudeMultimodelBridgeService', () => {
selectable: true,
available: true,
state: 'ready',
audience: 'internal',
statusMessage: 'Ready for internal use',
audience: 'general',
statusMessage: 'Ready',
});
});
@ -517,8 +506,8 @@ describe('ClaudeMultimodelBridgeService', () => {
authMethod: null,
verificationState: 'unknown',
canLoginFromUi: false,
statusMessage: 'Codex native runtime not ready',
detailMessage: 'Codex native runtime requires the codex CLI binary to be installed.',
statusMessage: 'Codex native runtime unavailable',
detailMessage: 'Codex native runtime requires the codex CLI binary to be installed and discoverable.',
selectedBackendId: 'codex-native',
resolvedBackendId: null,
availableBackends: [
@ -529,9 +518,9 @@ describe('ClaudeMultimodelBridgeService', () => {
recommended: false,
available: false,
state: 'runtime-missing',
audience: 'internal',
audience: 'general',
statusMessage: 'Codex CLI not found',
detailMessage: 'Install the codex CLI before enabling the lane.',
detailMessage: 'Codex native runtime requires the codex CLI binary to be installed and discoverable.',
},
],
capabilities: {
@ -563,7 +552,7 @@ describe('ClaudeMultimodelBridgeService', () => {
selectable: false,
available: false,
state: 'runtime-missing',
audience: 'internal',
audience: 'general',
statusMessage: 'Codex CLI not found',
});
});

View file

@ -11,28 +11,21 @@ describe('ProviderConnectionService', () => {
const originalOpenAiApiKey = process.env.OPENAI_API_KEY;
const originalCodexApiKey = process.env.CODEX_API_KEY;
function createConfig(
authMode: 'auto' | 'oauth' | 'api_key' = 'auto',
overrides?: {
codexAuthMode?: 'oauth' | 'api_key';
codexApiKeyBetaEnabled?: boolean;
codexRuntimeBackend?: 'auto' | 'adapter' | 'api' | 'codex-native';
}
) {
function createConfig(authMode: 'auto' | 'oauth' | 'api_key' = 'auto') {
return {
providerConnections: {
anthropic: {
authMode,
},
codex: {
apiKeyBetaEnabled: overrides?.codexApiKeyBetaEnabled ?? false,
authMode: overrides?.codexAuthMode ?? ('oauth' as const),
apiKeyBetaEnabled: false,
authMode: 'oauth' as const,
},
},
runtime: {
providerBackends: {
gemini: 'auto' as const,
codex: overrides?.codexRuntimeBackend ?? ('auto' as const),
codex: 'codex-native' as const,
},
},
};
@ -55,10 +48,9 @@ describe('ProviderConnectionService', () => {
if (originalCodexApiKey === undefined) {
delete process.env.CODEX_API_KEY;
return;
} else {
process.env.CODEX_API_KEY = originalCodexApiKey;
}
process.env.CODEX_API_KEY = originalCodexApiKey;
});
it('removes Anthropic environment credentials when OAuth mode is selected', async () => {
@ -116,30 +108,6 @@ describe('ProviderConnectionService', () => {
expect(result.ANTHROPIC_AUTH_TOKEN).toBeUndefined();
});
it('does not treat ANTHROPIC_AUTH_TOKEN as an API key in api_key mode', async () => {
const { ProviderConnectionService } =
await import('@main/services/runtime/ProviderConnectionService');
const service = new ProviderConnectionService(
{
lookupPreferred: vi.fn().mockResolvedValue(null),
} as never,
{
getConfig: () => createConfig('api_key'),
} as never
);
const result = await service.applyConfiguredConnectionEnv(
{
ANTHROPIC_AUTH_TOKEN: 'oauth-token',
},
'anthropic'
);
expect(result.ANTHROPIC_API_KEY).toBeUndefined();
expect(result.ANTHROPIC_AUTH_TOKEN).toBeUndefined();
});
it('reports a missing Anthropic API key when api_key mode is selected', async () => {
const { ProviderConnectionService } =
await import('@main/services/runtime/ProviderConnectionService');
@ -159,57 +127,7 @@ describe('ProviderConnectionService', () => {
expect(issue).toContain('ANTHROPIC_API_KEY');
});
it('does not report a missing Anthropic API key once env is populated', async () => {
const { ProviderConnectionService } =
await import('@main/services/runtime/ProviderConnectionService');
const service = new ProviderConnectionService(
{
lookupPreferred: vi.fn().mockResolvedValue(null),
} as never,
{
getConfig: () => createConfig('api_key'),
} as never
);
const issue = await service.getConfiguredConnectionIssue(
{
ANTHROPIC_API_KEY: 'env-key',
},
'anthropic'
);
expect(issue).toBeNull();
});
it('augments PTY env with stored Anthropic API key without stripping auth token', async () => {
const { ProviderConnectionService } =
await import('@main/services/runtime/ProviderConnectionService');
const service = new ProviderConnectionService(
{
lookupPreferred: vi.fn().mockResolvedValue({
envVarName: 'ANTHROPIC_API_KEY',
value: 'stored-key',
}),
} as never,
{
getConfig: () => createConfig('api_key'),
} as never
);
const result = await service.augmentConfiguredConnectionEnv(
{
ANTHROPIC_AUTH_TOKEN: 'oauth-token',
},
'anthropic'
);
expect(result.ANTHROPIC_API_KEY).toBe('stored-key');
expect(result.ANTHROPIC_AUTH_TOKEN).toBe('oauth-token');
});
it('prefers stored API key status over environment detection', async () => {
it('prefers stored API key status over environment detection for Anthropic', async () => {
getCachedShellEnvMock.mockReturnValue({
ANTHROPIC_API_KEY: 'shell-key',
});
@ -241,31 +159,7 @@ describe('ProviderConnectionService', () => {
});
});
it('does not report ANTHROPIC_AUTH_TOKEN as an API key credential source', async () => {
getCachedShellEnvMock.mockReturnValue({
ANTHROPIC_AUTH_TOKEN: 'oauth-token',
});
const { ProviderConnectionService } =
await import('@main/services/runtime/ProviderConnectionService');
const service = new ProviderConnectionService(
{
lookupPreferred: vi.fn().mockResolvedValue(null),
} as never,
{
getConfig: () => createConfig('auto'),
} as never
);
const info = await service.getConnectionInfo('anthropic');
expect(info.apiKeyConfigured).toBe(false);
expect(info.apiKeySource).toBeNull();
expect(info.apiKeySourceLabel).toBeNull();
});
it('keeps Codex API key beta opt-in disabled by default', async () => {
it('exposes Codex as native-only API-key runtime', async () => {
const { ProviderConnectionService } =
await import('@main/services/runtime/ProviderConnectionService');
@ -281,17 +175,19 @@ describe('ProviderConnectionService', () => {
const info = await service.getConnectionInfo('codex');
expect(info).toMatchObject({
supportsOAuth: true,
supportsOAuth: false,
supportsApiKey: true,
configurableAuthModes: [],
configuredAuthMode: null,
apiKeyBetaAvailable: true,
apiKeyBetaEnabled: false,
apiKeyConfigured: false,
apiKeySource: null,
apiKeySourceLabel: null,
});
expect(info.apiKeyBetaAvailable).toBeUndefined();
expect(info.apiKeyBetaEnabled).toBeUndefined();
});
it('injects OPENAI_API_KEY and selects the API backend when Codex API key mode is enabled', async () => {
it('mirrors a stored OpenAI key into CODEX_API_KEY for native Codex launches', async () => {
const lookupPreferred = vi.fn().mockResolvedValue({
envVarName: 'OPENAI_API_KEY',
value: 'openai-stored-key',
@ -304,31 +200,18 @@ describe('ProviderConnectionService', () => {
lookupPreferred,
} as never,
{
getConfig: () => ({
...createConfig('auto', {
codexApiKeyBetaEnabled: true,
codexAuthMode: 'api_key',
codexRuntimeBackend: 'api',
}),
}),
getConfig: () => createConfig('auto'),
} as never
);
const result = await service.applyConfiguredConnectionEnv(
{
OPENAI_API_KEY: undefined,
CLAUDE_CODE_CODEX_BACKEND: 'auto',
},
'codex'
);
const result = await service.applyConfiguredConnectionEnv({}, 'codex');
expect(lookupPreferred).toHaveBeenCalledWith('OPENAI_API_KEY');
expect(result.OPENAI_API_KEY).toBe('openai-stored-key');
expect(result.CLAUDE_CODE_CODEX_BACKEND).toBe('auto');
expect(result.CLAUDE_CODE_CODEX_API_KEY_BETA).toBe('1');
expect(result.CODEX_API_KEY).toBe('openai-stored-key');
});
it('keeps the configured Codex backend and strips OPENAI_API_KEY in oauth mode', async () => {
it('keeps ambient OpenAI credentials for native Codex launches', async () => {
const { ProviderConnectionService } =
await import('@main/services/runtime/ProviderConnectionService');
@ -337,145 +220,22 @@ describe('ProviderConnectionService', () => {
lookupPreferred: vi.fn().mockResolvedValue(null),
} as never,
{
getConfig: () => createConfig('auto', {
codexApiKeyBetaEnabled: true,
codexAuthMode: 'oauth',
codexRuntimeBackend: 'api',
}),
getConfig: () => createConfig('auto'),
} as never
);
const result = await service.applyConfiguredConnectionEnv(
{
OPENAI_API_KEY: 'shell-openai-key',
CLAUDE_CODE_CODEX_BACKEND: 'auto',
},
'codex'
);
expect(result.OPENAI_API_KEY).toBeUndefined();
expect(result.CLAUDE_CODE_CODEX_BACKEND).toBe('auto');
expect(result.CLAUDE_CODE_CODEX_API_KEY_BETA).toBe('1');
expect(result.OPENAI_API_KEY).toBe('shell-openai-key');
expect(result.CODEX_API_KEY).toBe('shell-openai-key');
});
it('reports a missing Codex API key when beta api_key mode is enabled', async () => {
const { ProviderConnectionService } =
await import('@main/services/runtime/ProviderConnectionService');
const service = new ProviderConnectionService(
{
lookupPreferred: vi.fn().mockResolvedValue(null),
} as never,
{
getConfig: () =>
createConfig('auto', {
codexApiKeyBetaEnabled: true,
codexAuthMode: 'api_key',
codexRuntimeBackend: 'api',
}),
} as never
);
const issue = await service.getConfiguredConnectionIssue({}, 'codex');
expect(issue).toContain('Codex API key mode is enabled');
expect(issue).toContain('OPENAI_API_KEY');
});
it('augments PTY env for Codex without rewriting the configured backend in oauth mode', async () => {
const { ProviderConnectionService } =
await import('@main/services/runtime/ProviderConnectionService');
const service = new ProviderConnectionService(
{
lookupPreferred: vi.fn().mockResolvedValue(null),
} as never,
{
getConfig: () =>
createConfig('auto', {
codexApiKeyBetaEnabled: true,
codexAuthMode: 'oauth',
codexRuntimeBackend: 'api',
}),
} as never
);
const result = await service.augmentConfiguredConnectionEnv(
{
OPENAI_API_KEY: 'shell-key',
},
'codex'
);
expect(result.OPENAI_API_KEY).toBe('shell-key');
expect(result.CLAUDE_CODE_CODEX_BACKEND).toBeUndefined();
expect(result.CLAUDE_CODE_CODEX_API_KEY_BETA).toBe('1');
});
it('exposes Codex connection modes when codex-native is selected even without the old API beta toggle', async () => {
const { ProviderConnectionService } =
await import('@main/services/runtime/ProviderConnectionService');
const service = new ProviderConnectionService(
{
lookupPreferred: vi.fn().mockResolvedValue(null),
} as never,
{
getConfig: () =>
createConfig('auto', {
codexApiKeyBetaEnabled: false,
codexAuthMode: 'oauth',
codexRuntimeBackend: 'codex-native',
}),
} as never
);
const info = await service.getConnectionInfo('codex');
expect(info).toMatchObject({
configurableAuthModes: ['oauth', 'api_key'],
configuredAuthMode: 'oauth',
apiKeyBetaEnabled: false,
});
});
it('mirrors a stored OpenAI key into CODEX_API_KEY for codex-native without changing the selected backend', async () => {
const lookupPreferred = vi.fn().mockResolvedValue({
envVarName: 'OPENAI_API_KEY',
value: 'openai-stored-key',
});
const { ProviderConnectionService } =
await import('@main/services/runtime/ProviderConnectionService');
const service = new ProviderConnectionService(
{
lookupPreferred,
} as never,
{
getConfig: () =>
createConfig('auto', {
codexApiKeyBetaEnabled: false,
codexAuthMode: 'api_key',
codexRuntimeBackend: 'codex-native',
}),
} as never
);
const result = await service.applyConfiguredConnectionEnv(
{
CLAUDE_CODE_CODEX_BACKEND: 'codex-native',
},
'codex'
);
expect(lookupPreferred).toHaveBeenCalledWith('OPENAI_API_KEY');
expect(result.OPENAI_API_KEY).toBe('openai-stored-key');
expect(result.CODEX_API_KEY).toBe('openai-stored-key');
expect(result.CLAUDE_CODE_CODEX_BACKEND).toBe('codex-native');
expect(result.CLAUDE_CODE_CODEX_API_KEY_BETA).toBeUndefined();
});
it('accepts CODEX_API_KEY as the native external credential source for codex-native', async () => {
it('accepts CODEX_API_KEY as the native external credential source for Codex', async () => {
getCachedShellEnvMock.mockReturnValue({
CODEX_API_KEY: 'native-key',
});
@ -488,12 +248,7 @@ describe('ProviderConnectionService', () => {
lookupPreferred: vi.fn().mockResolvedValue(null),
} as never,
{
getConfig: () =>
createConfig('auto', {
codexApiKeyBetaEnabled: false,
codexAuthMode: 'api_key',
codexRuntimeBackend: 'codex-native',
}),
getConfig: () => createConfig('auto'),
} as never
);
@ -510,4 +265,46 @@ describe('ProviderConnectionService', () => {
expect(info.apiKeySourceLabel).toBe('Detected from CODEX_API_KEY');
expect(issue).toBeNull();
});
it('reports a missing native Codex credential when neither OPENAI_API_KEY nor CODEX_API_KEY exist', async () => {
const { ProviderConnectionService } =
await import('@main/services/runtime/ProviderConnectionService');
const service = new ProviderConnectionService(
{
lookupPreferred: vi.fn().mockResolvedValue(null),
} as never,
{
getConfig: () => createConfig('auto'),
} as never
);
const issue = await service.getConfiguredConnectionIssue({}, 'codex');
expect(issue).toContain('Codex native requires OPENAI_API_KEY or CODEX_API_KEY');
});
it('augments PTY env for native Codex without dropping existing OpenAI credentials', async () => {
const { ProviderConnectionService } =
await import('@main/services/runtime/ProviderConnectionService');
const service = new ProviderConnectionService(
{
lookupPreferred: vi.fn().mockResolvedValue(null),
} as never,
{
getConfig: () => createConfig('auto'),
} as never
);
const result = await service.augmentConfiguredConnectionEnv(
{
OPENAI_API_KEY: 'shell-key',
},
'codex'
);
expect(result.OPENAI_API_KEY).toBe('shell-key');
expect(result.CODEX_API_KEY).toBe('shell-key');
});
});

View file

@ -25,7 +25,7 @@ vi.mock('../../../../src/main/services/infrastructure/ConfigManager', () => ({
runtime: {
providerBackends: {
gemini: 'cli',
codex: 'adapter',
codex: 'codex-native',
},
},
}),
@ -185,17 +185,16 @@ describe('buildProviderAwareCliEnv', () => {
expect(augmentAllConfiguredConnectionEnvMock).toHaveBeenCalledWith(
expect.objectContaining({
CLAUDE_CODE_GEMINI_BACKEND: 'api',
CLAUDE_CODE_CODEX_BACKEND: 'adapter',
CLAUDE_CODE_CODEX_BACKEND: 'codex-native',
})
);
expect(result.env.CLAUDE_CODE_GEMINI_BACKEND).toBe('api');
expect(result.env.CLAUDE_CODE_CODEX_BACKEND).toBe('adapter');
expect(result.env.CLAUDE_CODE_CODEX_BACKEND).toBe('codex-native');
});
it('preserves codex-native internal unlock env across provider-aware child env building', async () => {
it('preserves codex-native backend env across provider-aware child env building', async () => {
buildEnrichedEnvMock.mockReturnValue({
PATH: '/usr/bin',
CLAUDE_CODE_CODEX_NATIVE_INTERNAL_UNLOCK: '1',
});
const { buildProviderAwareCliEnv } = await import(
@ -207,12 +206,11 @@ describe('buildProviderAwareCliEnv', () => {
expect(applyConfiguredConnectionEnvMock).toHaveBeenCalledWith(
expect.objectContaining({
CLAUDE_CODE_CODEX_BACKEND: 'adapter',
CLAUDE_CODE_CODEX_NATIVE_INTERNAL_UNLOCK: '1',
CLAUDE_CODE_CODEX_BACKEND: 'codex-native',
}),
'codex',
undefined
);
expect(result.env.CLAUDE_CODE_CODEX_NATIVE_INTERNAL_UNLOCK).toBe('1');
expect(result.env.CLAUDE_CODE_CODEX_BACKEND).toBe('codex-native');
});
});

View file

@ -1013,7 +1013,7 @@ describe('TeamProvisioningService', () => {
expect(launchArgs).toContain(leadSessionId);
});
it('skips --resume when the persisted runtime backend lane changed', async () => {
it('keeps --resume when a persisted legacy Codex backend normalizes to codex-native', async () => {
allowConsoleLogs();
const teamName = 'resume-backend-change-team';
const leadSessionId = 'lead-session-backend-change';
@ -1066,8 +1066,8 @@ describe('TeamProvisioningService', () => {
const launchArgs = vi.mocked(spawnCli).mock.calls.at(-1)?.[1] as string[];
expect(launchArgs).toBeTruthy();
expect(launchArgs).not.toContain('--resume');
expect(launchArgs).not.toContain(leadSessionId);
expect(launchArgs).toContain('--resume');
expect(launchArgs).toContain(leadSessionId);
});
it('seeds the current lead session id immediately when launch resumes an existing session', async () => {

View file

@ -166,21 +166,21 @@ function createApiKeyMisconfiguredProvider(
statusMessage:
providerId === 'anthropic'
? 'Anthropic API key mode is enabled, but no ANTHROPIC_API_KEY is configured.'
: 'Codex API key mode is enabled, but no OPENAI_API_KEY is configured.',
: 'Codex native runtime requires OPENAI_API_KEY or CODEX_API_KEY.',
models: [],
canLoginFromUi: true,
canLoginFromUi: providerId === 'anthropic',
capabilities: {
teamLaunch: true,
oneShot: true,
},
connection: {
supportsOAuth: true,
supportsOAuth: providerId === 'anthropic',
supportsApiKey: true,
configurableAuthModes:
providerId === 'anthropic' ? ['auto', 'oauth', 'api_key'] : ['oauth', 'api_key'],
configuredAuthMode: 'api_key',
apiKeyBetaAvailable: providerId === 'codex' ? true : undefined,
apiKeyBetaEnabled: providerId === 'codex' ? true : undefined,
providerId === 'anthropic' ? ['auto', 'oauth', 'api_key'] : [],
configuredAuthMode: providerId === 'anthropic' ? 'api_key' : null,
apiKeyBetaAvailable: undefined,
apiKeyBetaEnabled: undefined,
apiKeyConfigured: false,
apiKeySource: null,
apiKeySourceLabel: null,
@ -194,22 +194,22 @@ function createApiKeyModeProviderIssue(providerId: 'anthropic' | 'codex'): Recor
statusMessage:
providerId === 'anthropic'
? 'Anthropic API key was rejected by the runtime.'
: 'OpenAI API key was rejected by the runtime.',
: 'Codex native runtime is unavailable because the configured API key was rejected.',
connection: {
...(createApiKeyMisconfiguredProvider(providerId) as { connection: Record<string, unknown> })
.connection,
apiKeyConfigured: true,
apiKeySource: 'stored',
apiKeySourceLabel:
providerId === 'anthropic' ? 'Stored Anthropic API key' : 'Stored OpenAI API key',
providerId === 'anthropic' ? 'Stored Anthropic API key' : 'Stored Codex API key',
},
};
}
function createCodexNativeRolloutProvider(
overrides?: Partial<Record<string, unknown>> & {
state?: 'ready' | 'locked' | 'authentication-required' | 'runtime-missing' | 'degraded';
audience?: 'general' | 'internal';
state?: 'ready' | 'authentication-required' | 'runtime-missing' | 'degraded';
audience?: 'general';
selectable?: boolean;
available?: boolean;
statusMessage?: string | null;
@ -220,23 +220,16 @@ function createCodexNativeRolloutProvider(
providerId: 'codex',
displayName: 'Codex',
supported: true,
authenticated:
overrides?.state === 'ready' || overrides?.state === 'locked' || overrides?.available === true,
authMethod:
overrides?.state === 'ready' || overrides?.state === 'locked' || overrides?.available === true
? 'api_key'
: null,
authenticated: overrides?.state === 'ready' || overrides?.available === true,
authMethod: overrides?.state === 'ready' || overrides?.available === true ? 'api_key' : null,
verificationState:
overrides?.state === 'ready' || overrides?.state === 'locked' || overrides?.available === true
? 'verified'
: 'unknown',
statusMessage: overrides?.statusMessage ?? 'Ready but locked',
detailMessage: overrides?.detailMessage ?? 'Internal rollout only.',
overrides?.state === 'ready' || overrides?.available === true ? 'verified' : 'unknown',
statusMessage: overrides?.statusMessage ?? 'Ready',
detailMessage:
overrides?.detailMessage ?? 'Codex native runtime is ready through the local codex exec seam.',
selectedBackendId: 'codex-native',
resolvedBackendId:
overrides?.state === 'ready' || overrides?.state === 'locked' || overrides?.available === true
? 'codex-native'
: null,
overrides?.state === 'ready' || overrides?.available === true ? 'codex-native' : null,
models: ['gpt-5-codex'],
canLoginFromUi: false,
capabilities: {
@ -248,17 +241,18 @@ function createCodexNativeRolloutProvider(
id: 'codex-native',
label: 'Codex native',
description: 'Use codex exec JSON mode.',
selectable: overrides?.selectable ?? false,
recommended: false,
selectable: overrides?.selectable ?? true,
recommended: true,
available: overrides?.available ?? true,
state: overrides?.state ?? 'locked',
audience: overrides?.audience ?? 'internal',
statusMessage: overrides?.statusMessage ?? 'Ready but locked',
detailMessage: overrides?.detailMessage ?? 'Internal rollout only.',
state: overrides?.state ?? 'ready',
audience: overrides?.audience ?? 'general',
statusMessage: overrides?.statusMessage ?? 'Ready',
detailMessage:
overrides?.detailMessage ?? 'Codex native runtime is ready through the local codex exec seam.',
},
],
backend:
overrides?.state === 'ready' || overrides?.state === 'locked' || overrides?.available === true
overrides?.state === 'ready' || overrides?.available === true
? {
kind: 'codex-native',
label: 'Codex native',
@ -403,12 +397,12 @@ describe('CLI status visibility during completed install state', () => {
const onSelectBackend = providerRuntimeSettingsDialogProps?.onSelectBackend;
expect(onSelectBackend).toBeTypeOf('function');
await expect(onSelectBackend?.('codex', 'api')).rejects.toThrow(
await expect(onSelectBackend?.('codex', 'codex-native')).rejects.toThrow(
'Runtime updated, but failed to refresh provider status.'
);
expect(storeState.updateConfig).toHaveBeenCalledWith('runtime', {
providerBackends: {
codex: 'api',
codex: 'codex-native',
},
});
expect(storeState.fetchCliProviderStatus).toHaveBeenCalledWith('codex');
@ -770,7 +764,7 @@ describe('CLI status visibility during completed install state', () => {
});
});
it('shows runtime model availability badges on the dashboard', async () => {
it('shows runtime model availability badges on the dashboard without hiding native Codex models', async () => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
storeState.cliInstallerState = 'idle';
storeState.cliStatus = createInstalledCliStatus({
@ -786,7 +780,7 @@ describe('CLI status visibility during completed install state', () => {
displayName: 'Codex',
supported: true,
authenticated: true,
authMethod: 'oauth_token',
authMethod: 'api_key',
verificationState: 'verified',
modelVerificationState: 'verified',
statusMessage: null,
@ -806,15 +800,15 @@ describe('CLI status visibility during completed install state', () => {
checkedAt: '2026-04-16T12:00:00.000Z',
},
],
canLoginFromUi: true,
canLoginFromUi: false,
capabilities: {
teamLaunch: true,
oneShot: true,
},
backend: {
kind: 'openai',
label: 'OpenAI',
endpointLabel: 'chatgpt.com/backend-api/codex/responses',
kind: 'codex-native',
label: 'Codex native',
endpointLabel: 'codex exec --json',
},
},
],
@ -830,9 +824,9 @@ describe('CLI status visibility during completed install state', () => {
});
expect(host.textContent).toContain('5.4');
expect(host.textContent).not.toContain('5.1-codex-max');
expect(host.textContent).toContain('5.1-codex-max');
expect(host.textContent).not.toContain('5.2-codex');
expect(host.textContent).not.toContain('Unavailable');
expect(host.textContent).toContain('Unavailable');
await act(async () => {
root.unmount();
@ -840,7 +834,7 @@ describe('CLI status visibility during completed install state', () => {
});
});
it('keeps dashboard codex-native rollout truth explicit for locked internal lanes', async () => {
it('keeps dashboard codex-native truth explicit for ready native lanes', async () => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
storeState.cliInstallerState = 'idle';
storeState.cliStatus = createInstalledCliStatus({
@ -852,10 +846,12 @@ describe('CLI status visibility during completed install state', () => {
authLoggedIn: true,
providers: [
createCodexNativeRolloutProvider({
state: 'locked',
state: 'ready',
available: true,
selectable: false,
statusMessage: 'Ready but locked',
selectable: true,
audience: 'general',
statusMessage: 'Ready',
detailMessage: 'Codex native runtime is ready through the local codex exec seam.',
}),
],
});
@ -869,8 +865,8 @@ describe('CLI status visibility during completed install state', () => {
await Promise.resolve();
});
expect(host.textContent).toContain('Ready but locked');
expect(host.textContent).toContain('Runtime: Codex native - internal - locked');
expect(host.textContent).toContain('Ready');
expect(host.textContent).toContain('Runtime: Codex native');
expect(host.textContent).not.toContain('Connected via API key');
await act(async () => {
@ -898,7 +894,7 @@ describe('CLI status visibility during completed install state', () => {
available: false,
selectable: false,
statusMessage: 'Codex CLI not found',
detailMessage: 'Install the codex CLI before enabling the lane.',
detailMessage: 'Codex native runtime requires the codex CLI binary to be installed and discoverable.',
backend: null,
resolvedBackendId: null,
}),
@ -915,7 +911,7 @@ describe('CLI status visibility during completed install state', () => {
});
expect(host.textContent).toContain('Codex CLI not found');
expect(host.textContent).toContain('Runtime: Codex native - internal - runtime missing');
expect(host.textContent).toContain('Runtime: Codex native - runtime missing');
expect(host.textContent).not.toContain('Connected via API key');
await act(async () => {

View file

@ -32,16 +32,6 @@ function createCodexProvider(
selectedBackendId: 'codex-native',
resolvedBackendId: 'codex-native',
availableBackends: [
{
id: 'auto',
label: 'Auto',
description: 'Automatically choose the best backend.',
selectable: true,
recommended: true,
available: true,
state: 'ready',
audience: 'general',
},
{
id: 'codex-native',
label: 'Codex native',
@ -109,29 +99,9 @@ describe('ProviderRuntimeBackendSelector helpers', () => {
);
});
it('hides codex legacy fallbacks from normal selectors when native is selected', () => {
it('shows the single native-only codex option after phase 4 migration', () => {
const provider = createCodexProvider({
availableBackends: [
{
id: 'auto',
label: 'Auto',
description: 'Automatically choose the best backend.',
selectable: true,
recommended: false,
available: true,
state: 'ready',
audience: 'general',
},
{
id: 'api',
label: 'OpenAI API',
description: 'Legacy public Responses API fallback.',
selectable: true,
recommended: false,
available: true,
state: 'ready',
audience: 'internal',
},
{
id: 'codex-native',
label: 'Codex native',
@ -150,21 +120,11 @@ describe('ProviderRuntimeBackendSelector helpers', () => {
]);
});
it('keeps an explicitly selected legacy codex fallback readable during the soak', () => {
it('normalizes migrated legacy codex fallback rows to Codex native', () => {
const provider = createCodexProvider({
selectedBackendId: 'api',
resolvedBackendId: 'api',
selectedBackendId: 'codex-native',
resolvedBackendId: 'codex-native',
availableBackends: [
{
id: 'api',
label: 'OpenAI API',
description: 'Legacy public Responses API fallback.',
selectable: true,
recommended: false,
available: true,
state: 'ready',
audience: 'internal',
},
{
id: 'codex-native',
label: 'Codex native',
@ -179,10 +139,8 @@ describe('ProviderRuntimeBackendSelector helpers', () => {
});
const visibleOptions = getVisibleProviderRuntimeBackendOptions(provider);
expect(visibleOptions.map((option) => option.id)).toEqual(['api', 'codex-native']);
expect(getOptionDisplayLabel(provider, visibleOptions[0], null)).toBe(
'Legacy OpenAI fallback'
);
expect(getProviderRuntimeBackendSummary(provider)).toBe('Legacy OpenAI fallback - internal');
expect(visibleOptions.map((option) => option.id)).toEqual(['codex-native']);
expect(getOptionDisplayLabel(provider, visibleOptions[0], null)).toBe('Codex native');
expect(getProviderRuntimeBackendSummary(provider)).toBe('Codex native');
});
});

View file

@ -138,6 +138,8 @@ vi.mock('@renderer/components/runtime/ProviderRuntimeBackendSelector', () => ({
'Select runtime backend'
),
getProviderRuntimeBackendSummary: () => null,
getVisibleProviderRuntimeBackendOptions: (provider: CliProviderStatus) =>
provider.availableBackends ?? [],
}));
vi.mock('@renderer/components/common/ProviderBrandLogo', () => ({
@ -155,6 +157,10 @@ function createCodexProvider(
overrides?: Partial<CliProviderStatus['connection']> & {
authenticated?: boolean;
authMethod?: string | null;
selectedBackendId?: string | null;
resolvedBackendId?: string | null;
availableBackends?: CliProviderStatus['availableBackends'];
canLoginFromUi?: boolean;
}
): CliProviderStatus {
return {
@ -162,30 +168,45 @@ function createCodexProvider(
displayName: 'Codex',
supported: true,
authenticated: overrides?.authenticated ?? true,
authMethod: overrides?.authMethod ?? 'oauth_token',
authMethod: overrides?.authMethod ?? 'api_key',
verificationState: 'verified',
statusMessage: 'Connected',
statusMessage: 'Codex native ready',
models: ['gpt-5-codex'],
canLoginFromUi: true,
canLoginFromUi: overrides?.canLoginFromUi ?? false,
capabilities: {
teamLaunch: true,
oneShot: true,
extensions: createDefaultCliExtensionCapabilities(),
},
selectedBackendId: 'auto',
resolvedBackendId: 'adapter',
availableBackends: [],
selectedBackendId: overrides?.selectedBackendId ?? 'codex-native',
resolvedBackendId: overrides?.resolvedBackendId ?? 'codex-native',
availableBackends:
overrides?.availableBackends ??
[
{
id: 'codex-native',
label: 'Codex native',
description: 'Use the local codex exec JSON seam.',
selectable: true,
recommended: true,
available: true,
state: 'ready',
audience: 'general',
statusMessage: 'Codex native ready',
},
],
externalRuntimeDiagnostics: [],
backend: {
kind: 'adapter',
label: 'Codex subscription',
kind: 'codex-native',
label: 'Codex native',
},
connection: {
supportsOAuth: true,
supportsOAuth: false,
supportsApiKey: true,
configurableAuthModes: overrides?.apiKeyBetaEnabled ? ['oauth', 'api_key'] : [],
configuredAuthMode: overrides?.configuredAuthMode ?? null,
apiKeyBetaAvailable: true,
apiKeyBetaEnabled: overrides?.apiKeyBetaEnabled ?? false,
configurableAuthModes: [],
configuredAuthMode: null,
apiKeyBetaAvailable: undefined,
apiKeyBetaEnabled: undefined,
apiKeyConfigured: overrides?.apiKeyConfigured ?? false,
apiKeySource: overrides?.apiKeySource ?? null,
apiKeySourceLabel: overrides?.apiKeySourceLabel ?? null,
@ -217,6 +238,7 @@ function createAnthropicProvider(
selectedBackendId: null,
resolvedBackendId: null,
availableBackends: [],
externalRuntimeDiagnostics: [],
backend: null,
connection: {
supportsOAuth: true,
@ -266,6 +288,7 @@ function createGeminiProvider(): CliProviderStatus {
available: true,
},
],
externalRuntimeDiagnostics: [],
backend: {
kind: 'api',
label: 'Gemini API',
@ -292,11 +315,7 @@ function findButtonByText(container: HTMLElement, text: string): HTMLButtonEleme
return button;
}
function countOccurrences(text: string, fragment: string): number {
return text.split(fragment).length - 1;
}
describe('ProviderRuntimeSettingsDialog Codex connection flows', () => {
describe('ProviderRuntimeSettingsDialog', () => {
beforeEach(() => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
storeState.appConfig = {
@ -346,139 +365,6 @@ describe('ProviderRuntimeSettingsDialog Codex connection flows', () => {
vi.unstubAllGlobals();
});
it('switches Codex into api_key mode when enabling API key mode', async () => {
const host = document.createElement('div');
document.body.appendChild(host);
const root = createRoot(host);
const onRefreshProvider = vi.fn(() => Promise.resolve(undefined));
await act(async () => {
root.render(
React.createElement(ProviderRuntimeSettingsDialog, {
open: true,
onOpenChange: vi.fn(),
providers: [
createCodexProvider({
apiKeyBetaEnabled: false,
configuredAuthMode: null,
apiKeyConfigured: true,
apiKeySource: 'environment',
apiKeySourceLabel: 'Detected from OPENAI_API_KEY',
}),
],
initialProviderId: 'codex',
onSelectBackend: vi.fn(),
onRefreshProvider,
})
);
await Promise.resolve();
});
await act(async () => {
findButtonByText(host, 'Enable API key mode').click();
await Promise.resolve();
});
expect(storeState.updateConfig).toHaveBeenCalledWith('providerConnections', {
codex: {
apiKeyBetaEnabled: true,
authMode: 'api_key',
},
});
expect(onRefreshProvider).toHaveBeenCalledWith('codex');
});
it('shows a loading message while switching Codex to OpenAI API key', async () => {
const host = document.createElement('div');
document.body.appendChild(host);
const root = createRoot(host);
let resolveUpdate: (() => void) | null = null;
storeState.appConfig.providerConnections.codex = {
apiKeyBetaEnabled: true,
authMode: 'oauth',
};
storeState.updateConfig = vi.fn(
() =>
new Promise<void>((resolve) => {
resolveUpdate = resolve;
})
);
await act(async () => {
root.render(
React.createElement(ProviderRuntimeSettingsDialog, {
open: true,
onOpenChange: vi.fn(),
providers: [
createCodexProvider({
apiKeyBetaEnabled: true,
configuredAuthMode: 'oauth',
apiKeyConfigured: true,
apiKeySource: 'environment',
apiKeySourceLabel: 'Detected from OPENAI_API_KEY',
}),
],
initialProviderId: 'codex',
onSelectBackend: vi.fn(),
onRefreshProvider: vi.fn(() => Promise.resolve(undefined)),
})
);
await Promise.resolve();
});
await act(async () => {
findButtonByText(host, 'OpenAI API key').click();
await Promise.resolve();
});
expect(host.textContent).toContain('Switching to OpenAI API key...');
expect(host.textContent).toContain('Switching...');
await act(async () => {
resolveUpdate?.();
await Promise.resolve();
});
});
it('removes duplicate Codex summary and API key source text when connection cards are visible', async () => {
const host = document.createElement('div');
document.body.appendChild(host);
const root = createRoot(host);
storeState.appConfig.providerConnections.codex = {
apiKeyBetaEnabled: true,
authMode: 'oauth',
};
await act(async () => {
root.render(
React.createElement(ProviderRuntimeSettingsDialog, {
open: true,
onOpenChange: vi.fn(),
providers: [
createCodexProvider({
apiKeyBetaEnabled: true,
configuredAuthMode: 'oauth',
apiKeyConfigured: true,
apiKeySource: 'environment',
apiKeySourceLabel: 'Detected from OPENAI_API_KEY',
}),
],
initialProviderId: 'codex',
onSelectBackend: vi.fn(),
onRefreshProvider: vi.fn(() => Promise.resolve(undefined)),
})
);
await Promise.resolve();
});
expect(host.textContent).not.toContain('Current runtime: Codex subscription');
expect(host.textContent).not.toContain('Mode: Codex subscription');
expect(host.textContent).not.toContain('Runtime: Default adapter');
expect(countOccurrences(host.textContent ?? '', 'Using Codex subscription')).toBe(0);
expect(countOccurrences(host.textContent ?? '', 'Detected from OPENAI_API_KEY')).toBe(1);
expect(host.textContent).not.toContain('Connected');
});
it('renders provider logos inside the provider tabs', async () => {
const host = document.createElement('div');
document.body.appendChild(host);
@ -502,10 +388,11 @@ describe('ProviderRuntimeSettingsDialog Codex connection flows', () => {
expect(host.querySelector('[data-testid="provider-logo-codex"]')).not.toBeNull();
});
it('renders Anthropics connection methods as cards and hides the empty runtime section', async () => {
it('renders anthropic connection cards and can switch to API key mode', async () => {
const host = document.createElement('div');
document.body.appendChild(host);
const root = createRoot(host);
const onRefreshProvider = vi.fn(() => Promise.resolve(undefined));
await act(async () => {
root.render(
@ -516,27 +403,70 @@ describe('ProviderRuntimeSettingsDialog Codex connection flows', () => {
createAnthropicProvider({
configuredAuthMode: 'auto',
apiKeyConfigured: true,
apiKeySource: 'environment',
apiKeySourceLabel: 'Detected from ANTHROPIC_API_KEY',
apiKeySource: 'stored',
apiKeySourceLabel: 'Stored in app',
}),
],
initialProviderId: 'anthropic',
onSelectBackend: vi.fn(),
onRefreshProvider: vi.fn(() => Promise.resolve(undefined)),
onRefreshProvider,
})
);
await Promise.resolve();
});
expect(host.textContent).toContain('Connection method');
expect(host.textContent).toContain('Auto');
expect(host.textContent).toContain('Anthropic subscription');
expect(host.textContent).toContain('API key');
expect(host.textContent).not.toContain('Authentication method');
expect(host.textContent).not.toContain('Runtime backend is not configurable');
expect(host.textContent).not.toContain('Mode: Auto');
expect(countOccurrences(host.textContent ?? '', 'Using Anthropic subscription')).toBe(1);
expect(countOccurrences(host.textContent ?? '', 'Detected from ANTHROPIC_API_KEY')).toBe(1);
await act(async () => {
findButtonByText(host, 'API key').click();
await Promise.resolve();
});
expect(storeState.updateConfig).toHaveBeenCalledWith('providerConnections', {
anthropic: {
authMode: 'api_key',
},
});
expect(onRefreshProvider).toHaveBeenCalledWith('anthropic');
});
it('shows native-only Codex connection copy and API-key management without login actions', async () => {
const host = document.createElement('div');
document.body.appendChild(host);
const root = createRoot(host);
await act(async () => {
root.render(
React.createElement(ProviderRuntimeSettingsDialog, {
open: true,
onOpenChange: vi.fn(),
providers: [
createCodexProvider({
authenticated: false,
authMethod: null,
apiKeyConfigured: true,
apiKeySource: 'stored',
apiKeySourceLabel: 'Stored in app',
}),
],
initialProviderId: 'codex',
onSelectBackend: vi.fn(),
onRefreshProvider: vi.fn(() => Promise.resolve(undefined)),
onRequestLogin: vi.fn(),
})
);
await Promise.resolve();
});
expect(host.textContent).toContain(
'Codex launches always use the native runtime now. Manage API-key credentials here before launching teams or one-shot Codex runs.'
);
expect(host.textContent).toContain('Set API key');
expect(host.textContent).not.toContain('Connection method');
expect(host.textContent).not.toContain('Connect Codex');
expect(host.textContent).not.toContain('Reconnect Codex');
});
it('keeps the API key icon container square', async () => {
@ -565,79 +495,6 @@ describe('ProviderRuntimeSettingsDialog Codex connection flows', () => {
expect(icon?.className).toContain('shrink-0');
});
it('switches Anthropic to API key mode from the connection cards', async () => {
const host = document.createElement('div');
document.body.appendChild(host);
const root = createRoot(host);
const onRefreshProvider = vi.fn(() => Promise.resolve(undefined));
await act(async () => {
root.render(
React.createElement(ProviderRuntimeSettingsDialog, {
open: true,
onOpenChange: vi.fn(),
providers: [
createAnthropicProvider({
configuredAuthMode: 'auto',
apiKeyConfigured: true,
apiKeySource: 'stored',
apiKeySourceLabel: 'Stored in app',
}),
],
initialProviderId: 'anthropic',
onSelectBackend: vi.fn(),
onRefreshProvider,
})
);
await Promise.resolve();
});
await act(async () => {
findButtonByText(host, 'API key').click();
await Promise.resolve();
});
expect(storeState.updateConfig).toHaveBeenCalledWith('providerConnections', {
anthropic: {
authMode: 'api_key',
},
});
expect(onRefreshProvider).toHaveBeenCalledWith('anthropic');
});
it('does not show Connect Anthropic when Auto is already authenticated via API key', async () => {
const host = document.createElement('div');
document.body.appendChild(host);
const root = createRoot(host);
await act(async () => {
root.render(
React.createElement(ProviderRuntimeSettingsDialog, {
open: true,
onOpenChange: vi.fn(),
providers: [
createAnthropicProvider({
authenticated: true,
authMethod: 'api_key',
configuredAuthMode: 'auto',
apiKeyConfigured: true,
apiKeySource: 'environment',
apiKeySourceLabel: 'Detected from ANTHROPIC_API_KEY',
}),
],
initialProviderId: 'anthropic',
onSelectBackend: vi.fn(),
onRefreshProvider: vi.fn(() => Promise.resolve(undefined)),
onRequestLogin: vi.fn(),
})
);
await Promise.resolve();
});
expect(host.textContent).not.toContain('Connect Anthropic');
expect(host.textContent).not.toContain('Reconnect Anthropic');
});
it('keeps the API key form open and shows an error when delete fails', async () => {
const host = document.createElement('div');
document.body.appendChild(host);
@ -646,10 +503,10 @@ describe('ProviderRuntimeSettingsDialog Codex connection flows', () => {
storeState.apiKeys = [
{
id: 'key-1',
envVarName: 'ANTHROPIC_API_KEY',
envVarName: 'OPENAI_API_KEY',
scope: 'user',
name: 'Anthropic API Key',
maskedValue: 'sk-ant-...1234',
name: 'OpenAI API Key',
maskedValue: 'sk-proj-...1234',
createdAt: Date.now(),
},
];
@ -661,14 +518,13 @@ describe('ProviderRuntimeSettingsDialog Codex connection flows', () => {
open: true,
onOpenChange: vi.fn(),
providers: [
createAnthropicProvider({
configuredAuthMode: 'api_key',
createCodexProvider({
apiKeyConfigured: true,
apiKeySource: 'stored',
apiKeySourceLabel: 'Stored in app',
}),
],
initialProviderId: 'anthropic',
initialProviderId: 'codex',
onSelectBackend: vi.fn(),
onRefreshProvider,
})
@ -691,289 +547,6 @@ describe('ProviderRuntimeSettingsDialog Codex connection flows', () => {
expect(onRefreshProvider).not.toHaveBeenCalled();
});
it('shows a deleted stored key as removed even if provider refresh fails afterwards', async () => {
const host = document.createElement('div');
document.body.appendChild(host);
const root = createRoot(host);
const onRefreshProvider = vi.fn(() => Promise.reject(new Error('refresh failed')));
storeState.apiKeys = [
{
id: 'key-1',
envVarName: 'ANTHROPIC_API_KEY',
scope: 'user',
name: 'Anthropic API Key',
maskedValue: 'sk-ant-...1234',
createdAt: Date.now(),
},
];
storeState.deleteApiKey = vi.fn((id: string) => {
storeState.apiKeys = storeState.apiKeys.filter((entry) => entry.id !== id);
return Promise.resolve(undefined);
});
await act(async () => {
root.render(
React.createElement(ProviderRuntimeSettingsDialog, {
open: true,
onOpenChange: vi.fn(),
providers: [
createAnthropicProvider({
configuredAuthMode: 'api_key',
apiKeyConfigured: true,
apiKeySource: 'stored',
apiKeySourceLabel: 'Stored in app',
}),
],
initialProviderId: 'anthropic',
onSelectBackend: vi.fn(),
onRefreshProvider,
})
);
await Promise.resolve();
});
await act(async () => {
findButtonByText(host, 'Replace key').click();
await Promise.resolve();
});
await act(async () => {
findButtonByText(host, 'Delete').click();
await Promise.resolve();
});
expect(host.textContent).toContain('API key deleted, but failed to refresh provider status.');
expect(host.textContent).toContain('Not configured');
expect(host.textContent).not.toContain('sk-ant-...1234');
});
it('shows a connection error and skips refresh when auth mode update fails', async () => {
const host = document.createElement('div');
document.body.appendChild(host);
const root = createRoot(host);
const onRefreshProvider = vi.fn(() => Promise.resolve(undefined));
storeState.updateConfig = vi.fn(() => Promise.reject(new Error('Config update failed')));
await act(async () => {
root.render(
React.createElement(ProviderRuntimeSettingsDialog, {
open: true,
onOpenChange: vi.fn(),
providers: [
createAnthropicProvider({
configuredAuthMode: 'auto',
apiKeyConfigured: true,
apiKeySource: 'stored',
apiKeySourceLabel: 'Stored in app',
}),
],
initialProviderId: 'anthropic',
onSelectBackend: vi.fn(),
onRefreshProvider,
})
);
await Promise.resolve();
});
await act(async () => {
findButtonByText(host, 'API key').click();
await Promise.resolve();
});
expect(host.textContent).toContain('Config update failed');
expect(host.textContent).not.toContain('Switching to API key...');
expect(host.textContent).not.toContain('Switching...');
expect(onRefreshProvider).not.toHaveBeenCalled();
});
it('clears Codex beta loading state when enabling API key mode fails early', async () => {
const host = document.createElement('div');
document.body.appendChild(host);
const root = createRoot(host);
const onRefreshProvider = vi.fn(() => Promise.resolve(undefined));
storeState.updateConfig = vi.fn(() => Promise.reject(new Error('Config update failed')));
await act(async () => {
root.render(
React.createElement(ProviderRuntimeSettingsDialog, {
open: true,
onOpenChange: vi.fn(),
providers: [
createCodexProvider({
apiKeyBetaEnabled: false,
configuredAuthMode: null,
apiKeyConfigured: true,
apiKeySource: 'stored',
apiKeySourceLabel: 'Stored in app',
}),
],
initialProviderId: 'codex',
onSelectBackend: vi.fn(),
onRefreshProvider,
})
);
await Promise.resolve();
});
await act(async () => {
findButtonByText(host, 'Enable API key mode').click();
await Promise.resolve();
});
expect(host.textContent).toContain('Config update failed');
expect(host.textContent).not.toContain('Enabling API key mode...');
expect(onRefreshProvider).not.toHaveBeenCalled();
});
it('reports refresh failures separately after a successful auth mode update', async () => {
const host = document.createElement('div');
document.body.appendChild(host);
const root = createRoot(host);
const onRefreshProvider = vi.fn(() => Promise.reject(new Error('refresh failed')));
storeState.updateConfig = vi.fn((section: string, data: Record<string, unknown>) => {
if (section === 'providerConnections') {
const nextProviderConnections = data as Partial<StoreState['appConfig']['providerConnections']>;
storeState.appConfig = {
...storeState.appConfig,
providerConnections: {
anthropic: {
...storeState.appConfig.providerConnections.anthropic,
...(nextProviderConnections.anthropic ?? {}),
},
codex: {
...storeState.appConfig.providerConnections.codex,
...(nextProviderConnections.codex ?? {}),
},
},
};
}
return Promise.resolve(undefined);
});
await act(async () => {
root.render(
React.createElement(ProviderRuntimeSettingsDialog, {
open: true,
onOpenChange: vi.fn(),
providers: [
createAnthropicProvider({
configuredAuthMode: 'auto',
apiKeyConfigured: true,
apiKeySource: 'stored',
apiKeySourceLabel: 'Stored in app',
}),
],
initialProviderId: 'anthropic',
onSelectBackend: vi.fn(),
onRefreshProvider,
})
);
await Promise.resolve();
});
await act(async () => {
findButtonByText(host, 'API key').click();
await Promise.resolve();
});
expect(storeState.updateConfig).toHaveBeenCalled();
expect(onRefreshProvider).toHaveBeenCalledWith('anthropic');
expect(host.textContent).not.toContain('Mode: API key');
expect(host.textContent).toContain('API keySelected');
expect(host.textContent).toContain('Connection updated, but failed to refresh provider status.');
expect(host.textContent).not.toContain('Failed to update connection');
});
it('shows subscription recovery actions when OAuth mode is selected but stale status still says API key', async () => {
const host = document.createElement('div');
document.body.appendChild(host);
const root = createRoot(host);
const onRefreshProvider = vi.fn(() => Promise.reject(new Error('refresh failed')));
await act(async () => {
root.render(
React.createElement(ProviderRuntimeSettingsDialog, {
open: true,
onOpenChange: vi.fn(),
providers: [
createAnthropicProvider({
authenticated: true,
authMethod: 'api_key',
configuredAuthMode: 'auto',
apiKeyConfigured: true,
apiKeySource: 'stored',
apiKeySourceLabel: 'Stored in app',
}),
],
initialProviderId: 'anthropic',
onSelectBackend: vi.fn(),
onRefreshProvider,
onRequestLogin: vi.fn(),
})
);
await Promise.resolve();
});
await act(async () => {
findButtonByText(host, 'Anthropic subscription').click();
await Promise.resolve();
});
expect(host.textContent).not.toContain('Mode: Anthropic subscription');
expect(host.textContent).toContain('Anthropic subscriptionSelected');
expect(host.textContent).toContain('Connect Anthropic');
expect(host.textContent).toContain(
'Anthropic subscription mode is selected. Sign in with Anthropic to use this provider.'
);
expect(host.textContent).toContain('Connection updated, but failed to refresh provider status.');
});
it('keeps the Codex API key mode UI in sync with config when refresh fails after enabling beta', async () => {
const host = document.createElement('div');
document.body.appendChild(host);
const root = createRoot(host);
const onRefreshProvider = vi.fn(() => Promise.reject(new Error('refresh failed')));
await act(async () => {
root.render(
React.createElement(ProviderRuntimeSettingsDialog, {
open: true,
onOpenChange: vi.fn(),
providers: [
createCodexProvider({
apiKeyBetaEnabled: false,
configuredAuthMode: null,
apiKeyConfigured: true,
apiKeySource: 'stored',
apiKeySourceLabel: 'Stored in app',
}),
],
initialProviderId: 'codex',
onSelectBackend: vi.fn(),
onRefreshProvider,
})
);
await Promise.resolve();
});
await act(async () => {
findButtonByText(host, 'Enable API key mode').click();
await Promise.resolve();
});
expect(storeState.updateConfig).toHaveBeenCalledWith('providerConnections', {
codex: {
apiKeyBetaEnabled: true,
authMode: 'api_key',
},
});
expect(host.textContent).not.toContain('Mode: API key');
expect(host.textContent).toContain('Selected');
expect(host.textContent).toContain('Disable API key mode');
expect(host.textContent).toContain('Connection updated, but failed to refresh provider status.');
});
it('shows a runtime error when backend selection refresh fails after a successful update', async () => {
const host = document.createElement('div');
document.body.appendChild(host);

View file

@ -6,6 +6,7 @@ import {
getProviderCredentialSummary,
getProviderCurrentRuntimeSummary,
isConnectionManagedRuntimeProvider,
shouldShowProviderConnectAction,
} from '@renderer/components/runtime/providerConnectionUi';
import { createDefaultCliExtensionCapabilities } from '@shared/utils/providerExtensionCapabilities';
@ -57,6 +58,8 @@ function createCodexProvider(
resolvedBackendId?: string | null;
availableBackends?: CliProviderStatus['availableBackends'];
backend?: CliProviderStatus['backend'];
statusMessage?: string | null;
canLoginFromUi?: boolean;
}
): CliProviderStatus {
return {
@ -64,33 +67,45 @@ function createCodexProvider(
displayName: 'Codex',
supported: true,
authenticated: overrides?.authenticated ?? true,
authMethod: overrides?.authMethod ?? 'oauth_token',
authMethod: overrides?.authMethod ?? 'api_key',
verificationState: 'verified',
statusMessage: 'Connected',
statusMessage: overrides?.statusMessage ?? 'Codex native ready',
models: ['gpt-5-codex'],
canLoginFromUi: true,
canLoginFromUi: overrides?.canLoginFromUi ?? false,
capabilities: {
teamLaunch: true,
oneShot: true,
extensions: createDefaultCliExtensionCapabilities(),
},
selectedBackendId: overrides?.selectedBackendId ?? 'auto',
resolvedBackendId: overrides?.resolvedBackendId ?? 'adapter',
availableBackends: overrides?.availableBackends ?? [],
selectedBackendId: overrides?.selectedBackendId ?? 'codex-native',
resolvedBackendId: overrides?.resolvedBackendId ?? 'codex-native',
availableBackends:
overrides?.availableBackends ??
[
{
id: 'codex-native',
label: 'Codex native',
description: 'Use codex exec JSON mode.',
selectable: true,
recommended: true,
available: true,
state: 'ready',
audience: 'general',
statusMessage: 'Codex native ready',
},
],
externalRuntimeDiagnostics: [],
backend:
overrides?.backend ??
({
kind: 'adapter',
label: 'Codex subscription',
kind: 'codex-native',
label: 'Codex native',
} satisfies NonNullable<CliProviderStatus['backend']>),
connection: {
supportsOAuth: true,
supportsOAuth: false,
supportsApiKey: true,
configurableAuthModes: ['oauth', 'api_key'],
configuredAuthMode: overrides?.configuredAuthMode ?? 'oauth',
apiKeyBetaAvailable: true,
apiKeyBetaEnabled: overrides?.apiKeyBetaEnabled ?? true,
configurableAuthModes: [],
configuredAuthMode: overrides?.configuredAuthMode ?? null,
apiKeyConfigured: overrides?.apiKeyConfigured ?? false,
apiKeySource: overrides?.apiKeySource ?? null,
apiKeySourceLabel: overrides?.apiKeySourceLabel ?? null,
@ -124,202 +139,62 @@ describe('providerConnectionUi', () => {
);
});
it('prefers the actual Codex runtime once the provider is already authenticated', () => {
it('treats Codex as lane-managed and hides the old connection-managed runtime summary', () => {
const provider = createCodexProvider({
authenticated: true,
authMethod: 'oauth_token',
configuredAuthMode: 'api_key',
apiKeyConfigured: true,
apiKeySource: 'stored',
apiKeySourceLabel: 'Stored in app',
});
expect(getProviderCurrentRuntimeSummary(provider)).toBe(
'Current runtime: Codex subscription'
);
});
it('shows the selected Codex runtime when the provider is not authenticated yet', () => {
const provider = createCodexProvider({
authenticated: false,
authMethod: null,
configuredAuthMode: 'api_key',
apiKeyConfigured: true,
apiKeySource: 'stored',
apiKeySourceLabel: 'Stored in app',
});
expect(getProviderCurrentRuntimeSummary(provider)).toBe('Selected runtime: OpenAI API key');
});
it('reports an environment Anthropic API key without claiming it is stored in Manage', () => {
const provider = createAnthropicProvider({
authenticated: true,
authMethod: 'oauth_token',
configuredAuthMode: 'oauth',
apiKeyConfigured: true,
apiKeySource: 'environment',
apiKeySourceLabel: 'Detected from ANTHROPIC_API_KEY',
});
expect(getProviderCredentialSummary(provider)).toBe('Detected from ANTHROPIC_API_KEY');
});
it('reports an environment Codex API key without claiming it is stored in Manage', () => {
const provider = createCodexProvider({
authenticated: true,
authMethod: 'oauth_token',
configuredAuthMode: 'oauth',
apiKeyConfigured: true,
apiKeySource: 'environment',
apiKeySourceLabel: 'Detected from OPENAI_API_KEY',
});
expect(getProviderCredentialSummary(provider)).toBe('Detected from OPENAI_API_KEY');
});
it('tells the user when a stored Codex key exists but API key mode is still disabled', () => {
const provider = createCodexProvider({
authenticated: true,
authMethod: 'oauth_token',
configuredAuthMode: 'oauth',
apiKeyBetaEnabled: false,
apiKeyConfigured: true,
apiKeySource: 'stored',
apiKeySourceLabel: 'Stored in app',
});
expect(getProviderCredentialSummary(provider)).toBe(
'OpenAI API key is saved in Manage. Enable API key mode to use it.'
);
});
it('treats Codex as lane-managed once explicit backend options exist', () => {
const provider = createCodexProvider({
availableBackends: [
{
id: 'auto',
label: 'Auto',
description: 'Automatically choose the best backend.',
selectable: true,
recommended: true,
available: true,
},
{
id: 'codex-native',
label: 'Codex native',
description: 'Use codex exec JSON mode.',
selectable: true,
recommended: false,
available: true,
},
],
selectedBackendId: 'codex-native',
resolvedBackendId: 'codex-native',
backend: {
kind: 'codex-native',
label: 'Codex native',
},
});
expect(isConnectionManagedRuntimeProvider(provider)).toBe(false);
expect(getProviderCurrentRuntimeSummary(provider)).toBeNull();
});
it('does not tell the user to enable API key mode when codex-native is already selected', () => {
it('shows stored Codex API keys as immediately usable for native runtime', () => {
const provider = createCodexProvider({
apiKeyBetaEnabled: false,
configuredAuthMode: 'api_key',
apiKeyConfigured: true,
apiKeySource: 'stored',
apiKeySourceLabel: 'Stored in app',
selectedBackendId: 'codex-native',
resolvedBackendId: 'codex-native',
availableBackends: [
{
id: 'codex-native',
label: 'Codex native',
description: 'Use codex exec JSON mode.',
selectable: true,
recommended: false,
available: true,
},
],
backend: {
kind: 'codex-native',
label: 'Codex native',
},
});
expect(getProviderCredentialSummary(provider)).toBe('Saved API key available in Manage');
});
it('keeps locked codex-native lanes visible instead of flattening them to connected-via-api-key', () => {
it('shows environment Codex credentials without claiming they are stored in Manage', () => {
const provider = createCodexProvider({
authenticated: true,
authMethod: 'api_key',
statusMessage: 'Codex native runtime ready',
selectedBackendId: 'codex-native',
resolvedBackendId: 'codex-native',
availableBackends: [
{
id: 'codex-native',
label: 'Codex native',
description: 'Use codex exec JSON mode.',
selectable: false,
recommended: false,
available: true,
state: 'locked',
audience: 'internal',
statusMessage: 'Ready but locked',
detailMessage: 'Internal rollout only.',
},
],
backend: {
kind: 'codex-native',
label: 'Codex native',
},
apiKeyConfigured: true,
apiKeySource: 'environment',
apiKeySourceLabel: 'Detected from CODEX_API_KEY',
});
expect(formatProviderStatusText(provider)).toBe('Ready but locked');
expect(getProviderCredentialSummary(provider)).toBe('Detected from CODEX_API_KEY');
});
it('keeps internal codex-native ready state explicit instead of showing a generic auth label', () => {
it('surfaces native backend status instead of flattening Codex to connected-via-api-key text', () => {
const provider = createCodexProvider({
authenticated: true,
authMethod: 'api_key',
statusMessage: 'Codex native runtime ready',
selectedBackendId: 'codex-native',
resolvedBackendId: 'codex-native',
availableBackends: [
{
id: 'codex-native',
label: 'Codex native',
description: 'Use codex exec JSON mode.',
selectable: true,
recommended: false,
recommended: true,
available: true,
state: 'ready',
audience: 'internal',
statusMessage: 'Ready for internal use',
detailMessage: 'Internal rollout only.',
audience: 'general',
statusMessage: 'Codex native ready',
},
],
backend: {
kind: 'codex-native',
label: 'Codex native',
},
});
expect(formatProviderStatusText(provider)).toBe('Ready for internal use');
expect(formatProviderStatusText(provider)).toBe('Codex native ready');
});
it('surfaces native auth-required state from the selected backend option', () => {
const provider = createCodexProvider({
authenticated: false,
authMethod: null,
statusMessage: 'Codex native runtime not ready',
selectedBackendId: 'codex-native',
statusMessage: 'Codex native not ready',
resolvedBackendId: null,
availableBackends: [
{
@ -327,10 +202,10 @@ describe('providerConnectionUi', () => {
label: 'Codex native',
description: 'Use codex exec JSON mode.',
selectable: false,
recommended: false,
recommended: true,
available: false,
state: 'authentication-required',
audience: 'internal',
audience: 'general',
statusMessage: 'Authentication required',
detailMessage: 'Set CODEX_API_KEY.',
},
@ -341,30 +216,13 @@ describe('providerConnectionUi', () => {
expect(formatProviderStatusText(provider)).toBe('Authentication required');
});
it('surfaces native runtime-missing state from the selected backend option', () => {
it('never shows a Connect action for Codex after the native-only cutover', () => {
const provider = createCodexProvider({
authenticated: false,
authMethod: null,
statusMessage: 'Codex native runtime not ready',
selectedBackendId: 'codex-native',
resolvedBackendId: null,
availableBackends: [
{
id: 'codex-native',
label: 'Codex native',
description: 'Use codex exec JSON mode.',
selectable: false,
recommended: false,
available: false,
state: 'runtime-missing',
audience: 'internal',
statusMessage: 'Codex CLI not found',
detailMessage: 'Install the codex CLI before enabling the lane.',
},
],
backend: null,
canLoginFromUi: false,
});
expect(formatProviderStatusText(provider)).toBe('Codex CLI not found');
expect(shouldShowProviderConnectAction(provider)).toBe(false);
});
});

View file

@ -6,7 +6,6 @@ import {
} from '@renderer/components/team/dialogs/TeamModelSelector';
import {
GPT_5_1_CODEX_MINI_UI_DISABLED_REASON,
GPT_5_1_CODEX_MAX_CHATGPT_UI_DISABLED_REASON,
GPT_5_2_CODEX_UI_DISABLED_REASON,
GPT_5_3_CODEX_SPARK_UI_DISABLED_REASON,
getAvailableTeamProviderModels,
@ -49,15 +48,15 @@ describe('formatTeamModelSummary', () => {
expect(getTeamModelUiDisabledReason('anthropic', 'gpt-5.1-codex-mini')).toBeNull();
});
it('disables 5.1 Codex Max only on the Codex ChatGPT subscription path', () => {
const chatgptCodexProviderStatus = {
it('keeps 5.1 Codex Max available on the native Codex path', () => {
const nativeCodexProviderStatus = {
providerId: 'codex' as const,
models: ['gpt-5.4', 'gpt-5.1-codex-max'],
authMethod: 'oauth_token' as const,
authMethod: 'api_key' as const,
backend: {
kind: 'adapter',
label: 'Default adapter',
endpointLabel: 'chatgpt.com/backend-api/codex/responses',
kind: 'codex-native',
label: 'Codex native',
endpointLabel: 'codex exec --json',
},
modelVerificationState: 'verified' as const,
modelAvailability: [],
@ -66,14 +65,14 @@ describe('formatTeamModelSummary', () => {
};
expect(
getTeamModelUiDisabledReason('codex', 'gpt-5.1-codex-max', chatgptCodexProviderStatus)
).toBe(GPT_5_1_CODEX_MAX_CHATGPT_UI_DISABLED_REASON);
expect(normalizeTeamModelForUi('codex', 'gpt-5.1-codex-max', chatgptCodexProviderStatus)).toBe(
''
getTeamModelUiDisabledReason('codex', 'gpt-5.1-codex-max', nativeCodexProviderStatus)
).toBeNull();
expect(normalizeTeamModelForUi('codex', 'gpt-5.1-codex-max', nativeCodexProviderStatus)).toBe(
'gpt-5.1-codex-max'
);
expect(
getTeamModelSelectionError('codex', 'gpt-5.1-codex-max', chatgptCodexProviderStatus)
).toContain('Temporarily disabled for team agents when using Codex ChatGPT subscription');
getTeamModelSelectionError('codex', 'gpt-5.1-codex-max', nativeCodexProviderStatus)
).toBeNull();
expect(getTeamModelUiDisabledReason('codex', 'gpt-5.1-codex-max')).toBeNull();
});
@ -88,11 +87,11 @@ describe('formatTeamModelSummary', () => {
const codexProviderStatus = {
providerId: 'codex' as const,
models: ['gpt-5.4', 'gpt-5.3-codex'],
authMethod: 'oauth_token' as const,
authMethod: 'api_key' as const,
backend: {
kind: 'adapter',
label: 'Default adapter',
endpointLabel: 'chatgpt.com/backend-api/codex/responses',
kind: 'codex-native',
label: 'Codex native',
endpointLabel: 'codex exec --json',
},
modelVerificationState: 'verified' as const,
modelAvailability: [

View file

@ -259,17 +259,17 @@ describe('TeamModelSelector disabled Codex models', () => {
});
});
it('shows 5.1 Codex Max as a disabled tile on the ChatGPT subscription path', async () => {
it('keeps 5.1 Codex Max selectable on the native Codex path', async () => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
storeState.cliStatus = {
providers: [
{
providerId: 'codex',
authMethod: 'oauth_token',
authMethod: 'api_key',
backend: {
kind: 'adapter',
label: 'Default adapter',
endpointLabel: 'chatgpt.com/backend-api/codex/responses',
kind: 'codex-native',
label: 'Codex native',
endpointLabel: 'codex exec --json',
},
models: ['gpt-5.4', 'gpt-5.1-codex-max'],
modelVerificationState: 'idle',
@ -295,23 +295,20 @@ describe('TeamModelSelector disabled Codex models', () => {
await Promise.resolve();
});
const disabledButton = Array.from(host.querySelectorAll('button')).find((button) =>
const button = Array.from(host.querySelectorAll('button')).find((button) =>
button.textContent?.includes('5.1 Codex Max')
);
expect(disabledButton).not.toBeNull();
expect(disabledButton?.getAttribute('aria-disabled')).toBe('true');
expect(disabledButton?.textContent).toContain('Disabled');
expect(disabledButton?.getAttribute('title')).toContain(
'Not available with Codex ChatGPT subscription'
);
expect(button).not.toBeNull();
expect(button?.getAttribute('aria-disabled')).toBe('false');
expect(button?.textContent).not.toContain('Disabled');
await act(async () => {
disabledButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
button?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
await Promise.resolve();
});
expect(onValueChange).not.toHaveBeenCalled();
expect(onValueChange).toHaveBeenCalledWith('gpt-5.1-codex-max');
await act(async () => {
root.unmount();

View file

@ -51,7 +51,7 @@ describe('ProvisioningProviderStatusList', () => {
{
providerId: 'codex',
status: 'failed',
backendSummary: 'Default adapter',
backendSummary: 'Codex native',
details: [
'5.4 Mini - verified',
'5.1 Codex Max - unavailable - Not available with Codex ChatGPT subscription',
@ -64,7 +64,7 @@ describe('ProvisioningProviderStatusList', () => {
});
expect(host.textContent).toContain(
'Codex (Default adapter): Selected model checks - 1 model unavailable, 1 verified'
'Codex (Codex native): Selected model checks - 1 model unavailable, 1 verified'
);
expect(host.textContent).toContain('5.4 Mini - verified');
expect(host.textContent).toContain(
@ -110,7 +110,7 @@ describe('ProvisioningProviderStatusList', () => {
{
providerId: 'codex',
status: 'notes',
backendSummary: 'Default adapter',
backendSummary: 'Codex native',
details: ['5.3 Codex - check failed - Model verification timed out'],
},
],
@ -120,7 +120,7 @@ describe('ProvisioningProviderStatusList', () => {
});
expect(host.textContent).toContain(
'Codex (Default adapter): Selected model checks - 1 model timed out'
'Codex (Codex native): Selected model checks - 1 model timed out'
);
expect(host.textContent).toContain('5.3 Codex - check failed - Model verification timed out');
@ -143,7 +143,7 @@ describe('ProvisioningProviderStatusList', () => {
{
providerId: 'codex',
status: 'notes',
backendSummary: 'Default adapter',
backendSummary: 'Codex native',
details: [
'Preflight check for `orchestrator-cli -p` did not complete. Proceeding anyway. Details: Timeout running: orchestrator-cli -p Output only the single word PONG. --output-format text --model gpt-5.4-mini --max-turns 1 --no-session-persistence',
],
@ -154,7 +154,7 @@ describe('ProvisioningProviderStatusList', () => {
await Promise.resolve();
});
expect(host.textContent).toContain('Codex (Default adapter): CLI preflight did not complete');
expect(host.textContent).toContain('Codex (Codex native): CLI preflight did not complete');
await act(async () => {
root.unmount();
@ -179,36 +179,26 @@ describe('ProvisioningProviderStatusList', () => {
selectable: false,
recommended: false,
available: true,
state: 'locked',
audience: 'internal',
statusMessage: 'Ready but locked',
state: 'ready',
audience: 'general',
statusMessage: 'Ready',
},
],
})
).toBe('Codex native - internal, locked');
).toBe('Codex native');
});
it('marks explicit legacy codex fallback summaries as legacy during the soak', () => {
it('normalizes persisted legacy codex fallback summaries to Codex native', () => {
expect(
getProvisioningProviderBackendSummary({
providerId: 'codex',
selectedBackendId: 'api',
resolvedBackendId: 'api',
backend: {
kind: 'api',
label: 'OpenAI API',
kind: 'codex-native',
label: 'Codex native',
},
availableBackends: [
{
id: 'api',
label: 'OpenAI API',
description: 'Legacy public Responses API fallback.',
selectable: true,
recommended: false,
available: true,
state: 'ready',
audience: 'internal',
},
{
id: 'codex-native',
label: 'Codex native',
@ -221,6 +211,6 @@ describe('ProvisioningProviderStatusList', () => {
},
],
})
).toBe('Legacy OpenAI fallback - internal');
).toBe('Codex native');
});
});

View file

@ -27,7 +27,7 @@ describe('buildProviderPrepareModelCacheKey', () => {
const input = {
cwd: '/tmp/project',
providerId: 'codex' as const,
backendSummary: 'Default adapter',
backendSummary: 'Codex native',
limitContext: false,
};

View file

@ -41,7 +41,7 @@ describe('resolveMemberRuntimeSummary', () => {
const spawnEntry = createSpawnEntry({ runtimeModel: 'claude-opus-4-7', runtimeAlive: true });
expect(resolveMemberRuntimeSummary(member, undefined, spawnEntry)).toBe(
'Anthropic · Opus 4.7 · Medium'
'Anthropic · Opus 4.7 · Medium · Codex native'
);
});
@ -61,7 +61,9 @@ describe('resolveMemberRuntimeSummary', () => {
runtimeModel: 'gpt-5.4-mini',
});
expect(resolveMemberRuntimeSummary(member, undefined, spawnEntry)).toBe('5.4 Mini · Medium');
expect(resolveMemberRuntimeSummary(member, undefined, spawnEntry)).toBe(
'5.4 Mini · Medium · Codex native'
);
});
it('appends runtime memory when a live process snapshot is available', () => {
@ -77,7 +79,7 @@ describe('resolveMemberRuntimeSummary', () => {
};
expect(resolveMemberRuntimeSummary(member, undefined, undefined, runtimeEntry)).toBe(
'5.4 Mini · Medium · 256.0 MB'
'5.4 Mini · Medium · Codex native · 256.0 MB'
);
});
@ -99,7 +101,7 @@ describe('resolveMemberRuntimeSummary', () => {
).toBe('5.4 Mini · Medium · Codex native');
});
it('marks persisted legacy Codex lanes as legacy fallbacks in the runtime summary', () => {
it('normalizes persisted legacy Codex lanes to the native runtime summary', () => {
const member = createMember({ model: 'gpt-5.4-mini' });
expect(
@ -114,6 +116,6 @@ describe('resolveMemberRuntimeSummary', () => {
},
undefined
)
).toBe('5.4 Mini · Medium · Legacy OpenAI fallback');
).toBe('5.4 Mini · Medium · Codex native');
});
});

View file

@ -15,11 +15,11 @@ function createCodexProviderStatus(
return {
providerId: 'codex',
models,
authMethod: 'oauth_token',
authMethod: 'api_key',
backend: {
kind: 'adapter',
label: 'Default adapter',
endpointLabel: 'chatgpt.com/backend-api/codex/responses',
kind: 'codex-native',
label: 'Codex native',
endpointLabel: 'codex exec --json',
},
authenticated: true,
supported: true,
@ -39,7 +39,7 @@ describe('teamModelAvailability', () => {
]);
});
it('filters Codex models that are UI-disabled even if runtime reports them', () => {
it('filters only the Codex models that remain UI-disabled on the native runtime path', () => {
const providerStatus = createCodexProviderStatus([
'gpt-5.4',
'gpt-5.3-codex-spark',
@ -48,16 +48,19 @@ describe('teamModelAvailability', () => {
'gpt-5.1-codex-max',
]);
expect(getAvailableTeamProviderModels('codex', providerStatus)).toEqual(['gpt-5.4']);
expect(getAvailableTeamProviderModels('codex', providerStatus)).toEqual([
'gpt-5.4',
'gpt-5.1-codex-max',
]);
});
it('keeps 5.1 Codex Max available outside the ChatGPT subscription path', () => {
it('keeps 5.1 Codex Max available on the native runtime path', () => {
const providerStatus = createCodexProviderStatus(['gpt-5.4', 'gpt-5.1-codex-max'], {
authMethod: 'api_key',
backend: {
kind: 'openai',
label: 'OpenAI',
endpointLabel: 'api.openai.com/v1/responses',
kind: 'codex-native',
label: 'Codex native',
endpointLabel: 'codex exec --json',
},
});