fix(codex): self-heal ccsxp cliproxy provider (#1243)

This commit is contained in:
Kai (Tam Nhu) Tran
2026-05-14 12:35:31 -04:00
committed by GitHub
parent 9748ba4ee0
commit 4748c452bd
12 changed files with 636 additions and 24 deletions
+2 -1
View File
@@ -273,7 +273,8 @@ src/
### Native Codex Runtime Target
- Dedicated runtime entrypoints: `ccs-codex` and `ccsx` resolve through `src/bin/codex-runtime.ts`, while `ccsxp` resolves through `src/bin/ccsxp-runtime.ts`; all three set `CCS_INTERNAL_ENTRY_TARGET=codex` before delegating to `src/targets/target-resolver.ts`.
- Provider shortcut behavior: `ccsxp` strips user-supplied `--target` overrides and prepends `--config model_provider="cliproxy"` so it behaves like native Codex plus the CLIProxy provider recipe. The stricter CCS-managed bridge remains available explicitly through `ccs codex --target codex`. It pins `CODEX_HOME` to native `~/.codex` by default so inherited launcher state does not send history/config writes to a nonstandard Codex root; `CCSXP_CODEX_HOME` is the explicit override.
- Provider shortcut behavior: `ccsxp` strips user-supplied `--target` overrides and prepends `--config model_provider="cliproxy"` so it behaves like native Codex plus the CLIProxy provider recipe. The stricter CCS-managed bridge remains available explicitly through `ccs codex --target codex`. It pins `CODEX_HOME` to native `~/.codex` by default so inherited launcher state does not send history/config writes to a nonstandard Codex root; `CCSXP_CODEX_HOME` is the explicit override. On launch, CCS repairs the native `[model_providers.cliproxy]` stanza in `config.toml`, reads that provider's configured `env_key` (default `CLIPROXY_API_KEY`), and injects the effective CLIProxy auth token into that key for the child Codex process.
- Implicit Codex launches such as `ccs --target codex` and `ccsxp` use native Codex default mode even when the CCS default profile is a Claude account. Explicit unsupported profiles such as `ccs work --target codex` still fail fast with native-vs-pool guidance.
- `argv[0]` alias mapping still exists in `src/targets/target-resolver.ts` for same-binary/custom alias scenarios, but the built-in npm bins above do not depend on that map at runtime.
- Metadata boundary: `src/targets/target-metadata.ts` keeps Codex runtime-only in v1, so persisted default targets remain `claude | droid`.
- Compatibility guardrails: `src/targets/target-runtime-compatibility.ts` centralizes which profile types can execute on Codex.
@@ -521,6 +521,9 @@ ccsxp
CCS_INTERNAL_ENTRY_TARGET=codex
→ injects native `model_provider="cliproxy"` override
→ pins CODEX_HOME to native `~/.codex` unless `CCSXP_CODEX_HOME` is set
→ repairs `[model_providers.cliproxy]` in the active Codex `config.toml`
→ injects the effective CCS CLIProxy auth token into the provider's configured `env_key`
→ ignores the configured CCS default account/profile and stays in native Codex default mode
```
If a user launches CCS through a custom shim instead of the built-in package bins, target
+2
View File
@@ -1,10 +1,12 @@
const os = require('os');
const path = require('path');
const { CCSXP_CLIPROXY_SHORTCUT_ENV } = require('../targets/codex-cliproxy-provider-config');
const { stripTargetFlag } = require('../targets/target-resolver');
const { expandPath } = require('../utils/helpers');
const { fail } = require('../utils/ui');
process.env.CCS_INTERNAL_ENTRY_TARGET = 'codex';
process.env[CCSXP_CLIPROXY_SHORTCUT_ENV] = '1';
const CCSXP_CLIPROXY_OVERRIDE = 'model_provider="cliproxy"';
const DISALLOWED_CCSXP_CONFIG_KEY_REGEX =
/^(model_provider|local_provider|profile)\s*=|^model_providers\./i;
@@ -252,6 +252,65 @@ describe('resolveProfileAndTarget', () => {
expect(exitSpy).not.toHaveBeenCalled();
});
it('keeps explicit codex cliproxy profile on the Codex target', async () => {
const mockCodexAdapter = {
displayName: 'Codex CLI',
detectBinary: mock(() => ({ path: '/usr/local/bin/codex', version: '1.0.0' })),
supportsProfileType: mock(() => true),
};
mockGetTarget.mockImplementation((_name: string) => mockCodexAdapter);
mockResolveTargetType.mockImplementation((_args: string[]) => 'codex' as const);
mockStripTargetFlag.mockImplementation((args: string[]) => {
const next: string[] = [];
for (let index = 0; index < args.length; index += 1) {
if (args[index] === '--target') {
index += 1;
continue;
}
next.push(args[index]);
}
return next;
});
mockDetectProfile.mockImplementation((args: string[]) => ({
profile: args[0] || 'default',
remainingArgs: args.slice(1),
}));
mockDetectProfileType.mockImplementation((profile: string) =>
profile === 'codex'
? {
type: 'cliproxy' as const,
name: 'codex',
provider: 'codex',
target: undefined,
}
: {
type: 'account' as const,
name: 'work',
target: undefined,
}
);
const result = await resolveProfileAndTarget({
args: ['codex', '--target', 'codex', 'fix failing tests'],
browserLaunchOverride: undefined,
cliLogger: stubLogger,
});
expect(result.profile).toBe('codex');
expect(result.profileInfo.type).toBe('cliproxy');
expect(result.resolvedTarget).toBe('codex');
expect(result.targetAdapter).toBe(mockCodexAdapter);
expect(result.targetRemainingArgs).toEqual(['fix failing tests']);
expect(mockEvaluateTargetRuntimeCompatibility).toHaveBeenCalledWith(
expect.objectContaining({
target: 'codex',
profileType: 'cliproxy',
cliproxyProvider: 'codex',
})
);
expect(exitSpy).not.toHaveBeenCalled();
});
it('resolves settings profile (glm) with --target droid and loads settings', async () => {
// Settings loading only runs in the non-claude preflight block (resolvedTarget !== 'claude').
const mockDroidAdapter = {
+17 -1
View File
@@ -79,6 +79,18 @@ export interface ResolvedProfile {
detector: InstanceType<typeof ProfileDetector>;
}
function usesImplicitDefaultProfile(cleanArgs: string[]): boolean {
return cleanArgs.length === 0 || cleanArgs[0]?.startsWith('-') === true;
}
function buildNativeCodexDefaultProfile(): ProfileDetectionResult {
return {
type: 'default',
name: 'default',
message: 'Using native Codex auth; CCS default profile is not applied to Codex target.',
};
}
// ========== Profile and Target Resolver ==========
/**
@@ -112,7 +124,7 @@ export async function resolveProfileAndTarget(
// Detect profile (strip --target flags before profile detection)
const cleanArgs = stripTargetFlag(args);
const { profile, remainingArgs } = detectProfile(cleanArgs);
const profileInfo: ProfileDetectionResult = detector.detectProfileType(profile);
let profileInfo: ProfileDetectionResult = detector.detectProfileType(profile);
let resolvedTarget: ReturnType<typeof resolveTargetType>;
try {
@@ -127,6 +139,10 @@ export async function resolveProfileAndTarget(
throw error;
}
if (resolvedTarget === 'codex' && usesImplicitDefaultProfile(cleanArgs)) {
profileInfo = buildNativeCodexDefaultProfile();
}
// Detect Claude CLI (needed for claude target and all CLIProxy-derived flows)
const claudeCliRaw = detectClaudeCli();
if (resolvedTarget === 'claude' && !claudeCliRaw) {
+25 -1
View File
@@ -24,6 +24,14 @@ import {
readCodexVersion,
} from './codex-detector';
import { createLogger } from '../services/logging';
import { getEffectiveApiKey } from '../cliproxy/auth/auth-token-manager';
import { resolveLifecyclePort } from '../cliproxy/config/port-manager';
import {
CCSXP_CLIPROXY_SHORTCUT_ENV,
CODEX_CLIPROXY_PROVIDER_ENV_KEY,
ensureCodexCliproxyProviderConfig,
isCcsxpCliproxyShortcut,
} from './codex-cliproxy-provider-config';
const adapterLogger = createLogger('targets:codex');
@@ -176,13 +184,25 @@ function prepareExplicitCodexHome(
export class CodexAdapter implements TargetAdapter {
readonly type: TargetType = 'codex';
readonly displayName = 'Codex CLI';
private ccsxpCliproxyEnvKey = CODEX_CLIPROXY_PROVIDER_ENV_KEY;
detectBinary(): TargetBinaryInfo | null {
return getCodexBinaryInfo({ includeVersion: false, includeFeatures: false });
}
async prepareCredentials(_creds: TargetCredentials): Promise<void> {
// Codex uses transient -c overrides plus env_key injection.
if (!isCcsxpCliproxyShortcut()) {
return;
}
try {
const providerRepair = await ensureCodexCliproxyProviderConfig(resolveLifecyclePort());
this.ccsxpCliproxyEnvKey = providerRepair.envKey;
} catch (error) {
throw new Error(
`ccsxp could not repair the native Codex cliproxy provider: ${(error as Error).message}`
);
}
}
buildArgs(
@@ -258,7 +278,11 @@ export class CodexAdapter implements TargetAdapter {
const env: NodeJS.ProcessEnv = {
...stripBrowserEnv(stripCodexSessionEnv(stripAnthropicEnv(process.env))),
};
delete env[CCSXP_CLIPROXY_SHORTCUT_ENV];
delete env[CODEX_RUNTIME_ENV_KEY];
if (profileType === 'default' && isCcsxpCliproxyShortcut()) {
env[this.ccsxpCliproxyEnvKey || CODEX_CLIPROXY_PROVIDER_ENV_KEY] = getEffectiveApiKey();
}
if (profileType !== 'default') {
if (!creds.apiKey?.trim()) {
throw new Error('Codex target requires an API key for CCS-backed profile launches.');
@@ -0,0 +1,174 @@
import * as os from 'os';
import * as path from 'path';
import { expandPath } from '../utils/helpers';
import {
probeTomlObjectFile,
stringifyTomlObject,
writeTomlFileAtomic,
} from '../web-server/services/compatible-cli-toml-file-service';
export const CCSXP_CLIPROXY_SHORTCUT_ENV = 'CCSXP_CLIPROXY_SHORTCUT';
export const CODEX_CLIPROXY_PROVIDER_ID = 'cliproxy';
export const CODEX_CLIPROXY_PROVIDER_ENV_KEY = 'CLIPROXY_API_KEY';
export const CODEX_CLIPROXY_PROVIDER_NAME = 'CLIProxy Codex';
export interface CodexCliproxyProviderRepairResult {
changed: boolean;
configPath: string;
displayPath: string;
envKey: string;
}
function resolveCodexConfigPath(env: NodeJS.ProcessEnv = process.env): {
configPath: string;
displayPath: string;
} {
const baseDir = path.resolve(
env.CODEX_HOME ? expandPath(env.CODEX_HOME) : path.join(os.homedir(), '.codex')
);
const displayBase = env.CODEX_HOME ? '$CODEX_HOME' : '~/.codex';
return {
configPath: path.join(baseDir, 'config.toml'),
displayPath: `${displayBase}/config.toml`,
};
}
export function buildCodexCliproxyProviderBaseUrl(port: number): string {
return `http://127.0.0.1:${port}/api/provider/codex`;
}
export function isCcsxpCliproxyShortcut(env: NodeJS.ProcessEnv = process.env): boolean {
return env[CCSXP_CLIPROXY_SHORTCUT_ENV] === '1';
}
function asObject(value: unknown): Record<string, unknown> | null {
return typeof value === 'object' && value !== null && !Array.isArray(value)
? (value as Record<string, unknown>)
: null;
}
function normalizeLocalProviderUrl(value: unknown): string | null {
if (typeof value !== 'string') return null;
const trimmed = value.trim();
try {
const url = new URL(trimmed);
if (
(url.hostname === 'localhost' || url.hostname === '127.0.0.1') &&
url.pathname === '/api/provider/codex'
) {
url.hostname = '127.0.0.1';
return url.toString().replace(/\/$/, '');
}
} catch {
return null;
}
return null;
}
function resolveProviderEnvKey(provider: Record<string, unknown> | null): string {
const envKey = provider?.env_key;
if (typeof envKey === 'string' && envKey.trim()) {
return envKey.trim();
}
return CODEX_CLIPROXY_PROVIDER_ENV_KEY;
}
function isProviderReady(
provider: Record<string, unknown>,
expectedBaseUrl: string,
envKey: string
): boolean {
return (
provider.name === CODEX_CLIPROXY_PROVIDER_NAME &&
normalizeLocalProviderUrl(provider.base_url) === expectedBaseUrl &&
provider.env_key === envKey &&
provider.wire_api === 'responses' &&
provider.requires_openai_auth === false &&
provider.supports_websockets === false
);
}
function buildProviderConfig(baseUrl: string, envKey: string): Record<string, unknown> {
return {
name: CODEX_CLIPROXY_PROVIDER_NAME,
base_url: baseUrl,
env_key: envKey,
wire_api: 'responses',
requires_openai_auth: false,
supports_websockets: false,
};
}
function buildProviderBlock(baseUrl: string): string {
return stringifyTomlObject({
model_providers: {
[CODEX_CLIPROXY_PROVIDER_ID]: buildProviderConfig(baseUrl, CODEX_CLIPROXY_PROVIDER_ENV_KEY),
},
});
}
function appendProviderBlock(rawText: string, baseUrl: string): string {
const prefix = rawText.trimEnd();
const providerBlock = buildProviderBlock(baseUrl).trimEnd();
return prefix ? `${prefix}\n\n${providerBlock}\n` : `${providerBlock}\n`;
}
export async function ensureCodexCliproxyProviderConfig(
port: number,
env: NodeJS.ProcessEnv = process.env
): Promise<CodexCliproxyProviderRepairResult> {
const { configPath, displayPath } = resolveCodexConfigPath(env);
const fileProbe = await probeTomlObjectFile(configPath, 'Codex user config', displayPath);
if (fileProbe.diagnostics.readError) {
throw new Error(`Cannot repair ${displayPath}: ${fileProbe.diagnostics.readError}`);
}
if (fileProbe.diagnostics.parseError) {
throw new Error(`Cannot repair ${displayPath}: ${fileProbe.diagnostics.parseError}`);
}
const config = fileProbe.config ?? {};
const modelProvidersValue = config.model_providers;
const providers = asObject(modelProvidersValue);
const expectedBaseUrl = buildCodexCliproxyProviderBaseUrl(port);
if (modelProvidersValue !== undefined && !providers) {
throw new Error(`Cannot repair ${displayPath}: [model_providers] must be a table.`);
}
if (!providers || !Object.prototype.hasOwnProperty.call(providers, CODEX_CLIPROXY_PROVIDER_ID)) {
await writeTomlFileAtomic({
filePath: configPath,
rawText: appendProviderBlock(fileProbe.rawText, expectedBaseUrl),
expectedMtime: fileProbe.diagnostics.mtimeMs ?? undefined,
fileLabel: 'config.toml',
});
return { changed: true, configPath, displayPath, envKey: CODEX_CLIPROXY_PROVIDER_ENV_KEY };
}
const currentProvider = asObject(providers[CODEX_CLIPROXY_PROVIDER_ID]);
if (!currentProvider) {
throw new Error(
`Cannot repair ${displayPath}: [model_providers.${CODEX_CLIPROXY_PROVIDER_ID}] must be a table.`
);
}
const envKey = resolveProviderEnvKey(currentProvider);
if (isProviderReady(currentProvider, expectedBaseUrl, envKey)) {
return { changed: false, configPath, displayPath, envKey };
}
providers[CODEX_CLIPROXY_PROVIDER_ID] = {
...currentProvider,
...buildProviderConfig(expectedBaseUrl, envKey),
};
await writeTomlFileAtomic({
filePath: configPath,
rawText: stringifyTomlObject(config),
expectedMtime: fileProbe.diagnostics.mtimeMs ?? undefined,
fileLabel: 'config.toml',
});
return { changed: true, configPath, displayPath, envKey };
}
+1 -1
View File
@@ -46,7 +46,7 @@ export function evaluateTargetRuntimeCompatibility(
if (input.profileType === 'account') {
return unsupported(
'Codex CLI does not support Claude account-based profiles.',
'Use native Codex auth with: ccs --target codex'
'Native Codex: ccs --target codex. CLIProxy Codex pool: ccs codex --target codex or ccsxp. Manage pool/routing with: ccs codex --auth and ccs cliproxy routing.'
);
}
@@ -0,0 +1,126 @@
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import {
buildCodexCliproxyProviderBaseUrl,
ensureCodexCliproxyProviderConfig,
} from '../../../src/targets/codex-cliproxy-provider-config';
describe('codex cliproxy provider config repair', () => {
let tempHome: string;
let codexHome: string;
let configPath: string;
let env: NodeJS.ProcessEnv;
beforeEach(() => {
tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-codex-provider-config-'));
codexHome = path.join(tempHome, '.codex');
configPath = path.join(codexHome, 'config.toml');
env = { CODEX_HOME: codexHome } as NodeJS.ProcessEnv;
});
afterEach(() => {
fs.rmSync(tempHome, { recursive: true, force: true });
});
it('creates the cliproxy model provider when config.toml is missing', async () => {
const result = await ensureCodexCliproxyProviderConfig(8317, env);
expect(result.changed).toBe(true);
expect(result.envKey).toBe('CLIPROXY_API_KEY');
const rawText = fs.readFileSync(configPath, 'utf8');
expect(rawText).toContain('[model_providers.cliproxy]');
expect(rawText).toContain('name = "CLIProxy Codex"');
expect(rawText).toContain('env_key = "CLIPROXY_API_KEY"');
expect(rawText).not.toContain('model_provider = "cliproxy"');
});
it('appends the missing provider while preserving existing raw config text', async () => {
fs.mkdirSync(codexHome, { recursive: true });
fs.writeFileSync(configPath, '# user note\nmodel = "gpt-5.4"\n', 'utf8');
const result = await ensureCodexCliproxyProviderConfig(8317, env);
expect(result.changed).toBe(true);
expect(result.envKey).toBe('CLIPROXY_API_KEY');
const rawText = fs.readFileSync(configPath, 'utf8');
expect(rawText.startsWith('# user note\nmodel = "gpt-5.4"\n\n')).toBe(true);
expect(rawText).toContain('[model_providers.cliproxy]');
});
it('repairs an existing incomplete cliproxy provider', async () => {
fs.mkdirSync(codexHome, { recursive: true });
fs.writeFileSync(
configPath,
`[model_providers.cliproxy]
base_url = "http://localhost:8317/api/provider/codex"
wire_api = "responses"
`,
'utf8'
);
const result = await ensureCodexCliproxyProviderConfig(9321, env);
expect(result.changed).toBe(true);
expect(result.envKey).toBe('CLIPROXY_API_KEY');
const rawText = fs.readFileSync(configPath, 'utf8');
expect(rawText).toContain(`base_url = "${buildCodexCliproxyProviderBaseUrl(9321)}"`);
expect(rawText).toContain('env_key = "CLIPROXY_API_KEY"');
expect(rawText).toContain('requires_openai_auth = false');
expect(rawText).toContain('supports_websockets = false');
});
it('preserves a custom cliproxy provider env key while repairing other fields', async () => {
fs.mkdirSync(codexHome, { recursive: true });
fs.writeFileSync(
configPath,
`[model_providers.cliproxy]
name = "Old Name"
base_url = "http://localhost:8317/api/provider/codex"
env_key = "CCS_CUSTOM_CLIPROXY_TOKEN"
wire_api = "chat"
`,
'utf8'
);
const result = await ensureCodexCliproxyProviderConfig(9321, env);
expect(result.changed).toBe(true);
expect(result.envKey).toBe('CCS_CUSTOM_CLIPROXY_TOKEN');
const rawText = fs.readFileSync(configPath, 'utf8');
expect(rawText).toContain(`base_url = "${buildCodexCliproxyProviderBaseUrl(9321)}"`);
expect(rawText).toContain('env_key = "CCS_CUSTOM_CLIPROXY_TOKEN"');
expect(rawText).toContain('wire_api = "responses"');
});
it('rejects invalid non-table model_providers values without appending broken TOML', async () => {
fs.mkdirSync(codexHome, { recursive: true });
const rawText = 'model_providers = "legacy"\n';
fs.writeFileSync(configPath, rawText, 'utf8');
await expect(ensureCodexCliproxyProviderConfig(8317, env)).rejects.toThrow(
'[model_providers] must be a table'
);
expect(fs.readFileSync(configPath, 'utf8')).toBe(rawText);
});
it('leaves a ready localhost provider unchanged', async () => {
fs.mkdirSync(codexHome, { recursive: true });
const rawText = `[model_providers.cliproxy]
name = "CLIProxy Codex"
base_url = "http://localhost:8317/api/provider/codex"
env_key = "CLIPROXY_API_KEY"
wire_api = "responses"
requires_openai_auth = false
supports_websockets = false
`;
fs.writeFileSync(configPath, rawText, 'utf8');
const result = await ensureCodexCliproxyProviderConfig(8317, env);
expect(result.changed).toBe(false);
expect(result.envKey).toBe('CLIPROXY_API_KEY');
expect(fs.readFileSync(configPath, 'utf8')).toBe(rawText);
});
});
@@ -116,18 +116,32 @@ if (out) {
}
const envOut = process.env.CCS_TEST_CODEX_ENV_OUT;
if (envOut) {
const loggedEnv = {
CODEX_HOME: process.env.CODEX_HOME,
CODEX_CI: process.env.CODEX_CI,
CODEX_MANAGED_BY_BUN: process.env.CODEX_MANAGED_BY_BUN,
CODEX_THREAD_ID: process.env.CODEX_THREAD_ID,
ANTHROPIC_BASE_URL: process.env.ANTHROPIC_BASE_URL,
CCS_BROWSER_USER_DATA_DIR: process.env.CCS_BROWSER_USER_DATA_DIR,
CCS_BROWSER_PROFILE_DIR: process.env.CCS_BROWSER_PROFILE_DIR,
CCS_BROWSER_DEVTOOLS_WS_URL: process.env.CCS_BROWSER_DEVTOOLS_WS_URL,
};
if (
process.env.CCS_TEST_CODEX_LOG_CLIPROXY_API_KEY === '1' &&
process.env.CLIPROXY_API_KEY !== undefined
) {
loggedEnv.CLIPROXY_API_KEY = process.env.CLIPROXY_API_KEY;
}
const extraEnvKeys = (process.env.CCS_TEST_CODEX_LOG_ENV_KEYS || '')
.split(',')
.map((key) => key.trim())
.filter(Boolean);
for (const key of extraEnvKeys) {
loggedEnv[key] = process.env[key];
}
fs.appendFileSync(
envOut,
JSON.stringify({
CODEX_HOME: process.env.CODEX_HOME,
CODEX_CI: process.env.CODEX_CI,
CODEX_MANAGED_BY_BUN: process.env.CODEX_MANAGED_BY_BUN,
CODEX_THREAD_ID: process.env.CODEX_THREAD_ID,
ANTHROPIC_BASE_URL: process.env.ANTHROPIC_BASE_URL,
CCS_BROWSER_USER_DATA_DIR: process.env.CCS_BROWSER_USER_DATA_DIR,
CCS_BROWSER_PROFILE_DIR: process.env.CCS_BROWSER_PROFILE_DIR,
CCS_BROWSER_DEVTOOLS_WS_URL: process.env.CCS_BROWSER_DEVTOOLS_WS_URL,
}) + '\\n'
JSON.stringify(loggedEnv) + '\\n'
);
}
const configFlagIndex = cliArgs.findIndex((arg) => arg === '-c' || arg === '--config');
@@ -396,6 +410,7 @@ process.exit(0);
...process.env,
CI: '1',
NO_COLOR: '1',
HOME: tmpHome,
CCS_HOME: tmpHome,
CCS_CODEX_PATH: fakeCodexPath,
CCS_TEST_CODEX_ARGS_OUT: codexArgsLogPath,
@@ -413,6 +428,7 @@ process.exit(0);
...process.env,
CI: '1',
NO_COLOR: '1',
HOME: tmpHome,
CCS_HOME: tmpHome,
CCS_CODEX_PATH: fakeCodexPath,
CCS_TEST_CODEX_ARGS_OUT: codexArgsLogPath,
@@ -647,6 +663,7 @@ process.exit(0);
...process.env,
CI: '1',
NO_COLOR: '1',
HOME: tmpHome,
CCS_HOME: tmpHome,
CCS_CODEX_PATH: fakeCodexPath,
CCS_TEST_CODEX_ARGS_OUT: codexArgsLogPath,
@@ -668,21 +685,24 @@ process.exit(0);
...process.env,
CI: '1',
NO_COLOR: '1',
HOME: tmpHome,
CCS_HOME: tmpHome,
CCS_CODEX_PATH: fakeCodexPath,
CCS_TEST_CODEX_ENV_OUT: codexEnvLogPath,
CCS_TEST_CODEX_VERSION: 'codex-cli 9.9.9-test',
CCS_TEST_CODEX_LOG_CLIPROXY_API_KEY: '1',
CODEX_HOME: inheritedCodexHome,
});
expect(result.status).toBe(0);
expect(readLoggedCodexEnv(codexEnvLogPath)).toEqual([
{
CODEX_HOME: path.join(os.homedir(), '.codex'),
CODEX_HOME: path.join(tmpHome, '.codex'),
CODEX_CI: undefined,
CODEX_MANAGED_BY_BUN: undefined,
CODEX_THREAD_ID: undefined,
ANTHROPIC_BASE_URL: undefined,
CLIPROXY_API_KEY: 'ccs-internal-managed',
CCS_BROWSER_USER_DATA_DIR: undefined,
CCS_BROWSER_PROFILE_DIR: undefined,
CCS_BROWSER_DEVTOOLS_WS_URL: undefined,
@@ -697,10 +717,112 @@ process.exit(0);
...process.env,
CI: '1',
NO_COLOR: '1',
HOME: tmpHome,
CCS_HOME: tmpHome,
CCS_CODEX_PATH: fakeCodexPath,
CCS_TEST_CODEX_ARGS_OUT: codexArgsLogPath,
CCS_TEST_CODEX_ENV_OUT: codexEnvLogPath,
CCS_TEST_CODEX_LOG_CLIPROXY_API_KEY: '1',
});
expect(result.status).toBe(0);
expect(readLoggedCodexCalls(codexArgsLogPath)).toEqual([
['--config', 'model_provider="cliproxy"', 'fix failing tests'],
]);
const codexConfig = fs.readFileSync(path.join(tmpHome, '.codex', 'config.toml'), 'utf8');
expect(codexConfig).toContain('[model_providers.cliproxy]');
expect(codexConfig).toContain('env_key = "CLIPROXY_API_KEY"');
expect(readLoggedCodexEnv(codexEnvLogPath)).toEqual([
{
CODEX_HOME: path.join(tmpHome, '.codex'),
CODEX_CI: undefined,
CODEX_MANAGED_BY_BUN: undefined,
CODEX_THREAD_ID: undefined,
ANTHROPIC_BASE_URL: undefined,
CLIPROXY_API_KEY: 'ccs-internal-managed',
CCS_BROWSER_USER_DATA_DIR: undefined,
CCS_BROWSER_PROFILE_DIR: undefined,
CCS_BROWSER_DEVTOOLS_WS_URL: undefined,
},
]);
});
it('loads the configured cliproxy provider env_key for ccsxp launches', () => {
if (process.platform === 'win32') return;
const codexHome = path.join(tmpHome, '.codex');
fs.mkdirSync(codexHome, { recursive: true });
fs.writeFileSync(
path.join(codexHome, 'config.toml'),
`[model_providers.cliproxy]
name = "CLIProxy Codex"
base_url = "http://localhost:8317/api/provider/codex"
env_key = "CCS_CUSTOM_CLIPROXY_TOKEN"
wire_api = "responses"
requires_openai_auth = false
supports_websockets = false
`,
'utf8'
);
const result = runCcsxpAlias(['fix failing tests'], {
...process.env,
CI: '1',
NO_COLOR: '1',
HOME: tmpHome,
CCS_HOME: tmpHome,
CCS_CODEX_PATH: fakeCodexPath,
CCS_TEST_CODEX_ARGS_OUT: codexArgsLogPath,
CCS_TEST_CODEX_ENV_OUT: codexEnvLogPath,
CCS_TEST_CODEX_LOG_ENV_KEYS: 'CCS_CUSTOM_CLIPROXY_TOKEN',
});
expect(result.status).toBe(0);
expect(readLoggedCodexCalls(codexArgsLogPath)).toEqual([
['--config', 'model_provider="cliproxy"', 'fix failing tests'],
]);
const codexConfig = fs.readFileSync(path.join(codexHome, 'config.toml'), 'utf8');
expect(codexConfig).toContain('env_key = "CCS_CUSTOM_CLIPROXY_TOKEN"');
expect(readLoggedCodexEnv(codexEnvLogPath)).toEqual([
{
CODEX_HOME: codexHome,
CODEX_CI: undefined,
CODEX_MANAGED_BY_BUN: undefined,
CODEX_THREAD_ID: undefined,
ANTHROPIC_BASE_URL: undefined,
CCS_CUSTOM_CLIPROXY_TOKEN: 'ccs-internal-managed',
CCS_BROWSER_USER_DATA_DIR: undefined,
CCS_BROWSER_PROFILE_DIR: undefined,
CCS_BROWSER_DEVTOOLS_WS_URL: undefined,
},
]);
});
it('keeps ccsxp native when the CCS default profile is a Claude account', () => {
if (process.platform === 'win32') return;
fs.writeFileSync(
path.join(ccsDir, 'config.yaml'),
[
'version: 2',
'default: work',
'accounts:',
' work:',
' created: "2026-01-01"',
' last_used: "2026-01-01"',
].join('\n')
);
const result = runCcsxpAlias(['fix failing tests'], {
...process.env,
CI: '1',
NO_COLOR: '1',
HOME: tmpHome,
CCS_HOME: tmpHome,
CCS_CODEX_PATH: fakeCodexPath,
CCS_TEST_CODEX_ARGS_OUT: codexArgsLogPath,
CCS_TEST_CODEX_ENV_OUT: codexEnvLogPath,
CCS_TEST_CODEX_LOG_CLIPROXY_API_KEY: '1',
});
expect(result.status).toBe(0);
@@ -709,7 +831,51 @@ process.exit(0);
]);
expect(readLoggedCodexEnv(codexEnvLogPath)).toEqual([
{
CODEX_HOME: path.join(os.homedir(), '.codex'),
CODEX_HOME: path.join(tmpHome, '.codex'),
CODEX_CI: undefined,
CODEX_MANAGED_BY_BUN: undefined,
CODEX_THREAD_ID: undefined,
ANTHROPIC_BASE_URL: undefined,
CLIPROXY_API_KEY: 'ccs-internal-managed',
CCS_BROWSER_USER_DATA_DIR: undefined,
CCS_BROWSER_PROFILE_DIR: undefined,
CCS_BROWSER_DEVTOOLS_WS_URL: undefined,
},
]);
});
it('keeps implicit ccs --target codex launches native when the CCS default is a Claude account', () => {
if (process.platform === 'win32') return;
fs.writeFileSync(
path.join(ccsDir, 'config.yaml'),
[
'version: 2',
'default: work',
'accounts:',
' work:',
' created: "2026-01-01"',
' last_used: "2026-01-01"',
].join('\n')
);
const result = runCcs(['--target', 'codex'], {
...process.env,
CI: '1',
NO_COLOR: '1',
HOME: tmpHome,
CCS_HOME: tmpHome,
CCS_CODEX_PATH: fakeCodexPath,
CCS_TEST_CODEX_ARGS_OUT: codexArgsLogPath,
CCS_TEST_CODEX_ENV_OUT: codexEnvLogPath,
CCS_THINKING: '8192',
});
expect(result.status).toBe(0);
expect(readLoggedCodexCalls(codexArgsLogPath)).toEqual([[]]);
expect(readLoggedCodexEnv(codexEnvLogPath)).toEqual([
{
CODEX_HOME: undefined,
CODEX_CI: undefined,
CODEX_MANAGED_BY_BUN: undefined,
CODEX_THREAD_ID: undefined,
@@ -721,6 +887,35 @@ process.exit(0);
]);
});
it('still rejects an explicit Claude account profile on the Codex target', () => {
if (process.platform === 'win32') return;
fs.writeFileSync(
path.join(ccsDir, 'config.yaml'),
[
'version: 2',
'accounts:',
' work:',
' created: "2026-01-01"',
' last_used: "2026-01-01"',
].join('\n')
);
const result = runCcs(['work', '--target', 'codex', 'fix failing tests'], {
...process.env,
CI: '1',
NO_COLOR: '1',
CCS_HOME: tmpHome,
CCS_CODEX_PATH: fakeCodexPath,
CCS_TEST_CODEX_ARGS_OUT: codexArgsLogPath,
});
expect(result.status).toBe(1);
expect(result.stderr).toContain('Codex CLI does not support Claude account-based profiles.');
expect(result.stderr).toContain('CLIProxy Codex pool: ccs codex --target codex or ccsxp');
expect(readLoggedCodexCalls(codexArgsLogPath)).toEqual([]);
});
it('rejects conflicting native provider config overrides for ccsxp', () => {
if (process.platform === 'win32') return;
@@ -728,6 +923,7 @@ process.exit(0);
...process.env,
CI: '1',
NO_COLOR: '1',
HOME: tmpHome,
CCS_HOME: tmpHome,
CCS_CODEX_PATH: fakeCodexPath,
CCS_TEST_CODEX_ARGS_OUT: codexArgsLogPath,
@@ -745,6 +941,7 @@ process.exit(0);
...process.env,
CI: '1',
NO_COLOR: '1',
HOME: tmpHome,
CCS_HOME: tmpHome,
CCS_CODEX_PATH: fakeCodexPath,
CCS_TEST_CODEX_ARGS_OUT: codexArgsLogPath,
@@ -767,6 +964,7 @@ process.exit(0);
CCS_CODEX_PATH: fakeCodexPath,
CCS_TEST_CODEX_ENV_OUT: codexEnvLogPath,
CCS_TEST_CODEX_VERSION: 'codex-cli 9.9.9-test',
CCS_TEST_CODEX_LOG_CLIPROXY_API_KEY: '1',
CODEX_HOME: path.join(tmpHome, 'inherited-codex-home'),
CCSXP_CODEX_HOME: explicitCodexHome,
});
@@ -779,6 +977,7 @@ process.exit(0);
CODEX_MANAGED_BY_BUN: undefined,
CODEX_THREAD_ID: undefined,
ANTHROPIC_BASE_URL: undefined,
CLIPROXY_API_KEY: 'ccs-internal-managed',
CCS_BROWSER_USER_DATA_DIR: undefined,
CCS_BROWSER_PROFILE_DIR: undefined,
CCS_BROWSER_DEVTOOLS_WS_URL: undefined,
@@ -87,16 +87,21 @@ describe('evaluateTargetRuntimeCompatibility', () => {
profileType: 'settings',
});
expect(genericSettingsCompatibility.supported).toBe(false);
expect(genericSettingsCompatibility.reason).toMatch(/currently supports native default sessions/);
expect(genericSettingsCompatibility.reason).toMatch(
/currently supports native default sessions/
);
});
test('rejects account, copilot, and cursor profiles on Codex target', () => {
expect(
evaluateTargetRuntimeCompatibility({
target: 'codex',
profileType: 'account',
}).supported
).toBe(false);
const accountCompatibility = evaluateTargetRuntimeCompatibility({
target: 'codex',
profileType: 'account',
});
expect(accountCompatibility.supported).toBe(false);
expect(accountCompatibility.suggestion).toContain('ccs --target codex');
expect(accountCompatibility.suggestion).toContain('ccs codex --target codex');
expect(accountCompatibility.suggestion).toContain('ccs cliproxy routing');
expect(
evaluateTargetRuntimeCompatibility({
target: 'codex',
+4 -1
View File
@@ -58,9 +58,12 @@ export interface CodexFeatureCatalogEntry {
export const CLIPROXY_NATIVE_CODEX_RECIPE = `model_provider = "cliproxy"
[model_providers.cliproxy]
name = "CLIProxy Codex"
base_url = "http://127.0.0.1:8317/api/provider/codex"
env_key = "CLIPROXY_API_KEY"
wire_api = "responses"`;
wire_api = "responses"
requires_openai_auth = false
supports_websockets = false`;
export const KNOWN_CODEX_FEATURE_NAMES = [
'multi_agent',