mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 02:14:32 +00:00
fix(codex): self-heal ccsxp cliproxy provider (#1243)
This commit is contained in:
1 parent
9748ba4ee0
commit
4748c452bd
12 files changed
+636
-24
No files matched your search
@@ -273,7 +273,8 @@ src/
|
|||||||
### Native Codex Runtime Target
|
### 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`.
|
- 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.
|
- `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`.
|
- 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.
|
- 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
|
→ CCS_INTERNAL_ENTRY_TARGET=codex
|
||||||
→ injects native `model_provider="cliproxy"` override
|
→ injects native `model_provider="cliproxy"` override
|
||||||
→ pins CODEX_HOME to native `~/.codex` unless `CCSXP_CODEX_HOME` is set
|
→ 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
|
If a user launches CCS through a custom shim instead of the built-in package bins, target
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
const os = require('os');
|
const os = require('os');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const { CCSXP_CLIPROXY_SHORTCUT_ENV } = require('../targets/codex-cliproxy-provider-config');
|
||||||
const { stripTargetFlag } = require('../targets/target-resolver');
|
const { stripTargetFlag } = require('../targets/target-resolver');
|
||||||
const { expandPath } = require('../utils/helpers');
|
const { expandPath } = require('../utils/helpers');
|
||||||
const { fail } = require('../utils/ui');
|
const { fail } = require('../utils/ui');
|
||||||
|
|
||||||
process.env.CCS_INTERNAL_ENTRY_TARGET = 'codex';
|
process.env.CCS_INTERNAL_ENTRY_TARGET = 'codex';
|
||||||
|
process.env[CCSXP_CLIPROXY_SHORTCUT_ENV] = '1';
|
||||||
const CCSXP_CLIPROXY_OVERRIDE = 'model_provider="cliproxy"';
|
const CCSXP_CLIPROXY_OVERRIDE = 'model_provider="cliproxy"';
|
||||||
const DISALLOWED_CCSXP_CONFIG_KEY_REGEX =
|
const DISALLOWED_CCSXP_CONFIG_KEY_REGEX =
|
||||||
/^(model_provider|local_provider|profile)\s*=|^model_providers\./i;
|
/^(model_provider|local_provider|profile)\s*=|^model_providers\./i;
|
||||||
|
|||||||
@@ -252,6 +252,65 @@ describe('resolveProfileAndTarget', () => {
|
|||||||
expect(exitSpy).not.toHaveBeenCalled();
|
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 () => {
|
it('resolves settings profile (glm) with --target droid and loads settings', async () => {
|
||||||
// Settings loading only runs in the non-claude preflight block (resolvedTarget !== 'claude').
|
// Settings loading only runs in the non-claude preflight block (resolvedTarget !== 'claude').
|
||||||
const mockDroidAdapter = {
|
const mockDroidAdapter = {
|
||||||
|
|||||||
@@ -79,6 +79,18 @@ export interface ResolvedProfile {
|
|||||||
detector: InstanceType<typeof ProfileDetector>;
|
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 ==========
|
// ========== Profile and Target Resolver ==========
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -112,7 +124,7 @@ export async function resolveProfileAndTarget(
|
|||||||
// Detect profile (strip --target flags before profile detection)
|
// Detect profile (strip --target flags before profile detection)
|
||||||
const cleanArgs = stripTargetFlag(args);
|
const cleanArgs = stripTargetFlag(args);
|
||||||
const { profile, remainingArgs } = detectProfile(cleanArgs);
|
const { profile, remainingArgs } = detectProfile(cleanArgs);
|
||||||
const profileInfo: ProfileDetectionResult = detector.detectProfileType(profile);
|
let profileInfo: ProfileDetectionResult = detector.detectProfileType(profile);
|
||||||
|
|
||||||
let resolvedTarget: ReturnType<typeof resolveTargetType>;
|
let resolvedTarget: ReturnType<typeof resolveTargetType>;
|
||||||
try {
|
try {
|
||||||
@@ -127,6 +139,10 @@ export async function resolveProfileAndTarget(
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (resolvedTarget === 'codex' && usesImplicitDefaultProfile(cleanArgs)) {
|
||||||
|
profileInfo = buildNativeCodexDefaultProfile();
|
||||||
|
}
|
||||||
|
|
||||||
// Detect Claude CLI (needed for claude target and all CLIProxy-derived flows)
|
// Detect Claude CLI (needed for claude target and all CLIProxy-derived flows)
|
||||||
const claudeCliRaw = detectClaudeCli();
|
const claudeCliRaw = detectClaudeCli();
|
||||||
if (resolvedTarget === 'claude' && !claudeCliRaw) {
|
if (resolvedTarget === 'claude' && !claudeCliRaw) {
|
||||||
|
|||||||
@@ -24,6 +24,14 @@ import {
|
|||||||
readCodexVersion,
|
readCodexVersion,
|
||||||
} from './codex-detector';
|
} from './codex-detector';
|
||||||
import { createLogger } from '../services/logging';
|
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');
|
const adapterLogger = createLogger('targets:codex');
|
||||||
|
|
||||||
@@ -176,13 +184,25 @@ function prepareExplicitCodexHome(
|
|||||||
export class CodexAdapter implements TargetAdapter {
|
export class CodexAdapter implements TargetAdapter {
|
||||||
readonly type: TargetType = 'codex';
|
readonly type: TargetType = 'codex';
|
||||||
readonly displayName = 'Codex CLI';
|
readonly displayName = 'Codex CLI';
|
||||||
|
private ccsxpCliproxyEnvKey = CODEX_CLIPROXY_PROVIDER_ENV_KEY;
|
||||||
|
|
||||||
detectBinary(): TargetBinaryInfo | null {
|
detectBinary(): TargetBinaryInfo | null {
|
||||||
return getCodexBinaryInfo({ includeVersion: false, includeFeatures: false });
|
return getCodexBinaryInfo({ includeVersion: false, includeFeatures: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
async prepareCredentials(_creds: TargetCredentials): Promise<void> {
|
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(
|
buildArgs(
|
||||||
@@ -258,7 +278,11 @@ export class CodexAdapter implements TargetAdapter {
|
|||||||
const env: NodeJS.ProcessEnv = {
|
const env: NodeJS.ProcessEnv = {
|
||||||
...stripBrowserEnv(stripCodexSessionEnv(stripAnthropicEnv(process.env))),
|
...stripBrowserEnv(stripCodexSessionEnv(stripAnthropicEnv(process.env))),
|
||||||
};
|
};
|
||||||
|
delete env[CCSXP_CLIPROXY_SHORTCUT_ENV];
|
||||||
delete env[CODEX_RUNTIME_ENV_KEY];
|
delete env[CODEX_RUNTIME_ENV_KEY];
|
||||||
|
if (profileType === 'default' && isCcsxpCliproxyShortcut()) {
|
||||||
|
env[this.ccsxpCliproxyEnvKey || CODEX_CLIPROXY_PROVIDER_ENV_KEY] = getEffectiveApiKey();
|
||||||
|
}
|
||||||
if (profileType !== 'default') {
|
if (profileType !== 'default') {
|
||||||
if (!creds.apiKey?.trim()) {
|
if (!creds.apiKey?.trim()) {
|
||||||
throw new Error('Codex target requires an API key for CCS-backed profile launches.');
|
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 };
|
||||||
|
}
|
||||||
@@ -46,7 +46,7 @@ export function evaluateTargetRuntimeCompatibility(
|
|||||||
if (input.profileType === 'account') {
|
if (input.profileType === 'account') {
|
||||||
return unsupported(
|
return unsupported(
|
||||||
'Codex CLI does not support Claude account-based profiles.',
|
'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;
|
const envOut = process.env.CCS_TEST_CODEX_ENV_OUT;
|
||||||
if (envOut) {
|
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(
|
fs.appendFileSync(
|
||||||
envOut,
|
envOut,
|
||||||
JSON.stringify({
|
JSON.stringify(loggedEnv) + '\\n'
|
||||||
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'
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const configFlagIndex = cliArgs.findIndex((arg) => arg === '-c' || arg === '--config');
|
const configFlagIndex = cliArgs.findIndex((arg) => arg === '-c' || arg === '--config');
|
||||||
@@ -396,6 +410,7 @@ process.exit(0);
|
|||||||
...process.env,
|
...process.env,
|
||||||
CI: '1',
|
CI: '1',
|
||||||
NO_COLOR: '1',
|
NO_COLOR: '1',
|
||||||
|
HOME: tmpHome,
|
||||||
CCS_HOME: tmpHome,
|
CCS_HOME: tmpHome,
|
||||||
CCS_CODEX_PATH: fakeCodexPath,
|
CCS_CODEX_PATH: fakeCodexPath,
|
||||||
CCS_TEST_CODEX_ARGS_OUT: codexArgsLogPath,
|
CCS_TEST_CODEX_ARGS_OUT: codexArgsLogPath,
|
||||||
@@ -413,6 +428,7 @@ process.exit(0);
|
|||||||
...process.env,
|
...process.env,
|
||||||
CI: '1',
|
CI: '1',
|
||||||
NO_COLOR: '1',
|
NO_COLOR: '1',
|
||||||
|
HOME: tmpHome,
|
||||||
CCS_HOME: tmpHome,
|
CCS_HOME: tmpHome,
|
||||||
CCS_CODEX_PATH: fakeCodexPath,
|
CCS_CODEX_PATH: fakeCodexPath,
|
||||||
CCS_TEST_CODEX_ARGS_OUT: codexArgsLogPath,
|
CCS_TEST_CODEX_ARGS_OUT: codexArgsLogPath,
|
||||||
@@ -647,6 +663,7 @@ process.exit(0);
|
|||||||
...process.env,
|
...process.env,
|
||||||
CI: '1',
|
CI: '1',
|
||||||
NO_COLOR: '1',
|
NO_COLOR: '1',
|
||||||
|
HOME: tmpHome,
|
||||||
CCS_HOME: tmpHome,
|
CCS_HOME: tmpHome,
|
||||||
CCS_CODEX_PATH: fakeCodexPath,
|
CCS_CODEX_PATH: fakeCodexPath,
|
||||||
CCS_TEST_CODEX_ARGS_OUT: codexArgsLogPath,
|
CCS_TEST_CODEX_ARGS_OUT: codexArgsLogPath,
|
||||||
@@ -668,21 +685,24 @@ process.exit(0);
|
|||||||
...process.env,
|
...process.env,
|
||||||
CI: '1',
|
CI: '1',
|
||||||
NO_COLOR: '1',
|
NO_COLOR: '1',
|
||||||
|
HOME: tmpHome,
|
||||||
CCS_HOME: tmpHome,
|
CCS_HOME: tmpHome,
|
||||||
CCS_CODEX_PATH: fakeCodexPath,
|
CCS_CODEX_PATH: fakeCodexPath,
|
||||||
CCS_TEST_CODEX_ENV_OUT: codexEnvLogPath,
|
CCS_TEST_CODEX_ENV_OUT: codexEnvLogPath,
|
||||||
CCS_TEST_CODEX_VERSION: 'codex-cli 9.9.9-test',
|
CCS_TEST_CODEX_VERSION: 'codex-cli 9.9.9-test',
|
||||||
|
CCS_TEST_CODEX_LOG_CLIPROXY_API_KEY: '1',
|
||||||
CODEX_HOME: inheritedCodexHome,
|
CODEX_HOME: inheritedCodexHome,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.status).toBe(0);
|
expect(result.status).toBe(0);
|
||||||
expect(readLoggedCodexEnv(codexEnvLogPath)).toEqual([
|
expect(readLoggedCodexEnv(codexEnvLogPath)).toEqual([
|
||||||
{
|
{
|
||||||
CODEX_HOME: path.join(os.homedir(), '.codex'),
|
CODEX_HOME: path.join(tmpHome, '.codex'),
|
||||||
CODEX_CI: undefined,
|
CODEX_CI: undefined,
|
||||||
CODEX_MANAGED_BY_BUN: undefined,
|
CODEX_MANAGED_BY_BUN: undefined,
|
||||||
CODEX_THREAD_ID: undefined,
|
CODEX_THREAD_ID: undefined,
|
||||||
ANTHROPIC_BASE_URL: undefined,
|
ANTHROPIC_BASE_URL: undefined,
|
||||||
|
CLIPROXY_API_KEY: 'ccs-internal-managed',
|
||||||
CCS_BROWSER_USER_DATA_DIR: undefined,
|
CCS_BROWSER_USER_DATA_DIR: undefined,
|
||||||
CCS_BROWSER_PROFILE_DIR: undefined,
|
CCS_BROWSER_PROFILE_DIR: undefined,
|
||||||
CCS_BROWSER_DEVTOOLS_WS_URL: undefined,
|
CCS_BROWSER_DEVTOOLS_WS_URL: undefined,
|
||||||
@@ -697,10 +717,112 @@ process.exit(0);
|
|||||||
...process.env,
|
...process.env,
|
||||||
CI: '1',
|
CI: '1',
|
||||||
NO_COLOR: '1',
|
NO_COLOR: '1',
|
||||||
|
HOME: tmpHome,
|
||||||
CCS_HOME: tmpHome,
|
CCS_HOME: tmpHome,
|
||||||
CCS_CODEX_PATH: fakeCodexPath,
|
CCS_CODEX_PATH: fakeCodexPath,
|
||||||
CCS_TEST_CODEX_ARGS_OUT: codexArgsLogPath,
|
CCS_TEST_CODEX_ARGS_OUT: codexArgsLogPath,
|
||||||
CCS_TEST_CODEX_ENV_OUT: codexEnvLogPath,
|
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);
|
expect(result.status).toBe(0);
|
||||||
@@ -709,7 +831,51 @@ process.exit(0);
|
|||||||
]);
|
]);
|
||||||
expect(readLoggedCodexEnv(codexEnvLogPath)).toEqual([
|
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_CI: undefined,
|
||||||
CODEX_MANAGED_BY_BUN: undefined,
|
CODEX_MANAGED_BY_BUN: undefined,
|
||||||
CODEX_THREAD_ID: 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', () => {
|
it('rejects conflicting native provider config overrides for ccsxp', () => {
|
||||||
if (process.platform === 'win32') return;
|
if (process.platform === 'win32') return;
|
||||||
|
|
||||||
@@ -728,6 +923,7 @@ process.exit(0);
|
|||||||
...process.env,
|
...process.env,
|
||||||
CI: '1',
|
CI: '1',
|
||||||
NO_COLOR: '1',
|
NO_COLOR: '1',
|
||||||
|
HOME: tmpHome,
|
||||||
CCS_HOME: tmpHome,
|
CCS_HOME: tmpHome,
|
||||||
CCS_CODEX_PATH: fakeCodexPath,
|
CCS_CODEX_PATH: fakeCodexPath,
|
||||||
CCS_TEST_CODEX_ARGS_OUT: codexArgsLogPath,
|
CCS_TEST_CODEX_ARGS_OUT: codexArgsLogPath,
|
||||||
@@ -745,6 +941,7 @@ process.exit(0);
|
|||||||
...process.env,
|
...process.env,
|
||||||
CI: '1',
|
CI: '1',
|
||||||
NO_COLOR: '1',
|
NO_COLOR: '1',
|
||||||
|
HOME: tmpHome,
|
||||||
CCS_HOME: tmpHome,
|
CCS_HOME: tmpHome,
|
||||||
CCS_CODEX_PATH: fakeCodexPath,
|
CCS_CODEX_PATH: fakeCodexPath,
|
||||||
CCS_TEST_CODEX_ARGS_OUT: codexArgsLogPath,
|
CCS_TEST_CODEX_ARGS_OUT: codexArgsLogPath,
|
||||||
@@ -767,6 +964,7 @@ process.exit(0);
|
|||||||
CCS_CODEX_PATH: fakeCodexPath,
|
CCS_CODEX_PATH: fakeCodexPath,
|
||||||
CCS_TEST_CODEX_ENV_OUT: codexEnvLogPath,
|
CCS_TEST_CODEX_ENV_OUT: codexEnvLogPath,
|
||||||
CCS_TEST_CODEX_VERSION: 'codex-cli 9.9.9-test',
|
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'),
|
CODEX_HOME: path.join(tmpHome, 'inherited-codex-home'),
|
||||||
CCSXP_CODEX_HOME: explicitCodexHome,
|
CCSXP_CODEX_HOME: explicitCodexHome,
|
||||||
});
|
});
|
||||||
@@ -779,6 +977,7 @@ process.exit(0);
|
|||||||
CODEX_MANAGED_BY_BUN: undefined,
|
CODEX_MANAGED_BY_BUN: undefined,
|
||||||
CODEX_THREAD_ID: undefined,
|
CODEX_THREAD_ID: undefined,
|
||||||
ANTHROPIC_BASE_URL: undefined,
|
ANTHROPIC_BASE_URL: undefined,
|
||||||
|
CLIPROXY_API_KEY: 'ccs-internal-managed',
|
||||||
CCS_BROWSER_USER_DATA_DIR: undefined,
|
CCS_BROWSER_USER_DATA_DIR: undefined,
|
||||||
CCS_BROWSER_PROFILE_DIR: undefined,
|
CCS_BROWSER_PROFILE_DIR: undefined,
|
||||||
CCS_BROWSER_DEVTOOLS_WS_URL: undefined,
|
CCS_BROWSER_DEVTOOLS_WS_URL: undefined,
|
||||||
|
|||||||
@@ -87,16 +87,21 @@ describe('evaluateTargetRuntimeCompatibility', () => {
|
|||||||
profileType: 'settings',
|
profileType: 'settings',
|
||||||
});
|
});
|
||||||
expect(genericSettingsCompatibility.supported).toBe(false);
|
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', () => {
|
test('rejects account, copilot, and cursor profiles on Codex target', () => {
|
||||||
expect(
|
const accountCompatibility = evaluateTargetRuntimeCompatibility({
|
||||||
evaluateTargetRuntimeCompatibility({
|
target: 'codex',
|
||||||
target: 'codex',
|
profileType: 'account',
|
||||||
profileType: 'account',
|
});
|
||||||
}).supported
|
expect(accountCompatibility.supported).toBe(false);
|
||||||
).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(
|
expect(
|
||||||
evaluateTargetRuntimeCompatibility({
|
evaluateTargetRuntimeCompatibility({
|
||||||
target: 'codex',
|
target: 'codex',
|
||||||
|
|||||||
@@ -58,9 +58,12 @@ export interface CodexFeatureCatalogEntry {
|
|||||||
export const CLIPROXY_NATIVE_CODEX_RECIPE = `model_provider = "cliproxy"
|
export const CLIPROXY_NATIVE_CODEX_RECIPE = `model_provider = "cliproxy"
|
||||||
|
|
||||||
[model_providers.cliproxy]
|
[model_providers.cliproxy]
|
||||||
|
name = "CLIProxy Codex"
|
||||||
base_url = "http://127.0.0.1:8317/api/provider/codex"
|
base_url = "http://127.0.0.1:8317/api/provider/codex"
|
||||||
env_key = "CLIPROXY_API_KEY"
|
env_key = "CLIPROXY_API_KEY"
|
||||||
wire_api = "responses"`;
|
wire_api = "responses"
|
||||||
|
requires_openai_auth = false
|
||||||
|
supports_websockets = false`;
|
||||||
|
|
||||||
export const KNOWN_CODEX_FEATURE_NAMES = [
|
export const KNOWN_CODEX_FEATURE_NAMES = [
|
||||||
'multi_agent',
|
'multi_agent',
|
||||||
|
|||||||
Reference in new issue
Block a user