From deb1e9d71eaa226494ec2f60a52cab20b4b0a546 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Sun, 29 Mar 2026 19:43:26 -0400 Subject: [PATCH 1/4] feat(codex): add ccsxp runtime shortcut --- package.json | 3 +- src/bin/ccsxp-runtime.ts | 9 +++ src/ccs.ts | 28 ++++++-- tests/npm/cross-platform.test.js | 17 ++++- tests/unit/bin/ccsxp-runtime.test.ts | 55 +++++++++++++++ .../targets/codex-runtime-integration.test.ts | 67 +++++++++++++++++-- 6 files changed, 164 insertions(+), 15 deletions(-) create mode 100644 src/bin/ccsxp-runtime.ts create mode 100644 tests/unit/bin/ccsxp-runtime.test.ts diff --git a/package.json b/package.json index 5f9fcd31..b47e3bcc 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,8 @@ "ccs-droid": "dist/bin/droid-runtime.js", "ccsd": "dist/bin/droid-runtime.js", "ccs-codex": "dist/bin/codex-runtime.js", - "ccsx": "dist/bin/codex-runtime.js" + "ccsx": "dist/bin/codex-runtime.js", + "ccsxp": "dist/bin/ccsxp-runtime.js" }, "files": [ "dist/", diff --git a/src/bin/ccsxp-runtime.ts b/src/bin/ccsxp-runtime.ts new file mode 100644 index 00000000..6758e655 --- /dev/null +++ b/src/bin/ccsxp-runtime.ts @@ -0,0 +1,9 @@ +const { stripTargetFlag } = require('../targets/target-resolver'); + +process.env.CCS_INTERNAL_ENTRY_TARGET = 'codex'; + +// ccsxp is an opinionated shortcut for the built-in Codex-on-Codex route. +// Strip user-supplied target overrides before forcing the shortcut target. +const forwardedArgs = stripTargetFlag(process.argv.slice(2)); +process.argv.splice(2, process.argv.length - 2, 'codex', '--target', 'codex', ...forwardedArgs); +require('../ccs'); diff --git a/src/ccs.ts b/src/ccs.ts index 9ae26160..ca2625fb 100644 --- a/src/ccs.ts +++ b/src/ccs.ts @@ -241,14 +241,26 @@ function resolveNativeClaudeLaunchArgs( } function shouldPassthroughNativeCodexFlagCommand(args: string[]): boolean { + return getNativeCodexPassthroughArgs(args) !== null; +} + +function getNativeCodexPassthroughArgs(args: string[]): string[] | null { const targetArgs = stripTargetFlag(args); - if (targetArgs.length === 0) { - return false; + if (resolveTargetType(args) !== 'codex' || targetArgs.length === 0) { + return null; } - return ( - resolveTargetType(args) === 'codex' && CODEX_NATIVE_PASSTHROUGH_FLAGS.has(targetArgs[0] || '') - ); + const firstArg = targetArgs[0] || ''; + if (CODEX_NATIVE_PASSTHROUGH_FLAGS.has(firstArg)) { + return targetArgs; + } + + const secondArg = targetArgs[1] || ''; + if (firstArg === 'codex' && CODEX_NATIVE_PASSTHROUGH_FLAGS.has(secondArg)) { + return targetArgs.slice(1); + } + + return null; } function execNativeCodexFlagCommand(args: string[]): void { @@ -265,7 +277,11 @@ function execNativeCodexFlagCommand(args: string[]): void { process.exit(1); } - const targetArgs = stripTargetFlag(args); + const targetArgs = getNativeCodexPassthroughArgs(args); + if (!targetArgs) { + console.error(fail('Native Codex passthrough args could not be resolved.')); + process.exit(1); + } const creds: TargetCredentials = { profile: 'default', baseUrl: '', diff --git a/tests/npm/cross-platform.test.js b/tests/npm/cross-platform.test.js index 1382ed88..f3416fef 100644 --- a/tests/npm/cross-platform.test.js +++ b/tests/npm/cross-platform.test.js @@ -8,7 +8,7 @@ try { expandPath = require('../../dist/utils/helpers').expandPath; } catch (e) { // If helpers module doesn't exist or doesn't export expandPath, create a mock - expandPath = function(p) { + expandPath = function (p) { if (!p || typeof p !== 'string') return p; if (p.startsWith('~/')) { return path.join(os.homedir(), p.slice(2)); @@ -76,7 +76,10 @@ describe('cross-platform', () => { describe('platform-specific behavior', () => { it('detects platform correctly', () => { const platform = os.platform(); - assert(['darwin', 'linux', 'win32'].includes(platform), 'Should be running on supported platform'); + assert( + ['darwin', 'linux', 'win32'].includes(platform), + 'Should be running on supported platform' + ); }); it('handles path separators correctly', () => { @@ -133,6 +136,7 @@ describe('cross-platform', () => { assert(packageJson.bin.ccsd, 'bin field should specify ccsd command'); assert(packageJson.bin['ccs-codex'], 'bin field should specify ccs-codex command'); assert(packageJson.bin.ccsx, 'bin field should specify ccsx command'); + assert(packageJson.bin.ccsxp, 'bin field should specify ccsxp command'); assert.notStrictEqual( packageJson.bin['ccs-droid'], packageJson.bin.ccs, @@ -153,6 +157,11 @@ describe('cross-platform', () => { packageJson.bin.ccsx, 'ccsx should share the dedicated codex runtime entrypoint' ); + assert.notStrictEqual( + packageJson.bin.ccsxp, + packageJson.bin.ccsx, + 'ccsxp should use a dedicated shortcut entrypoint' + ); assert( fs.existsSync(path.join(__dirname, '..', '..', packageJson.bin['ccs-droid'])), 'dedicated droid runtime entrypoint should exist' @@ -161,6 +170,10 @@ describe('cross-platform', () => { fs.existsSync(path.join(__dirname, '..', '..', packageJson.bin['ccs-codex'])), 'dedicated codex runtime entrypoint should exist' ); + assert( + fs.existsSync(path.join(__dirname, '..', '..', packageJson.bin.ccsxp)), + 'dedicated ccsxp shortcut entrypoint should exist' + ); assert(packageJson.scripts, 'package.json should have scripts field'); }); }); diff --git a/tests/unit/bin/ccsxp-runtime.test.ts b/tests/unit/bin/ccsxp-runtime.test.ts new file mode 100644 index 00000000..ee4d0f40 --- /dev/null +++ b/tests/unit/bin/ccsxp-runtime.test.ts @@ -0,0 +1,55 @@ +import { afterEach, beforeEach, describe, expect, it } from 'bun:test'; + +const wrapperPath = require.resolve('../../../src/bin/ccsxp-runtime.ts'); +const ccsPath = require.resolve('../../../src/ccs.ts'); + +describe('ccsxp runtime wrapper', () => { + const originalArgv = process.argv; + const originalEntryTarget = process.env.CCS_INTERNAL_ENTRY_TARGET; + + beforeEach(() => { + delete require.cache[wrapperPath]; + delete require.cache[ccsPath]; + }); + + afterEach(() => { + process.argv = originalArgv; + + if (originalEntryTarget === undefined) { + delete process.env.CCS_INTERNAL_ENTRY_TARGET; + } else { + process.env.CCS_INTERNAL_ENTRY_TARGET = originalEntryTarget; + } + + delete require.cache[wrapperPath]; + delete require.cache[ccsPath]; + }); + + it('prepends the built-in codex profile and target before loading CCS', () => { + process.argv = ['node', wrapperPath, 'fix failing tests']; + require.cache[ccsPath] = { exports: {} } as NodeJS.Module; + + require(wrapperPath); + + expect(process.env.CCS_INTERNAL_ENTRY_TARGET).toBe('codex'); + expect(process.argv.slice(2)).toEqual(['codex', '--target', 'codex', 'fix failing tests']); + }); + + it('keeps flag-only invocations routed through the built-in codex profile shortcut', () => { + process.argv = ['node', wrapperPath, '--version']; + require.cache[ccsPath] = { exports: {} } as NodeJS.Module; + + require(wrapperPath); + + expect(process.argv.slice(2)).toEqual(['codex', '--target', 'codex', '--version']); + }); + + it('strips user-supplied target overrides before forcing the codex shortcut target', () => { + process.argv = ['node', wrapperPath, '--target', 'claude', '--version']; + require.cache[ccsPath] = { exports: {} } as NodeJS.Module; + + require(wrapperPath); + + expect(process.argv.slice(2)).toEqual(['codex', '--target', 'codex', '--version']); + }); +}); diff --git a/tests/unit/targets/codex-runtime-integration.test.ts b/tests/unit/targets/codex-runtime-integration.test.ts index 161348fd..3c186354 100644 --- a/tests/unit/targets/codex-runtime-integration.test.ts +++ b/tests/unit/targets/codex-runtime-integration.test.ts @@ -40,6 +40,21 @@ function runCodexAlias(args: string[], env: NodeJS.ProcessEnv): RunResult { }; } +function runCcsxpAlias(args: string[], env: NodeJS.ProcessEnv): RunResult { + const ccsxpEntry = path.join(process.cwd(), 'src', 'bin', 'ccsxp-runtime.ts'); + const result = spawnSync(process.execPath, [ccsxpEntry, ...args], { + encoding: 'utf8', + env, + timeout: 20000, + }); + + return { + status: result.status, + stdout: result.stdout || '', + stderr: result.stderr || '', + }; +} + function readLoggedCodexCalls(logPath: string): string[][] { if (!fs.existsSync(logPath)) { return []; @@ -94,8 +109,9 @@ describe('codex runtime integration', () => { `#!/usr/bin/env node const fs = require('fs'); const out = process.env.CCS_TEST_CODEX_ARGS_OUT; +const cliArgs = process.argv.slice(2); if (out) { - fs.appendFileSync(out, JSON.stringify(process.argv.slice(2)) + '\\n'); + fs.appendFileSync(out, JSON.stringify(cliArgs) + '\\n'); } const envOut = process.env.CCS_TEST_CODEX_ENV_OUT; if (envOut) { @@ -110,11 +126,11 @@ if (envOut) { }) + '\\n' ); } -if (process.argv[2] === '--version' || process.argv[2] === '-v') { +if (cliArgs.includes('--version') || cliArgs.includes('-v')) { process.stdout.write(process.env.CCS_TEST_CODEX_VERSION || 'codex-cli 0.118.0-alpha.3'); process.exit(0); } -if (process.argv[2] === '--help' || process.argv[2] === '-h') { +if (cliArgs.includes('--help') || cliArgs.includes('-h')) { process.stdout.write( process.env.CCS_TEST_CODEX_HELP || ' -c, --config \\n -p, --profile \\n' @@ -244,10 +260,10 @@ process.exit(0); ]); }); - it('fails fast when native Codex reasoning overrides need unsupported --config support', () => { + it('keeps ccsxp pinned to native Codex even when a user passes another --target override', () => { if (process.platform === 'win32') return; - const result = runCcs(['default', '--target', 'codex', '--effort', 'high', 'fix failing tests'], { + const result = runCcsxpAlias(['--target', 'claude', '--version'], { ...process.env, CI: '1', NO_COLOR: '1', @@ -255,9 +271,48 @@ process.exit(0); CCS_CODEX_PATH: fakeCodexPath, CCS_TEST_CODEX_ARGS_OUT: codexArgsLogPath, CCS_TEST_CODEX_VERSION: 'codex-cli 9.9.9-test', - CCS_TEST_CODEX_HELP: ' -p, --profile \\n', }); + expect(result.status).toBe(0); + expect(result.stdout).toContain('codex-cli 9.9.9-test'); + expect(readLoggedCodexCalls(codexArgsLogPath)).toEqual([['--version']]); + }); + + it('passes ccs codex --target codex --version through to the native Codex binary', () => { + if (process.platform === 'win32') return; + + const result = runCcs(['codex', '--target', 'codex', '--version'], { + ...process.env, + CI: '1', + NO_COLOR: '1', + CCS_HOME: tmpHome, + CCS_CODEX_PATH: fakeCodexPath, + CCS_TEST_CODEX_ARGS_OUT: codexArgsLogPath, + CCS_TEST_CODEX_VERSION: 'codex-cli 9.9.9-test', + }); + + expect(result.status).toBe(0); + expect(result.stdout).toContain('codex-cli 9.9.9-test'); + expect(readLoggedCodexCalls(codexArgsLogPath)).toEqual([['--version']]); + }); + + it('fails fast when native Codex reasoning overrides need unsupported --config support', () => { + if (process.platform === 'win32') return; + + const result = runCcs( + ['default', '--target', 'codex', '--effort', 'high', 'fix failing tests'], + { + ...process.env, + CI: '1', + NO_COLOR: '1', + CCS_HOME: tmpHome, + CCS_CODEX_PATH: fakeCodexPath, + CCS_TEST_CODEX_ARGS_OUT: codexArgsLogPath, + CCS_TEST_CODEX_VERSION: 'codex-cli 9.9.9-test', + CCS_TEST_CODEX_HELP: ' -p, --profile \\n', + } + ); + expect(result.status).toBe(1); expect(result.stderr).toContain('Codex CLI (codex-cli 9.9.9-test)'); expect(result.stderr).toContain('does not advertise --config overrides'); From 318b57bedc12f5c3602916418b8ce08bb46d4218 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Sun, 29 Mar 2026 19:43:46 -0400 Subject: [PATCH 2/4] docs(codex): clarify ccsxp and cliproxy setup --- README.md | 53 +++++++++++++++++-- src/commands/help-command.ts | 18 ++++++- src/targets/target-runtime-compatibility.ts | 2 +- .../unit/commands/help-command-parity.test.ts | 4 ++ 4 files changed, 70 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 47db2780..3bc3f421 100644 --- a/README.md +++ b/README.md @@ -197,13 +197,19 @@ ccs api import ./glm.ccs-profile.json # Import exported profile bundle ### Runtime Aliases (built-in bins / `argv[0]` pattern) -Built-in Droid runtime aliases are installed with the package: +Built-in Droid and native Codex runtime aliases are installed with the package: ```bash ccs-droid glm # explicit alias ccsd glm # legacy shortcut -ccs-codex # explicit Codex alias -ccsx # short Codex alias +ccs-codex # explicit native Codex alias +ccsx # short native Codex alias +``` + +CCS also ships an opinionated Codex provider shortcut: + +```bash +ccsxp # same as: ccs codex --target codex ``` Need additional alias names? First create the matching symlink or another launcher that @@ -255,8 +261,9 @@ Supported in v1: ```bash ccs --target codex # native Codex default session ccs-codex # explicit Codex alias -ccsx # short alias -ccs codex --target codex # built-in CLIProxy Codex on native Codex +ccsx # short native Codex alias +ccsxp # built-in CCS Codex provider on native Codex +ccs codex --target codex # explicit equivalent of ccsxp ccs api create codex-api --cliproxy-provider codex ccs codex-api --target codex # Codex bridge profile on native Codex ``` @@ -287,6 +294,42 @@ with `Use default`. CCS also keeps warning that transient runtime overrides such `codex -c key=value` and `CCS_CODEX_API_KEY` can change the effective runtime without persisting back into the user config file. +#### CLIProxy-backed native Codex + +There are two supported ways to use CLIProxy with native Codex: + +1. `ccsxp` or `ccs codex --target codex` + Uses the built-in CCS Codex provider route on native Codex. This path relies on transient + CCS-managed `-c` overrides and does **not** require changing your saved `model_provider`. + +2. Plain `codex` or a personal alias like `cxp` + Save CLIProxy as the native default provider in `~/.codex/config.toml` (or + `$CODEX_HOME/config.toml`), then export `CLIPROXY_API_KEY` in your shell. + +```toml +model_provider = "cliproxy" + +[model_providers.cliproxy] +base_url = "http://127.0.0.1:8317/api/provider/codex" +env_key = "CLIPROXY_API_KEY" +wire_api = "responses" +``` + +The top-level `model_provider = "cliproxy"` line is required. Defining only +`[model_providers.cliproxy]` is not enough for plain `codex` to pick it by default. + +```bash +export CLIPROXY_API_KEY="ccs-internal-managed" +ccsxp "your prompt" # CCS shortcut for the built-in provider route +codex "your prompt" # native Codex using your saved cliproxy default +``` + +Dashboard parity: `ccs config` -> `Compatible` -> `Codex CLI` + +- `Overview`: explains `ccsx` vs `ccsxp` +- `Top-level settings`: set **Default provider** to `cliproxy` +- `Model providers`: save `cliproxy` with `env_key = "CLIPROXY_API_KEY"` + ### Per-Profile Target Defaults You can pin a default target (`claude` or `droid`) per profile: diff --git a/src/commands/help-command.ts b/src/commands/help-command.ts index 0d76bd59..c1e878ba 100644 --- a/src/commands/help-command.ts +++ b/src/commands/help-command.ts @@ -407,6 +407,7 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim(); ['ccsd [args]', 'Legacy shortcut for: ccs-droid [args]'], ['ccs-codex [args]', 'Explicit Codex runtime alias'], ['ccsx [args]', 'Short alias for: ccs-codex [args]'], + ['ccsxp [args]', 'Shortcut for: ccs codex --target codex [args]'], ], writeLine ); @@ -421,7 +422,8 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim(); ['ccs --target codex', 'Open a native Codex session with your existing ~/.codex setup'], ['ccs-codex', 'Same as above (explicit Codex alias)'], ['ccsx', 'Short alias for ccs-codex'], - ['ccs codex --target codex', 'Run built-in CLIProxy Codex on native Codex CLI'], + ['ccsxp "your prompt"', 'Run built-in CLIProxy Codex on native Codex CLI'], + ['ccs codex --target codex', 'Explicit form of ccsxp'], [ 'ccs api create codex-api --cliproxy-provider codex', 'Create a routed API bridge that can also run on Codex', @@ -450,6 +452,20 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim(); writeLine ); + printSubSection( + 'Codex + CLIProxy', + [ + ['ccsxp "your prompt"', 'Use the built-in CCS Codex provider shortcut on native Codex'], + ['ccs config', 'Open Compatible -> Codex CLI for native Codex setup and diagnostics'], + [ + 'Default provider', + 'Set to cliproxy if plain codex or a personal cxp alias should use CLIProxy', + ], + ['Model provider auth', 'Save cliproxy with env_key = "CLIPROXY_API_KEY"'], + ], + writeLine + ); + // Configuration printConfigSection( 'Configuration', diff --git a/src/targets/target-runtime-compatibility.ts b/src/targets/target-runtime-compatibility.ts index 65fd6024..9c9a83ca 100644 --- a/src/targets/target-runtime-compatibility.ts +++ b/src/targets/target-runtime-compatibility.ts @@ -65,7 +65,7 @@ export function evaluateTargetRuntimeCompatibility( if (input.cliproxyProvider !== 'codex') { return unsupported( `Codex CLI only supports CLIProxy provider "codex". This profile routes to "${input.cliproxyProvider || 'unknown'}".`, - 'Use: ccs codex --target codex, ccs-codex codex, or stay on Claude/Droid for other providers.' + 'Use: ccsxp, ccs codex --target codex, or stay on Claude/Droid for other providers.' ); } return { supported: true }; diff --git a/tests/unit/commands/help-command-parity.test.ts b/tests/unit/commands/help-command-parity.test.ts index 41619e6b..687fd2e5 100644 --- a/tests/unit/commands/help-command-parity.test.ts +++ b/tests/unit/commands/help-command-parity.test.ts @@ -106,8 +106,12 @@ describe('help command parity', () => { const rendered = stripAnsi(lines.join('\n')); expect(rendered.includes('ccs-codex [args]')).toBe(true); expect(rendered.includes('ccsx [args]')).toBe(true); + expect(rendered.includes('ccsxp [args]')).toBe(true); expect(rendered.includes('ccs --target codex')).toBe(true); + expect(rendered.includes('ccsxp "your prompt"')).toBe(true); expect(rendered.includes('ccs codex-api --target codex')).toBe(true); + expect(rendered.includes('Compatible -> Codex CLI')).toBe(true); + expect(rendered.includes('CLIPROXY_API_KEY')).toBe(true); expect(rendered.includes('codex (runtime-only)')).toBe(true); }); From 0ffde4f3a2688ea1864fab3c94d3d020d4b33a93 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Sun, 29 Mar 2026 19:44:02 -0400 Subject: [PATCH 3/4] feat(codex-ui): surface cliproxy setup guidance --- .../services/codex-dashboard-service.ts | 16 ++++ .../codex-dashboard-service.test.ts | 16 ++++ .../provider-editor/provider-info-tab.tsx | 12 +-- .../compatible-cli/codex-docs-tab.tsx | 37 ++++++++ .../codex-model-providers-card.tsx | 33 +++++++ .../compatible-cli/codex-overview-tab.tsx | 86 +++++++++++++++---- ui/src/lib/support-updates-catalog.ts | 26 ++++-- 7 files changed, 196 insertions(+), 30 deletions(-) diff --git a/src/web-server/services/codex-dashboard-service.ts b/src/web-server/services/codex-dashboard-service.ts index 505c84b1..3285f29b 100644 --- a/src/web-server/services/codex-dashboard-service.ts +++ b/src/web-server/services/codex-dashboard-service.ts @@ -702,6 +702,7 @@ export async function getCodexDashboardDiagnostics(): Promise left.localeCompare(right)) : []; const activeProfile = asString(config?.profile); + const activeModelProvider = asString(config?.model_provider); const profileNames = Object.keys(asObject(config?.profiles) ?? {}).sort((left, right) => left.localeCompare(right) ); @@ -726,6 +727,21 @@ export async function getCodexDashboardDiagnostics(): Promise provider.name === activeModelProvider); + if (!activeProvider) { + warnings.push( + `Active model provider "${activeModelProvider}" is selected but missing from [model_providers].` + ); + } else { + if (!activeProvider.baseUrl) { + warnings.push(`Active model provider "${activeProvider.name}" is missing base_url.`); + } + if (!activeProvider.envKey) { + warnings.push(`Active model provider "${activeProvider.name}" is missing env_key.`); + } + } + } if (modelProviders.some((provider) => provider.usesExperimentalBearerToken)) { warnings.push( 'One or more model_providers entries use experimental_bearer_token; prefer env_key-backed auth.' diff --git a/tests/unit/web-server/codex-dashboard-service.test.ts b/tests/unit/web-server/codex-dashboard-service.test.ts index f5950d91..08855f0e 100644 --- a/tests/unit/web-server/codex-dashboard-service.test.ts +++ b/tests/unit/web-server/codex-dashboard-service.test.ts @@ -218,6 +218,22 @@ model = "gpt-5.4" expect(diagnostics.supportMatrix.some((entry) => entry.id === 'default')).toBe(true); }); + it('warns when the active model provider is selected without base_url or env_key', async () => { + fs.writeFileSync( + path.join(codexHome, 'config.toml'), + `model_provider = "cliproxy" + +[model_providers.cliproxy] +wire_api = "responses" +` + ); + + const diagnostics = await getCodexDashboardDiagnostics(); + + expect(diagnostics.warnings.some((warning) => warning.includes('missing base_url'))).toBe(true); + expect(diagnostics.warnings.some((warning) => warning.includes('missing env_key'))).toBe(true); + }); + it('summarizes granular approval policies without flattening them to null', async () => { fs.writeFileSync( path.join(codexHome, 'config.toml'), diff --git a/ui/src/components/cliproxy/provider-editor/provider-info-tab.tsx b/ui/src/components/cliproxy/provider-editor/provider-info-tab.tsx index 8cde0f8c..4d2c30f2 100644 --- a/ui/src/components/cliproxy/provider-editor/provider-info-tab.tsx +++ b/ui/src/components/cliproxy/provider-editor/provider-info-tab.tsx @@ -91,18 +91,14 @@ export function ProviderInfoTab({ {isCodexProvider && ( <> + - - )}
+ + + + + CCS bridge recipe + + + +

+ Use ccsxp if you want the built-in CCS Codex provider shortcut on native + Codex. Use the saved recipe below if you want plain codex or a personal + alias like cxp to default to CLIProxy. +

+
+              {CLIPROXY_NATIVE_CODEX_RECIPE}
+            
+
+

+ 1. Save the cliproxy provider in your user config. +

+

+ 2. Set top-level model_provider to cliproxy. +

+

+ 3. Export CLIPROXY_API_KEY in your shell before launching native Codex. +

+
+
+
+ diff --git a/ui/src/components/compatible-cli/codex-model-providers-card.tsx b/ui/src/components/compatible-cli/codex-model-providers-card.tsx index 7d3b9814..e1449789 100644 --- a/ui/src/components/compatible-cli/codex-model-providers-card.tsx +++ b/ui/src/components/compatible-cli/codex-model-providers-card.tsx @@ -33,6 +33,16 @@ const EMPTY_MODEL_PROVIDER_DRAFT: CodexModelProviderEntry = { supportsWebsockets: false, }; +const CLIPROXY_CODEX_PROVIDER_PRESET: CodexModelProviderEntry = { + name: 'cliproxy', + displayName: 'CLIProxy Codex', + baseUrl: 'http://127.0.0.1:8317/api/provider/codex', + envKey: 'CLIPROXY_API_KEY', + wireApi: 'responses', + requiresOpenaiAuth: false, + supportsWebsockets: false, +}; + interface ModelProviderEditorProps { initialDraft: CodexModelProviderEntry; isNew: boolean; @@ -56,6 +66,23 @@ function ModelProviderEditor({ return ( <> + {isNew && ( +
+

+ Quick start: apply the CLIProxy Codex preset here, then set{' '} + Default provider to cliproxy in Top-level settings. +

+ +
+ )} +
+ +

+ If you want plain native codex to default to CLIProxy, save a provider named{' '} + cliproxy with CLIPROXY_API_KEY here, then pick{' '} + cliproxy in the Default provider control above. +

); } diff --git a/ui/src/components/compatible-cli/codex-overview-tab.tsx b/ui/src/components/compatible-cli/codex-overview-tab.tsx index 9a2f34ee..accf30fb 100644 --- a/ui/src/components/compatible-cli/codex-overview-tab.tsx +++ b/ui/src/components/compatible-cli/codex-overview-tab.tsx @@ -24,6 +24,13 @@ import { import type { CodexDashboardDiagnostics } from '@/hooks/use-codex-types'; import { cn } from '@/lib/utils'; +const CLIPROXY_NATIVE_CODEX_RECIPE = `model_provider = "cliproxy" + +[model_providers.cliproxy] +base_url = "http://127.0.0.1:8317/api/provider/codex" +env_key = "CLIPROXY_API_KEY" +wire_api = "responses"`; + function formatTimestamp(value: number | null | undefined): string { if (!value || !Number.isFinite(value)) return 'N/A'; return new Date(value).toLocaleString(); @@ -70,6 +77,17 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {

Codex is a first-class runtime target in CCS, but it stays runtime-only in v1.

+

+ ccs-codex and ccsx launch native Codex against your saved + native config, while ccsxp is the opinionated shortcut for{' '} + ccs codex --target codex. +

+

+ Plain codex or a personal alias like cxp needs{' '} + model_provider = "cliproxy" plus a matching{' '} + [model_providers.cliproxy] entry if you want CLIProxy as the saved native + default. +

Saved default targets for API profiles and variants still remain on Claude or Droid.

@@ -103,7 +121,8 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) { mono /> - + +
--config override support @@ -113,6 +132,40 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) { + + + + + CLIProxy-backed native Codex + + + +

+ There are two supported paths. Use ccsxp if you want the built-in CCS + Codex provider shortcut. Use the saved recipe below if you want plain{' '} + codex or a personal alias like cxp to default to CLIProxy. +

+
+

Saved native Codex recipe

+
+                {CLIPROXY_NATIVE_CODEX_RECIPE}
+              
+
+
+

+ 1. Save a provider named cliproxy with the base URL and env key above. +

+

+ 2. In Top-level settings, set Default provider to{' '} + cliproxy. +

+

+ 3. Export CLIPROXY_API_KEY in your shell before launching native Codex. +

+
+
+
+ @@ -213,19 +266,19 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {

Native Codex runtime

- Use ccs-codex, ccsx, or --target codex. CCS - launches the local Codex CLI and depends on native Codex capabilities such as{' '} - --config overrides. + Use ccs-codex, ccsx, or --target codex when + you want the local Codex CLI to honor your saved native user config.

-

Codex provider / bridge

+

CCS Codex provider / bridge

- CCS can route provider credentials transiently through CLIProxy. That is not the - same as editing local config.toml, and some routed values may never - persist here. + Use ccsxp or ccs codex --target codex when you want the + built-in CCS Codex provider on native Codex. That path uses transient CCS-managed + overrides and is separate from the saved cliproxy recipe above.

diff --git a/ui/src/lib/support-updates-catalog.ts b/ui/src/lib/support-updates-catalog.ts index 67a82731..c67dadf4 100644 --- a/ui/src/lib/support-updates-catalog.ts +++ b/ui/src/lib/support-updates-catalog.ts @@ -59,7 +59,7 @@ export const SUPPORT_NOTICES: SupportNotice[] = [ id: 'codex-target-runtime-support', title: 'Native Codex runtime support is live', summary: - 'Codex now participates as a first-class runtime target through ccs-codex, ccsx, or --target codex.', + 'Codex now participates as a first-class runtime target through ccs-codex, ccsx, ccsxp, or --target codex.', primaryAction: 'Use Codex as a runtime target for native Codex sessions and Codex-routed CLIProxy flows.', publishedAt: '2026-03-28', @@ -68,6 +68,7 @@ export const SUPPORT_NOTICES: SupportNotice[] = [ entryIds: ['codex-target', 'codex-cliproxy'], highlights: [ 'Use ccs-codex or ccsx for native Codex runs.', + 'Use ccsxp for the built-in CCS Codex provider shortcut on native Codex.', 'Built-in Codex and Codex bridge profiles can run on native Codex with --target codex.', 'Saved default targets for API profiles and variants remain claude or droid.', ], @@ -82,7 +83,14 @@ export const SUPPORT_NOTICES: SupportNotice[] = [ { id: 'copy-codex-provider-command', label: 'Run built-in Codex on Codex', - description: 'Use the built-in Codex provider with native Codex runtime.', + description: 'Use the built-in Codex provider shortcut on native Codex.', + type: 'command', + command: 'ccsxp "your prompt"', + }, + { + id: 'copy-codex-provider-command-explicit', + label: 'Run built-in Codex on Codex (explicit)', + description: 'Use the explicit built-in Codex provider route on native Codex.', type: 'command', command: 'ccs codex --target codex "your prompt"', }, @@ -95,7 +103,12 @@ export const SUPPORT_NOTICES: SupportNotice[] = [ }, ], routes: [{ label: 'Codex CLI', path: '/codex' }], - commands: ['ccs-codex', 'ccsx', 'ccs codex --target codex "your prompt"'], + commands: [ + 'ccs-codex', + 'ccsx', + 'ccsxp "your prompt"', + 'ccs codex --target codex "your prompt"', + ], }, { id: 'droid-target-support', @@ -250,7 +263,7 @@ export const CLI_SUPPORT_ENTRIES: CliSupportEntry[] = [ name: 'Codex via CLIProxy', scope: 'cliproxy', status: 'stable', - summary: 'OAuth-backed provider with configurable variant model and target.', + summary: 'OAuth-backed provider with configurable variant model and a native Codex shortcut.', pillars: { baseUrl: 'Managed by CLIProxy backend', auth: 'OAuth account via CLIProxy auth flow', @@ -261,11 +274,14 @@ export const CLI_SUPPORT_ENTRIES: CliSupportEntry[] = [ { label: 'Control Panel', path: '/cliproxy/control-panel' }, ], commands: [ - 'ccs codex', + 'ccsxp "your prompt"', 'ccs codex --target codex', + 'ccs codex', 'ccs cliproxy create mycodex --provider codex', 'ccs api create codex-api --cliproxy-provider codex', ], + notes: + 'Use ccsxp when you want the built-in Codex provider on native Codex without retyping --target codex.', }, { id: 'gemini-cliproxy', From bd1ff02521ac75d6e06b336bc9030da084d6a8df Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Sun, 29 Mar 2026 21:04:59 -0400 Subject: [PATCH 4/4] fix(codex): align cliproxy guidance with runtime behavior --- docs/code-standards.md | 11 ++- docs/codebase-summary.md | 14 +-- docs/system-architecture/index.md | 5 +- docs/system-architecture/target-adapters.md | 47 +++++++--- src/bin/ccsxp-runtime.ts | 12 ++- src/commands/config-channels-command.ts | 4 +- src/commands/config-command-options.ts | 4 +- src/commands/help-command.ts | 4 +- .../services/codex-dashboard-service.ts | 11 ++- .../unit/commands/help-command-parity.test.ts | 3 +- .../targets/codex-runtime-integration.test.ts | 16 ++++ .../codex-dashboard-service.test.ts | 26 ++++++ .../cliproxy/provider-editor/index.tsx | 1 + .../provider-editor/provider-info-tab.tsx | 34 ++++++- .../compatible-cli/codex-docs-tab.tsx | 8 +- .../compatible-cli/codex-overview-tab.tsx | 26 +++--- ui/src/lib/codex-config.ts | 7 ++ ui/src/lib/i18n.ts | 8 +- ui/src/lib/support-updates-catalog.ts | 8 +- .../provider-info-tab.test.tsx | 54 +++++++++++ .../codex-overview-tab.test.tsx | 89 +++++++++++++++++++ .../ui/lib/support-updates-catalog.test.ts | 14 +++ 22 files changed, 342 insertions(+), 64 deletions(-) create mode 100644 ui/tests/unit/components/cliproxy/provider-editor/provider-info-tab.test.tsx create mode 100644 ui/tests/unit/components/compatible-cli/codex-overview-tab.test.tsx diff --git a/docs/code-standards.md b/docs/code-standards.md index 76eae3d6..7e45c745 100644 --- a/docs/code-standards.md +++ b/docs/code-standards.md @@ -198,16 +198,21 @@ Resolves which adapter to use via `resolveTargetType()`: ``` 1. --target flag (highest priority) ↓ -2. argv[0] detection (runtime alias pattern): +2. explicit runtime entrypoint (`CCS_INTERNAL_ENTRY_TARGET`): + - ccs-droid / ccsd → droid + - ccs-codex / ccsx → codex + - ccsxp → codex (provider shortcut) + ↓ +3. argv[0] detection (runtime alias pattern / custom alias map): - ccs-droid → droid - ccsd → droid - ccs-codex → codex - ccsx → codex - ccs → default ↓ -3. Profile config: profileConfig.target field +4. Profile config: profileConfig.target field ↓ -4. Fallback: 'claude' (lowest priority) +5. Fallback: 'claude' (lowest priority) ``` ### Registration Pattern diff --git a/docs/codebase-summary.md b/docs/codebase-summary.md index d682c11a..3e3e1607 100644 --- a/docs/codebase-summary.md +++ b/docs/codebase-summary.md @@ -36,8 +36,9 @@ The main CLI is organized into domain-specific modules with barrel exports. src/ ├── ccs.ts # Main entry point & profile execution flow ├── bin/ # Dedicated runtime entrypoints -│ ├── droid-runtime.ts # argv[0] shim for ccs-droid / ccsd -│ └── codex-runtime.ts # argv[0] shim for ccs-codex / ccsx +│ ├── droid-runtime.ts # Forces droid target for ccs-droid / ccsd package bins +│ ├── codex-runtime.ts # Forces codex target for ccs-codex / ccsx package bins +│ └── ccsxp-runtime.ts # Forces built-in codex profile + codex target for ccsxp ├── types/ # TypeScript type definitions │ ├── index.ts # Barrel export (aggregates all types) │ ├── cli.ts # CLI types (ParsedArgs, ExitCode) @@ -69,7 +70,7 @@ src/ │ ├── index.ts # Barrel export │ ├── target-adapter.ts # TargetAdapter interface contract │ ├── target-registry.ts # Registry for runtime adapter lookup -│ ├── target-resolver.ts # Resolution logic (flag > argv[0] > config) +│ ├── target-resolver.ts # Resolution logic (flag > runtime entrypoint / argv[0] > config) │ ├── target-metadata.ts # Runtime vs persisted target metadata and alias lists │ ├── target-runtime-compatibility.ts # Guardrails for target/profile combinations │ ├── claude-adapter.ts # Claude Code CLI implementation @@ -248,7 +249,9 @@ src/ ### Native Codex Runtime Target -- Runtime aliases: `ccs-codex` and `ccsx` resolve through `src/bin/codex-runtime.ts` and `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` also strips user-supplied `--target` overrides and rewrites argv to `ccs codex --target codex ...`, so it always lands on the built-in Codex-via-CLIProxy route. +- `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. - Adapter behavior: `src/targets/codex-adapter.ts` and `src/targets/codex-detector.ts` launch native Codex without rewriting `~/.codex/config.toml`; CCS-backed routes use transient `codex -c key=value` overrides and env-key injection. @@ -279,7 +282,8 @@ The targets module provides an extensible interface for dispatching profiles to 2. **Target Resolution** - Priority order: - `--target ` flag (CLI argument) - - `argv[0]` detection (runtime alias pattern: `ccs-droid` / `ccsd` → droid) + - Explicit runtime entrypoint via `CCS_INTERNAL_ENTRY_TARGET` (used by `src/bin/droid-runtime.ts`, `src/bin/codex-runtime.ts`, and `src/bin/ccsxp-runtime.ts`) + - `argv[0]` detection for custom/same-binary runtime aliases - Per-profile `target` field (from config.yaml) - Default: `claude` diff --git a/docs/system-architecture/index.md b/docs/system-architecture/index.md index ce103371..338934b4 100644 --- a/docs/system-architecture/index.md +++ b/docs/system-architecture/index.md @@ -58,7 +58,7 @@ CCS v7.45 introduces the Target Adapter pattern, enabling seamless integration w Profile Resolution (CLIProxy, Settings/API, Account-based) | v -Target Resolution (--target flag > argv[0] > config > default) +Target Resolution (--target flag > runtime entrypoint / argv[0] > config > default) | v Get Target Adapter (Claude, Droid, or Codex) @@ -91,7 +91,7 @@ Spawn Target Process - Preserves native `~/.codex/config.toml` ownership - Dashboard page reads/writes only the user config layer with explicit runtime-vs-provider warnings -**Runtime alias pattern (built-in bins / argv[0]-style):** +**Runtime entrypoints (built-in bins) and argv[0]-style aliases:** ``` ccs → Target: claude (default) @@ -99,6 +99,7 @@ ccs-droid → Target: droid (explicit alias) ccsd → Target: droid (legacy shortcut) ccs-codex → Target: codex (explicit alias) ccsx → Target: codex (short alias) +ccsxp → Target: codex (provider shortcut; rewrites argv to `ccs codex --target codex`) ``` For details on the adapter architecture, see [Target Adapters](./target-adapters.md). diff --git a/docs/system-architecture/target-adapters.md b/docs/system-architecture/target-adapters.md index d78ae7e9..9371ab72 100644 --- a/docs/system-architecture/target-adapters.md +++ b/docs/system-architecture/target-adapters.md @@ -85,20 +85,25 @@ CCS resolves which adapter to use via priority-ordered checks: └─ ccs --target droid glm └─ ccs --target codex -2. argv[0] detection (runtime alias pattern) — binary name mapping +2. Explicit runtime entrypoint (`CCS_INTERNAL_ENTRY_TARGET`) — dedicated bin shims + └─ ccs-droid / ccsd → droid + └─ ccs-codex / ccsx → codex + └─ ccsxp → codex, then rewrites argv to `ccs codex --target codex ...` + +3. argv[0] detection (runtime alias pattern) — binary name mapping for same-binary/custom aliases └─ ccs-droid (explicit alias) → droid └─ ccsd (legacy shortcut) → droid └─ ccs-codex (explicit alias) → codex └─ ccsx (short alias) → codex └─ ccs (regular command) → default -3. Per-profile config (from ~/.ccs/config.yaml or settings.json) +4. Per-profile config (from ~/.ccs/config.yaml or settings.json) └─ persisted targets are currently only `claude` and `droid` └─ profiles: glm: target: droid -4. Fallback: 'claude' — lowest priority +5. Fallback: 'claude' — lowest priority ``` ### Implementation @@ -117,19 +122,25 @@ export function resolveTargetType( return parsed.targetOverride; } - // 2. Check argv[0] (binary name) + // 2. Check explicit runtime entrypoint shim + const entrypointTarget = resolveEntrypointTarget(); + if (entrypointTarget) { + return entrypointTarget; + } + + // 3. Check argv[0] (binary name / custom alias map) const binName = path.basename(process.argv[1] || process.argv0 || '').replace(/\.(cmd|bat|ps1|exe)$/i, ''); if (ARGV0_TARGET_MAP[binName]) { return ARGV0_TARGET_MAP[binName]; } - // 3. Check profile config + // 4. Check profile config if (profileConfig?.target) { // Persisted targets intentionally exclude runtime-only codex. return profileConfig.target; } - // 4. Default to claude + // 5. Default to claude return 'claude'; } ``` @@ -488,20 +499,27 @@ longer read-mostly: This keeps the dashboard honest about Codex's merged configuration model while still giving users one place to inspect and manage the user-owned layer safely. -### Runtime Alias Pattern +### Runtime Entrypoints and argv[0] Fallback ```bash -# Built-in package bin aliases +# Built-in package bin entrypoints ccs-codex -→ Target: codex (forced by runtime alias) +→ dist/bin/codex-runtime.js +→ CCS_INTERNAL_ENTRY_TARGET=codex -ccsx codex -→ Target: codex (forced by runtime alias) -→ codex ...args +ccsx +→ dist/bin/codex-runtime.js +→ CCS_INTERNAL_ENTRY_TARGET=codex + +ccsxp +→ dist/bin/ccsxp-runtime.js +→ CCS_INTERNAL_ENTRY_TARGET=codex +→ injects built-in codex profile shortcut ``` -Runtime aliases can also be extended with `CCS_TARGET_ALIASES` or legacy -`CCS_CODEX_ALIASES` after creating a matching launcher: +If a user launches CCS through a custom shim instead of the built-in package bins, target +resolution falls back to `argv[0]` aliases from `CCS_TARGET_ALIASES` or legacy +`CCS_CODEX_ALIASES`: ```bash ln -s /path/to/ccs /path/to/mycodex @@ -757,6 +775,7 @@ ccs --target droid help # Test Codex adapter (if installed) ccs --target codex ccs-codex +ccsxp # Test argv[0] detection ccs-droid help diff --git a/src/bin/ccsxp-runtime.ts b/src/bin/ccsxp-runtime.ts index 6758e655..f488e5fb 100644 --- a/src/bin/ccsxp-runtime.ts +++ b/src/bin/ccsxp-runtime.ts @@ -1,9 +1,19 @@ const { stripTargetFlag } = require('../targets/target-resolver'); +const { fail } = require('../utils/ui'); process.env.CCS_INTERNAL_ENTRY_TARGET = 'codex'; // ccsxp is an opinionated shortcut for the built-in Codex-on-Codex route. // Strip user-supplied target overrides before forcing the shortcut target. -const forwardedArgs = stripTargetFlag(process.argv.slice(2)); +const forwardedArgs = (() => { + try { + return stripTargetFlag(process.argv.slice(2)); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + console.error(fail(message)); + process.exit(1); + } +})(); + process.argv.splice(2, process.argv.length - 2, 'codex', '--target', 'codex', ...forwardedArgs); require('../ccs'); diff --git a/src/commands/config-channels-command.ts b/src/commands/config-channels-command.ts index 6ccf49d9..b06c654d 100644 --- a/src/commands/config-channels-command.ts +++ b/src/commands/config-channels-command.ts @@ -177,9 +177,7 @@ function showHelp(): void { console.log( ` $ ${color('ccs config channels --clear-token discord', 'command')} ${dim('# Clear one token')}` ); - console.log( - ` ${dim('Official Channels only work on native Claude default/account sessions, not on ccs glm or other API/OAuth/Droid targets.')}` - ); + console.log(` ${dim(getOfficialChannelsSupportMessage())}`); console.log(''); } diff --git a/src/commands/config-command-options.ts b/src/commands/config-command-options.ts index 24d31a38..f219136d 100644 --- a/src/commands/config-command-options.ts +++ b/src/commands/config-command-options.ts @@ -1,4 +1,5 @@ import { extractOption, hasAnyFlag, scanCommandArgs } from './arg-extractor'; +import { getOfficialChannelsSupportMessage } from '../channels/official-channels-runtime'; const CONFIG_COMMAND_FLAGS = ['--help', '-h', '--port', '-p', '--host', '-H', '--dev'] as const; @@ -92,8 +93,7 @@ export function showConfigCommandHelp(): void { console.log(' --set-token Save channel token (telegram= or discord=)'); console.log(' --clear-token Remove all saved channel tokens'); console.log(' --clear-token Remove one saved channel token'); - console.log(' Works only for native Claude default/account sessions'); - console.log(' Not for ccs glm, other API/OAuth profiles, or Droid targets'); + console.log(` ${getOfficialChannelsSupportMessage()}`); console.log(''); console.log(' auth Manage dashboard authentication'); console.log(' auth setup Configure username and password'); diff --git a/src/commands/help-command.ts b/src/commands/help-command.ts index c1e878ba..a30e6829 100644 --- a/src/commands/help-command.ts +++ b/src/commands/help-command.ts @@ -4,6 +4,7 @@ import { initUI, box, color, dim, sectionHeader, subheader } from '../utils/ui'; import { isUnifiedMode } from '../config/unified-config-loader'; import { getCcsDir, getCcsDirSource } from '../utils/config-manager'; import { CLIPROXY_DEFAULT_PORT } from '../cliproxy/config/port-manager'; +import { getOfficialChannelsSupportMessage } from '../channels/official-channels-runtime'; type HelpWriter = (line: string) => void; @@ -608,8 +609,7 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim(); ['ccs config channels --clear-token [channel]', 'Remove one or all saved channel tokens'], ['', ''], ['', 'Fastest path: turn on the channel, save the token if needed, then run ccs.'], - ['Note:', 'Runtime-only. Applies to native Claude default/account sessions only.'], - ['', 'Not supported for ccs glm, other API/OAuth profiles, or Droid targets.'], + ['Note:', getOfficialChannelsSupportMessage()], ['', 'Telegram/Discord tokens live in ~/.claude/channels//.env.'], ['', 'Current-process TELEGRAM_BOT_TOKEN / DISCORD_BOT_TOKEN also work for that launch.'], ['', 'iMessage is macOS-only and requires local OS permissions instead of a bot token.'], diff --git a/src/web-server/services/codex-dashboard-service.ts b/src/web-server/services/codex-dashboard-service.ts index 3285f29b..ce95894f 100644 --- a/src/web-server/services/codex-dashboard-service.ts +++ b/src/web-server/services/codex-dashboard-service.ts @@ -72,6 +72,7 @@ const SANDBOX_MODE_VALUES = new Set(['read-only', 'workspace-write', 'danger-ful const WEB_SEARCH_VALUES = new Set(['cached', 'live', 'disabled']); const PERSONALITY_VALUES = new Set(['none', 'friendly', 'pragmatic']); const PROJECT_TRUST_LEVEL_VALUES = new Set(['trusted', 'untrusted']); +const BUILT_IN_CODEX_MODEL_PROVIDERS = new Set(['openai', 'oss']); function isObject(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value); @@ -572,6 +573,10 @@ export function summarizeCodexModelProviders(value: unknown): CodexModelProvider .sort((left, right) => left.name.localeCompare(right.name)); } +function isBuiltInCodexModelProvider(name: string | null): boolean { + return name !== null && BUILT_IN_CODEX_MODEL_PROVIDERS.has(name); +} + export function summarizeCodexFeatureFlags(value: unknown): { all: CodexFeatureFlagDiagnostics[]; enabled: CodexFeatureFlagDiagnostics[]; @@ -729,15 +734,15 @@ export async function getCodexDashboardDiagnostics(): Promise provider.name === activeModelProvider); - if (!activeProvider) { + if (!activeProvider && !isBuiltInCodexModelProvider(activeModelProvider)) { warnings.push( `Active model provider "${activeModelProvider}" is selected but missing from [model_providers].` ); - } else { + } else if (activeProvider) { if (!activeProvider.baseUrl) { warnings.push(`Active model provider "${activeProvider.name}" is missing base_url.`); } - if (!activeProvider.envKey) { + if (!activeProvider.envKey && !activeProvider.requiresOpenaiAuth) { warnings.push(`Active model provider "${activeProvider.name}" is missing env_key.`); } } diff --git a/tests/unit/commands/help-command-parity.test.ts b/tests/unit/commands/help-command-parity.test.ts index 687fd2e5..08397323 100644 --- a/tests/unit/commands/help-command-parity.test.ts +++ b/tests/unit/commands/help-command-parity.test.ts @@ -81,7 +81,8 @@ describe('help command parity', () => { 'Fastest path: turn on the channel, save the token if needed, then run ccs.' ) ).toBe(true); - expect(rendered.includes('Not supported for ccs glm')).toBe(true); + expect(rendered.includes('ccs codex')).toBe(true); + expect(rendered.includes('ccs --target codex')).toBe(true); expect( rendered.includes('Current-process TELEGRAM_BOT_TOKEN / DISCORD_BOT_TOKEN also work') ).toBe(true); diff --git a/tests/unit/targets/codex-runtime-integration.test.ts b/tests/unit/targets/codex-runtime-integration.test.ts index 3c186354..7e06621d 100644 --- a/tests/unit/targets/codex-runtime-integration.test.ts +++ b/tests/unit/targets/codex-runtime-integration.test.ts @@ -278,6 +278,22 @@ process.exit(0); expect(readLoggedCodexCalls(codexArgsLogPath)).toEqual([['--version']]); }); + it('fails with a clean CLI error when ccsxp receives a malformed --target flag', () => { + if (process.platform === 'win32') return; + + const result = runCcsxpAlias(['--target'], { + ...process.env, + CI: '1', + NO_COLOR: '1', + CCS_HOME: tmpHome, + CCS_CODEX_PATH: fakeCodexPath, + }); + + expect(result.status).toBe(1); + expect(result.stderr).toContain('[X] --target requires a value (claude, droid, codex)'); + expect(result.stderr).not.toContain('at parseTargetFlags'); + }); + it('passes ccs codex --target codex --version through to the native Codex binary', () => { if (process.platform === 'win32') return; diff --git a/tests/unit/web-server/codex-dashboard-service.test.ts b/tests/unit/web-server/codex-dashboard-service.test.ts index 08855f0e..2700622b 100644 --- a/tests/unit/web-server/codex-dashboard-service.test.ts +++ b/tests/unit/web-server/codex-dashboard-service.test.ts @@ -234,6 +234,32 @@ wire_api = "responses" expect(diagnostics.warnings.some((warning) => warning.includes('missing env_key'))).toBe(true); }); + it('does not warn for built-in openai provider without a custom model_providers entry', async () => { + fs.writeFileSync(path.join(codexHome, 'config.toml'), 'model_provider = "openai"\n'); + + const diagnostics = await getCodexDashboardDiagnostics(); + + expect(diagnostics.warnings.some((warning) => warning.includes('missing from [model_providers]'))).toBe( + false + ); + }); + + it('allows custom providers that use requires_openai_auth without env_key warnings', async () => { + fs.writeFileSync( + path.join(codexHome, 'config.toml'), + `model_provider = "corp-openai" + +[model_providers.corp-openai] +base_url = "https://example.test/v1" +requires_openai_auth = true +` + ); + + const diagnostics = await getCodexDashboardDiagnostics(); + + expect(diagnostics.warnings.some((warning) => warning.includes('missing env_key'))).toBe(false); + }); + it('summarizes granular approval policies without flattening them to null', async () => { fs.writeFileSync( path.join(codexHome, 'config.toml'), diff --git a/ui/src/components/cliproxy/provider-editor/index.tsx b/ui/src/components/cliproxy/provider-editor/index.tsx index 55d858f0..bee323a6 100644 --- a/ui/src/components/cliproxy/provider-editor/index.tsx +++ b/ui/src/components/cliproxy/provider-editor/index.tsx @@ -297,6 +297,7 @@ export function ProviderEditor({ defaultTarget={defaultTarget} data={data} authStatus={authStatus} + supportsModelConfig={Boolean(catalog)} />
diff --git a/ui/src/components/cliproxy/provider-editor/provider-info-tab.tsx b/ui/src/components/cliproxy/provider-editor/provider-info-tab.tsx index 4d2c30f2..d8af6ad2 100644 --- a/ui/src/components/cliproxy/provider-editor/provider-info-tab.tsx +++ b/ui/src/components/cliproxy/provider-editor/provider-info-tab.tsx @@ -17,6 +17,7 @@ interface ProviderInfoTabProps { defaultTarget?: CliTarget; data?: SettingsResponse; authStatus: AuthStatus; + supportsModelConfig?: boolean; } export function ProviderInfoTab({ @@ -25,10 +26,16 @@ export function ProviderInfoTab({ defaultTarget, data, authStatus, + supportsModelConfig = false, }: ProviderInfoTabProps) { const resolvedTarget = defaultTarget || 'claude'; const isDroidTarget = resolvedTarget === 'droid'; const isCodexProvider = provider === 'codex'; + const managementPrefix = + resolvedTarget === 'claude' ? `ccs ${provider}` : `ccs ${provider} --target claude`; + const changeModelCommand = `${managementPrefix} --config`; + const addAccountCommand = `${managementPrefix} --auth --add`; + const listAccountsCommand = `${managementPrefix} --accounts`; return ( @@ -109,9 +116,30 @@ export function ProviderInfoTab({ label={isDroidTarget ? 'Override to Claude' : 'Run on Droid (--target)'} command={`ccs ${provider} --target ${isDroidTarget ? 'claude' : 'droid'} "your prompt"`} /> - - - + {supportsModelConfig && ( + + )} + + + {resolvedTarget !== 'claude' && ( +

+ Account and model-management flags stay on Claude target. Codex and Droid runtime + launches reject those CLIProxy management commands. +

+ )} diff --git a/ui/src/components/compatible-cli/codex-docs-tab.tsx b/ui/src/components/compatible-cli/codex-docs-tab.tsx index 6cce782d..1474155d 100644 --- a/ui/src/components/compatible-cli/codex-docs-tab.tsx +++ b/ui/src/components/compatible-cli/codex-docs-tab.tsx @@ -7,6 +7,7 @@ import type { CompatibleCliProviderDocLink, CodexDashboardDiagnostics, } from '@/hooks/use-codex-types'; +import { CLIPROXY_NATIVE_CODEX_RECIPE } from '@/lib/codex-config'; const DEFAULT_CODEX_DOC_LINKS = [ { @@ -44,13 +45,6 @@ const DEFAULT_PROVIDER_DOCS: CompatibleCliProviderDocLink[] = [ }, ]; -const CLIPROXY_NATIVE_CODEX_RECIPE = `model_provider = "cliproxy" - -[model_providers.cliproxy] -base_url = "http://127.0.0.1:8317/api/provider/codex" -env_key = "CLIPROXY_API_KEY" -wire_api = "responses"`; - function renderTextWithLinks(text: string): ReactNode[] { const urlPattern = /https?:\/\/[^\s)]+/g; const nodes: ReactNode[] = []; diff --git a/ui/src/components/compatible-cli/codex-overview-tab.tsx b/ui/src/components/compatible-cli/codex-overview-tab.tsx index accf30fb..ee0bcda3 100644 --- a/ui/src/components/compatible-cli/codex-overview-tab.tsx +++ b/ui/src/components/compatible-cli/codex-overview-tab.tsx @@ -22,15 +22,9 @@ import { TableRow, } from '@/components/ui/table'; import type { CodexDashboardDiagnostics } from '@/hooks/use-codex-types'; +import { CLIPROXY_NATIVE_CODEX_RECIPE } from '@/lib/codex-config'; import { cn } from '@/lib/utils'; -const CLIPROXY_NATIVE_CODEX_RECIPE = `model_provider = "cliproxy" - -[model_providers.cliproxy] -base_url = "http://127.0.0.1:8317/api/provider/codex" -env_key = "CLIPROXY_API_KEY" -wire_api = "responses"`; - function formatTimestamp(value: number | null | undefined): string { if (!value || !Number.isFinite(value)) return 'N/A'; return new Date(value).toLocaleString(); @@ -65,6 +59,10 @@ interface CodexOverviewTabProps { } export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) { + const inspectProfileCommand = diagnostics.config.activeProfile + ? `codex --profile ${diagnostics.config.activeProfile}` + : 'codex'; + return (
@@ -88,6 +86,10 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) { [model_providers.cliproxy] entry if you want CLIProxy as the saved native default.

+

+ Built-in openai and oss providers are also valid native + defaults and do not need a custom [model_providers] stanza. +

Saved default targets for API profiles and variants still remain on Claude or Droid.

@@ -281,9 +283,13 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) { description: 'Use the explicit built-in Codex provider route.', }, { - label: 'Workspace trust', - command: `codex --profile ${diagnostics.config.activeProfile || 'default'}`, - description: 'Inspect the active profile directly in native Codex.', + label: diagnostics.config.activeProfile + ? 'Inspect active profile' + : 'Open native Codex', + command: inspectProfileCommand, + description: diagnostics.config.activeProfile + ? 'Inspect the active named profile directly in native Codex.' + : 'Open native Codex without forcing a named profile overlay.', }, ]} /> diff --git a/ui/src/lib/codex-config.ts b/ui/src/lib/codex-config.ts index b13a16b8..cdc0b386 100644 --- a/ui/src/lib/codex-config.ts +++ b/ui/src/lib/codex-config.ts @@ -49,6 +49,13 @@ export interface CodexFeatureCatalogEntry { description: string; } +export const CLIPROXY_NATIVE_CODEX_RECIPE = `model_provider = "cliproxy" + +[model_providers.cliproxy] +base_url = "http://127.0.0.1:8317/api/provider/codex" +env_key = "CLIPROXY_API_KEY" +wire_api = "responses"`; + export const KNOWN_CODEX_FEATURES: CodexFeatureCatalogEntry[] = [ { name: 'multi_agent', diff --git a/ui/src/lib/i18n.ts b/ui/src/lib/i18n.ts index f825d60f..8038a835 100644 --- a/ui/src/lib/i18n.ts +++ b/ui/src/lib/i18n.ts @@ -950,7 +950,7 @@ const resources = { actionRequired: 'Action Required', done: 'Done', all: 'All', - inboxTitle: 'Updates Inbox', + inboxTitle: 'Updates Center', inboxSubtitle: 'Focus on actions, then mark updates done or dismissed.', needsAction: 'Needs Action', doneCount: 'Done', @@ -2118,7 +2118,7 @@ const resources = { actionRequired: '待处理', done: '已完成', all: '全部', - inboxTitle: '更新收件箱', + inboxTitle: '更新中心', inboxSubtitle: '先处理操作,再将更新标记为完成或忽略。', needsAction: '需处理', doneCount: '已完成', @@ -3344,7 +3344,7 @@ const resources = { actionRequired: 'Hành động bắt buộc', done: 'Xong', all: 'Tất cả', - inboxTitle: 'Hộp thư cập nhật', + inboxTitle: 'Trung tâm cập nhật', inboxSubtitle: 'Tập trung vào các hành động, sau đó đánh dấu các cập nhật đã hoàn tất hoặc bị loại bỏ.', needsAction: 'Cần hành động', @@ -4580,7 +4580,7 @@ const resources = { actionRequired: '対応が必要', done: '完了', all: 'すべて', - inboxTitle: '更新インボックス', + inboxTitle: '更新センター', inboxSubtitle: '対応を優先し、その後で完了または非表示にします。', needsAction: '要対応', doneCount: '完了', diff --git a/ui/src/lib/support-updates-catalog.ts b/ui/src/lib/support-updates-catalog.ts index c67dadf4..33c48e2d 100644 --- a/ui/src/lib/support-updates-catalog.ts +++ b/ui/src/lib/support-updates-catalog.ts @@ -169,9 +169,9 @@ export const SUPPORT_NOTICES: SupportNotice[] = [ }, { id: 'updates-center-launch', - title: 'Updates inbox is available for rollout tasks', + title: 'Updates Center is available for rollout tasks', summary: - 'A focused updates inbox exists for setup tasks and rollout guidance when you need it.', + 'A focused Updates Center exists for setup tasks and rollout guidance when you need it.', primaryAction: 'Use this page only when needed for rollout tasks, then return to your normal workflow.', publishedAt: '2026-02-25', @@ -186,7 +186,7 @@ export const SUPPORT_NOTICES: SupportNotice[] = [ actions: [ { id: 'open-updates-page', - label: 'Open updates inbox when needed', + label: 'Open Updates Center when needed', description: 'Review rollout tasks only when you want guided setup changes.', type: 'route', path: '/updates', @@ -199,7 +199,7 @@ export const SUPPORT_NOTICES: SupportNotice[] = [ command: 'ccs config', }, ], - routes: [{ label: 'Updates Inbox', path: '/updates' }], + routes: [{ label: 'Updates Center', path: '/updates' }], commands: ['ccs config'], }, ]; diff --git a/ui/tests/unit/components/cliproxy/provider-editor/provider-info-tab.test.tsx b/ui/tests/unit/components/cliproxy/provider-editor/provider-info-tab.test.tsx new file mode 100644 index 00000000..9d71278d --- /dev/null +++ b/ui/tests/unit/components/cliproxy/provider-editor/provider-info-tab.test.tsx @@ -0,0 +1,54 @@ +import { describe, expect, it } from 'vitest'; +import { render, screen } from '@tests/setup/test-utils'; +import { ProviderInfoTab } from '@/components/cliproxy/provider-editor/provider-info-tab'; + +const authenticatedStatus = { + provider: 'codex', + displayName: 'Codex', + authenticated: true, + lastAuth: null, + tokenFiles: 1, + accounts: [], +}; + +describe('ProviderInfoTab', () => { + it('routes management commands through Claude target for codex-target variants', () => { + render( + + ); + + expect(screen.getByText('ccs codex --target claude --config')).toBeInTheDocument(); + expect(screen.getByText('ccs codex --target claude --auth --add')).toBeInTheDocument(); + expect(screen.getByText('ccs codex --target claude --accounts')).toBeInTheDocument(); + expect( + screen.getByText( + /Codex and Droid runtime launches reject those CLIProxy management commands\./ + ) + ).toBeInTheDocument(); + }); + + it('hides unsupported model-config commands when the provider has no catalog support', () => { + render( + + ); + + expect(screen.queryByText('Change model')).not.toBeInTheDocument(); + expect(screen.getByText('ccs custom-provider --auth --add')).toBeInTheDocument(); + }); +}); diff --git a/ui/tests/unit/components/compatible-cli/codex-overview-tab.test.tsx b/ui/tests/unit/components/compatible-cli/codex-overview-tab.test.tsx new file mode 100644 index 00000000..ed8c8bac --- /dev/null +++ b/ui/tests/unit/components/compatible-cli/codex-overview-tab.test.tsx @@ -0,0 +1,89 @@ +import { describe, expect, it } from 'vitest'; +import { render, screen } from '@tests/setup/test-utils'; +import { CodexOverviewTab } from '@/components/compatible-cli/codex-overview-tab'; +import type { CodexDashboardDiagnostics } from '@/hooks/use-codex-types'; + +function buildDiagnostics(activeProfile: string | null): CodexDashboardDiagnostics { + return { + binary: { + installed: true, + path: '/tmp/codex', + installDir: '/tmp', + source: 'PATH', + version: 'codex-cli 0.118.0-alpha.3', + overridePath: null, + supportsConfigOverrides: true, + }, + file: { + label: 'Codex user config', + path: '$CODEX_HOME/config.toml', + resolvedPath: '/tmp/.codex/config.toml', + exists: true, + isSymlink: false, + isRegularFile: true, + sizeBytes: 64, + mtimeMs: 100, + parseError: null, + readError: null, + }, + workspacePath: '/tmp/workspace', + config: { + model: 'gpt-5.4', + modelReasoningEffort: null, + modelProvider: 'openai', + activeProfile, + approvalPolicy: null, + sandboxMode: null, + webSearch: null, + toolOutputTokenLimit: null, + personality: null, + topLevelKeys: ['model_provider'], + profileCount: activeProfile ? 1 : 0, + profileNames: activeProfile ? [activeProfile] : [], + modelProviderCount: 0, + modelProviders: [], + featureCount: 0, + enabledFeatures: [], + disabledFeatures: [], + trustedProjectCount: 0, + untrustedProjectCount: 0, + projectTrust: [], + mcpServerCount: 0, + mcpServers: [], + }, + supportMatrix: [], + warnings: [], + docsReference: { + providerValues: ['openai', 'oss', 'custom model_providers'], + settingsHierarchy: [], + notes: [], + links: [], + providerDocs: [], + }, + }; +} + +describe('CodexOverviewTab', () => { + it('uses a bare codex command when no active profile is set', () => { + render(); + + expect(screen.getByText('Open native Codex')).toBeInTheDocument(); + expect(screen.getAllByText('codex').length).toBeGreaterThan(0); + expect(screen.queryByText('codex --profile default')).not.toBeInTheDocument(); + expect( + screen.getAllByText( + (_, node) => + node?.textContent?.includes( + 'Built-in openai and oss providers are also valid native defaults' + ) ?? false + ).length + ).toBeGreaterThan(0); + }); + + it('uses the active named profile when one is selected', () => { + render(); + + expect(screen.getByText('Inspect active profile')).toBeInTheDocument(); + expect(screen.getByText('codex --profile work')).toBeInTheDocument(); + }); +}); diff --git a/ui/tests/unit/ui/lib/support-updates-catalog.test.ts b/ui/tests/unit/ui/lib/support-updates-catalog.test.ts index b7e69f84..85410d35 100644 --- a/ui/tests/unit/ui/lib/support-updates-catalog.test.ts +++ b/ui/tests/unit/ui/lib/support-updates-catalog.test.ts @@ -22,4 +22,18 @@ describe('support-updates catalog codex routing', () => { expect(entry).toBeDefined(); expect(entry?.routes).toEqual([{ label: 'Codex CLI', path: '/codex' }]); }); + + it('uses Updates Center naming consistently for the rollout notice', () => { + const notice = SUPPORT_NOTICES.find((entry) => entry.id === 'updates-center-launch'); + + expect(notice).toBeDefined(); + expect(notice?.title).toContain('Updates Center'); + expect(notice?.actions).toContainEqual( + expect.objectContaining({ + id: 'open-updates-page', + label: 'Open Updates Center when needed', + }) + ); + expect(notice?.routes).toContainEqual({ label: 'Updates Center', path: '/updates' }); + }); });