refactor(runtime): remove legacy codex lanes
This commit is contained in:
parent
e90bdc5b7f
commit
1f7dd2100f
33 changed files with 816 additions and 1726 deletions
|
|
@ -18,6 +18,32 @@ Record the chosen direction for improving Codex integration in the multimodel ru
|
||||||
- Assessment: `🎯 9 🛡️ 9 🧠 7`
|
- Assessment: `🎯 9 🛡️ 9 🧠 7`
|
||||||
- Estimated first serious wave: `2200-4500` lines across `agent_teams_orchestrator`, `claude_team`, and `plugin-kit-ai`
|
- 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
|
## One-Page Summary
|
||||||
|
|
||||||
We are **not** doing a one-shot swap from the current Codex backend to `@openai/codex-sdk / codex exec`.
|
We are **not** doing a one-shot swap from the current Codex backend to `@openai/codex-sdk / codex exec`.
|
||||||
|
|
|
||||||
199
docs/research/codex-native-runtime-phase-4-signoff-evidence.md
Normal file
199
docs/research/codex-native-runtime-phase-4-signoff-evidence.md
Normal 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.
|
||||||
|
|
@ -5,6 +5,8 @@
|
||||||
|
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
|
import { migrateProviderBackendId } from '@shared/utils/providerBackend';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
AppConfig,
|
AppConfig,
|
||||||
DisplayConfig,
|
DisplayConfig,
|
||||||
|
|
@ -450,11 +452,13 @@ function validateRuntimeSection(data: unknown): ValidationSuccess<'runtime'> | V
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
error:
|
error: 'runtime.providerBackends.codex must be one of: codex-native',
|
||||||
'runtime.providerBackends.codex must be one of: auto, adapter, api, codex-native',
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
providerBackends.codex = backendId;
|
providerBackends.codex = migrateProviderBackendId(
|
||||||
|
'codex',
|
||||||
|
backendId
|
||||||
|
) as RuntimeConfig['providerBackends']['codex'];
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,7 @@ import {
|
||||||
PROTECTED_CLI_FLAGS,
|
PROTECTED_CLI_FLAGS,
|
||||||
} from '@shared/utils/cliArgsParser';
|
} from '@shared/utils/cliArgsParser';
|
||||||
import { createLogger } from '@shared/utils/logger';
|
import { createLogger } from '@shared/utils/logger';
|
||||||
|
import { migrateProviderBackendId } from '@shared/utils/providerBackend';
|
||||||
import { isRateLimitMessage } from '@shared/utils/rateLimitDetector';
|
import { isRateLimitMessage } from '@shared/utils/rateLimitDetector';
|
||||||
import {
|
import {
|
||||||
buildStandaloneSlashCommandMeta,
|
buildStandaloneSlashCommandMeta,
|
||||||
|
|
@ -1434,6 +1435,17 @@ async function handleLaunchTeam(
|
||||||
const membersMeta = await membersStore.getMeta(tn);
|
const membersMeta = await membersStore.getMeta(tn);
|
||||||
const members = membersMeta?.members ?? [];
|
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 = {
|
const createRequest: TeamCreateRequest = {
|
||||||
teamName: tn,
|
teamName: tn,
|
||||||
displayName: meta?.displayName,
|
displayName: meta?.displayName,
|
||||||
|
|
@ -1441,20 +1453,11 @@ async function handleLaunchTeam(
|
||||||
color: meta?.color,
|
color: meta?.color,
|
||||||
cwd,
|
cwd,
|
||||||
prompt: typeof payload.prompt === 'string' ? payload.prompt.trim() || undefined : undefined,
|
prompt: typeof payload.prompt === 'string' ? payload.prompt.trim() || undefined : undefined,
|
||||||
providerId:
|
providerId: resolvedProviderId,
|
||||||
payload.providerId === 'codex'
|
providerBackendId: migrateProviderBackendId(
|
||||||
? 'codex'
|
resolvedProviderId,
|
||||||
: payload.providerId === 'gemini'
|
providerBackendValidation.value ?? meta?.providerBackendId ?? membersMeta?.providerBackendId
|
||||||
? 'gemini'
|
),
|
||||||
: meta?.providerId === 'codex'
|
|
||||||
? 'codex'
|
|
||||||
: meta?.providerId === 'gemini'
|
|
||||||
? 'gemini'
|
|
||||||
: 'anthropic',
|
|
||||||
providerBackendId:
|
|
||||||
providerBackendValidation.value ??
|
|
||||||
meta?.providerBackendId ??
|
|
||||||
membersMeta?.providerBackendId,
|
|
||||||
model: typeof payload.model === 'string' ? payload.model.trim() || undefined : undefined,
|
model: typeof payload.model === 'string' ? payload.model.trim() || undefined : undefined,
|
||||||
effort: isValidEffort(payload.effort) ? payload.effort : undefined,
|
effort: isValidEffort(payload.effort) ? payload.effort : undefined,
|
||||||
limitContext: typeof payload.limitContext === 'boolean' ? payload.limitContext : undefined,
|
limitContext: typeof payload.limitContext === 'boolean' ? payload.limitContext : undefined,
|
||||||
|
|
@ -3926,6 +3929,8 @@ async function handleGetSavedRequest(
|
||||||
const membersMeta = await membersStore.getMeta(tn);
|
const membersMeta = await membersStore.getMeta(tn);
|
||||||
const members = membersMeta?.members ?? [];
|
const members = membersMeta?.members ?? [];
|
||||||
|
|
||||||
|
const resolvedProviderId = meta.providerId ?? 'anthropic';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
|
|
@ -3935,8 +3940,11 @@ async function handleGetSavedRequest(
|
||||||
color: meta.color,
|
color: meta.color,
|
||||||
cwd: meta.cwd,
|
cwd: meta.cwd,
|
||||||
prompt: meta.prompt,
|
prompt: meta.prompt,
|
||||||
providerId: meta.providerId ?? 'anthropic',
|
providerId: resolvedProviderId,
|
||||||
providerBackendId: meta.providerBackendId ?? membersMeta?.providerBackendId,
|
providerBackendId: migrateProviderBackendId(
|
||||||
|
resolvedProviderId,
|
||||||
|
meta.providerBackendId ?? membersMeta?.providerBackendId
|
||||||
|
),
|
||||||
model: meta.model,
|
model: meta.model,
|
||||||
effort: meta.effort as TeamCreateRequest['effort'],
|
effort: meta.effort as TeamCreateRequest['effort'],
|
||||||
skipPermissions: meta.skipPermissions,
|
skipPermissions: meta.skipPermissions,
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
|
|
||||||
import { getClaudeBasePath, setClaudeBasePathOverride } from '@main/utils/pathDecoder';
|
import { getClaudeBasePath, setClaudeBasePathOverride } from '@main/utils/pathDecoder';
|
||||||
import { validateRegexPattern } from '@main/utils/regexValidation';
|
import { validateRegexPattern } from '@main/utils/regexValidation';
|
||||||
|
import { migrateProviderBackendId } from '@shared/utils/providerBackend';
|
||||||
import { createLogger } from '@shared/utils/logger';
|
import { createLogger } from '@shared/utils/logger';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as fsp from 'fs/promises';
|
import * as fsp from 'fs/promises';
|
||||||
|
|
@ -226,7 +227,7 @@ export interface GeneralConfig {
|
||||||
export interface RuntimeConfig {
|
export interface RuntimeConfig {
|
||||||
providerBackends: {
|
providerBackends: {
|
||||||
gemini: 'auto' | 'api' | 'cli-sdk';
|
gemini: 'auto' | 'api' | 'cli-sdk';
|
||||||
codex: 'auto' | 'adapter' | 'api' | 'codex-native';
|
codex: 'codex-native';
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -575,6 +576,10 @@ export class ConfigManager {
|
||||||
providerBackends: {
|
providerBackends: {
|
||||||
...DEFAULT_CONFIG.runtime.providerBackends,
|
...DEFAULT_CONFIG.runtime.providerBackends,
|
||||||
...(loaded.runtime?.providerBackends ?? {}),
|
...(loaded.runtime?.providerBackends ?? {}),
|
||||||
|
codex: migrateProviderBackendId(
|
||||||
|
'codex',
|
||||||
|
loaded.runtime?.providerBackends?.codex
|
||||||
|
) as RuntimeConfig['providerBackends']['codex'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
display: {
|
display: {
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ const PROVIDER_CAPABILITIES: Record<
|
||||||
configurableAuthModes: ['auto', 'oauth', 'api_key'],
|
configurableAuthModes: ['auto', 'oauth', 'api_key'],
|
||||||
},
|
},
|
||||||
codex: {
|
codex: {
|
||||||
supportsOAuth: true,
|
supportsOAuth: false,
|
||||||
supportsApiKey: true,
|
supportsApiKey: true,
|
||||||
configurableAuthModes: [],
|
configurableAuthModes: [],
|
||||||
},
|
},
|
||||||
|
|
@ -42,7 +42,6 @@ const PROVIDER_API_KEY_ENV_VARS: Partial<Record<CliProviderId, string>> = {
|
||||||
gemini: 'GEMINI_API_KEY',
|
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_API_KEY_ENV_VAR = 'CODEX_API_KEY';
|
||||||
const CODEX_NATIVE_BACKEND_ID = 'codex-native';
|
const CODEX_NATIVE_BACKEND_ID = 'codex-native';
|
||||||
|
|
||||||
|
|
@ -65,8 +64,7 @@ export class ProviderConnectionService {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (providerId === 'codex') {
|
if (providerId === 'codex') {
|
||||||
const codexConnection = this.configManager.getConfig().providerConnections.codex;
|
return null;
|
||||||
return this.shouldExposeCodexConnectionModes() ? codexConnection.authMode : null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -109,27 +107,7 @@ export class ProviderConnectionService {
|
||||||
return env;
|
return env;
|
||||||
}
|
}
|
||||||
|
|
||||||
const codexConnection = this.configManager.getConfig().providerConnections.codex;
|
|
||||||
const codexRuntimeBackend = this.getConfiguredCodexRuntimeBackend(runtimeBackendOverride);
|
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 storedKey = await this.apiKeyService.lookupPreferred('OPENAI_API_KEY');
|
||||||
const existingOpenAiKey =
|
const existingOpenAiKey =
|
||||||
typeof env.OPENAI_API_KEY === 'string' && env.OPENAI_API_KEY.trim()
|
typeof env.OPENAI_API_KEY === 'string' && env.OPENAI_API_KEY.trim()
|
||||||
|
|
@ -147,11 +125,7 @@ export class ProviderConnectionService {
|
||||||
|
|
||||||
if (resolvedApiKey) {
|
if (resolvedApiKey) {
|
||||||
env.OPENAI_API_KEY = resolvedApiKey;
|
env.OPENAI_API_KEY = resolvedApiKey;
|
||||||
if (codexRuntimeBackend === CODEX_NATIVE_BACKEND_ID) {
|
env[CODEX_NATIVE_API_KEY_ENV_VAR] = resolvedApiKey;
|
||||||
env[CODEX_NATIVE_API_KEY_ENV_VAR] = resolvedApiKey;
|
|
||||||
} else {
|
|
||||||
delete env[CODEX_NATIVE_API_KEY_ENV_VAR];
|
|
||||||
}
|
|
||||||
return env;
|
return env;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -192,22 +166,7 @@ export class ProviderConnectionService {
|
||||||
return env;
|
return env;
|
||||||
}
|
}
|
||||||
|
|
||||||
const codexConnection = this.configManager.getConfig().providerConnections.codex;
|
|
||||||
const codexRuntimeBackend = this.getConfiguredCodexRuntimeBackend(runtimeBackendOverride);
|
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 storedKey = await this.apiKeyService.lookupPreferred('OPENAI_API_KEY');
|
||||||
const existingOpenAiKey =
|
const existingOpenAiKey =
|
||||||
typeof env.OPENAI_API_KEY === 'string' && env.OPENAI_API_KEY.trim()
|
typeof env.OPENAI_API_KEY === 'string' && env.OPENAI_API_KEY.trim()
|
||||||
|
|
@ -272,30 +231,18 @@ export class ProviderConnectionService {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const codexConnection = this.configManager.getConfig().providerConnections.codex;
|
|
||||||
const codexRuntimeBackend = this.getConfiguredCodexRuntimeBackend(runtimeBackendOverride);
|
const codexRuntimeBackend = this.getConfiguredCodexRuntimeBackend(runtimeBackendOverride);
|
||||||
if (
|
if (
|
||||||
!this.shouldExposeCodexConnectionModes(runtimeBackendOverride) ||
|
(typeof env.OPENAI_API_KEY === 'string' && env.OPENAI_API_KEY.trim()) ||
|
||||||
codexConnection.authMode !== 'api_key'
|
(typeof env[CODEX_NATIVE_API_KEY_ENV_VAR] === 'string' &&
|
||||||
) {
|
env[CODEX_NATIVE_API_KEY_ENV_VAR]?.trim())
|
||||||
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()
|
|
||||||
) {
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return codexRuntimeBackend === CODEX_NATIVE_BACKEND_ID
|
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 native requires OPENAI_API_KEY or CODEX_API_KEY. Add a stored or environment API key before launching Codex.'
|
||||||
: '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 requires OPENAI_API_KEY or CODEX_API_KEY. Add a stored or environment API key before launching Codex.';
|
||||||
}
|
}
|
||||||
|
|
||||||
async getConfiguredConnectionIssues(
|
async getConfiguredConnectionIssues(
|
||||||
|
|
@ -336,27 +283,17 @@ export class ProviderConnectionService {
|
||||||
const codexRuntimeBackend =
|
const codexRuntimeBackend =
|
||||||
providerId === 'codex' ? this.getConfiguredCodexRuntimeBackend() : null;
|
providerId === 'codex' ? this.getConfiguredCodexRuntimeBackend() : null;
|
||||||
const externalCredential = this.getExternalCredential(providerId, codexRuntimeBackend);
|
const externalCredential = this.getExternalCredential(providerId, codexRuntimeBackend);
|
||||||
const codexBetaEnabled =
|
|
||||||
providerId === 'codex'
|
|
||||||
? this.configManager.getConfig().providerConnections.codex.apiKeyBetaEnabled
|
|
||||||
: undefined;
|
|
||||||
const configurableAuthModes =
|
const configurableAuthModes =
|
||||||
providerId === 'codex' &&
|
providerId === 'codex' ? ([] as CliProviderAuthMode[]) : capabilities.configurableAuthModes;
|
||||||
(codexBetaEnabled || codexRuntimeBackend === CODEX_NATIVE_BACKEND_ID)
|
|
||||||
? (['oauth', 'api_key'] as CliProviderAuthMode[])
|
|
||||||
: capabilities.configurableAuthModes;
|
|
||||||
const configuredAuthMode =
|
const configuredAuthMode =
|
||||||
providerId === 'codex' &&
|
providerId === 'codex' ? null : this.getConfiguredAuthMode(providerId);
|
||||||
!(codexBetaEnabled || codexRuntimeBackend === CODEX_NATIVE_BACKEND_ID)
|
|
||||||
? null
|
|
||||||
: this.getConfiguredAuthMode(providerId);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...capabilities,
|
...capabilities,
|
||||||
configurableAuthModes,
|
configurableAuthModes,
|
||||||
configuredAuthMode,
|
configuredAuthMode,
|
||||||
apiKeyBetaAvailable: providerId === 'codex' ? true : undefined,
|
apiKeyBetaAvailable: providerId === 'codex' ? undefined : undefined,
|
||||||
apiKeyBetaEnabled: codexBetaEnabled,
|
apiKeyBetaEnabled: providerId === 'codex' ? undefined : undefined,
|
||||||
apiKeyConfigured: Boolean(storedApiKey?.value.trim() || externalCredential?.value.trim()),
|
apiKeyConfigured: Boolean(storedApiKey?.value.trim() || externalCredential?.value.trim()),
|
||||||
apiKeySource: storedApiKey?.value.trim()
|
apiKeySource: storedApiKey?.value.trim()
|
||||||
? 'stored'
|
? 'stored'
|
||||||
|
|
@ -380,31 +317,16 @@ export class ProviderConnectionService {
|
||||||
return this.apiKeyService.lookupPreferred(envVarName);
|
return this.apiKeyService.lookupPreferred(envVarName);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getConfiguredCodexRuntimeBackend(
|
private getConfiguredCodexRuntimeBackend(runtimeBackendOverride?: string | null): 'codex-native' {
|
||||||
runtimeBackendOverride?: string | null
|
if (runtimeBackendOverride === CODEX_NATIVE_BACKEND_ID) {
|
||||||
): 'auto' | 'adapter' | 'api' | 'codex-native' {
|
|
||||||
if (
|
|
||||||
runtimeBackendOverride === 'auto' ||
|
|
||||||
runtimeBackendOverride === 'adapter' ||
|
|
||||||
runtimeBackendOverride === 'api' ||
|
|
||||||
runtimeBackendOverride === CODEX_NATIVE_BACKEND_ID
|
|
||||||
) {
|
|
||||||
return runtimeBackendOverride;
|
return runtimeBackendOverride;
|
||||||
}
|
}
|
||||||
return this.configManager.getConfig().runtime.providerBackends.codex;
|
return CODEX_NATIVE_BACKEND_ID;
|
||||||
}
|
|
||||||
|
|
||||||
private shouldExposeCodexConnectionModes(runtimeBackendOverride?: string | null): boolean {
|
|
||||||
const config = this.configManager.getConfig();
|
|
||||||
return (
|
|
||||||
config.providerConnections.codex.apiKeyBetaEnabled ||
|
|
||||||
this.getConfiguredCodexRuntimeBackend(runtimeBackendOverride) === CODEX_NATIVE_BACKEND_ID
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getExternalCredential(
|
private getExternalCredential(
|
||||||
providerId: CliProviderId,
|
providerId: CliProviderId,
|
||||||
codexRuntimeBackend: 'auto' | 'adapter' | 'api' | 'codex-native' | null = null
|
codexRuntimeBackend: 'codex-native' | null = null
|
||||||
): ExternalCredential {
|
): ExternalCredential {
|
||||||
const shellEnv = getCachedShellEnv() ?? {};
|
const shellEnv = getCachedShellEnv() ?? {};
|
||||||
const sources = [shellEnv, process.env];
|
const sources = [shellEnv, process.env];
|
||||||
|
|
@ -440,14 +362,12 @@ export class ProviderConnectionService {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (providerId === 'codex') {
|
if (providerId === 'codex') {
|
||||||
if (codexRuntimeBackend === CODEX_NATIVE_BACKEND_ID) {
|
const nativeApiKey = findEnvValue(CODEX_NATIVE_API_KEY_ENV_VAR);
|
||||||
const nativeApiKey = findEnvValue(CODEX_NATIVE_API_KEY_ENV_VAR);
|
if (nativeApiKey) {
|
||||||
if (nativeApiKey) {
|
return {
|
||||||
return {
|
label: `Detected from ${CODEX_NATIVE_API_KEY_ENV_VAR}`,
|
||||||
label: `Detected from ${CODEX_NATIVE_API_KEY_ENV_VAR}`,
|
value: nativeApiKey,
|
||||||
value: nativeApiKey,
|
};
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiKey = findEnvValue('OPENAI_API_KEY');
|
const apiKey = findEnvValue('OPENAI_API_KEY');
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { FileReadTimeoutError, readFileUtf8WithTimeout } from '@main/utils/fsRead';
|
import { FileReadTimeoutError, readFileUtf8WithTimeout } from '@main/utils/fsRead';
|
||||||
|
import { migrateProviderBackendId } from '@shared/utils/providerBackend';
|
||||||
import { getTeamsBasePath } from '@main/utils/pathDecoder';
|
import { getTeamsBasePath } from '@main/utils/pathDecoder';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
@ -83,6 +84,11 @@ export class TeamMetaStore {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const providerId =
|
||||||
|
file.providerId === 'anthropic' || file.providerId === 'codex' || file.providerId === 'gemini'
|
||||||
|
? file.providerId
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
version: 1,
|
version: 1,
|
||||||
displayName:
|
displayName:
|
||||||
|
|
@ -92,13 +98,11 @@ export class TeamMetaStore {
|
||||||
color: typeof file.color === 'string' ? file.color.trim() || undefined : undefined,
|
color: typeof file.color === 'string' ? file.color.trim() || undefined : undefined,
|
||||||
cwd: file.cwd.trim(),
|
cwd: file.cwd.trim(),
|
||||||
prompt: typeof file.prompt === 'string' ? file.prompt.trim() || undefined : undefined,
|
prompt: typeof file.prompt === 'string' ? file.prompt.trim() || undefined : undefined,
|
||||||
providerId:
|
providerId,
|
||||||
file.providerId === 'anthropic' ||
|
providerBackendId: migrateProviderBackendId(
|
||||||
file.providerId === 'codex' ||
|
providerId,
|
||||||
file.providerId === 'gemini'
|
normalizeOptionalBackendId(file.providerBackendId)
|
||||||
? file.providerId
|
),
|
||||||
: undefined,
|
|
||||||
providerBackendId: normalizeOptionalBackendId(file.providerBackendId),
|
|
||||||
model: typeof file.model === 'string' ? file.model.trim() || undefined : undefined,
|
model: typeof file.model === 'string' ? file.model.trim() || undefined : undefined,
|
||||||
effort: typeof file.effort === 'string' ? file.effort.trim() || undefined : undefined,
|
effort: typeof file.effort === 'string' ? file.effort.trim() || undefined : undefined,
|
||||||
skipPermissions: typeof file.skipPermissions === 'boolean' ? file.skipPermissions : undefined,
|
skipPermissions: typeof file.skipPermissions === 'boolean' ? file.skipPermissions : undefined,
|
||||||
|
|
@ -119,7 +123,10 @@ export class TeamMetaStore {
|
||||||
cwd: data.cwd.trim(),
|
cwd: data.cwd.trim(),
|
||||||
prompt: data.prompt?.trim() || undefined,
|
prompt: data.prompt?.trim() || undefined,
|
||||||
providerId: data.providerId,
|
providerId: data.providerId,
|
||||||
providerBackendId: normalizeOptionalBackendId(data.providerBackendId),
|
providerBackendId: migrateProviderBackendId(
|
||||||
|
data.providerId,
|
||||||
|
normalizeOptionalBackendId(data.providerBackendId)
|
||||||
|
),
|
||||||
model: data.model?.trim() || undefined,
|
model: data.model?.trim() || undefined,
|
||||||
effort: data.effort?.trim() || undefined,
|
effort: data.effort?.trim() || undefined,
|
||||||
skipPermissions: data.skipPermissions,
|
skipPermissions: data.skipPermissions,
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ import {
|
||||||
} from '@shared/utils/inboxNoise';
|
} from '@shared/utils/inboxNoise';
|
||||||
import { isLeadAgentType, isLeadMember } from '@shared/utils/leadDetection';
|
import { isLeadAgentType, isLeadMember } from '@shared/utils/leadDetection';
|
||||||
import { createLogger } from '@shared/utils/logger';
|
import { createLogger } from '@shared/utils/logger';
|
||||||
|
import { migrateProviderBackendId } from '@shared/utils/providerBackend';
|
||||||
import { isDefaultProviderModelSelection } from '@shared/utils/providerModelSelection';
|
import { isDefaultProviderModelSelection } from '@shared/utils/providerModelSelection';
|
||||||
import { formatTaskDisplayLabel } from '@shared/utils/taskIdentity';
|
import { formatTaskDisplayLabel } from '@shared/utils/taskIdentity';
|
||||||
import {
|
import {
|
||||||
|
|
@ -390,7 +391,7 @@ function getConfiguredRuntimeBackend(providerId: TeamProviderId): string | null
|
||||||
case 'gemini':
|
case 'gemini':
|
||||||
return runtimeConfig.gemini;
|
return runtimeConfig.gemini;
|
||||||
case 'codex':
|
case 'codex':
|
||||||
return runtimeConfig.codex;
|
return migrateProviderBackendId('codex', runtimeConfig.codex) ?? 'codex-native';
|
||||||
case 'anthropic':
|
case 'anthropic':
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -420,7 +421,9 @@ function buildRuntimeLaunchWarning(
|
||||||
const providerLabel = getTeamProviderLabel(providerId);
|
const providerLabel = getTeamProviderLabel(providerId);
|
||||||
const modelLabel = request.model?.trim() || 'default';
|
const modelLabel = request.model?.trim() || 'default';
|
||||||
const effortLabel = request.effort ?? '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[] = [];
|
const flags: string[] = [];
|
||||||
if (env.CLAUDE_CODE_USE_GEMINI === '1') flags.push('USE_GEMINI');
|
if (env.CLAUDE_CODE_USE_GEMINI === '1') flags.push('USE_GEMINI');
|
||||||
if (env.CLAUDE_CODE_USE_OPENAI === '1') flags.push('USE_OPENAI');
|
if (env.CLAUDE_CODE_USE_OPENAI === '1') flags.push('USE_OPENAI');
|
||||||
|
|
@ -466,10 +469,12 @@ function logRuntimeLaunchSnapshot(
|
||||||
const providerId = resolveTeamProviderId(request.providerId);
|
const providerId = resolveTeamProviderId(request.providerId);
|
||||||
const snapshot = {
|
const snapshot = {
|
||||||
providerId,
|
providerId,
|
||||||
providerBackendId: request.providerBackendId ?? null,
|
providerBackendId: migrateProviderBackendId(providerId, request.providerBackendId) ?? null,
|
||||||
model: request.model ?? null,
|
model: request.model ?? null,
|
||||||
effort: request.effort ?? null,
|
effort: request.effort ?? null,
|
||||||
configuredBackend: request.providerBackendId?.trim() || getConfiguredRuntimeBackend(providerId),
|
configuredBackend:
|
||||||
|
migrateProviderBackendId(providerId, request.providerBackendId?.trim()) ||
|
||||||
|
getConfiguredRuntimeBackend(providerId),
|
||||||
promptSize: options?.promptSize ?? null,
|
promptSize: options?.promptSize ?? null,
|
||||||
expectedMembersCount: options?.expectedMembersCount ?? null,
|
expectedMembersCount: options?.expectedMembersCount ?? null,
|
||||||
geminiRuntimeAuth:
|
geminiRuntimeAuth:
|
||||||
|
|
@ -1159,8 +1164,10 @@ function shouldSkipResumeForProviderRuntimeChange(
|
||||||
return { skip: false };
|
return { skip: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestedBackendId = request.providerBackendId?.trim() || null;
|
const requestedBackendId =
|
||||||
const previousBackendId = persistedProviderBackendId?.trim() || null;
|
migrateProviderBackendId(providerId, request.providerBackendId?.trim()) || null;
|
||||||
|
const previousBackendId =
|
||||||
|
migrateProviderBackendId(providerId, persistedProviderBackendId?.trim()) || null;
|
||||||
if (requestedBackendId && previousBackendId && requestedBackendId !== previousBackendId) {
|
if (requestedBackendId && previousBackendId && requestedBackendId !== previousBackendId) {
|
||||||
return {
|
return {
|
||||||
skip: true,
|
skip: true,
|
||||||
|
|
|
||||||
|
|
@ -244,7 +244,7 @@ function getProviderTerminalCommand(provider: CliProviderStatus): {
|
||||||
return {
|
return {
|
||||||
args: ['auth', 'login', '--provider', provider.providerId],
|
args: ['auth', 'login', '--provider', provider.providerId],
|
||||||
env: {
|
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 {
|
return {
|
||||||
args: ['auth', 'logout', '--provider', provider.providerId],
|
args: ['auth', 'logout', '--provider', provider.providerId],
|
||||||
env: {
|
env: {
|
||||||
CLAUDE_CODE_CODEX_BACKEND: provider.selectedBackendId ?? 'auto',
|
CLAUDE_CODE_CODEX_BACKEND: provider.selectedBackendId ?? 'codex-native',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,7 @@ import {
|
||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from '@renderer/components/ui/tooltip';
|
} from '@renderer/components/ui/tooltip';
|
||||||
import {
|
import { formatProviderBackendLabel } from '@renderer/utils/providerBackendIdentity';
|
||||||
formatProviderBackendLabel,
|
|
||||||
isLegacyCodexProviderBackendId,
|
|
||||||
} from '@renderer/utils/providerBackendIdentity';
|
|
||||||
|
|
||||||
import type { CliProviderStatus } from '@shared/types';
|
import type { CliProviderStatus } from '@shared/types';
|
||||||
|
|
||||||
|
|
@ -60,15 +57,7 @@ export function getProviderRuntimeBackendAudienceLabel(
|
||||||
export function getVisibleProviderRuntimeBackendOptions(
|
export function getVisibleProviderRuntimeBackendOptions(
|
||||||
provider: CliProviderStatus
|
provider: CliProviderStatus
|
||||||
): NonNullable<CliProviderStatus['availableBackends']> {
|
): NonNullable<CliProviderStatus['availableBackends']> {
|
||||||
const options = provider.availableBackends ?? [];
|
return provider.availableBackends ?? [];
|
||||||
if (provider.providerId !== 'codex') {
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectedBackendId = provider.selectedBackendId ?? null;
|
|
||||||
return options.filter(
|
|
||||||
(option) => !isLegacyCodexProviderBackendId(option.id) || option.id === selectedBackendId
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getOptionDisplayLabel(
|
export function getOptionDisplayLabel(
|
||||||
|
|
@ -77,17 +66,6 @@ export function getOptionDisplayLabel(
|
||||||
resolvedOption: NonNullable<CliProviderStatus['availableBackends']>[number] | null
|
resolvedOption: NonNullable<CliProviderStatus['availableBackends']>[number] | null
|
||||||
): string {
|
): string {
|
||||||
if (provider.providerId === 'codex') {
|
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);
|
const legacyLabel = formatProviderBackendLabel(provider.providerId, option.id);
|
||||||
if (legacyLabel) {
|
if (legacyLabel) {
|
||||||
return legacyLabel;
|
return legacyLabel;
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ import {
|
||||||
isConnectionManagedRuntimeProvider,
|
isConnectionManagedRuntimeProvider,
|
||||||
} from './providerConnectionUi';
|
} from './providerConnectionUi';
|
||||||
import {
|
import {
|
||||||
|
getVisibleProviderRuntimeBackendOptions,
|
||||||
getProviderRuntimeBackendSummary,
|
getProviderRuntimeBackendSummary,
|
||||||
ProviderRuntimeBackendSelector,
|
ProviderRuntimeBackendSelector,
|
||||||
} from './ProviderRuntimeBackendSelector';
|
} from './ProviderRuntimeBackendSelector';
|
||||||
|
|
@ -38,13 +39,7 @@ import type { CliProviderAuthMode, CliProviderId, CliProviderStatus } from '@sha
|
||||||
import type { ApiKeyEntry } from '@shared/types/extensions';
|
import type { ApiKeyEntry } from '@shared/types/extensions';
|
||||||
|
|
||||||
type ApiKeyProviderId = 'anthropic' | 'codex' | 'gemini';
|
type ApiKeyProviderId = 'anthropic' | 'codex' | 'gemini';
|
||||||
type PendingConnectionAction =
|
type PendingConnectionAction = 'auto' | 'oauth' | 'api_key' | null;
|
||||||
| 'auto'
|
|
||||||
| 'oauth'
|
|
||||||
| 'api_key'
|
|
||||||
| 'codex-beta-on'
|
|
||||||
| 'codex-beta-off'
|
|
||||||
| null;
|
|
||||||
interface ConnectionMethodCardOption {
|
interface ConnectionMethodCardOption {
|
||||||
readonly authMode: CliProviderAuthMode;
|
readonly authMode: CliProviderAuthMode;
|
||||||
readonly title: string;
|
readonly title: string;
|
||||||
|
|
@ -86,7 +81,7 @@ const API_KEY_PROVIDER_CONFIG: Record<
|
||||||
name: 'OpenAI API Key',
|
name: 'OpenAI API Key',
|
||||||
title: 'API key',
|
title: 'API key',
|
||||||
description:
|
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-...',
|
placeholder: 'sk-proj-...',
|
||||||
},
|
},
|
||||||
gemini: {
|
gemini: {
|
||||||
|
|
@ -103,10 +98,6 @@ function isApiKeyProviderId(providerId: CliProviderId): providerId is ApiKeyProv
|
||||||
return providerId === 'anthropic' || providerId === 'codex' || providerId === 'gemini';
|
return providerId === 'anthropic' || providerId === 'codex' || providerId === 'gemini';
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasExplicitRuntimeBackends(provider: CliProviderStatus): boolean {
|
|
||||||
return (provider.availableBackends?.length ?? 0) > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isCodexNativeLane(provider: CliProviderStatus): boolean {
|
function isCodexNativeLane(provider: CliProviderStatus): boolean {
|
||||||
return (
|
return (
|
||||||
provider.providerId === 'codex' &&
|
provider.providerId === 'codex' &&
|
||||||
|
|
@ -124,11 +115,7 @@ function getConnectionDescription(provider: CliProviderStatus): string {
|
||||||
case 'anthropic':
|
case 'anthropic':
|
||||||
return 'Choose how app-launched Anthropic sessions authenticate.';
|
return 'Choose how app-launched Anthropic sessions authenticate.';
|
||||||
case 'codex':
|
case 'codex':
|
||||||
return hasExplicitRuntimeBackends(provider)
|
return 'Codex launches always use the native runtime now. Manage API-key credentials here before launching teams or one-shot Codex runs.';
|
||||||
? '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.';
|
|
||||||
case 'gemini':
|
case 'gemini':
|
||||||
return 'Configure optional API access. CLI SDK and ADC are still discovered automatically.';
|
return 'Configure optional API access. CLI SDK and ADC are still discovered automatically.';
|
||||||
}
|
}
|
||||||
|
|
@ -139,9 +126,7 @@ function getRuntimeDescription(provider: CliProviderStatus): string {
|
||||||
case 'anthropic':
|
case 'anthropic':
|
||||||
return 'Anthropic currently has no separate runtime backend selector.';
|
return 'Anthropic currently has no separate runtime backend selector.';
|
||||||
case 'codex':
|
case 'codex':
|
||||||
return hasExplicitRuntimeBackends(provider)
|
return 'Codex now runs only through the native runtime path.';
|
||||||
? '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.';
|
|
||||||
case 'gemini':
|
case 'gemini':
|
||||||
return 'Choose which Gemini runtime backend multimodel should use.';
|
return 'Choose which Gemini runtime backend multimodel should use.';
|
||||||
}
|
}
|
||||||
|
|
@ -160,9 +145,7 @@ function getAuthModeDescription(providerId: CliProviderId, authMode: CliProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
if (providerId === 'codex') {
|
if (providerId === 'codex') {
|
||||||
return authMode === 'api_key'
|
return 'Codex always launches through the native runtime and requires API-key credentials.';
|
||||||
? '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 '';
|
return '';
|
||||||
|
|
@ -172,7 +155,6 @@ function getConnectionAlert(provider: CliProviderStatus): string | null {
|
||||||
const authMode = provider.connection?.configuredAuthMode;
|
const authMode = provider.connection?.configuredAuthMode;
|
||||||
const hasAnthropicSubscriptionSession =
|
const hasAnthropicSubscriptionSession =
|
||||||
provider.authMethod === 'oauth_token' || provider.authMethod === 'claude.ai';
|
provider.authMethod === 'oauth_token' || provider.authMethod === 'claude.ai';
|
||||||
const hasCodexSubscriptionSession = provider.authMethod === 'oauth_token';
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
provider.providerId === 'anthropic' &&
|
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.';
|
return 'A saved API key is available, but app-launched Anthropic sessions use it only after you switch to API key mode.';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (provider.providerId === 'codex' && !provider.connection?.apiKeyConfigured) {
|
||||||
provider.providerId === 'codex' &&
|
|
||||||
authMode === 'api_key' &&
|
|
||||||
!provider.connection?.apiKeyConfigured
|
|
||||||
) {
|
|
||||||
return isCodexNativeLane(provider)
|
return isCodexNativeLane(provider)
|
||||||
? 'API key mode is selected, but no OPENAI_API_KEY or CODEX_API_KEY credential is available yet.'
|
? '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.';
|
: '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.';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|
@ -253,22 +219,7 @@ function getConnectionMethodCardOptions(
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
case 'codex':
|
case 'codex':
|
||||||
if (!provider.connection?.apiKeyBetaEnabled) {
|
return null;
|
||||||
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.',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -276,9 +227,7 @@ function getConnectionMethodCardOptions(
|
||||||
|
|
||||||
function getConnectionMethodCardsHint(provider: CliProviderStatus): string | null {
|
function getConnectionMethodCardsHint(provider: CliProviderStatus): string | null {
|
||||||
if (provider.providerId === 'codex') {
|
if (provider.providerId === 'codex') {
|
||||||
return hasExplicitRuntimeBackends(provider)
|
return 'Codex uses saved or environment API-key credentials for the native runtime.';
|
||||||
? 'Connection method controls credentials only. Runtime backend selection is independent.'
|
|
||||||
: 'Runtime follows your connection method automatically.';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (provider.providerId === 'anthropic') {
|
if (provider.providerId === 'anthropic') {
|
||||||
|
|
@ -461,15 +410,6 @@ export const ProviderRuntimeSettingsDialog = ({
|
||||||
statusSelectedProvider.connection.configuredAuthMode;
|
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 (statusApiKeyConfig) {
|
||||||
if (nextConnection.apiKeySource === 'stored') {
|
if (nextConnection.apiKeySource === 'stored') {
|
||||||
nextConnection.apiKeyConfigured = Boolean(selectedApiKey);
|
nextConnection.apiKeyConfigured = Boolean(selectedApiKey);
|
||||||
|
|
@ -488,8 +428,6 @@ export const ProviderRuntimeSettingsDialog = ({
|
||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
appConfig?.providerConnections?.anthropic.authMode,
|
appConfig?.providerConnections?.anthropic.authMode,
|
||||||
appConfig?.providerConnections?.codex.apiKeyBetaEnabled,
|
|
||||||
appConfig?.providerConnections?.codex.authMode,
|
|
||||||
selectedApiKey,
|
selectedApiKey,
|
||||||
statusApiKeyConfig,
|
statusApiKeyConfig,
|
||||||
statusSelectedProvider,
|
statusSelectedProvider,
|
||||||
|
|
@ -517,7 +455,10 @@ export const ProviderRuntimeSettingsDialog = ({
|
||||||
: false;
|
: false;
|
||||||
const hideConnectionMethodMeta = showConnectionMethodCards;
|
const hideConnectionMethodMeta = showConnectionMethodCards;
|
||||||
const canConfigureRuntime =
|
const canConfigureRuntime =
|
||||||
!connectionManagedRuntime && (selectedProvider?.availableBackends?.length ?? 0) > 0;
|
!connectionManagedRuntime &&
|
||||||
|
(selectedProvider
|
||||||
|
? getVisibleProviderRuntimeBackendOptions(selectedProvider).length > 1
|
||||||
|
: false);
|
||||||
|
|
||||||
const apiKeyConfig =
|
const apiKeyConfig =
|
||||||
selectedProvider && isApiKeyProviderId(selectedProvider.providerId)
|
selectedProvider && isApiKeyProviderId(selectedProvider.providerId)
|
||||||
|
|
@ -527,9 +468,9 @@ export const ProviderRuntimeSettingsDialog = ({
|
||||||
selectedProvider &&
|
selectedProvider &&
|
||||||
isApiKeyProviderId(selectedProvider.providerId) &&
|
isApiKeyProviderId(selectedProvider.providerId) &&
|
||||||
activeApiKeyFormProviderId === selectedProvider.providerId;
|
activeApiKeyFormProviderId === selectedProvider.providerId;
|
||||||
const codexApiKeyBetaEnabled = selectedProvider?.connection?.apiKeyBetaEnabled === true;
|
|
||||||
const showApiKeySection = Boolean(
|
const showApiKeySection = Boolean(
|
||||||
apiKeyConfig && (selectedProvider?.providerId !== 'codex' || codexApiKeyBetaEnabled)
|
apiKeyConfig &&
|
||||||
|
(selectedProvider?.providerId !== 'codex' || !selectedProvider.connection?.supportsOAuth)
|
||||||
);
|
);
|
||||||
const connectionAlert = selectedProvider ? getConnectionAlert(selectedProvider) : null;
|
const connectionAlert = selectedProvider ? getConnectionAlert(selectedProvider) : null;
|
||||||
const connectionLoading = selectedProviderLoading || connectionSaving;
|
const connectionLoading = selectedProviderLoading || connectionSaving;
|
||||||
|
|
@ -541,9 +482,7 @@ export const ProviderRuntimeSettingsDialog = ({
|
||||||
const hasSubscriptionSession =
|
const hasSubscriptionSession =
|
||||||
selectedProvider?.providerId === 'anthropic'
|
selectedProvider?.providerId === 'anthropic'
|
||||||
? selectedProvider.authMethod === 'oauth_token' || selectedProvider.authMethod === 'claude.ai'
|
? selectedProvider.authMethod === 'oauth_token' || selectedProvider.authMethod === 'claude.ai'
|
||||||
: selectedProvider?.providerId === 'codex'
|
: false;
|
||||||
? selectedProvider.authMethod === 'oauth_token'
|
|
||||||
: false;
|
|
||||||
const canRequestSubscriptionLogin =
|
const canRequestSubscriptionLogin =
|
||||||
Boolean(selectedProvider?.connection?.supportsOAuth && onRequestLogin) &&
|
Boolean(selectedProvider?.connection?.supportsOAuth && onRequestLogin) &&
|
||||||
configuredAuthMode !== 'api_key' &&
|
configuredAuthMode !== 'api_key' &&
|
||||||
|
|
@ -567,21 +506,6 @@ export const ProviderRuntimeSettingsDialog = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (connectionSaving) {
|
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') {
|
if (selectedProvider.providerId === 'anthropic') {
|
||||||
switch (pendingConnectionAction) {
|
switch (pendingConnectionAction) {
|
||||||
case 'api_key':
|
case 'api_key':
|
||||||
|
|
@ -679,7 +603,7 @@ export const ProviderRuntimeSettingsDialog = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAuthModeChange = async (authMode: string): Promise<void> => {
|
const handleAuthModeChange = async (authMode: string): Promise<void> => {
|
||||||
if (selectedProvider?.providerId !== 'anthropic' && selectedProvider?.providerId !== 'codex') {
|
if (selectedProvider?.providerId !== 'anthropic') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -692,54 +616,10 @@ export const ProviderRuntimeSettingsDialog = ({
|
||||||
setPendingConnectionAction(nextAuthMode);
|
setPendingConnectionAction(nextAuthMode);
|
||||||
setConnectionError(null);
|
setConnectionError(null);
|
||||||
let updateSucceeded = false;
|
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 {
|
try {
|
||||||
await updateConfig('providerConnections', {
|
await updateConfig('providerConnections', {
|
||||||
codex: {
|
anthropic: {
|
||||||
apiKeyBetaEnabled: enabled,
|
authMode: nextAuthMode,
|
||||||
authMode: enabled ? 'api_key' : 'oauth',
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
updateSucceeded = true;
|
updateSucceeded = true;
|
||||||
|
|
@ -747,15 +627,8 @@ export const ProviderRuntimeSettingsDialog = ({
|
||||||
setConnectionError(error instanceof Error ? error.message : 'Failed to update connection');
|
setConnectionError(error instanceof Error ? error.message : 'Failed to update connection');
|
||||||
} finally {
|
} finally {
|
||||||
if (updateSucceeded) {
|
if (updateSucceeded) {
|
||||||
if (shouldOpenApiKeyForm) {
|
|
||||||
setActiveApiKeyFormProviderId('codex');
|
|
||||||
setApiKeyScope(fallbackApiKeyScope);
|
|
||||||
setApiKeyValue('');
|
|
||||||
setApiKeyError(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await onRefreshProvider?.('codex');
|
await onRefreshProvider?.(selectedProvider.providerId);
|
||||||
} catch {
|
} catch {
|
||||||
setConnectionError('Connection updated, but failed to refresh provider status.');
|
setConnectionError('Connection updated, but failed to refresh provider status.');
|
||||||
}
|
}
|
||||||
|
|
@ -901,78 +774,12 @@ export const ProviderRuntimeSettingsDialog = ({
|
||||||
{selectedProvider.authenticated &&
|
{selectedProvider.authenticated &&
|
||||||
(selectedProvider.authMethod === 'oauth_token' ||
|
(selectedProvider.authMethod === 'oauth_token' ||
|
||||||
selectedProvider.authMethod === 'claude.ai')
|
selectedProvider.authMethod === 'claude.ai')
|
||||||
? selectedProvider.providerId === 'codex'
|
? 'Reconnect Anthropic'
|
||||||
? 'Reconnect Codex'
|
|
||||||
: 'Reconnect Anthropic'
|
|
||||||
: getProviderConnectLabel(selectedProvider)}
|
: getProviderConnectLabel(selectedProvider)}
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</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 ? (
|
{showConnectionMethodCards ? (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="text-xs">Connection method</Label>
|
<Label className="text-xs">Connection method</Label>
|
||||||
|
|
@ -1056,20 +863,6 @@ export const ProviderRuntimeSettingsDialog = ({
|
||||||
{selectedProvider.connection.apiKeySourceLabel}
|
{selectedProvider.connection.apiKeySourceLabel}
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : 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>
|
</div>
|
||||||
|
|
||||||
{showApiKeySection && apiKeyConfig ? (
|
{showApiKeySection && apiKeyConfig ? (
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import type { CliProviderAuthMode, CliProviderStatus } from '@shared/types';
|
import type { CliProviderAuthMode, CliProviderStatus } from '@shared/types';
|
||||||
|
|
||||||
const CODEX_SUBSCRIPTION_LABEL = 'Codex subscription';
|
const CODEX_NATIVE_LABEL = 'Codex native';
|
||||||
const CODEX_API_KEY_LABEL = 'OpenAI API key';
|
|
||||||
const ANTHROPIC_SUBSCRIPTION_LABEL = 'Anthropic subscription';
|
const ANTHROPIC_SUBSCRIPTION_LABEL = 'Anthropic subscription';
|
||||||
|
|
||||||
const AUTH_MODE_LABELS: Record<CliProviderAuthMode, string> = {
|
const AUTH_MODE_LABELS: Record<CliProviderAuthMode, string> = {
|
||||||
|
|
@ -23,7 +22,7 @@ export function formatProviderAuthModeLabelForProvider(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (providerId === 'codex' && authMode === 'oauth') {
|
if (providerId === 'codex' && authMode === 'oauth') {
|
||||||
return CODEX_SUBSCRIPTION_LABEL;
|
return CODEX_NATIVE_LABEL;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (providerId === 'anthropic' && authMode === 'oauth') {
|
if (providerId === 'anthropic' && authMode === 'oauth') {
|
||||||
|
|
@ -59,7 +58,7 @@ export function formatProviderAuthMethodLabelForProvider(
|
||||||
authMethod: string | null
|
authMethod: string | null
|
||||||
): string {
|
): string {
|
||||||
if (providerId === 'codex' && authMethod === 'oauth_token') {
|
if (providerId === 'codex' && authMethod === 'oauth_token') {
|
||||||
return CODEX_SUBSCRIPTION_LABEL;
|
return CODEX_NATIVE_LABEL;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (providerId === 'anthropic' && (authMethod === 'oauth_token' || authMethod === 'claude.ai')) {
|
if (providerId === 'anthropic' && (authMethod === 'oauth_token' || authMethod === 'claude.ai')) {
|
||||||
|
|
@ -95,23 +94,11 @@ function getSelectedRuntimeBackendOption(
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isConnectionManagedRuntimeProvider(provider: CliProviderStatus): boolean {
|
export function isConnectionManagedRuntimeProvider(provider: CliProviderStatus): boolean {
|
||||||
return provider.providerId === 'codex' && (provider.availableBackends?.length ?? 0) === 0;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCodexCurrentRuntimeLabel(provider: CliProviderStatus): string {
|
function getCodexCurrentRuntimeLabel(provider: CliProviderStatus): string {
|
||||||
if (isCodexNativeLane(provider) && provider.backend?.label) {
|
return provider.backend?.label ?? CODEX_NATIVE_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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getProviderCurrentRuntimeSummary(provider: CliProviderStatus): string | null {
|
export function getProviderCurrentRuntimeSummary(provider: CliProviderStatus): string | null {
|
||||||
|
|
@ -126,6 +113,15 @@ export function getProviderCurrentRuntimeSummary(provider: CliProviderStatus): s
|
||||||
export function formatProviderStatusText(provider: CliProviderStatus): string {
|
export function formatProviderStatusText(provider: CliProviderStatus): string {
|
||||||
const selectedBackendOption = getSelectedRuntimeBackendOption(provider);
|
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 (
|
if (
|
||||||
isCodexNativeLane(provider) &&
|
isCodexNativeLane(provider) &&
|
||||||
selectedBackendOption &&
|
selectedBackendOption &&
|
||||||
|
|
@ -168,11 +164,7 @@ export function getProviderConnectionModeSummary(provider: CliProviderStatus): s
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (provider.providerId === 'codex') {
|
if (provider.authenticated) {
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (provider.providerId === 'anthropic' && provider.authenticated) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -212,22 +204,10 @@ export function getProviderCredentialSummary(provider: CliProviderStatus): strin
|
||||||
: (provider.connection.apiKeySourceLabel ?? 'API key is configured');
|
: (provider.connection.apiKeySourceLabel ?? 'API key is configured');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (provider.providerId === 'codex' && provider.connection?.apiKeyBetaEnabled !== true) {
|
if (provider.providerId === 'codex') {
|
||||||
if (isCodexNativeLane(provider)) {
|
|
||||||
return provider.connection.apiKeySource === 'stored'
|
|
||||||
? 'Saved API key available in Manage'
|
|
||||||
: (provider.connection.apiKeySourceLabel ?? 'API key is configured');
|
|
||||||
}
|
|
||||||
|
|
||||||
return provider.connection.apiKeySource === 'stored'
|
return provider.connection.apiKeySource === 'stored'
|
||||||
? 'OpenAI API key is saved in Manage. Enable API key mode to use it.'
|
? 'Saved API key available in Manage'
|
||||||
: 'OpenAI API key detected. Enable API key mode in Manage to use it.';
|
: (provider.connection.apiKeySourceLabel ?? 'API key is configured');
|
||||||
}
|
|
||||||
|
|
||||||
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');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return provider.connection.apiKeySourceLabel ?? null;
|
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') {
|
if (provider.providerId === 'gemini' && provider.authMethod === 'cli_oauth_personal') {
|
||||||
return {
|
return {
|
||||||
label: 'Disconnect',
|
label: 'Disconnect',
|
||||||
|
|
@ -288,7 +257,7 @@ export function getProviderConnectLabel(provider: CliProviderStatus): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (provider.providerId === 'codex') {
|
if (provider.providerId === 'codex') {
|
||||||
return 'Connect Codex';
|
return 'Configure API key';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (provider.providerId === 'gemini') {
|
if (provider.providerId === 'gemini') {
|
||||||
|
|
@ -299,6 +268,10 @@ export function getProviderConnectLabel(provider: CliProviderStatus): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function shouldShowProviderConnectAction(provider: CliProviderStatus): boolean {
|
export function shouldShowProviderConnectAction(provider: CliProviderStatus): boolean {
|
||||||
|
if (provider.providerId === 'codex') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (!provider.canLoginFromUi || provider.authenticated) {
|
if (!provider.canLoginFromUi || provider.authenticated) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -109,7 +109,7 @@ function getProviderTerminalCommand(provider: CliProviderStatus): {
|
||||||
return {
|
return {
|
||||||
args: ['auth', 'login', '--provider', provider.providerId],
|
args: ['auth', 'login', '--provider', provider.providerId],
|
||||||
env: {
|
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 {
|
return {
|
||||||
args: ['auth', 'logout', '--provider', provider.providerId],
|
args: ['auth', 'logout', '--provider', provider.providerId],
|
||||||
env: {
|
env: {
|
||||||
CLAUDE_CODE_CODEX_BACKEND: provider.selectedBackendId ?? 'auto',
|
CLAUDE_CODE_CODEX_BACKEND: provider.selectedBackendId ?? 'codex-native',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,9 +50,7 @@ export function getProvisioningProviderBackendSummary(
|
||||||
const effectiveOption = options.find((option) => option.id === effectiveBackendId) ?? null;
|
const effectiveOption = options.find((option) => option.id === effectiveBackendId) ?? null;
|
||||||
const inferredProviderId =
|
const inferredProviderId =
|
||||||
provider.providerId ??
|
provider.providerId ??
|
||||||
(effectiveBackendId === 'codex-native' ||
|
(effectiveBackendId === 'codex-native' || options.some((option) => option.id === 'codex-native')
|
||||||
effectiveBackendId === 'adapter' ||
|
|
||||||
options.some((option) => option.id === 'codex-native' || option.id === 'adapter')
|
|
||||||
? 'codex'
|
? 'codex'
|
||||||
: undefined);
|
: undefined);
|
||||||
const normalizedLabel =
|
const normalizedLabel =
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
|
import {
|
||||||
|
formatProviderBackendLabel,
|
||||||
|
getDefaultProviderBackendId,
|
||||||
|
migrateProviderBackendId,
|
||||||
|
} from '@shared/utils/providerBackend';
|
||||||
|
|
||||||
import type { CliProviderStatus, TeamProviderId } from '@shared/types';
|
import type { CliProviderStatus, TeamProviderId } from '@shared/types';
|
||||||
|
|
||||||
function normalizeOptionalBackendId(value: string | null | undefined): string | undefined {
|
function normalizeOptionalBackendId(value: string | null | undefined): string | undefined {
|
||||||
|
|
@ -5,22 +11,7 @@ function normalizeOptionalBackendId(value: string | null | undefined): string |
|
||||||
return trimmed ? trimmed : undefined;
|
return trimmed ? trimmed : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getDefaultProviderBackendId(
|
export { formatProviderBackendLabel, 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 function resolveEffectiveProviderBackendId(
|
export function resolveEffectiveProviderBackendId(
|
||||||
provider: Pick<CliProviderStatus, 'selectedBackendId' | 'resolvedBackendId'> | null | undefined
|
provider: Pick<CliProviderStatus, 'selectedBackendId' | 'resolvedBackendId'> | null | undefined
|
||||||
|
|
@ -32,57 +23,10 @@ export function resolveUiOwnedProviderBackendId(
|
||||||
providerId: TeamProviderId | CliProviderStatus['providerId'] | undefined,
|
providerId: TeamProviderId | CliProviderStatus['providerId'] | undefined,
|
||||||
provider: Pick<CliProviderStatus, 'selectedBackendId' | 'resolvedBackendId'> | null | undefined
|
provider: Pick<CliProviderStatus, 'selectedBackendId' | 'resolvedBackendId'> | null | undefined
|
||||||
): string | undefined {
|
): string | undefined {
|
||||||
const normalizedProviderId = providerId ?? undefined;
|
return migrateProviderBackendId(
|
||||||
if (normalizedProviderId === 'codex') {
|
providerId,
|
||||||
const selectedBackendId = normalizeOptionalBackendId(provider?.selectedBackendId);
|
provider?.selectedBackendId ?? provider?.resolvedBackendId
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatTeamProviderBackendLabel(
|
export function formatTeamProviderBackendLabel(
|
||||||
|
|
|
||||||
|
|
@ -323,16 +323,8 @@ export function sortTeamProviderModels(
|
||||||
export function isCodexChatGptSubscriptionProviderStatus(
|
export function isCodexChatGptSubscriptionProviderStatus(
|
||||||
providerStatus?: RuntimeAwareProviderStatus | null
|
providerStatus?: RuntimeAwareProviderStatus | null
|
||||||
): boolean {
|
): boolean {
|
||||||
if (providerStatus?.providerId !== 'codex') {
|
void providerStatus;
|
||||||
return false;
|
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'))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isRuntimeHiddenTeamModel(
|
function isRuntimeHiddenTeamModel(
|
||||||
|
|
|
||||||
|
|
@ -337,7 +337,7 @@ export interface AppConfig {
|
||||||
runtime: {
|
runtime: {
|
||||||
providerBackends: {
|
providerBackends: {
|
||||||
gemini: 'auto' | 'api' | 'cli-sdk';
|
gemini: 'auto' | 'api' | 'cli-sdk';
|
||||||
codex: 'auto' | 'adapter' | 'api' | 'codex-native';
|
codex: 'codex-native';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
/** Display and UI settings */
|
/** Display and UI settings */
|
||||||
|
|
|
||||||
76
src/shared/utils/providerBackend.ts
Normal file
76
src/shared/utils/providerBackend.ts
Normal 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;
|
||||||
|
}
|
||||||
|
|
@ -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', {
|
const apiResult = validateConfigUpdatePayload('runtime', {
|
||||||
providerBackends: {
|
providerBackends: {
|
||||||
codex: 'api',
|
codex: 'api',
|
||||||
|
|
@ -251,7 +251,7 @@ describe('configValidation', () => {
|
||||||
if (apiResult.valid) {
|
if (apiResult.valid) {
|
||||||
expect(apiResult.data).toEqual({
|
expect(apiResult.data).toEqual({
|
||||||
providerBackends: {
|
providerBackends: {
|
||||||
codex: 'api',
|
codex: 'codex-native',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -281,7 +281,7 @@ describe('configValidation', () => {
|
||||||
|
|
||||||
expect(result.valid).toBe(false);
|
expect(result.valid).toBe(false);
|
||||||
if (!result.valid) {
|
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');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -325,31 +325,24 @@ describe('ClaudeMultimodelBridgeService', () => {
|
||||||
codex: {
|
codex: {
|
||||||
supported: true,
|
supported: true,
|
||||||
authenticated: true,
|
authenticated: true,
|
||||||
authMethod: 'oauth_token',
|
authMethod: 'api_key',
|
||||||
verificationState: 'verified',
|
verificationState: 'verified',
|
||||||
canLoginFromUi: true,
|
canLoginFromUi: false,
|
||||||
statusMessage: 'Codex native lane is wired but remains locked for normal selection.',
|
statusMessage: 'Codex native runtime ready',
|
||||||
detailMessage: 'Use the fallback adapter/API lane unless the experimental native lane is explicitly enabled.',
|
detailMessage: 'Codex native runtime is ready through the local codex exec seam.',
|
||||||
selectedBackendId: 'codex-native',
|
selectedBackendId: 'codex-native',
|
||||||
resolvedBackendId: 'codex-native',
|
resolvedBackendId: 'codex-native',
|
||||||
availableBackends: [
|
availableBackends: [
|
||||||
{
|
{
|
||||||
id: 'auto',
|
id: 'codex-native',
|
||||||
label: 'Auto',
|
label: 'Codex native',
|
||||||
selectable: true,
|
selectable: true,
|
||||||
recommended: true,
|
recommended: true,
|
||||||
available: true,
|
available: true,
|
||||||
},
|
state: 'ready',
|
||||||
{
|
audience: 'general',
|
||||||
id: 'codex-native',
|
statusMessage: 'Ready',
|
||||||
label: 'Codex native',
|
detailMessage: 'Codex native runtime is ready through the local codex exec seam.',
|
||||||
selectable: false,
|
|
||||||
recommended: false,
|
|
||||||
available: true,
|
|
||||||
state: 'locked',
|
|
||||||
audience: 'internal',
|
|
||||||
statusMessage: 'Experimental native lane',
|
|
||||||
detailMessage: 'Phase 0 keeps the lane locked behind rollout policy.',
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
externalRuntimeDiagnostics: [
|
externalRuntimeDiagnostics: [
|
||||||
|
|
@ -368,7 +361,7 @@ describe('ClaudeMultimodelBridgeService', () => {
|
||||||
plugins: {
|
plugins: {
|
||||||
status: 'unsupported',
|
status: 'unsupported',
|
||||||
ownership: 'shared',
|
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: {
|
mcp: {
|
||||||
status: 'unsupported',
|
status: 'unsupported',
|
||||||
|
|
@ -416,17 +409,13 @@ describe('ClaudeMultimodelBridgeService', () => {
|
||||||
label: 'Codex native',
|
label: 'Codex native',
|
||||||
},
|
},
|
||||||
availableBackends: [
|
availableBackends: [
|
||||||
expect.objectContaining({
|
|
||||||
id: 'auto',
|
|
||||||
selectable: true,
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: 'codex-native',
|
id: 'codex-native',
|
||||||
selectable: false,
|
selectable: true,
|
||||||
available: true,
|
available: true,
|
||||||
state: 'locked',
|
state: 'ready',
|
||||||
audience: 'internal',
|
audience: 'general',
|
||||||
statusMessage: 'Experimental native lane',
|
statusMessage: 'Ready',
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
externalRuntimeDiagnostics: [
|
externalRuntimeDiagnostics: [
|
||||||
|
|
@ -444,7 +433,7 @@ describe('ClaudeMultimodelBridgeService', () => {
|
||||||
expect(getProviderCurrentRuntimeSummary(codex!)).toBeNull();
|
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({
|
execCliMock.mockResolvedValue({
|
||||||
stdout: JSON.stringify({
|
stdout: JSON.stringify({
|
||||||
providers: {
|
providers: {
|
||||||
|
|
@ -461,12 +450,12 @@ describe('ClaudeMultimodelBridgeService', () => {
|
||||||
id: 'codex-native',
|
id: 'codex-native',
|
||||||
label: 'Codex native',
|
label: 'Codex native',
|
||||||
selectable: true,
|
selectable: true,
|
||||||
recommended: false,
|
recommended: true,
|
||||||
available: true,
|
available: true,
|
||||||
state: 'ready',
|
state: 'ready',
|
||||||
audience: 'internal',
|
audience: 'general',
|
||||||
statusMessage: 'Ready for internal use',
|
statusMessage: 'Ready',
|
||||||
detailMessage: 'Internal rollout only.',
|
detailMessage: 'Codex native runtime is ready through the local codex exec seam.',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
capabilities: {
|
capabilities: {
|
||||||
|
|
@ -502,8 +491,8 @@ describe('ClaudeMultimodelBridgeService', () => {
|
||||||
selectable: true,
|
selectable: true,
|
||||||
available: true,
|
available: true,
|
||||||
state: 'ready',
|
state: 'ready',
|
||||||
audience: 'internal',
|
audience: 'general',
|
||||||
statusMessage: 'Ready for internal use',
|
statusMessage: 'Ready',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -517,8 +506,8 @@ describe('ClaudeMultimodelBridgeService', () => {
|
||||||
authMethod: null,
|
authMethod: null,
|
||||||
verificationState: 'unknown',
|
verificationState: 'unknown',
|
||||||
canLoginFromUi: false,
|
canLoginFromUi: false,
|
||||||
statusMessage: 'Codex native runtime not ready',
|
statusMessage: 'Codex native runtime unavailable',
|
||||||
detailMessage: 'Codex native runtime requires the codex CLI binary to be installed.',
|
detailMessage: 'Codex native runtime requires the codex CLI binary to be installed and discoverable.',
|
||||||
selectedBackendId: 'codex-native',
|
selectedBackendId: 'codex-native',
|
||||||
resolvedBackendId: null,
|
resolvedBackendId: null,
|
||||||
availableBackends: [
|
availableBackends: [
|
||||||
|
|
@ -529,9 +518,9 @@ describe('ClaudeMultimodelBridgeService', () => {
|
||||||
recommended: false,
|
recommended: false,
|
||||||
available: false,
|
available: false,
|
||||||
state: 'runtime-missing',
|
state: 'runtime-missing',
|
||||||
audience: 'internal',
|
audience: 'general',
|
||||||
statusMessage: 'Codex CLI not found',
|
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: {
|
capabilities: {
|
||||||
|
|
@ -563,7 +552,7 @@ describe('ClaudeMultimodelBridgeService', () => {
|
||||||
selectable: false,
|
selectable: false,
|
||||||
available: false,
|
available: false,
|
||||||
state: 'runtime-missing',
|
state: 'runtime-missing',
|
||||||
audience: 'internal',
|
audience: 'general',
|
||||||
statusMessage: 'Codex CLI not found',
|
statusMessage: 'Codex CLI not found',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -11,28 +11,21 @@ describe('ProviderConnectionService', () => {
|
||||||
const originalOpenAiApiKey = process.env.OPENAI_API_KEY;
|
const originalOpenAiApiKey = process.env.OPENAI_API_KEY;
|
||||||
const originalCodexApiKey = process.env.CODEX_API_KEY;
|
const originalCodexApiKey = process.env.CODEX_API_KEY;
|
||||||
|
|
||||||
function createConfig(
|
function createConfig(authMode: 'auto' | 'oauth' | 'api_key' = 'auto') {
|
||||||
authMode: 'auto' | 'oauth' | 'api_key' = 'auto',
|
|
||||||
overrides?: {
|
|
||||||
codexAuthMode?: 'oauth' | 'api_key';
|
|
||||||
codexApiKeyBetaEnabled?: boolean;
|
|
||||||
codexRuntimeBackend?: 'auto' | 'adapter' | 'api' | 'codex-native';
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
return {
|
return {
|
||||||
providerConnections: {
|
providerConnections: {
|
||||||
anthropic: {
|
anthropic: {
|
||||||
authMode,
|
authMode,
|
||||||
},
|
},
|
||||||
codex: {
|
codex: {
|
||||||
apiKeyBetaEnabled: overrides?.codexApiKeyBetaEnabled ?? false,
|
apiKeyBetaEnabled: false,
|
||||||
authMode: overrides?.codexAuthMode ?? ('oauth' as const),
|
authMode: 'oauth' as const,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
runtime: {
|
runtime: {
|
||||||
providerBackends: {
|
providerBackends: {
|
||||||
gemini: 'auto' as const,
|
gemini: 'auto' as const,
|
||||||
codex: overrides?.codexRuntimeBackend ?? ('auto' as const),
|
codex: 'codex-native' as const,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -55,10 +48,9 @@ describe('ProviderConnectionService', () => {
|
||||||
|
|
||||||
if (originalCodexApiKey === undefined) {
|
if (originalCodexApiKey === undefined) {
|
||||||
delete process.env.CODEX_API_KEY;
|
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 () => {
|
it('removes Anthropic environment credentials when OAuth mode is selected', async () => {
|
||||||
|
|
@ -116,30 +108,6 @@ describe('ProviderConnectionService', () => {
|
||||||
expect(result.ANTHROPIC_AUTH_TOKEN).toBeUndefined();
|
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 () => {
|
it('reports a missing Anthropic API key when api_key mode is selected', async () => {
|
||||||
const { ProviderConnectionService } =
|
const { ProviderConnectionService } =
|
||||||
await import('@main/services/runtime/ProviderConnectionService');
|
await import('@main/services/runtime/ProviderConnectionService');
|
||||||
|
|
@ -159,57 +127,7 @@ describe('ProviderConnectionService', () => {
|
||||||
expect(issue).toContain('ANTHROPIC_API_KEY');
|
expect(issue).toContain('ANTHROPIC_API_KEY');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not report a missing Anthropic API key once env is populated', async () => {
|
it('prefers stored API key status over environment detection for Anthropic', 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 () => {
|
|
||||||
getCachedShellEnvMock.mockReturnValue({
|
getCachedShellEnvMock.mockReturnValue({
|
||||||
ANTHROPIC_API_KEY: 'shell-key',
|
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 () => {
|
it('exposes Codex as native-only API-key runtime', 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 () => {
|
|
||||||
const { ProviderConnectionService } =
|
const { ProviderConnectionService } =
|
||||||
await import('@main/services/runtime/ProviderConnectionService');
|
await import('@main/services/runtime/ProviderConnectionService');
|
||||||
|
|
||||||
|
|
@ -281,17 +175,19 @@ describe('ProviderConnectionService', () => {
|
||||||
const info = await service.getConnectionInfo('codex');
|
const info = await service.getConnectionInfo('codex');
|
||||||
|
|
||||||
expect(info).toMatchObject({
|
expect(info).toMatchObject({
|
||||||
supportsOAuth: true,
|
supportsOAuth: false,
|
||||||
supportsApiKey: true,
|
supportsApiKey: true,
|
||||||
configurableAuthModes: [],
|
configurableAuthModes: [],
|
||||||
configuredAuthMode: null,
|
configuredAuthMode: null,
|
||||||
apiKeyBetaAvailable: true,
|
|
||||||
apiKeyBetaEnabled: false,
|
|
||||||
apiKeyConfigured: 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({
|
const lookupPreferred = vi.fn().mockResolvedValue({
|
||||||
envVarName: 'OPENAI_API_KEY',
|
envVarName: 'OPENAI_API_KEY',
|
||||||
value: 'openai-stored-key',
|
value: 'openai-stored-key',
|
||||||
|
|
@ -304,31 +200,18 @@ describe('ProviderConnectionService', () => {
|
||||||
lookupPreferred,
|
lookupPreferred,
|
||||||
} as never,
|
} as never,
|
||||||
{
|
{
|
||||||
getConfig: () => ({
|
getConfig: () => createConfig('auto'),
|
||||||
...createConfig('auto', {
|
|
||||||
codexApiKeyBetaEnabled: true,
|
|
||||||
codexAuthMode: 'api_key',
|
|
||||||
codexRuntimeBackend: 'api',
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
} as never
|
} as never
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = await service.applyConfiguredConnectionEnv(
|
const result = await service.applyConfiguredConnectionEnv({}, 'codex');
|
||||||
{
|
|
||||||
OPENAI_API_KEY: undefined,
|
|
||||||
CLAUDE_CODE_CODEX_BACKEND: 'auto',
|
|
||||||
},
|
|
||||||
'codex'
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(lookupPreferred).toHaveBeenCalledWith('OPENAI_API_KEY');
|
expect(lookupPreferred).toHaveBeenCalledWith('OPENAI_API_KEY');
|
||||||
expect(result.OPENAI_API_KEY).toBe('openai-stored-key');
|
expect(result.OPENAI_API_KEY).toBe('openai-stored-key');
|
||||||
expect(result.CLAUDE_CODE_CODEX_BACKEND).toBe('auto');
|
expect(result.CODEX_API_KEY).toBe('openai-stored-key');
|
||||||
expect(result.CLAUDE_CODE_CODEX_API_KEY_BETA).toBe('1');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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 } =
|
const { ProviderConnectionService } =
|
||||||
await import('@main/services/runtime/ProviderConnectionService');
|
await import('@main/services/runtime/ProviderConnectionService');
|
||||||
|
|
||||||
|
|
@ -337,145 +220,22 @@ describe('ProviderConnectionService', () => {
|
||||||
lookupPreferred: vi.fn().mockResolvedValue(null),
|
lookupPreferred: vi.fn().mockResolvedValue(null),
|
||||||
} as never,
|
} as never,
|
||||||
{
|
{
|
||||||
getConfig: () => createConfig('auto', {
|
getConfig: () => createConfig('auto'),
|
||||||
codexApiKeyBetaEnabled: true,
|
|
||||||
codexAuthMode: 'oauth',
|
|
||||||
codexRuntimeBackend: 'api',
|
|
||||||
}),
|
|
||||||
} as never
|
} as never
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = await service.applyConfiguredConnectionEnv(
|
const result = await service.applyConfiguredConnectionEnv(
|
||||||
{
|
{
|
||||||
OPENAI_API_KEY: 'shell-openai-key',
|
OPENAI_API_KEY: 'shell-openai-key',
|
||||||
CLAUDE_CODE_CODEX_BACKEND: 'auto',
|
|
||||||
},
|
},
|
||||||
'codex'
|
'codex'
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result.OPENAI_API_KEY).toBeUndefined();
|
expect(result.OPENAI_API_KEY).toBe('shell-openai-key');
|
||||||
expect(result.CLAUDE_CODE_CODEX_BACKEND).toBe('auto');
|
expect(result.CODEX_API_KEY).toBe('shell-openai-key');
|
||||||
expect(result.CLAUDE_CODE_CODEX_API_KEY_BETA).toBe('1');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('reports a missing Codex API key when beta api_key mode is enabled', async () => {
|
it('accepts CODEX_API_KEY as the native external credential source for Codex', 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 () => {
|
|
||||||
getCachedShellEnvMock.mockReturnValue({
|
getCachedShellEnvMock.mockReturnValue({
|
||||||
CODEX_API_KEY: 'native-key',
|
CODEX_API_KEY: 'native-key',
|
||||||
});
|
});
|
||||||
|
|
@ -488,12 +248,7 @@ describe('ProviderConnectionService', () => {
|
||||||
lookupPreferred: vi.fn().mockResolvedValue(null),
|
lookupPreferred: vi.fn().mockResolvedValue(null),
|
||||||
} as never,
|
} as never,
|
||||||
{
|
{
|
||||||
getConfig: () =>
|
getConfig: () => createConfig('auto'),
|
||||||
createConfig('auto', {
|
|
||||||
codexApiKeyBetaEnabled: false,
|
|
||||||
codexAuthMode: 'api_key',
|
|
||||||
codexRuntimeBackend: 'codex-native',
|
|
||||||
}),
|
|
||||||
} as never
|
} as never
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -510,4 +265,46 @@ describe('ProviderConnectionService', () => {
|
||||||
expect(info.apiKeySourceLabel).toBe('Detected from CODEX_API_KEY');
|
expect(info.apiKeySourceLabel).toBe('Detected from CODEX_API_KEY');
|
||||||
expect(issue).toBeNull();
|
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');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ vi.mock('../../../../src/main/services/infrastructure/ConfigManager', () => ({
|
||||||
runtime: {
|
runtime: {
|
||||||
providerBackends: {
|
providerBackends: {
|
||||||
gemini: 'cli',
|
gemini: 'cli',
|
||||||
codex: 'adapter',
|
codex: 'codex-native',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
@ -185,17 +185,16 @@ describe('buildProviderAwareCliEnv', () => {
|
||||||
expect(augmentAllConfiguredConnectionEnvMock).toHaveBeenCalledWith(
|
expect(augmentAllConfiguredConnectionEnvMock).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
CLAUDE_CODE_GEMINI_BACKEND: 'api',
|
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_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({
|
buildEnrichedEnvMock.mockReturnValue({
|
||||||
PATH: '/usr/bin',
|
PATH: '/usr/bin',
|
||||||
CLAUDE_CODE_CODEX_NATIVE_INTERNAL_UNLOCK: '1',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const { buildProviderAwareCliEnv } = await import(
|
const { buildProviderAwareCliEnv } = await import(
|
||||||
|
|
@ -207,12 +206,11 @@ describe('buildProviderAwareCliEnv', () => {
|
||||||
|
|
||||||
expect(applyConfiguredConnectionEnvMock).toHaveBeenCalledWith(
|
expect(applyConfiguredConnectionEnvMock).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
CLAUDE_CODE_CODEX_BACKEND: 'adapter',
|
CLAUDE_CODE_CODEX_BACKEND: 'codex-native',
|
||||||
CLAUDE_CODE_CODEX_NATIVE_INTERNAL_UNLOCK: '1',
|
|
||||||
}),
|
}),
|
||||||
'codex',
|
'codex',
|
||||||
undefined
|
undefined
|
||||||
);
|
);
|
||||||
expect(result.env.CLAUDE_CODE_CODEX_NATIVE_INTERNAL_UNLOCK).toBe('1');
|
expect(result.env.CLAUDE_CODE_CODEX_BACKEND).toBe('codex-native');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1013,7 +1013,7 @@ describe('TeamProvisioningService', () => {
|
||||||
expect(launchArgs).toContain(leadSessionId);
|
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();
|
allowConsoleLogs();
|
||||||
const teamName = 'resume-backend-change-team';
|
const teamName = 'resume-backend-change-team';
|
||||||
const leadSessionId = 'lead-session-backend-change';
|
const leadSessionId = 'lead-session-backend-change';
|
||||||
|
|
@ -1066,8 +1066,8 @@ describe('TeamProvisioningService', () => {
|
||||||
|
|
||||||
const launchArgs = vi.mocked(spawnCli).mock.calls.at(-1)?.[1] as string[];
|
const launchArgs = vi.mocked(spawnCli).mock.calls.at(-1)?.[1] as string[];
|
||||||
expect(launchArgs).toBeTruthy();
|
expect(launchArgs).toBeTruthy();
|
||||||
expect(launchArgs).not.toContain('--resume');
|
expect(launchArgs).toContain('--resume');
|
||||||
expect(launchArgs).not.toContain(leadSessionId);
|
expect(launchArgs).toContain(leadSessionId);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('seeds the current lead session id immediately when launch resumes an existing session', async () => {
|
it('seeds the current lead session id immediately when launch resumes an existing session', async () => {
|
||||||
|
|
|
||||||
|
|
@ -166,21 +166,21 @@ function createApiKeyMisconfiguredProvider(
|
||||||
statusMessage:
|
statusMessage:
|
||||||
providerId === 'anthropic'
|
providerId === 'anthropic'
|
||||||
? 'Anthropic API key mode is enabled, but no ANTHROPIC_API_KEY is configured.'
|
? '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: [],
|
models: [],
|
||||||
canLoginFromUi: true,
|
canLoginFromUi: providerId === 'anthropic',
|
||||||
capabilities: {
|
capabilities: {
|
||||||
teamLaunch: true,
|
teamLaunch: true,
|
||||||
oneShot: true,
|
oneShot: true,
|
||||||
},
|
},
|
||||||
connection: {
|
connection: {
|
||||||
supportsOAuth: true,
|
supportsOAuth: providerId === 'anthropic',
|
||||||
supportsApiKey: true,
|
supportsApiKey: true,
|
||||||
configurableAuthModes:
|
configurableAuthModes:
|
||||||
providerId === 'anthropic' ? ['auto', 'oauth', 'api_key'] : ['oauth', 'api_key'],
|
providerId === 'anthropic' ? ['auto', 'oauth', 'api_key'] : [],
|
||||||
configuredAuthMode: 'api_key',
|
configuredAuthMode: providerId === 'anthropic' ? 'api_key' : null,
|
||||||
apiKeyBetaAvailable: providerId === 'codex' ? true : undefined,
|
apiKeyBetaAvailable: undefined,
|
||||||
apiKeyBetaEnabled: providerId === 'codex' ? true : undefined,
|
apiKeyBetaEnabled: undefined,
|
||||||
apiKeyConfigured: false,
|
apiKeyConfigured: false,
|
||||||
apiKeySource: null,
|
apiKeySource: null,
|
||||||
apiKeySourceLabel: null,
|
apiKeySourceLabel: null,
|
||||||
|
|
@ -194,22 +194,22 @@ function createApiKeyModeProviderIssue(providerId: 'anthropic' | 'codex'): Recor
|
||||||
statusMessage:
|
statusMessage:
|
||||||
providerId === 'anthropic'
|
providerId === 'anthropic'
|
||||||
? 'Anthropic API key was rejected by the runtime.'
|
? '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: {
|
connection: {
|
||||||
...(createApiKeyMisconfiguredProvider(providerId) as { connection: Record<string, unknown> })
|
...(createApiKeyMisconfiguredProvider(providerId) as { connection: Record<string, unknown> })
|
||||||
.connection,
|
.connection,
|
||||||
apiKeyConfigured: true,
|
apiKeyConfigured: true,
|
||||||
apiKeySource: 'stored',
|
apiKeySource: 'stored',
|
||||||
apiKeySourceLabel:
|
apiKeySourceLabel:
|
||||||
providerId === 'anthropic' ? 'Stored Anthropic API key' : 'Stored OpenAI API key',
|
providerId === 'anthropic' ? 'Stored Anthropic API key' : 'Stored Codex API key',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createCodexNativeRolloutProvider(
|
function createCodexNativeRolloutProvider(
|
||||||
overrides?: Partial<Record<string, unknown>> & {
|
overrides?: Partial<Record<string, unknown>> & {
|
||||||
state?: 'ready' | 'locked' | 'authentication-required' | 'runtime-missing' | 'degraded';
|
state?: 'ready' | 'authentication-required' | 'runtime-missing' | 'degraded';
|
||||||
audience?: 'general' | 'internal';
|
audience?: 'general';
|
||||||
selectable?: boolean;
|
selectable?: boolean;
|
||||||
available?: boolean;
|
available?: boolean;
|
||||||
statusMessage?: string | null;
|
statusMessage?: string | null;
|
||||||
|
|
@ -220,23 +220,16 @@ function createCodexNativeRolloutProvider(
|
||||||
providerId: 'codex',
|
providerId: 'codex',
|
||||||
displayName: 'Codex',
|
displayName: 'Codex',
|
||||||
supported: true,
|
supported: true,
|
||||||
authenticated:
|
authenticated: overrides?.state === 'ready' || overrides?.available === true,
|
||||||
overrides?.state === 'ready' || overrides?.state === 'locked' || overrides?.available === true,
|
authMethod: overrides?.state === 'ready' || overrides?.available === true ? 'api_key' : null,
|
||||||
authMethod:
|
|
||||||
overrides?.state === 'ready' || overrides?.state === 'locked' || overrides?.available === true
|
|
||||||
? 'api_key'
|
|
||||||
: null,
|
|
||||||
verificationState:
|
verificationState:
|
||||||
overrides?.state === 'ready' || overrides?.state === 'locked' || overrides?.available === true
|
overrides?.state === 'ready' || overrides?.available === true ? 'verified' : 'unknown',
|
||||||
? 'verified'
|
statusMessage: overrides?.statusMessage ?? 'Ready',
|
||||||
: 'unknown',
|
detailMessage:
|
||||||
statusMessage: overrides?.statusMessage ?? 'Ready but locked',
|
overrides?.detailMessage ?? 'Codex native runtime is ready through the local codex exec seam.',
|
||||||
detailMessage: overrides?.detailMessage ?? 'Internal rollout only.',
|
|
||||||
selectedBackendId: 'codex-native',
|
selectedBackendId: 'codex-native',
|
||||||
resolvedBackendId:
|
resolvedBackendId:
|
||||||
overrides?.state === 'ready' || overrides?.state === 'locked' || overrides?.available === true
|
overrides?.state === 'ready' || overrides?.available === true ? 'codex-native' : null,
|
||||||
? 'codex-native'
|
|
||||||
: null,
|
|
||||||
models: ['gpt-5-codex'],
|
models: ['gpt-5-codex'],
|
||||||
canLoginFromUi: false,
|
canLoginFromUi: false,
|
||||||
capabilities: {
|
capabilities: {
|
||||||
|
|
@ -248,17 +241,18 @@ function createCodexNativeRolloutProvider(
|
||||||
id: 'codex-native',
|
id: 'codex-native',
|
||||||
label: 'Codex native',
|
label: 'Codex native',
|
||||||
description: 'Use codex exec JSON mode.',
|
description: 'Use codex exec JSON mode.',
|
||||||
selectable: overrides?.selectable ?? false,
|
selectable: overrides?.selectable ?? true,
|
||||||
recommended: false,
|
recommended: true,
|
||||||
available: overrides?.available ?? true,
|
available: overrides?.available ?? true,
|
||||||
state: overrides?.state ?? 'locked',
|
state: overrides?.state ?? 'ready',
|
||||||
audience: overrides?.audience ?? 'internal',
|
audience: overrides?.audience ?? 'general',
|
||||||
statusMessage: overrides?.statusMessage ?? 'Ready but locked',
|
statusMessage: overrides?.statusMessage ?? 'Ready',
|
||||||
detailMessage: overrides?.detailMessage ?? 'Internal rollout only.',
|
detailMessage:
|
||||||
|
overrides?.detailMessage ?? 'Codex native runtime is ready through the local codex exec seam.',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
backend:
|
backend:
|
||||||
overrides?.state === 'ready' || overrides?.state === 'locked' || overrides?.available === true
|
overrides?.state === 'ready' || overrides?.available === true
|
||||||
? {
|
? {
|
||||||
kind: 'codex-native',
|
kind: 'codex-native',
|
||||||
label: 'Codex native',
|
label: 'Codex native',
|
||||||
|
|
@ -403,12 +397,12 @@ describe('CLI status visibility during completed install state', () => {
|
||||||
const onSelectBackend = providerRuntimeSettingsDialogProps?.onSelectBackend;
|
const onSelectBackend = providerRuntimeSettingsDialogProps?.onSelectBackend;
|
||||||
expect(onSelectBackend).toBeTypeOf('function');
|
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.'
|
'Runtime updated, but failed to refresh provider status.'
|
||||||
);
|
);
|
||||||
expect(storeState.updateConfig).toHaveBeenCalledWith('runtime', {
|
expect(storeState.updateConfig).toHaveBeenCalledWith('runtime', {
|
||||||
providerBackends: {
|
providerBackends: {
|
||||||
codex: 'api',
|
codex: 'codex-native',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(storeState.fetchCliProviderStatus).toHaveBeenCalledWith('codex');
|
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);
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||||
storeState.cliInstallerState = 'idle';
|
storeState.cliInstallerState = 'idle';
|
||||||
storeState.cliStatus = createInstalledCliStatus({
|
storeState.cliStatus = createInstalledCliStatus({
|
||||||
|
|
@ -786,7 +780,7 @@ describe('CLI status visibility during completed install state', () => {
|
||||||
displayName: 'Codex',
|
displayName: 'Codex',
|
||||||
supported: true,
|
supported: true,
|
||||||
authenticated: true,
|
authenticated: true,
|
||||||
authMethod: 'oauth_token',
|
authMethod: 'api_key',
|
||||||
verificationState: 'verified',
|
verificationState: 'verified',
|
||||||
modelVerificationState: 'verified',
|
modelVerificationState: 'verified',
|
||||||
statusMessage: null,
|
statusMessage: null,
|
||||||
|
|
@ -806,15 +800,15 @@ describe('CLI status visibility during completed install state', () => {
|
||||||
checkedAt: '2026-04-16T12:00:00.000Z',
|
checkedAt: '2026-04-16T12:00:00.000Z',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
canLoginFromUi: true,
|
canLoginFromUi: false,
|
||||||
capabilities: {
|
capabilities: {
|
||||||
teamLaunch: true,
|
teamLaunch: true,
|
||||||
oneShot: true,
|
oneShot: true,
|
||||||
},
|
},
|
||||||
backend: {
|
backend: {
|
||||||
kind: 'openai',
|
kind: 'codex-native',
|
||||||
label: 'OpenAI',
|
label: 'Codex native',
|
||||||
endpointLabel: 'chatgpt.com/backend-api/codex/responses',
|
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).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('5.2-codex');
|
||||||
expect(host.textContent).not.toContain('Unavailable');
|
expect(host.textContent).toContain('Unavailable');
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
root.unmount();
|
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);
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||||
storeState.cliInstallerState = 'idle';
|
storeState.cliInstallerState = 'idle';
|
||||||
storeState.cliStatus = createInstalledCliStatus({
|
storeState.cliStatus = createInstalledCliStatus({
|
||||||
|
|
@ -852,10 +846,12 @@ describe('CLI status visibility during completed install state', () => {
|
||||||
authLoggedIn: true,
|
authLoggedIn: true,
|
||||||
providers: [
|
providers: [
|
||||||
createCodexNativeRolloutProvider({
|
createCodexNativeRolloutProvider({
|
||||||
state: 'locked',
|
state: 'ready',
|
||||||
available: true,
|
available: true,
|
||||||
selectable: false,
|
selectable: true,
|
||||||
statusMessage: 'Ready but locked',
|
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();
|
await Promise.resolve();
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(host.textContent).toContain('Ready but locked');
|
expect(host.textContent).toContain('Ready');
|
||||||
expect(host.textContent).toContain('Runtime: Codex native - internal - locked');
|
expect(host.textContent).toContain('Runtime: Codex native');
|
||||||
expect(host.textContent).not.toContain('Connected via API key');
|
expect(host.textContent).not.toContain('Connected via API key');
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
|
|
@ -898,7 +894,7 @@ describe('CLI status visibility during completed install state', () => {
|
||||||
available: false,
|
available: false,
|
||||||
selectable: false,
|
selectable: false,
|
||||||
statusMessage: 'Codex CLI not found',
|
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,
|
backend: null,
|
||||||
resolvedBackendId: 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('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');
|
expect(host.textContent).not.toContain('Connected via API key');
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
|
|
|
||||||
|
|
@ -32,16 +32,6 @@ function createCodexProvider(
|
||||||
selectedBackendId: 'codex-native',
|
selectedBackendId: 'codex-native',
|
||||||
resolvedBackendId: 'codex-native',
|
resolvedBackendId: 'codex-native',
|
||||||
availableBackends: [
|
availableBackends: [
|
||||||
{
|
|
||||||
id: 'auto',
|
|
||||||
label: 'Auto',
|
|
||||||
description: 'Automatically choose the best backend.',
|
|
||||||
selectable: true,
|
|
||||||
recommended: true,
|
|
||||||
available: true,
|
|
||||||
state: 'ready',
|
|
||||||
audience: 'general',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'codex-native',
|
id: 'codex-native',
|
||||||
label: '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({
|
const provider = createCodexProvider({
|
||||||
availableBackends: [
|
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',
|
id: 'codex-native',
|
||||||
label: '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({
|
const provider = createCodexProvider({
|
||||||
selectedBackendId: 'api',
|
selectedBackendId: 'codex-native',
|
||||||
resolvedBackendId: 'api',
|
resolvedBackendId: 'codex-native',
|
||||||
availableBackends: [
|
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',
|
id: 'codex-native',
|
||||||
label: 'Codex native',
|
label: 'Codex native',
|
||||||
|
|
@ -179,10 +139,8 @@ describe('ProviderRuntimeBackendSelector helpers', () => {
|
||||||
});
|
});
|
||||||
const visibleOptions = getVisibleProviderRuntimeBackendOptions(provider);
|
const visibleOptions = getVisibleProviderRuntimeBackendOptions(provider);
|
||||||
|
|
||||||
expect(visibleOptions.map((option) => option.id)).toEqual(['api', 'codex-native']);
|
expect(visibleOptions.map((option) => option.id)).toEqual(['codex-native']);
|
||||||
expect(getOptionDisplayLabel(provider, visibleOptions[0], null)).toBe(
|
expect(getOptionDisplayLabel(provider, visibleOptions[0], null)).toBe('Codex native');
|
||||||
'Legacy OpenAI fallback'
|
expect(getProviderRuntimeBackendSummary(provider)).toBe('Codex native');
|
||||||
);
|
|
||||||
expect(getProviderRuntimeBackendSummary(provider)).toBe('Legacy OpenAI fallback - internal');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -138,6 +138,8 @@ vi.mock('@renderer/components/runtime/ProviderRuntimeBackendSelector', () => ({
|
||||||
'Select runtime backend'
|
'Select runtime backend'
|
||||||
),
|
),
|
||||||
getProviderRuntimeBackendSummary: () => null,
|
getProviderRuntimeBackendSummary: () => null,
|
||||||
|
getVisibleProviderRuntimeBackendOptions: (provider: CliProviderStatus) =>
|
||||||
|
provider.availableBackends ?? [],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('@renderer/components/common/ProviderBrandLogo', () => ({
|
vi.mock('@renderer/components/common/ProviderBrandLogo', () => ({
|
||||||
|
|
@ -155,6 +157,10 @@ function createCodexProvider(
|
||||||
overrides?: Partial<CliProviderStatus['connection']> & {
|
overrides?: Partial<CliProviderStatus['connection']> & {
|
||||||
authenticated?: boolean;
|
authenticated?: boolean;
|
||||||
authMethod?: string | null;
|
authMethod?: string | null;
|
||||||
|
selectedBackendId?: string | null;
|
||||||
|
resolvedBackendId?: string | null;
|
||||||
|
availableBackends?: CliProviderStatus['availableBackends'];
|
||||||
|
canLoginFromUi?: boolean;
|
||||||
}
|
}
|
||||||
): CliProviderStatus {
|
): CliProviderStatus {
|
||||||
return {
|
return {
|
||||||
|
|
@ -162,30 +168,45 @@ function createCodexProvider(
|
||||||
displayName: 'Codex',
|
displayName: 'Codex',
|
||||||
supported: true,
|
supported: true,
|
||||||
authenticated: overrides?.authenticated ?? true,
|
authenticated: overrides?.authenticated ?? true,
|
||||||
authMethod: overrides?.authMethod ?? 'oauth_token',
|
authMethod: overrides?.authMethod ?? 'api_key',
|
||||||
verificationState: 'verified',
|
verificationState: 'verified',
|
||||||
statusMessage: 'Connected',
|
statusMessage: 'Codex native ready',
|
||||||
models: ['gpt-5-codex'],
|
models: ['gpt-5-codex'],
|
||||||
canLoginFromUi: true,
|
canLoginFromUi: overrides?.canLoginFromUi ?? false,
|
||||||
capabilities: {
|
capabilities: {
|
||||||
teamLaunch: true,
|
teamLaunch: true,
|
||||||
oneShot: true,
|
oneShot: true,
|
||||||
extensions: createDefaultCliExtensionCapabilities(),
|
extensions: createDefaultCliExtensionCapabilities(),
|
||||||
},
|
},
|
||||||
selectedBackendId: 'auto',
|
selectedBackendId: overrides?.selectedBackendId ?? 'codex-native',
|
||||||
resolvedBackendId: 'adapter',
|
resolvedBackendId: overrides?.resolvedBackendId ?? 'codex-native',
|
||||||
availableBackends: [],
|
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: {
|
backend: {
|
||||||
kind: 'adapter',
|
kind: 'codex-native',
|
||||||
label: 'Codex subscription',
|
label: 'Codex native',
|
||||||
},
|
},
|
||||||
connection: {
|
connection: {
|
||||||
supportsOAuth: true,
|
supportsOAuth: false,
|
||||||
supportsApiKey: true,
|
supportsApiKey: true,
|
||||||
configurableAuthModes: overrides?.apiKeyBetaEnabled ? ['oauth', 'api_key'] : [],
|
configurableAuthModes: [],
|
||||||
configuredAuthMode: overrides?.configuredAuthMode ?? null,
|
configuredAuthMode: null,
|
||||||
apiKeyBetaAvailable: true,
|
apiKeyBetaAvailable: undefined,
|
||||||
apiKeyBetaEnabled: overrides?.apiKeyBetaEnabled ?? false,
|
apiKeyBetaEnabled: undefined,
|
||||||
apiKeyConfigured: overrides?.apiKeyConfigured ?? false,
|
apiKeyConfigured: overrides?.apiKeyConfigured ?? false,
|
||||||
apiKeySource: overrides?.apiKeySource ?? null,
|
apiKeySource: overrides?.apiKeySource ?? null,
|
||||||
apiKeySourceLabel: overrides?.apiKeySourceLabel ?? null,
|
apiKeySourceLabel: overrides?.apiKeySourceLabel ?? null,
|
||||||
|
|
@ -217,6 +238,7 @@ function createAnthropicProvider(
|
||||||
selectedBackendId: null,
|
selectedBackendId: null,
|
||||||
resolvedBackendId: null,
|
resolvedBackendId: null,
|
||||||
availableBackends: [],
|
availableBackends: [],
|
||||||
|
externalRuntimeDiagnostics: [],
|
||||||
backend: null,
|
backend: null,
|
||||||
connection: {
|
connection: {
|
||||||
supportsOAuth: true,
|
supportsOAuth: true,
|
||||||
|
|
@ -266,6 +288,7 @@ function createGeminiProvider(): CliProviderStatus {
|
||||||
available: true,
|
available: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
externalRuntimeDiagnostics: [],
|
||||||
backend: {
|
backend: {
|
||||||
kind: 'api',
|
kind: 'api',
|
||||||
label: 'Gemini API',
|
label: 'Gemini API',
|
||||||
|
|
@ -292,11 +315,7 @@ function findButtonByText(container: HTMLElement, text: string): HTMLButtonEleme
|
||||||
return button;
|
return button;
|
||||||
}
|
}
|
||||||
|
|
||||||
function countOccurrences(text: string, fragment: string): number {
|
describe('ProviderRuntimeSettingsDialog', () => {
|
||||||
return text.split(fragment).length - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('ProviderRuntimeSettingsDialog Codex connection flows', () => {
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||||
storeState.appConfig = {
|
storeState.appConfig = {
|
||||||
|
|
@ -346,139 +365,6 @@ describe('ProviderRuntimeSettingsDialog Codex connection flows', () => {
|
||||||
vi.unstubAllGlobals();
|
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 () => {
|
it('renders provider logos inside the provider tabs', async () => {
|
||||||
const host = document.createElement('div');
|
const host = document.createElement('div');
|
||||||
document.body.appendChild(host);
|
document.body.appendChild(host);
|
||||||
|
|
@ -502,10 +388,11 @@ describe('ProviderRuntimeSettingsDialog Codex connection flows', () => {
|
||||||
expect(host.querySelector('[data-testid="provider-logo-codex"]')).not.toBeNull();
|
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');
|
const host = document.createElement('div');
|
||||||
document.body.appendChild(host);
|
document.body.appendChild(host);
|
||||||
const root = createRoot(host);
|
const root = createRoot(host);
|
||||||
|
const onRefreshProvider = vi.fn(() => Promise.resolve(undefined));
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
root.render(
|
root.render(
|
||||||
|
|
@ -516,27 +403,70 @@ describe('ProviderRuntimeSettingsDialog Codex connection flows', () => {
|
||||||
createAnthropicProvider({
|
createAnthropicProvider({
|
||||||
configuredAuthMode: 'auto',
|
configuredAuthMode: 'auto',
|
||||||
apiKeyConfigured: true,
|
apiKeyConfigured: true,
|
||||||
apiKeySource: 'environment',
|
apiKeySource: 'stored',
|
||||||
apiKeySourceLabel: 'Detected from ANTHROPIC_API_KEY',
|
apiKeySourceLabel: 'Stored in app',
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
initialProviderId: 'anthropic',
|
initialProviderId: 'anthropic',
|
||||||
onSelectBackend: vi.fn(),
|
onSelectBackend: vi.fn(),
|
||||||
onRefreshProvider: vi.fn(() => Promise.resolve(undefined)),
|
onRefreshProvider,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(host.textContent).toContain('Connection method');
|
expect(host.textContent).toContain('Connection method');
|
||||||
expect(host.textContent).toContain('Auto');
|
|
||||||
expect(host.textContent).toContain('Anthropic subscription');
|
expect(host.textContent).toContain('Anthropic subscription');
|
||||||
expect(host.textContent).toContain('API key');
|
expect(host.textContent).toContain('API key');
|
||||||
expect(host.textContent).not.toContain('Authentication method');
|
|
||||||
expect(host.textContent).not.toContain('Runtime backend is not configurable');
|
await act(async () => {
|
||||||
expect(host.textContent).not.toContain('Mode: Auto');
|
findButtonByText(host, 'API key').click();
|
||||||
expect(countOccurrences(host.textContent ?? '', 'Using Anthropic subscription')).toBe(1);
|
await Promise.resolve();
|
||||||
expect(countOccurrences(host.textContent ?? '', 'Detected from ANTHROPIC_API_KEY')).toBe(1);
|
});
|
||||||
|
|
||||||
|
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 () => {
|
it('keeps the API key icon container square', async () => {
|
||||||
|
|
@ -565,79 +495,6 @@ describe('ProviderRuntimeSettingsDialog Codex connection flows', () => {
|
||||||
expect(icon?.className).toContain('shrink-0');
|
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 () => {
|
it('keeps the API key form open and shows an error when delete fails', async () => {
|
||||||
const host = document.createElement('div');
|
const host = document.createElement('div');
|
||||||
document.body.appendChild(host);
|
document.body.appendChild(host);
|
||||||
|
|
@ -646,10 +503,10 @@ describe('ProviderRuntimeSettingsDialog Codex connection flows', () => {
|
||||||
storeState.apiKeys = [
|
storeState.apiKeys = [
|
||||||
{
|
{
|
||||||
id: 'key-1',
|
id: 'key-1',
|
||||||
envVarName: 'ANTHROPIC_API_KEY',
|
envVarName: 'OPENAI_API_KEY',
|
||||||
scope: 'user',
|
scope: 'user',
|
||||||
name: 'Anthropic API Key',
|
name: 'OpenAI API Key',
|
||||||
maskedValue: 'sk-ant-...1234',
|
maskedValue: 'sk-proj-...1234',
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
@ -661,14 +518,13 @@ describe('ProviderRuntimeSettingsDialog Codex connection flows', () => {
|
||||||
open: true,
|
open: true,
|
||||||
onOpenChange: vi.fn(),
|
onOpenChange: vi.fn(),
|
||||||
providers: [
|
providers: [
|
||||||
createAnthropicProvider({
|
createCodexProvider({
|
||||||
configuredAuthMode: 'api_key',
|
|
||||||
apiKeyConfigured: true,
|
apiKeyConfigured: true,
|
||||||
apiKeySource: 'stored',
|
apiKeySource: 'stored',
|
||||||
apiKeySourceLabel: 'Stored in app',
|
apiKeySourceLabel: 'Stored in app',
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
initialProviderId: 'anthropic',
|
initialProviderId: 'codex',
|
||||||
onSelectBackend: vi.fn(),
|
onSelectBackend: vi.fn(),
|
||||||
onRefreshProvider,
|
onRefreshProvider,
|
||||||
})
|
})
|
||||||
|
|
@ -691,289 +547,6 @@ describe('ProviderRuntimeSettingsDialog Codex connection flows', () => {
|
||||||
expect(onRefreshProvider).not.toHaveBeenCalled();
|
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 () => {
|
it('shows a runtime error when backend selection refresh fails after a successful update', async () => {
|
||||||
const host = document.createElement('div');
|
const host = document.createElement('div');
|
||||||
document.body.appendChild(host);
|
document.body.appendChild(host);
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import {
|
||||||
getProviderCredentialSummary,
|
getProviderCredentialSummary,
|
||||||
getProviderCurrentRuntimeSummary,
|
getProviderCurrentRuntimeSummary,
|
||||||
isConnectionManagedRuntimeProvider,
|
isConnectionManagedRuntimeProvider,
|
||||||
|
shouldShowProviderConnectAction,
|
||||||
} from '@renderer/components/runtime/providerConnectionUi';
|
} from '@renderer/components/runtime/providerConnectionUi';
|
||||||
import { createDefaultCliExtensionCapabilities } from '@shared/utils/providerExtensionCapabilities';
|
import { createDefaultCliExtensionCapabilities } from '@shared/utils/providerExtensionCapabilities';
|
||||||
|
|
||||||
|
|
@ -57,6 +58,8 @@ function createCodexProvider(
|
||||||
resolvedBackendId?: string | null;
|
resolvedBackendId?: string | null;
|
||||||
availableBackends?: CliProviderStatus['availableBackends'];
|
availableBackends?: CliProviderStatus['availableBackends'];
|
||||||
backend?: CliProviderStatus['backend'];
|
backend?: CliProviderStatus['backend'];
|
||||||
|
statusMessage?: string | null;
|
||||||
|
canLoginFromUi?: boolean;
|
||||||
}
|
}
|
||||||
): CliProviderStatus {
|
): CliProviderStatus {
|
||||||
return {
|
return {
|
||||||
|
|
@ -64,33 +67,45 @@ function createCodexProvider(
|
||||||
displayName: 'Codex',
|
displayName: 'Codex',
|
||||||
supported: true,
|
supported: true,
|
||||||
authenticated: overrides?.authenticated ?? true,
|
authenticated: overrides?.authenticated ?? true,
|
||||||
authMethod: overrides?.authMethod ?? 'oauth_token',
|
authMethod: overrides?.authMethod ?? 'api_key',
|
||||||
verificationState: 'verified',
|
verificationState: 'verified',
|
||||||
statusMessage: 'Connected',
|
statusMessage: overrides?.statusMessage ?? 'Codex native ready',
|
||||||
models: ['gpt-5-codex'],
|
models: ['gpt-5-codex'],
|
||||||
canLoginFromUi: true,
|
canLoginFromUi: overrides?.canLoginFromUi ?? false,
|
||||||
capabilities: {
|
capabilities: {
|
||||||
teamLaunch: true,
|
teamLaunch: true,
|
||||||
oneShot: true,
|
oneShot: true,
|
||||||
extensions: createDefaultCliExtensionCapabilities(),
|
extensions: createDefaultCliExtensionCapabilities(),
|
||||||
},
|
},
|
||||||
selectedBackendId: overrides?.selectedBackendId ?? 'auto',
|
selectedBackendId: overrides?.selectedBackendId ?? 'codex-native',
|
||||||
resolvedBackendId: overrides?.resolvedBackendId ?? 'adapter',
|
resolvedBackendId: overrides?.resolvedBackendId ?? 'codex-native',
|
||||||
availableBackends: overrides?.availableBackends ?? [],
|
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: [],
|
externalRuntimeDiagnostics: [],
|
||||||
backend:
|
backend:
|
||||||
overrides?.backend ??
|
overrides?.backend ??
|
||||||
({
|
({
|
||||||
kind: 'adapter',
|
kind: 'codex-native',
|
||||||
label: 'Codex subscription',
|
label: 'Codex native',
|
||||||
} satisfies NonNullable<CliProviderStatus['backend']>),
|
} satisfies NonNullable<CliProviderStatus['backend']>),
|
||||||
connection: {
|
connection: {
|
||||||
supportsOAuth: true,
|
supportsOAuth: false,
|
||||||
supportsApiKey: true,
|
supportsApiKey: true,
|
||||||
configurableAuthModes: ['oauth', 'api_key'],
|
configurableAuthModes: [],
|
||||||
configuredAuthMode: overrides?.configuredAuthMode ?? 'oauth',
|
configuredAuthMode: overrides?.configuredAuthMode ?? null,
|
||||||
apiKeyBetaAvailable: true,
|
|
||||||
apiKeyBetaEnabled: overrides?.apiKeyBetaEnabled ?? true,
|
|
||||||
apiKeyConfigured: overrides?.apiKeyConfigured ?? false,
|
apiKeyConfigured: overrides?.apiKeyConfigured ?? false,
|
||||||
apiKeySource: overrides?.apiKeySource ?? null,
|
apiKeySource: overrides?.apiKeySource ?? null,
|
||||||
apiKeySourceLabel: overrides?.apiKeySourceLabel ?? 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({
|
const provider = createCodexProvider({
|
||||||
authenticated: true,
|
|
||||||
authMethod: 'oauth_token',
|
|
||||||
configuredAuthMode: 'api_key',
|
|
||||||
apiKeyConfigured: true,
|
apiKeyConfigured: true,
|
||||||
apiKeySource: 'stored',
|
apiKeySource: 'stored',
|
||||||
apiKeySourceLabel: 'Stored in app',
|
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(isConnectionManagedRuntimeProvider(provider)).toBe(false);
|
||||||
expect(getProviderCurrentRuntimeSummary(provider)).toBeNull();
|
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({
|
const provider = createCodexProvider({
|
||||||
apiKeyBetaEnabled: false,
|
|
||||||
configuredAuthMode: 'api_key',
|
|
||||||
apiKeyConfigured: true,
|
apiKeyConfigured: true,
|
||||||
apiKeySource: 'stored',
|
apiKeySource: 'stored',
|
||||||
apiKeySourceLabel: 'Stored in app',
|
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');
|
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({
|
const provider = createCodexProvider({
|
||||||
authenticated: true,
|
apiKeyConfigured: true,
|
||||||
authMethod: 'api_key',
|
apiKeySource: 'environment',
|
||||||
statusMessage: 'Codex native runtime ready',
|
apiKeySourceLabel: 'Detected from CODEX_API_KEY',
|
||||||
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',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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({
|
const provider = createCodexProvider({
|
||||||
authenticated: true,
|
|
||||||
authMethod: 'api_key',
|
|
||||||
statusMessage: 'Codex native runtime ready',
|
|
||||||
selectedBackendId: 'codex-native',
|
|
||||||
resolvedBackendId: 'codex-native',
|
|
||||||
availableBackends: [
|
availableBackends: [
|
||||||
{
|
{
|
||||||
id: 'codex-native',
|
id: 'codex-native',
|
||||||
label: 'Codex native',
|
label: 'Codex native',
|
||||||
description: 'Use codex exec JSON mode.',
|
description: 'Use codex exec JSON mode.',
|
||||||
selectable: true,
|
selectable: true,
|
||||||
recommended: false,
|
recommended: true,
|
||||||
available: true,
|
available: true,
|
||||||
state: 'ready',
|
state: 'ready',
|
||||||
audience: 'internal',
|
audience: 'general',
|
||||||
statusMessage: 'Ready for internal use',
|
statusMessage: 'Codex native ready',
|
||||||
detailMessage: 'Internal rollout only.',
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
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', () => {
|
it('surfaces native auth-required state from the selected backend option', () => {
|
||||||
const provider = createCodexProvider({
|
const provider = createCodexProvider({
|
||||||
authenticated: false,
|
authenticated: false,
|
||||||
authMethod: null,
|
authMethod: null,
|
||||||
statusMessage: 'Codex native runtime not ready',
|
statusMessage: 'Codex native not ready',
|
||||||
selectedBackendId: 'codex-native',
|
|
||||||
resolvedBackendId: null,
|
resolvedBackendId: null,
|
||||||
availableBackends: [
|
availableBackends: [
|
||||||
{
|
{
|
||||||
|
|
@ -327,10 +202,10 @@ describe('providerConnectionUi', () => {
|
||||||
label: 'Codex native',
|
label: 'Codex native',
|
||||||
description: 'Use codex exec JSON mode.',
|
description: 'Use codex exec JSON mode.',
|
||||||
selectable: false,
|
selectable: false,
|
||||||
recommended: false,
|
recommended: true,
|
||||||
available: false,
|
available: false,
|
||||||
state: 'authentication-required',
|
state: 'authentication-required',
|
||||||
audience: 'internal',
|
audience: 'general',
|
||||||
statusMessage: 'Authentication required',
|
statusMessage: 'Authentication required',
|
||||||
detailMessage: 'Set CODEX_API_KEY.',
|
detailMessage: 'Set CODEX_API_KEY.',
|
||||||
},
|
},
|
||||||
|
|
@ -341,30 +216,13 @@ describe('providerConnectionUi', () => {
|
||||||
expect(formatProviderStatusText(provider)).toBe('Authentication required');
|
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({
|
const provider = createCodexProvider({
|
||||||
authenticated: false,
|
authenticated: false,
|
||||||
authMethod: null,
|
authMethod: null,
|
||||||
statusMessage: 'Codex native runtime not ready',
|
canLoginFromUi: false,
|
||||||
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,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(formatProviderStatusText(provider)).toBe('Codex CLI not found');
|
expect(shouldShowProviderConnectAction(provider)).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import {
|
||||||
} from '@renderer/components/team/dialogs/TeamModelSelector';
|
} from '@renderer/components/team/dialogs/TeamModelSelector';
|
||||||
import {
|
import {
|
||||||
GPT_5_1_CODEX_MINI_UI_DISABLED_REASON,
|
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_2_CODEX_UI_DISABLED_REASON,
|
||||||
GPT_5_3_CODEX_SPARK_UI_DISABLED_REASON,
|
GPT_5_3_CODEX_SPARK_UI_DISABLED_REASON,
|
||||||
getAvailableTeamProviderModels,
|
getAvailableTeamProviderModels,
|
||||||
|
|
@ -49,15 +48,15 @@ describe('formatTeamModelSummary', () => {
|
||||||
expect(getTeamModelUiDisabledReason('anthropic', 'gpt-5.1-codex-mini')).toBeNull();
|
expect(getTeamModelUiDisabledReason('anthropic', 'gpt-5.1-codex-mini')).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('disables 5.1 Codex Max only on the Codex ChatGPT subscription path', () => {
|
it('keeps 5.1 Codex Max available on the native Codex path', () => {
|
||||||
const chatgptCodexProviderStatus = {
|
const nativeCodexProviderStatus = {
|
||||||
providerId: 'codex' as const,
|
providerId: 'codex' as const,
|
||||||
models: ['gpt-5.4', 'gpt-5.1-codex-max'],
|
models: ['gpt-5.4', 'gpt-5.1-codex-max'],
|
||||||
authMethod: 'oauth_token' as const,
|
authMethod: 'api_key' as const,
|
||||||
backend: {
|
backend: {
|
||||||
kind: 'adapter',
|
kind: 'codex-native',
|
||||||
label: 'Default adapter',
|
label: 'Codex native',
|
||||||
endpointLabel: 'chatgpt.com/backend-api/codex/responses',
|
endpointLabel: 'codex exec --json',
|
||||||
},
|
},
|
||||||
modelVerificationState: 'verified' as const,
|
modelVerificationState: 'verified' as const,
|
||||||
modelAvailability: [],
|
modelAvailability: [],
|
||||||
|
|
@ -66,14 +65,14 @@ describe('formatTeamModelSummary', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
getTeamModelUiDisabledReason('codex', 'gpt-5.1-codex-max', chatgptCodexProviderStatus)
|
getTeamModelUiDisabledReason('codex', 'gpt-5.1-codex-max', nativeCodexProviderStatus)
|
||||||
).toBe(GPT_5_1_CODEX_MAX_CHATGPT_UI_DISABLED_REASON);
|
).toBeNull();
|
||||||
expect(normalizeTeamModelForUi('codex', 'gpt-5.1-codex-max', chatgptCodexProviderStatus)).toBe(
|
expect(normalizeTeamModelForUi('codex', 'gpt-5.1-codex-max', nativeCodexProviderStatus)).toBe(
|
||||||
''
|
'gpt-5.1-codex-max'
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
getTeamModelSelectionError('codex', 'gpt-5.1-codex-max', chatgptCodexProviderStatus)
|
getTeamModelSelectionError('codex', 'gpt-5.1-codex-max', nativeCodexProviderStatus)
|
||||||
).toContain('Temporarily disabled for team agents when using Codex ChatGPT subscription');
|
).toBeNull();
|
||||||
expect(getTeamModelUiDisabledReason('codex', 'gpt-5.1-codex-max')).toBeNull();
|
expect(getTeamModelUiDisabledReason('codex', 'gpt-5.1-codex-max')).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -88,11 +87,11 @@ describe('formatTeamModelSummary', () => {
|
||||||
const codexProviderStatus = {
|
const codexProviderStatus = {
|
||||||
providerId: 'codex' as const,
|
providerId: 'codex' as const,
|
||||||
models: ['gpt-5.4', 'gpt-5.3-codex'],
|
models: ['gpt-5.4', 'gpt-5.3-codex'],
|
||||||
authMethod: 'oauth_token' as const,
|
authMethod: 'api_key' as const,
|
||||||
backend: {
|
backend: {
|
||||||
kind: 'adapter',
|
kind: 'codex-native',
|
||||||
label: 'Default adapter',
|
label: 'Codex native',
|
||||||
endpointLabel: 'chatgpt.com/backend-api/codex/responses',
|
endpointLabel: 'codex exec --json',
|
||||||
},
|
},
|
||||||
modelVerificationState: 'verified' as const,
|
modelVerificationState: 'verified' as const,
|
||||||
modelAvailability: [
|
modelAvailability: [
|
||||||
|
|
|
||||||
|
|
@ -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);
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||||
storeState.cliStatus = {
|
storeState.cliStatus = {
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
providerId: 'codex',
|
providerId: 'codex',
|
||||||
authMethod: 'oauth_token',
|
authMethod: 'api_key',
|
||||||
backend: {
|
backend: {
|
||||||
kind: 'adapter',
|
kind: 'codex-native',
|
||||||
label: 'Default adapter',
|
label: 'Codex native',
|
||||||
endpointLabel: 'chatgpt.com/backend-api/codex/responses',
|
endpointLabel: 'codex exec --json',
|
||||||
},
|
},
|
||||||
models: ['gpt-5.4', 'gpt-5.1-codex-max'],
|
models: ['gpt-5.4', 'gpt-5.1-codex-max'],
|
||||||
modelVerificationState: 'idle',
|
modelVerificationState: 'idle',
|
||||||
|
|
@ -295,23 +295,20 @@ describe('TeamModelSelector disabled Codex models', () => {
|
||||||
await Promise.resolve();
|
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')
|
button.textContent?.includes('5.1 Codex Max')
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(disabledButton).not.toBeNull();
|
expect(button).not.toBeNull();
|
||||||
expect(disabledButton?.getAttribute('aria-disabled')).toBe('true');
|
expect(button?.getAttribute('aria-disabled')).toBe('false');
|
||||||
expect(disabledButton?.textContent).toContain('Disabled');
|
expect(button?.textContent).not.toContain('Disabled');
|
||||||
expect(disabledButton?.getAttribute('title')).toContain(
|
|
||||||
'Not available with Codex ChatGPT subscription'
|
|
||||||
);
|
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
disabledButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
button?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(onValueChange).not.toHaveBeenCalled();
|
expect(onValueChange).toHaveBeenCalledWith('gpt-5.1-codex-max');
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
root.unmount();
|
root.unmount();
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ describe('ProvisioningProviderStatusList', () => {
|
||||||
{
|
{
|
||||||
providerId: 'codex',
|
providerId: 'codex',
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
backendSummary: 'Default adapter',
|
backendSummary: 'Codex native',
|
||||||
details: [
|
details: [
|
||||||
'5.4 Mini - verified',
|
'5.4 Mini - verified',
|
||||||
'5.1 Codex Max - unavailable - Not available with Codex ChatGPT subscription',
|
'5.1 Codex Max - unavailable - Not available with Codex ChatGPT subscription',
|
||||||
|
|
@ -64,7 +64,7 @@ describe('ProvisioningProviderStatusList', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(host.textContent).toContain(
|
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('5.4 Mini - verified');
|
||||||
expect(host.textContent).toContain(
|
expect(host.textContent).toContain(
|
||||||
|
|
@ -110,7 +110,7 @@ describe('ProvisioningProviderStatusList', () => {
|
||||||
{
|
{
|
||||||
providerId: 'codex',
|
providerId: 'codex',
|
||||||
status: 'notes',
|
status: 'notes',
|
||||||
backendSummary: 'Default adapter',
|
backendSummary: 'Codex native',
|
||||||
details: ['5.3 Codex - check failed - Model verification timed out'],
|
details: ['5.3 Codex - check failed - Model verification timed out'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
@ -120,7 +120,7 @@ describe('ProvisioningProviderStatusList', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(host.textContent).toContain(
|
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');
|
expect(host.textContent).toContain('5.3 Codex - check failed - Model verification timed out');
|
||||||
|
|
||||||
|
|
@ -143,7 +143,7 @@ describe('ProvisioningProviderStatusList', () => {
|
||||||
{
|
{
|
||||||
providerId: 'codex',
|
providerId: 'codex',
|
||||||
status: 'notes',
|
status: 'notes',
|
||||||
backendSummary: 'Default adapter',
|
backendSummary: 'Codex native',
|
||||||
details: [
|
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',
|
'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();
|
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 () => {
|
await act(async () => {
|
||||||
root.unmount();
|
root.unmount();
|
||||||
|
|
@ -179,36 +179,26 @@ describe('ProvisioningProviderStatusList', () => {
|
||||||
selectable: false,
|
selectable: false,
|
||||||
recommended: false,
|
recommended: false,
|
||||||
available: true,
|
available: true,
|
||||||
state: 'locked',
|
state: 'ready',
|
||||||
audience: 'internal',
|
audience: 'general',
|
||||||
statusMessage: 'Ready but locked',
|
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(
|
expect(
|
||||||
getProvisioningProviderBackendSummary({
|
getProvisioningProviderBackendSummary({
|
||||||
providerId: 'codex',
|
providerId: 'codex',
|
||||||
selectedBackendId: 'api',
|
selectedBackendId: 'api',
|
||||||
resolvedBackendId: 'api',
|
resolvedBackendId: 'api',
|
||||||
backend: {
|
backend: {
|
||||||
kind: 'api',
|
kind: 'codex-native',
|
||||||
label: 'OpenAI API',
|
label: 'Codex native',
|
||||||
},
|
},
|
||||||
availableBackends: [
|
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',
|
id: 'codex-native',
|
||||||
label: 'Codex native',
|
label: 'Codex native',
|
||||||
|
|
@ -221,6 +211,6 @@ describe('ProvisioningProviderStatusList', () => {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
).toBe('Legacy OpenAI fallback - internal');
|
).toBe('Codex native');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ describe('buildProviderPrepareModelCacheKey', () => {
|
||||||
const input = {
|
const input = {
|
||||||
cwd: '/tmp/project',
|
cwd: '/tmp/project',
|
||||||
providerId: 'codex' as const,
|
providerId: 'codex' as const,
|
||||||
backendSummary: 'Default adapter',
|
backendSummary: 'Codex native',
|
||||||
limitContext: false,
|
limitContext: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ describe('resolveMemberRuntimeSummary', () => {
|
||||||
const spawnEntry = createSpawnEntry({ runtimeModel: 'claude-opus-4-7', runtimeAlive: true });
|
const spawnEntry = createSpawnEntry({ runtimeModel: 'claude-opus-4-7', runtimeAlive: true });
|
||||||
|
|
||||||
expect(resolveMemberRuntimeSummary(member, undefined, spawnEntry)).toBe(
|
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',
|
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', () => {
|
it('appends runtime memory when a live process snapshot is available', () => {
|
||||||
|
|
@ -77,7 +79,7 @@ describe('resolveMemberRuntimeSummary', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(resolveMemberRuntimeSummary(member, undefined, undefined, runtimeEntry)).toBe(
|
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');
|
).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' });
|
const member = createMember({ model: 'gpt-5.4-mini' });
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
|
|
@ -114,6 +116,6 @@ describe('resolveMemberRuntimeSummary', () => {
|
||||||
},
|
},
|
||||||
undefined
|
undefined
|
||||||
)
|
)
|
||||||
).toBe('5.4 Mini · Medium · Legacy OpenAI fallback');
|
).toBe('5.4 Mini · Medium · Codex native');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,11 @@ function createCodexProviderStatus(
|
||||||
return {
|
return {
|
||||||
providerId: 'codex',
|
providerId: 'codex',
|
||||||
models,
|
models,
|
||||||
authMethod: 'oauth_token',
|
authMethod: 'api_key',
|
||||||
backend: {
|
backend: {
|
||||||
kind: 'adapter',
|
kind: 'codex-native',
|
||||||
label: 'Default adapter',
|
label: 'Codex native',
|
||||||
endpointLabel: 'chatgpt.com/backend-api/codex/responses',
|
endpointLabel: 'codex exec --json',
|
||||||
},
|
},
|
||||||
authenticated: true,
|
authenticated: true,
|
||||||
supported: 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([
|
const providerStatus = createCodexProviderStatus([
|
||||||
'gpt-5.4',
|
'gpt-5.4',
|
||||||
'gpt-5.3-codex-spark',
|
'gpt-5.3-codex-spark',
|
||||||
|
|
@ -48,16 +48,19 @@ describe('teamModelAvailability', () => {
|
||||||
'gpt-5.1-codex-max',
|
'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'], {
|
const providerStatus = createCodexProviderStatus(['gpt-5.4', 'gpt-5.1-codex-max'], {
|
||||||
authMethod: 'api_key',
|
authMethod: 'api_key',
|
||||||
backend: {
|
backend: {
|
||||||
kind: 'openai',
|
kind: 'codex-native',
|
||||||
label: 'OpenAI',
|
label: 'Codex native',
|
||||||
endpointLabel: 'api.openai.com/v1/responses',
|
endpointLabel: 'codex exec --json',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue