diff --git a/docs/codebase-summary.md b/docs/codebase-summary.md index afe1d761..e9fe2bb3 100644 --- a/docs/codebase-summary.md +++ b/docs/codebase-summary.md @@ -273,7 +273,7 @@ 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. 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. +- 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`, preserves a valid custom `base_url`, 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`. diff --git a/docs/system-architecture/target-adapters.md b/docs/system-architecture/target-adapters.md index f3d06468..228ed888 100644 --- a/docs/system-architecture/target-adapters.md +++ b/docs/system-architecture/target-adapters.md @@ -522,6 +522,7 @@ ccsxp → 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` +→ preserves valid custom `base_url` values for remote or non-default CLIProxy endpoints → 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 ``` diff --git a/src/targets/codex-cliproxy-provider-config.ts b/src/targets/codex-cliproxy-provider-config.ts index 2944e9ba..c2d084fe 100644 --- a/src/targets/codex-cliproxy-provider-config.ts +++ b/src/targets/codex-cliproxy-provider-config.ts @@ -50,22 +50,16 @@ function asObject(value: unknown): Record | null { : null; } -function normalizeLocalProviderUrl(value: unknown): string | null { - if (typeof value !== 'string') return null; +function isValidCodexCliproxyBaseUrl(value: unknown): value is string { + if (typeof value !== 'string') return false; const trimmed = value.trim(); + if (!trimmed) return false; 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(/\/$/, ''); - } + return url.protocol === 'http:' || url.protocol === 'https:'; } catch { - return null; + return false; } - return null; } function resolveProviderEnvKey(provider: Record | null): string { @@ -76,14 +70,10 @@ function resolveProviderEnvKey(provider: Record | null): string return CODEX_CLIPROXY_PROVIDER_ENV_KEY; } -function isProviderReady( - provider: Record, - expectedBaseUrl: string, - envKey: string -): boolean { +function isProviderReady(provider: Record, envKey: string): boolean { return ( provider.name === CODEX_CLIPROXY_PROVIDER_NAME && - normalizeLocalProviderUrl(provider.base_url) === expectedBaseUrl && + isValidCodexCliproxyBaseUrl(provider.base_url) && provider.env_key === envKey && provider.wire_api === 'responses' && provider.requires_openai_auth === false && @@ -102,6 +92,17 @@ function buildProviderConfig(baseUrl: string, envKey: string): Record, + fallbackBaseUrl: string +): string { + const baseUrl = provider.base_url; + if (isValidCodexCliproxyBaseUrl(baseUrl)) { + return baseUrl.trim(); + } + return fallbackBaseUrl; +} + function appendProviderBlock(rawText: string, baseUrl: string): string { const prefix = rawText.trimEnd(); const providerBlock = stringifyTomlObject({ @@ -179,12 +180,12 @@ export async function ensureCodexCliproxyProviderConfig( } const envKey = resolveProviderEnvKey(currentProvider); - const providerReady = isProviderReady(currentProvider, expectedBaseUrl, envKey); + const providerReady = isProviderReady(currentProvider, envKey); if (!providerReady) { providers[CODEX_CLIPROXY_PROVIDER_ID] = { ...currentProvider, - ...buildProviderConfig(expectedBaseUrl, envKey), + ...buildProviderConfig(resolveProviderBaseUrl(currentProvider, expectedBaseUrl), envKey), }; } diff --git a/tests/unit/targets/codex-cliproxy-provider-config.test.ts b/tests/unit/targets/codex-cliproxy-provider-config.test.ts index f182ce2c..08dabe2e 100644 --- a/tests/unit/targets/codex-cliproxy-provider-config.test.ts +++ b/tests/unit/targets/codex-cliproxy-provider-config.test.ts @@ -78,19 +78,19 @@ wire_api = "responses" 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('base_url = "http://localhost:8317/api/provider/codex"'); 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 () => { + it('preserves custom cliproxy provider values 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" +base_url = "https://cliproxy.example.com/api/provider/codex/responses" env_key = "CCS_CUSTOM_CLIPROXY_TOKEN" wire_api = "chat" `, @@ -102,11 +102,35 @@ wire_api = "chat" 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( + 'base_url = "https://cliproxy.example.com/api/provider/codex/responses"' + ); expect(rawText).toContain('env_key = "CCS_CUSTOM_CLIPROXY_TOKEN"'); expect(rawText).toContain('wire_api = "responses"'); }); + it('repairs invalid cliproxy provider base URLs back to the managed local default', async () => { + fs.mkdirSync(codexHome, { recursive: true }); + fs.writeFileSync( + configPath, + `[model_providers.cliproxy] +name = "CLIProxy Codex" +base_url = "not-a-url" +env_key = "CLIPROXY_API_KEY" +wire_api = "responses" +requires_openai_auth = false +supports_websockets = false +`, + 'utf8' + ); + + const result = await ensureCodexCliproxyProviderConfig(9321, env); + + expect(result.changed).toBe(true); + const rawText = fs.readFileSync(configPath, 'utf8'); + expect(rawText).toContain(`base_url = "${buildCodexCliproxyProviderBaseUrl(9321)}"`); + }); + it('rejects invalid non-table model_providers values without appending broken TOML', async () => { fs.mkdirSync(codexHome, { recursive: true }); const rawText = 'model_providers = "legacy"\n'; @@ -137,6 +161,25 @@ supports_websockets = false expect(fs.readFileSync(configPath, 'utf8')).toBe(rawText); }); + it('leaves a ready remote provider unchanged', async () => { + fs.mkdirSync(codexHome, { recursive: true }); + const rawText = `[model_providers.cliproxy] +name = "CLIProxy Codex" +base_url = "https://cliproxy.example.com/api/provider/codex" +env_key = "CCS_REMOTE_CLIPROXY_TOKEN" +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('CCS_REMOTE_CLIPROXY_TOKEN'); + expect(fs.readFileSync(configPath, 'utf8')).toBe(rawText); + }); + it('normalizes a ready native Codex tuning alias before requests reach cliproxy', async () => { fs.mkdirSync(codexHome, { recursive: true }); fs.writeFileSync( diff --git a/tests/unit/targets/codex-runtime-integration.test.ts b/tests/unit/targets/codex-runtime-integration.test.ts index 3c0fc2a5..ab9cd0d5 100644 --- a/tests/unit/targets/codex-runtime-integration.test.ts +++ b/tests/unit/targets/codex-runtime-integration.test.ts @@ -828,6 +828,60 @@ supports_websockets = false ]); }); + it('preserves a custom cliproxy provider base_url 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 = "https://cliproxy.example.com/api/provider/codex/responses" +env_key = "CCS_REMOTE_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_REMOTE_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( + 'base_url = "https://cliproxy.example.com/api/provider/codex/responses"' + ); + expect(codexConfig).toContain('env_key = "CCS_REMOTE_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_REMOTE_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;