mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-02 06:20:04 +00:00
fix(codex): preserve custom ccsxp cliproxy base URLs
Preserve valid custom model_providers.cliproxy.base_url values during ccsxp Codex provider repair while keeping local fallback repair for missing or invalid URLs.\n\nCloses #1281
This commit is contained in:
@@ -273,7 +273,7 @@ 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. 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.
|
- 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`.
|
||||||
|
|||||||
@@ -522,6 +522,7 @@ ccsxp
|
|||||||
→ 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`
|
→ 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`
|
→ 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
|
→ ignores the configured CCS default account/profile and stays in native Codex default mode
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -50,22 +50,16 @@ function asObject(value: unknown): Record<string, unknown> | null {
|
|||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeLocalProviderUrl(value: unknown): string | null {
|
function isValidCodexCliproxyBaseUrl(value: unknown): value is string {
|
||||||
if (typeof value !== 'string') return null;
|
if (typeof value !== 'string') return false;
|
||||||
const trimmed = value.trim();
|
const trimmed = value.trim();
|
||||||
|
if (!trimmed) return false;
|
||||||
try {
|
try {
|
||||||
const url = new URL(trimmed);
|
const url = new URL(trimmed);
|
||||||
if (
|
return url.protocol === 'http:' || url.protocol === 'https:';
|
||||||
(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 {
|
} catch {
|
||||||
return null;
|
return false;
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveProviderEnvKey(provider: Record<string, unknown> | null): string {
|
function resolveProviderEnvKey(provider: Record<string, unknown> | null): string {
|
||||||
@@ -76,14 +70,10 @@ function resolveProviderEnvKey(provider: Record<string, unknown> | null): string
|
|||||||
return CODEX_CLIPROXY_PROVIDER_ENV_KEY;
|
return CODEX_CLIPROXY_PROVIDER_ENV_KEY;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isProviderReady(
|
function isProviderReady(provider: Record<string, unknown>, envKey: string): boolean {
|
||||||
provider: Record<string, unknown>,
|
|
||||||
expectedBaseUrl: string,
|
|
||||||
envKey: string
|
|
||||||
): boolean {
|
|
||||||
return (
|
return (
|
||||||
provider.name === CODEX_CLIPROXY_PROVIDER_NAME &&
|
provider.name === CODEX_CLIPROXY_PROVIDER_NAME &&
|
||||||
normalizeLocalProviderUrl(provider.base_url) === expectedBaseUrl &&
|
isValidCodexCliproxyBaseUrl(provider.base_url) &&
|
||||||
provider.env_key === envKey &&
|
provider.env_key === envKey &&
|
||||||
provider.wire_api === 'responses' &&
|
provider.wire_api === 'responses' &&
|
||||||
provider.requires_openai_auth === false &&
|
provider.requires_openai_auth === false &&
|
||||||
@@ -102,6 +92,17 @@ function buildProviderConfig(baseUrl: string, envKey: string): Record<string, un
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveProviderBaseUrl(
|
||||||
|
provider: Record<string, unknown>,
|
||||||
|
fallbackBaseUrl: string
|
||||||
|
): string {
|
||||||
|
const baseUrl = provider.base_url;
|
||||||
|
if (isValidCodexCliproxyBaseUrl(baseUrl)) {
|
||||||
|
return baseUrl.trim();
|
||||||
|
}
|
||||||
|
return fallbackBaseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
function appendProviderBlock(rawText: string, baseUrl: string): string {
|
function appendProviderBlock(rawText: string, baseUrl: string): string {
|
||||||
const prefix = rawText.trimEnd();
|
const prefix = rawText.trimEnd();
|
||||||
const providerBlock = stringifyTomlObject({
|
const providerBlock = stringifyTomlObject({
|
||||||
@@ -179,12 +180,12 @@ export async function ensureCodexCliproxyProviderConfig(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const envKey = resolveProviderEnvKey(currentProvider);
|
const envKey = resolveProviderEnvKey(currentProvider);
|
||||||
const providerReady = isProviderReady(currentProvider, expectedBaseUrl, envKey);
|
const providerReady = isProviderReady(currentProvider, envKey);
|
||||||
|
|
||||||
if (!providerReady) {
|
if (!providerReady) {
|
||||||
providers[CODEX_CLIPROXY_PROVIDER_ID] = {
|
providers[CODEX_CLIPROXY_PROVIDER_ID] = {
|
||||||
...currentProvider,
|
...currentProvider,
|
||||||
...buildProviderConfig(expectedBaseUrl, envKey),
|
...buildProviderConfig(resolveProviderBaseUrl(currentProvider, expectedBaseUrl), envKey),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -78,19 +78,19 @@ wire_api = "responses"
|
|||||||
expect(result.changed).toBe(true);
|
expect(result.changed).toBe(true);
|
||||||
expect(result.envKey).toBe('CLIPROXY_API_KEY');
|
expect(result.envKey).toBe('CLIPROXY_API_KEY');
|
||||||
const rawText = fs.readFileSync(configPath, 'utf8');
|
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('env_key = "CLIPROXY_API_KEY"');
|
||||||
expect(rawText).toContain('requires_openai_auth = false');
|
expect(rawText).toContain('requires_openai_auth = false');
|
||||||
expect(rawText).toContain('supports_websockets = 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.mkdirSync(codexHome, { recursive: true });
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
configPath,
|
configPath,
|
||||||
`[model_providers.cliproxy]
|
`[model_providers.cliproxy]
|
||||||
name = "Old Name"
|
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"
|
env_key = "CCS_CUSTOM_CLIPROXY_TOKEN"
|
||||||
wire_api = "chat"
|
wire_api = "chat"
|
||||||
`,
|
`,
|
||||||
@@ -102,11 +102,35 @@ wire_api = "chat"
|
|||||||
expect(result.changed).toBe(true);
|
expect(result.changed).toBe(true);
|
||||||
expect(result.envKey).toBe('CCS_CUSTOM_CLIPROXY_TOKEN');
|
expect(result.envKey).toBe('CCS_CUSTOM_CLIPROXY_TOKEN');
|
||||||
const rawText = fs.readFileSync(configPath, 'utf8');
|
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('env_key = "CCS_CUSTOM_CLIPROXY_TOKEN"');
|
||||||
expect(rawText).toContain('wire_api = "responses"');
|
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 () => {
|
it('rejects invalid non-table model_providers values without appending broken TOML', async () => {
|
||||||
fs.mkdirSync(codexHome, { recursive: true });
|
fs.mkdirSync(codexHome, { recursive: true });
|
||||||
const rawText = 'model_providers = "legacy"\n';
|
const rawText = 'model_providers = "legacy"\n';
|
||||||
@@ -137,6 +161,25 @@ supports_websockets = false
|
|||||||
expect(fs.readFileSync(configPath, 'utf8')).toBe(rawText);
|
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 () => {
|
it('normalizes a ready native Codex tuning alias before requests reach cliproxy', async () => {
|
||||||
fs.mkdirSync(codexHome, { recursive: true });
|
fs.mkdirSync(codexHome, { recursive: true });
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
|
|||||||
@@ -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', () => {
|
it('keeps ccsxp native when the CCS default profile is a Claude account', () => {
|
||||||
if (process.platform === 'win32') return;
|
if (process.platform === 'win32') return;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user