mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-14 04:15:52 +00:00
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.
357 lines
13 KiB
TypeScript
357 lines
13 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
|
|
import * as fs from 'fs';
|
|
import * as os from 'os';
|
|
import * as path from 'path';
|
|
import {
|
|
buildCodexCliproxyProviderBaseUrl,
|
|
ensureCodexCliproxyProviderConfig,
|
|
} from '../../../src/targets/codex-cliproxy-provider-config';
|
|
import { invalidateConfigCache } from '../../../src/config/config-loader-facade';
|
|
import { clearConfigCache } from '../../../src/cliproxy/config/base-config-loader';
|
|
|
|
describe('codex cliproxy provider config repair', () => {
|
|
let tempHome: string;
|
|
let codexHome: string;
|
|
let configPath: string;
|
|
let env: NodeJS.ProcessEnv;
|
|
let originalCcsHome: string | undefined;
|
|
|
|
beforeEach(() => {
|
|
originalCcsHome = process.env.CCS_HOME;
|
|
tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-codex-provider-config-'));
|
|
codexHome = path.join(tempHome, '.codex');
|
|
configPath = path.join(codexHome, 'config.toml');
|
|
env = { CODEX_HOME: codexHome } as NodeJS.ProcessEnv;
|
|
process.env.CCS_HOME = tempHome;
|
|
invalidateConfigCache();
|
|
clearConfigCache();
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (originalCcsHome !== undefined) {
|
|
process.env.CCS_HOME = originalCcsHome;
|
|
} else {
|
|
delete process.env.CCS_HOME;
|
|
}
|
|
invalidateConfigCache();
|
|
clearConfigCache();
|
|
fs.rmSync(tempHome, { recursive: true, force: true });
|
|
});
|
|
|
|
function writeBackendConfig(backend: 'original' | 'plus'): void {
|
|
const ccsDir = path.join(tempHome, '.ccs');
|
|
fs.mkdirSync(ccsDir, { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(ccsDir, 'config.yaml'),
|
|
['version: 1', 'cliproxy:', ` backend: ${backend}`, ''].join('\n'),
|
|
'utf8'
|
|
);
|
|
invalidateConfigCache();
|
|
clearConfigCache();
|
|
}
|
|
|
|
it('uses the original backend chatgpt_base_url alias by default', () => {
|
|
// Codex CLI (wire_api = "responses") appends "/responses" to base_url. 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
|
|
// 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');
|
|
});
|
|
|
|
it('produces a Codex Responses endpoint the original backend actually serves (regression #1597)', () => {
|
|
const baseUrl = buildCodexCliproxyProviderBaseUrl(8317);
|
|
const responsesEndpoint = `${baseUrl.replace(/\/+$/, '')}/responses`;
|
|
expect(responsesEndpoint).toBe('http://127.0.0.1:8317/backend-api/codex/responses');
|
|
expect(responsesEndpoint).not.toBe('http://127.0.0.1:8317/responses');
|
|
});
|
|
|
|
it('uses the plus backend scoped Codex URL when configured', () => {
|
|
writeBackendConfig('plus');
|
|
expect(buildCodexCliproxyProviderBaseUrl(8317)).toBe(
|
|
'http://127.0.0.1:8317/api/provider/codex'
|
|
);
|
|
});
|
|
|
|
it('creates the cliproxy model provider when config.toml is missing', async () => {
|
|
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('[model_providers.cliproxy]');
|
|
expect(rawText).toContain('name = "CLIProxy Codex"');
|
|
expect(rawText).toContain('base_url = "http://127.0.0.1:8317/backend-api/codex"');
|
|
expect(rawText).toContain('env_key = "CLIPROXY_API_KEY"');
|
|
expect(rawText).not.toContain('model_provider = "cliproxy"');
|
|
});
|
|
|
|
it('appends the missing provider while preserving existing raw config text', async () => {
|
|
fs.mkdirSync(codexHome, { recursive: true });
|
|
fs.writeFileSync(configPath, '# user note\nmodel = "gpt-5.4"\n', '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.startsWith('# user note\nmodel = "gpt-5.4"\n\n')).toBe(true);
|
|
expect(rawText).toContain('[model_providers.cliproxy]');
|
|
});
|
|
|
|
it('repairs Codex config files saved with a UTF-8 BOM', async () => {
|
|
fs.mkdirSync(codexHome, { recursive: true });
|
|
const rawText = '\ufeffmodel = "gpt-5.4"\n';
|
|
fs.writeFileSync(configPath, rawText, 'utf8');
|
|
|
|
const result = await ensureCodexCliproxyProviderConfig(8317, env);
|
|
|
|
expect(result.changed).toBe(true);
|
|
const repairedText = fs.readFileSync(configPath, 'utf8');
|
|
expect(repairedText.startsWith('\ufeffmodel = "gpt-5.4"\n\n')).toBe(true);
|
|
expect(repairedText).toContain('[model_providers.cliproxy]');
|
|
});
|
|
|
|
it('repairs an existing incomplete cliproxy provider', async () => {
|
|
fs.mkdirSync(codexHome, { recursive: true });
|
|
fs.writeFileSync(
|
|
configPath,
|
|
`[model_providers.cliproxy]
|
|
base_url = "http://localhost:8317/api/provider/codex"
|
|
wire_api = "responses"
|
|
`,
|
|
'utf8'
|
|
);
|
|
|
|
const result = await ensureCodexCliproxyProviderConfig(9321, env);
|
|
|
|
expect(result.changed).toBe(true);
|
|
expect(result.envKey).toBe('CLIPROXY_API_KEY');
|
|
const rawText = fs.readFileSync(configPath, 'utf8');
|
|
expect(rawText).toContain('base_url = "http://127.0.0.1:9321/backend-api/codex"');
|
|
expect(rawText).toContain('env_key = "CLIPROXY_API_KEY"');
|
|
expect(rawText).toContain('requires_openai_auth = 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 () => {
|
|
fs.mkdirSync(codexHome, { recursive: true });
|
|
fs.writeFileSync(
|
|
configPath,
|
|
`[model_providers.cliproxy]
|
|
name = "Old Name"
|
|
base_url = "https://cliproxy.example.com/api/provider/codex/responses"
|
|
env_key = "CCS_CUSTOM_CLIPROXY_TOKEN"
|
|
wire_api = "chat"
|
|
`,
|
|
'utf8'
|
|
);
|
|
|
|
const result = await ensureCodexCliproxyProviderConfig(9321, env);
|
|
|
|
expect(result.changed).toBe(true);
|
|
expect(result.envKey).toBe('CCS_CUSTOM_CLIPROXY_TOKEN');
|
|
const rawText = fs.readFileSync(configPath, 'utf8');
|
|
expect(rawText).toContain(
|
|
'base_url = "https://cliproxy.example.com/api/provider/codex/responses"'
|
|
);
|
|
expect(rawText).toContain('env_key = "CCS_CUSTOM_CLIPROXY_TOKEN"');
|
|
expect(rawText).toContain('wire_api = "responses"');
|
|
});
|
|
|
|
it('repairs invalid cliproxy provider base URLs back to the managed local default', async () => {
|
|
fs.mkdirSync(codexHome, { recursive: true });
|
|
fs.writeFileSync(
|
|
configPath,
|
|
`[model_providers.cliproxy]
|
|
name = "CLIProxy Codex"
|
|
base_url = "not-a-url"
|
|
env_key = "CLIPROXY_API_KEY"
|
|
wire_api = "responses"
|
|
requires_openai_auth = false
|
|
supports_websockets = false
|
|
`,
|
|
'utf8'
|
|
);
|
|
|
|
const result = await ensureCodexCliproxyProviderConfig(9321, env);
|
|
|
|
expect(result.changed).toBe(true);
|
|
const rawText = fs.readFileSync(configPath, 'utf8');
|
|
expect(rawText).toContain(`base_url = "${buildCodexCliproxyProviderBaseUrl(9321)}"`);
|
|
});
|
|
|
|
it('rejects invalid non-table model_providers values without appending broken TOML', async () => {
|
|
fs.mkdirSync(codexHome, { recursive: true });
|
|
const rawText = 'model_providers = "legacy"\n';
|
|
fs.writeFileSync(configPath, rawText, 'utf8');
|
|
|
|
await expect(ensureCodexCliproxyProviderConfig(8317, env)).rejects.toThrow(
|
|
'[model_providers] must be a table'
|
|
);
|
|
expect(fs.readFileSync(configPath, 'utf8')).toBe(rawText);
|
|
});
|
|
|
|
it('repairs a stale ready localhost provider to the original backend codex alias', async () => {
|
|
fs.mkdirSync(codexHome, { recursive: true });
|
|
const rawText = `[model_providers.cliproxy]
|
|
name = "CLIProxy Codex"
|
|
base_url = "http://localhost:8317/api/provider/codex"
|
|
env_key = "CLIPROXY_API_KEY"
|
|
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(true);
|
|
expect(result.envKey).toBe('CLIPROXY_API_KEY');
|
|
const repairedText = fs.readFileSync(configPath, 'utf8');
|
|
expect(repairedText).toContain('base_url = "http://127.0.0.1:8317/backend-api/codex"');
|
|
expect(repairedText).not.toContain('/api/provider/codex');
|
|
});
|
|
|
|
it('leaves a ready plus-backend localhost provider unchanged', async () => {
|
|
writeBackendConfig('plus');
|
|
fs.mkdirSync(codexHome, { recursive: true });
|
|
const rawText = `[model_providers.cliproxy]
|
|
name = "CLIProxy Codex"
|
|
base_url = "http://localhost:8317/api/provider/codex"
|
|
env_key = "CLIPROXY_API_KEY"
|
|
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('CLIPROXY_API_KEY');
|
|
expect(fs.readFileSync(configPath, 'utf8')).toBe(rawText);
|
|
});
|
|
|
|
it('leaves a ready remote provider unchanged', async () => {
|
|
fs.mkdirSync(codexHome, { recursive: true });
|
|
const rawText = `[model_providers.cliproxy]
|
|
name = "CLIProxy Codex"
|
|
base_url = "https://cliproxy.example.com/api/provider/codex"
|
|
env_key = "CCS_REMOTE_CLIPROXY_TOKEN"
|
|
wire_api = "responses"
|
|
requires_openai_auth = false
|
|
supports_websockets = false
|
|
`;
|
|
fs.writeFileSync(configPath, rawText, 'utf8');
|
|
|
|
const result = await ensureCodexCliproxyProviderConfig(8317, env);
|
|
|
|
expect(result.changed).toBe(false);
|
|
expect(result.envKey).toBe('CCS_REMOTE_CLIPROXY_TOKEN');
|
|
expect(fs.readFileSync(configPath, 'utf8')).toBe(rawText);
|
|
});
|
|
|
|
it('normalizes a ready native Codex tuning alias before requests reach cliproxy', async () => {
|
|
fs.mkdirSync(codexHome, { recursive: true });
|
|
fs.writeFileSync(
|
|
configPath,
|
|
`model = "gpt-5.5-high-fast"
|
|
|
|
[model_providers.cliproxy]
|
|
name = "CLIProxy Codex"
|
|
base_url = "http://127.0.0.1:8317"
|
|
env_key = "CLIPROXY_API_KEY"
|
|
wire_api = "responses"
|
|
requires_openai_auth = false
|
|
supports_websockets = false
|
|
`,
|
|
'utf8'
|
|
);
|
|
|
|
const result = await ensureCodexCliproxyProviderConfig(8317, env);
|
|
|
|
expect(result.changed).toBe(true);
|
|
const rawText = fs.readFileSync(configPath, 'utf8');
|
|
expect(rawText).toContain('model = "gpt-5.5"');
|
|
expect(rawText).toContain('model_reasoning_effort = "high"');
|
|
expect(rawText).toContain('service_tier = "priority"');
|
|
expect(rawText).toContain('[model_providers.cliproxy]');
|
|
expect(rawText).not.toContain('gpt-5.5-high-fast');
|
|
});
|
|
|
|
it('normalizes a native Codex effort alias when adding the missing provider', async () => {
|
|
fs.mkdirSync(codexHome, { recursive: true });
|
|
fs.writeFileSync(configPath, 'model = "gpt-5.5-xhigh"\n', 'utf8');
|
|
|
|
const result = await ensureCodexCliproxyProviderConfig(8317, env);
|
|
|
|
expect(result.changed).toBe(true);
|
|
const rawText = fs.readFileSync(configPath, 'utf8');
|
|
expect(rawText).toContain('model = "gpt-5.5"');
|
|
expect(rawText).toContain('model_reasoning_effort = "xhigh"');
|
|
});
|
|
|
|
it('normalizes a native Codex minimal effort alias when adding the missing provider', async () => {
|
|
fs.mkdirSync(codexHome, { recursive: true });
|
|
fs.writeFileSync(configPath, 'model = "gpt-5.5-minimal"\n', 'utf8');
|
|
|
|
const result = await ensureCodexCliproxyProviderConfig(8317, env);
|
|
|
|
expect(result.changed).toBe(true);
|
|
const rawText = fs.readFileSync(configPath, 'utf8');
|
|
expect(rawText).toContain('model = "gpt-5.5"');
|
|
expect(rawText).toContain('model_reasoning_effort = "minimal"');
|
|
expect(rawText).toContain('[model_providers.cliproxy]');
|
|
});
|
|
|
|
it('does not strip unknown top-level models that happen to end with effort tokens', async () => {
|
|
fs.mkdirSync(codexHome, { recursive: true });
|
|
const rawText = `model = "literal-high"
|
|
|
|
[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
|
|
`;
|
|
fs.writeFileSync(configPath, rawText, 'utf8');
|
|
|
|
const result = await ensureCodexCliproxyProviderConfig(8317, env);
|
|
|
|
expect(result.changed).toBe(false);
|
|
expect(fs.readFileSync(configPath, 'utf8')).toBe(rawText);
|
|
});
|
|
});
|