mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-04 16:13:40 +00:00
fix(codex): normalize CLIProxy responses routes (#1605)
Normalize Codex Responses base URLs so CCS-backed Codex launches use /backend-api/codex on the original CLIProxy backend. Share the Codex Responses route builder with ccsxp provider repair, and repair stale native Codex provider auth blocks when env_key token injection is used.
This commit is contained in:
1 parent
36ea9064e7
commit
ba0e768fbf
6 files changed
+100
-23
No files matched your search
@@ -40,3 +40,44 @@ export function buildLocalProviderBaseUrl(
|
||||
const rootUrl = `http://127.0.0.1:${port}`;
|
||||
return `${rootUrl}${buildCliproxyProviderPath(provider, backend)}`;
|
||||
}
|
||||
|
||||
export function buildCodexResponsesProviderPath(
|
||||
backend: CLIProxyBackend = getConfiguredCliproxyBackend()
|
||||
): string {
|
||||
return usesScopedProviderRoutes(backend) ? '/api/provider/codex' : '/backend-api/codex';
|
||||
}
|
||||
|
||||
export function buildLocalCodexResponsesBaseUrl(
|
||||
port: number,
|
||||
backend: CLIProxyBackend = getConfiguredCliproxyBackend()
|
||||
): string {
|
||||
return `http://127.0.0.1:${port}${buildCodexResponsesProviderPath(backend)}`;
|
||||
}
|
||||
|
||||
export function normalizeCodexResponsesBaseUrl(
|
||||
baseUrl: string,
|
||||
backend: CLIProxyBackend = getConfiguredCliproxyBackend()
|
||||
): string {
|
||||
const trimmed = baseUrl.trim();
|
||||
if (!trimmed) return baseUrl;
|
||||
|
||||
try {
|
||||
const parsed = new URL(trimmed);
|
||||
if (!['http:', 'https:'].includes(parsed.protocol)) return baseUrl;
|
||||
|
||||
const currentPath = parsed.pathname.replace(/\/+$/, '') || '/';
|
||||
const expectedPath = buildCodexResponsesProviderPath(backend);
|
||||
if (currentPath === expectedPath) return trimmed;
|
||||
|
||||
const legacyCodexPath = '/api/provider/codex';
|
||||
const managedPaths = new Set(['/', legacyCodexPath]);
|
||||
if (!managedPaths.has(currentPath)) return trimmed;
|
||||
|
||||
parsed.pathname = expectedPath;
|
||||
parsed.search = '';
|
||||
parsed.hash = '';
|
||||
return parsed.toString().replace(/\/$/, '');
|
||||
} catch {
|
||||
return baseUrl;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ChildProcess, spawn } from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import type { ProfileType } from '../types/profile';
|
||||
import { runCleanup } from '../errors';
|
||||
import { ConfigError, runCleanup } from '../errors';
|
||||
import { expandPath } from '../utils/helpers';
|
||||
import { wireChildProcessSignals } from '../utils/signal-forwarder';
|
||||
import {
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
} from './codex-detector';
|
||||
import { createLogger } from '../services/logging';
|
||||
import { getEffectiveApiKey } from '../cliproxy/auth/auth-token-manager';
|
||||
import { normalizeCodexResponsesBaseUrl } from '../cliproxy/config/provider-route';
|
||||
import { resolveLifecyclePort } from '../cliproxy/config/port-manager';
|
||||
import { getModelMaxLevel } from '../cliproxy/model-catalog';
|
||||
import { parseCodexModelTuningAlias } from '../cliproxy/ai-providers/model-id-normalizer';
|
||||
@@ -53,7 +54,7 @@ function buildConfigOverrideArgs(overrides: string[]): string[] {
|
||||
|
||||
function buildConfigOverrideSupportError(binaryInfo?: TargetBinaryInfo): Error {
|
||||
const versionSummary = binaryInfo?.version ? ` (${binaryInfo.version})` : '';
|
||||
return new Error(
|
||||
return new ConfigError(
|
||||
`Codex CLI${versionSummary} does not advertise --config overrides. Upgrade Codex before using CCS-backed Codex profiles or runtime reasoning overrides.`
|
||||
);
|
||||
}
|
||||
@@ -100,7 +101,7 @@ function normalizeCodexReasoningOverride(value: string | number | undefined): st
|
||||
if (typeof value === 'string' && CODEX_REASONING_LEVELS.has(value)) {
|
||||
return value;
|
||||
}
|
||||
throw new Error(
|
||||
throw new ConfigError(
|
||||
'Codex target supports reasoning levels only: minimal, low, medium, high, xhigh.'
|
||||
);
|
||||
}
|
||||
@@ -272,7 +273,7 @@ export class CodexAdapter implements TargetAdapter {
|
||||
const providerRepair = await ensureCodexCliproxyProviderConfig(resolveLifecyclePort());
|
||||
this.ccsxpCliproxyEnvKey = providerRepair.envKey;
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
throw new ConfigError(
|
||||
`ccsxp could not repair the native Codex cliproxy provider: ${(error as Error).message}`
|
||||
);
|
||||
}
|
||||
@@ -316,14 +317,14 @@ export class CodexAdapter implements TargetAdapter {
|
||||
}
|
||||
|
||||
if (!creds?.baseUrl?.trim() || !creds.apiKey?.trim()) {
|
||||
throw new Error(
|
||||
throw new ConfigError(
|
||||
'Codex target requires base URL and API key for CCS-backed profile launches.'
|
||||
);
|
||||
}
|
||||
|
||||
const disallowedFlags = findDisallowedCodexManagedFlags(userArgs);
|
||||
if (disallowedFlags.length > 0) {
|
||||
throw new Error(
|
||||
throw new ConfigError(
|
||||
`Codex target does not allow ${disallowedFlags.join(', ')} when CCS manages the runtime provider. Remove native Codex provider selection flags and retry.`
|
||||
);
|
||||
}
|
||||
@@ -331,7 +332,9 @@ export class CodexAdapter implements TargetAdapter {
|
||||
const overrides = [
|
||||
`model_provider=${formatTomlString(CODEX_RUNTIME_PROVIDER_ID)}`,
|
||||
`model_providers.${CODEX_RUNTIME_PROVIDER_ID}.name=${formatTomlString('CCS Runtime')}`,
|
||||
`model_providers.${CODEX_RUNTIME_PROVIDER_ID}.base_url=${formatTomlString(creds.baseUrl)}`,
|
||||
`model_providers.${CODEX_RUNTIME_PROVIDER_ID}.base_url=${formatTomlString(
|
||||
normalizeCodexResponsesBaseUrl(creds.baseUrl)
|
||||
)}`,
|
||||
`model_providers.${CODEX_RUNTIME_PROVIDER_ID}.env_key=${formatTomlString(CODEX_RUNTIME_ENV_KEY)}`,
|
||||
`model_providers.${CODEX_RUNTIME_PROVIDER_ID}.wire_api=${formatTomlString('responses')}`,
|
||||
];
|
||||
@@ -360,7 +363,7 @@ export class CodexAdapter implements TargetAdapter {
|
||||
}
|
||||
if (profileType !== 'default') {
|
||||
if (!creds.apiKey?.trim()) {
|
||||
throw new Error('Codex target requires an API key for CCS-backed profile launches.');
|
||||
throw new ConfigError('Codex target requires an API key for CCS-backed profile launches.');
|
||||
}
|
||||
env[CODEX_RUNTIME_ENV_KEY] = creds.apiKey;
|
||||
}
|
||||
|
||||
@@ -9,9 +9,8 @@ import {
|
||||
import { getModelMaxLevel } from '../cliproxy/model-catalog';
|
||||
import { parseCodexModelTuningAlias } from '../cliproxy/ai-providers/model-id-normalizer';
|
||||
import {
|
||||
buildLocalProviderBaseUrl,
|
||||
buildLocalCodexResponsesBaseUrl,
|
||||
getConfiguredCliproxyBackend,
|
||||
usesScopedProviderRoutes,
|
||||
} from '../cliproxy/config/provider-route';
|
||||
import { ConfigError } from '../errors/error-types';
|
||||
|
||||
@@ -52,11 +51,7 @@ export function buildCodexCliproxyProviderBaseUrl(port: number): string {
|
||||
// (issue #1597). Use the chatgpt_base_url-compatible "/backend-api/codex" alias,
|
||||
// which both backends serve; keep the provider-scoped alias for the Plus backend
|
||||
// to preserve its existing per-provider routing.
|
||||
const backend = getConfiguredCliproxyBackend();
|
||||
if (usesScopedProviderRoutes(backend)) {
|
||||
return buildLocalProviderBaseUrl('codex', port, backend);
|
||||
}
|
||||
return `http://127.0.0.1:${port}/backend-api/codex`;
|
||||
return buildLocalCodexResponsesBaseUrl(port, getConfiguredCliproxyBackend());
|
||||
}
|
||||
|
||||
export function isCcsxpCliproxyShortcut(env: NodeJS.ProcessEnv = process.env): boolean {
|
||||
@@ -150,6 +145,7 @@ function isProviderReady(
|
||||
typeof provider.base_url === 'string' &&
|
||||
resolveProviderBaseUrl(provider, expectedBaseUrl) === provider.base_url.trim() &&
|
||||
provider.env_key === envKey &&
|
||||
provider.auth === undefined &&
|
||||
provider.wire_api === 'responses' &&
|
||||
provider.requires_openai_auth === false &&
|
||||
provider.supports_websockets === false
|
||||
@@ -261,6 +257,7 @@ export async function ensureCodexCliproxyProviderConfig(
|
||||
...currentProvider,
|
||||
...buildProviderConfig(resolveProviderBaseUrl(currentProvider, expectedBaseUrl), envKey),
|
||||
};
|
||||
delete (providers[CODEX_CLIPROXY_PROVIDER_ID] as Record<string, unknown>).auth;
|
||||
}
|
||||
|
||||
if (providerReady && !normalizedModelAlias) {
|
||||
|
||||
@@ -128,7 +128,7 @@ describe('CodexAdapter', () => {
|
||||
profileType: 'cliproxy',
|
||||
creds: {
|
||||
profile: 'codex',
|
||||
baseUrl: 'http://127.0.0.1:8317/api/provider/codex',
|
||||
baseUrl: 'http://127.0.0.1:8317',
|
||||
apiKey: 'cliproxy-token',
|
||||
model: 'gpt-5.4',
|
||||
reasoningOverride: 'high',
|
||||
@@ -143,6 +143,9 @@ describe('CodexAdapter', () => {
|
||||
|
||||
expect(args).toContain('-c');
|
||||
expect(args).toContain('model_provider="ccs_runtime"');
|
||||
expect(args).toContain(
|
||||
'model_providers.ccs_runtime.base_url="http://127.0.0.1:8317/backend-api/codex"'
|
||||
);
|
||||
expect(args).toContain('model_providers.ccs_runtime.env_key="CCS_CODEX_API_KEY"');
|
||||
expect(args).toContain('model="gpt-5.4"');
|
||||
expect(args).toContain(
|
||||
|
||||
@@ -55,9 +55,7 @@ describe('codex cliproxy provider config repair', () => {
|
||||
// original CLIProxy backend serves the Codex Responses API only under the
|
||||
// "/backend-api/codex" alias, never at the bare root. Returning the root here
|
||||
// makes Codex call "http://127.0.0.1:8317/responses" -> 404 (issue #1597).
|
||||
expect(buildCodexCliproxyProviderBaseUrl(8317)).toBe(
|
||||
'http://127.0.0.1:8317/backend-api/codex'
|
||||
);
|
||||
expect(buildCodexCliproxyProviderBaseUrl(8317)).toBe('http://127.0.0.1:8317/backend-api/codex');
|
||||
});
|
||||
|
||||
it('produces a Codex Responses endpoint the original backend actually serves (regression #1597)', () => {
|
||||
@@ -135,6 +133,36 @@ wire_api = "responses"
|
||||
expect(rawText).toContain('supports_websockets = false');
|
||||
});
|
||||
|
||||
it('removes native provider auth when ccsxp injects the token through env_key', async () => {
|
||||
fs.mkdirSync(codexHome, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
configPath,
|
||||
`[model_providers.cliproxy]
|
||||
name = "CLIProxy Codex"
|
||||
base_url = "http://127.0.0.1:8317/backend-api/codex"
|
||||
env_key = "CLIPROXY_API_KEY"
|
||||
wire_api = "responses"
|
||||
requires_openai_auth = false
|
||||
supports_websockets = false
|
||||
|
||||
[model_providers.cliproxy.auth]
|
||||
command = "/tmp/cliproxy-token"
|
||||
timeout_ms = 5000
|
||||
refresh_interval_ms = 300000
|
||||
`,
|
||||
'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).toContain('env_key = "CLIPROXY_API_KEY"');
|
||||
expect(rawText).not.toContain('[model_providers.cliproxy.auth]');
|
||||
expect(rawText).not.toContain('cliproxy-token');
|
||||
});
|
||||
|
||||
it('preserves custom cliproxy provider values while repairing other fields', async () => {
|
||||
fs.mkdirSync(codexHome, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
|
||||
@@ -124,21 +124,26 @@ exit 0
|
||||
|
||||
const argsLog = fs.readFileSync(codexArgsLogPath, 'utf8');
|
||||
expect(argsLog).toContain('model_provider="ccs_runtime"');
|
||||
expect(argsLog).toContain('model_providers.ccs_runtime.base_url="http://127.0.0.1:8317/api/provider/codex"');
|
||||
expect(argsLog).toContain(
|
||||
'model_providers.ccs_runtime.base_url="http://127.0.0.1:8317/backend-api/codex"'
|
||||
);
|
||||
expect(argsLog).toContain('model_reasoning_effort="high"');
|
||||
expect(argsLog).not.toContain('mcp_servers.ccs_browser.command=');
|
||||
expect(argsLog).not.toContain('mcp_servers.ccs_browser.args=["-y","@playwright/mcp@0.0.70"]');
|
||||
expect(argsLog).toContain('smoke');
|
||||
expect(fs.readFileSync(codexEnvLogPath, 'utf8')).toBe('bridge-token');
|
||||
});
|
||||
}, 15000);
|
||||
|
||||
it('rejects native Codex profile flags when CCS manages the bridge runtime', () => {
|
||||
if (process.platform === 'win32') return;
|
||||
|
||||
const result = runCcs(['codex-api', '--target', 'codex', '--profile', 'other', 'smoke'], baseEnv);
|
||||
const result = runCcs(
|
||||
['codex-api', '--target', 'codex', '--profile', 'other', 'smoke'],
|
||||
baseEnv
|
||||
);
|
||||
|
||||
expect(result.status).toBe(1);
|
||||
expect(result.stderr).toContain('does not allow --profile/-p');
|
||||
expect(fs.existsSync(codexArgsLogPath)).toBe(false);
|
||||
});
|
||||
}, 15000);
|
||||
});
|
||||
Reference in new issue
Block a user