mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-12 16:15:13 +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}`;
|
const rootUrl = `http://127.0.0.1:${port}`;
|
||||||
return `${rootUrl}${buildCliproxyProviderPath(provider, backend)}`;
|
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 { ChildProcess, spawn } from 'child_process';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import type { ProfileType } from '../types/profile';
|
import type { ProfileType } from '../types/profile';
|
||||||
import { runCleanup } from '../errors';
|
import { ConfigError, runCleanup } from '../errors';
|
||||||
import { expandPath } from '../utils/helpers';
|
import { expandPath } from '../utils/helpers';
|
||||||
import { wireChildProcessSignals } from '../utils/signal-forwarder';
|
import { wireChildProcessSignals } from '../utils/signal-forwarder';
|
||||||
import {
|
import {
|
||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
} from './codex-detector';
|
} from './codex-detector';
|
||||||
import { createLogger } from '../services/logging';
|
import { createLogger } from '../services/logging';
|
||||||
import { getEffectiveApiKey } from '../cliproxy/auth/auth-token-manager';
|
import { getEffectiveApiKey } from '../cliproxy/auth/auth-token-manager';
|
||||||
|
import { normalizeCodexResponsesBaseUrl } from '../cliproxy/config/provider-route';
|
||||||
import { resolveLifecyclePort } from '../cliproxy/config/port-manager';
|
import { resolveLifecyclePort } from '../cliproxy/config/port-manager';
|
||||||
import { getModelMaxLevel } from '../cliproxy/model-catalog';
|
import { getModelMaxLevel } from '../cliproxy/model-catalog';
|
||||||
import { parseCodexModelTuningAlias } from '../cliproxy/ai-providers/model-id-normalizer';
|
import { parseCodexModelTuningAlias } from '../cliproxy/ai-providers/model-id-normalizer';
|
||||||
@@ -53,7 +54,7 @@ function buildConfigOverrideArgs(overrides: string[]): string[] {
|
|||||||
|
|
||||||
function buildConfigOverrideSupportError(binaryInfo?: TargetBinaryInfo): Error {
|
function buildConfigOverrideSupportError(binaryInfo?: TargetBinaryInfo): Error {
|
||||||
const versionSummary = binaryInfo?.version ? ` (${binaryInfo.version})` : '';
|
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.`
|
`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)) {
|
if (typeof value === 'string' && CODEX_REASONING_LEVELS.has(value)) {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
throw new Error(
|
throw new ConfigError(
|
||||||
'Codex target supports reasoning levels only: minimal, low, medium, high, xhigh.'
|
'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());
|
const providerRepair = await ensureCodexCliproxyProviderConfig(resolveLifecyclePort());
|
||||||
this.ccsxpCliproxyEnvKey = providerRepair.envKey;
|
this.ccsxpCliproxyEnvKey = providerRepair.envKey;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(
|
throw new ConfigError(
|
||||||
`ccsxp could not repair the native Codex cliproxy provider: ${(error as Error).message}`
|
`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()) {
|
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.'
|
'Codex target requires base URL and API key for CCS-backed profile launches.'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const disallowedFlags = findDisallowedCodexManagedFlags(userArgs);
|
const disallowedFlags = findDisallowedCodexManagedFlags(userArgs);
|
||||||
if (disallowedFlags.length > 0) {
|
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.`
|
`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 = [
|
const overrides = [
|
||||||
`model_provider=${formatTomlString(CODEX_RUNTIME_PROVIDER_ID)}`,
|
`model_provider=${formatTomlString(CODEX_RUNTIME_PROVIDER_ID)}`,
|
||||||
`model_providers.${CODEX_RUNTIME_PROVIDER_ID}.name=${formatTomlString('CCS Runtime')}`,
|
`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}.env_key=${formatTomlString(CODEX_RUNTIME_ENV_KEY)}`,
|
||||||
`model_providers.${CODEX_RUNTIME_PROVIDER_ID}.wire_api=${formatTomlString('responses')}`,
|
`model_providers.${CODEX_RUNTIME_PROVIDER_ID}.wire_api=${formatTomlString('responses')}`,
|
||||||
];
|
];
|
||||||
@@ -360,7 +363,7 @@ export class CodexAdapter implements TargetAdapter {
|
|||||||
}
|
}
|
||||||
if (profileType !== 'default') {
|
if (profileType !== 'default') {
|
||||||
if (!creds.apiKey?.trim()) {
|
if (!creds.apiKey?.trim()) {
|
||||||
throw new Error('Codex target requires an API key for CCS-backed profile launches.');
|
throw new ConfigError('Codex target requires an API key for CCS-backed profile launches.');
|
||||||
}
|
}
|
||||||
env[CODEX_RUNTIME_ENV_KEY] = creds.apiKey;
|
env[CODEX_RUNTIME_ENV_KEY] = creds.apiKey;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,9 +9,8 @@ import {
|
|||||||
import { getModelMaxLevel } from '../cliproxy/model-catalog';
|
import { getModelMaxLevel } from '../cliproxy/model-catalog';
|
||||||
import { parseCodexModelTuningAlias } from '../cliproxy/ai-providers/model-id-normalizer';
|
import { parseCodexModelTuningAlias } from '../cliproxy/ai-providers/model-id-normalizer';
|
||||||
import {
|
import {
|
||||||
buildLocalProviderBaseUrl,
|
buildLocalCodexResponsesBaseUrl,
|
||||||
getConfiguredCliproxyBackend,
|
getConfiguredCliproxyBackend,
|
||||||
usesScopedProviderRoutes,
|
|
||||||
} from '../cliproxy/config/provider-route';
|
} from '../cliproxy/config/provider-route';
|
||||||
import { ConfigError } from '../errors/error-types';
|
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,
|
// (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
|
// which both backends serve; keep the provider-scoped alias for the Plus backend
|
||||||
// to preserve its existing per-provider routing.
|
// to preserve its existing per-provider routing.
|
||||||
const backend = getConfiguredCliproxyBackend();
|
return buildLocalCodexResponsesBaseUrl(port, getConfiguredCliproxyBackend());
|
||||||
if (usesScopedProviderRoutes(backend)) {
|
|
||||||
return buildLocalProviderBaseUrl('codex', port, backend);
|
|
||||||
}
|
|
||||||
return `http://127.0.0.1:${port}/backend-api/codex`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isCcsxpCliproxyShortcut(env: NodeJS.ProcessEnv = process.env): boolean {
|
export function isCcsxpCliproxyShortcut(env: NodeJS.ProcessEnv = process.env): boolean {
|
||||||
@@ -150,6 +145,7 @@ function isProviderReady(
|
|||||||
typeof provider.base_url === 'string' &&
|
typeof provider.base_url === 'string' &&
|
||||||
resolveProviderBaseUrl(provider, expectedBaseUrl) === provider.base_url.trim() &&
|
resolveProviderBaseUrl(provider, expectedBaseUrl) === provider.base_url.trim() &&
|
||||||
provider.env_key === envKey &&
|
provider.env_key === envKey &&
|
||||||
|
provider.auth === undefined &&
|
||||||
provider.wire_api === 'responses' &&
|
provider.wire_api === 'responses' &&
|
||||||
provider.requires_openai_auth === false &&
|
provider.requires_openai_auth === false &&
|
||||||
provider.supports_websockets === false
|
provider.supports_websockets === false
|
||||||
@@ -261,6 +257,7 @@ export async function ensureCodexCliproxyProviderConfig(
|
|||||||
...currentProvider,
|
...currentProvider,
|
||||||
...buildProviderConfig(resolveProviderBaseUrl(currentProvider, expectedBaseUrl), envKey),
|
...buildProviderConfig(resolveProviderBaseUrl(currentProvider, expectedBaseUrl), envKey),
|
||||||
};
|
};
|
||||||
|
delete (providers[CODEX_CLIPROXY_PROVIDER_ID] as Record<string, unknown>).auth;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (providerReady && !normalizedModelAlias) {
|
if (providerReady && !normalizedModelAlias) {
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ describe('CodexAdapter', () => {
|
|||||||
profileType: 'cliproxy',
|
profileType: 'cliproxy',
|
||||||
creds: {
|
creds: {
|
||||||
profile: 'codex',
|
profile: 'codex',
|
||||||
baseUrl: 'http://127.0.0.1:8317/api/provider/codex',
|
baseUrl: 'http://127.0.0.1:8317',
|
||||||
apiKey: 'cliproxy-token',
|
apiKey: 'cliproxy-token',
|
||||||
model: 'gpt-5.4',
|
model: 'gpt-5.4',
|
||||||
reasoningOverride: 'high',
|
reasoningOverride: 'high',
|
||||||
@@ -143,6 +143,9 @@ describe('CodexAdapter', () => {
|
|||||||
|
|
||||||
expect(args).toContain('-c');
|
expect(args).toContain('-c');
|
||||||
expect(args).toContain('model_provider="ccs_runtime"');
|
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_providers.ccs_runtime.env_key="CCS_CODEX_API_KEY"');
|
||||||
expect(args).toContain('model="gpt-5.4"');
|
expect(args).toContain('model="gpt-5.4"');
|
||||||
expect(args).toContain(
|
expect(args).toContain(
|
||||||
|
|||||||
@@ -55,9 +55,7 @@ describe('codex cliproxy provider config repair', () => {
|
|||||||
// original CLIProxy backend serves the Codex Responses API only under the
|
// original CLIProxy backend serves the Codex Responses API only under the
|
||||||
// "/backend-api/codex" alias, never at the bare root. Returning the root here
|
// "/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).
|
// makes Codex call "http://127.0.0.1:8317/responses" -> 404 (issue #1597).
|
||||||
expect(buildCodexCliproxyProviderBaseUrl(8317)).toBe(
|
expect(buildCodexCliproxyProviderBaseUrl(8317)).toBe('http://127.0.0.1:8317/backend-api/codex');
|
||||||
'http://127.0.0.1:8317/backend-api/codex'
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('produces a Codex Responses endpoint the original backend actually serves (regression #1597)', () => {
|
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');
|
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 () => {
|
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(
|
||||||
|
|||||||
@@ -124,21 +124,26 @@ exit 0
|
|||||||
|
|
||||||
const argsLog = fs.readFileSync(codexArgsLogPath, 'utf8');
|
const argsLog = fs.readFileSync(codexArgsLogPath, 'utf8');
|
||||||
expect(argsLog).toContain('model_provider="ccs_runtime"');
|
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).toContain('model_reasoning_effort="high"');
|
||||||
expect(argsLog).not.toContain('mcp_servers.ccs_browser.command=');
|
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).not.toContain('mcp_servers.ccs_browser.args=["-y","@playwright/mcp@0.0.70"]');
|
||||||
expect(argsLog).toContain('smoke');
|
expect(argsLog).toContain('smoke');
|
||||||
expect(fs.readFileSync(codexEnvLogPath, 'utf8')).toBe('bridge-token');
|
expect(fs.readFileSync(codexEnvLogPath, 'utf8')).toBe('bridge-token');
|
||||||
});
|
}, 15000);
|
||||||
|
|
||||||
it('rejects native Codex profile flags when CCS manages the bridge runtime', () => {
|
it('rejects native Codex profile flags when CCS manages the bridge runtime', () => {
|
||||||
if (process.platform === 'win32') return;
|
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.status).toBe(1);
|
||||||
expect(result.stderr).toContain('does not allow --profile/-p');
|
expect(result.stderr).toContain('does not allow --profile/-p');
|
||||||
expect(fs.existsSync(codexArgsLogPath)).toBe(false);
|
expect(fs.existsSync(codexArgsLogPath)).toBe(false);
|
||||||
});
|
}, 15000);
|
||||||
});
|
});
|
||||||
Reference in new issue
Block a user