test(cliproxy): cover routing rule preservation

This commit is contained in:
Tam Nhu Tran committed 2026-07-29 14:02:40 -04:00
1 parent 9fb2016f90
commit 2ce3bfca26
4 files changed
+390 -3

No files matched your search

@@ -0,0 +1,164 @@
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 * as yaml from 'js-yaml';
import { configNeedsRegeneration, regenerateConfig } from '../generator';
interface GeneratedCliProxyConfig {
'oauth-model-alias'?: Record<string, Array<{ name: string; alias: string; fork?: boolean }>>;
payload?: {
override?: Array<{
models: Array<{ name: string; protocol?: string }>;
params: Record<string, unknown>;
}>;
[section: string]: unknown;
};
[key: string]: unknown;
}
describe('Codex user routing config generation', () => {
let testHome: string;
let originalCcsHome: string | undefined;
let generatedConfigPath: string;
let generatedAuthDir: string;
beforeEach(() => {
testHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-codex-routing-'));
originalCcsHome = process.env.CCS_HOME;
process.env.CCS_HOME = testHome;
const ccsDir = path.join(testHome, '.ccs');
const cliproxyDir = path.join(ccsDir, 'cliproxy');
fs.mkdirSync(cliproxyDir, { recursive: true });
generatedConfigPath = path.join(cliproxyDir, 'config.yaml');
generatedAuthDir = path.join(cliproxyDir, 'auth');
fs.writeFileSync(
path.join(ccsDir, 'config.yaml'),
`version: 14
cliproxy:
oauth_model_alias:
codex:
- name: gpt-5.6-sol
alias: gpt-5.6-sol-fast
fork: true
payload:
override:
- models:
- name: gpt-5.6-sol-fast
protocol: codex
params:
service_tier: priority
`,
'utf8'
);
});
afterEach(() => {
if (originalCcsHome === undefined) {
delete process.env.CCS_HOME;
} else {
process.env.CCS_HOME = originalCcsHome;
}
fs.rmSync(testHome, { recursive: true, force: true });
});
function readGeneratedConfig(): GeneratedCliProxyConfig {
return yaml.load(fs.readFileSync(generatedConfigPath, 'utf8')) as GeneratedCliProxyConfig;
}
it('generates external client alias semantics with a Codex-only priority override', () => {
regenerateConfig(8317, {
configPath: generatedConfigPath,
authDir: generatedAuthDir,
});
const generated = readGeneratedConfig();
expect(generated['oauth-model-alias']?.codex).toEqual([
{ name: 'gpt-5.6-sol', alias: 'gpt-5.6-sol-fast', fork: true },
]);
expect(generated.payload?.override).toEqual([
{
models: [{ name: 'gpt-5.6-sol-fast', protocol: 'codex' }],
params: { service_tier: 'priority' },
},
]);
expect(
generated.payload?.override?.some((rule) =>
rule.models.some((model) => model.name === 'gpt-5.6-sol')
)
).toBe(false);
});
it('preserves manual provider/payload sections and stays stable across regeneration', () => {
fs.writeFileSync(
generatedConfigPath,
`# CLIProxyAPI config generated by CCS v20
port: 8317
api-keys:
- "ccs-internal-managed"
auth-dir: "${generatedAuthDir}"
oauth-model-alias:
codex:
- name: existing-upstream
alias: existing-client
- name: gpt-5.6-sol
alias: gpt-5.6-sol-fast
future-provider:
- name: future-upstream
alias: future-client
payload:
default:
- models:
- name: existing-client
params:
temperature: 0.2
override:
- models:
- name: gpt-5.6-sol-fast
protocol: codex
params:
service_tier: standard
- models:
- name: manual-client
protocol: codex
params:
custom_flag: preserved
`,
'utf8'
);
expect(configNeedsRegeneration()).toBe(true);
regenerateConfig(8317, {
configPath: generatedConfigPath,
authDir: generatedAuthDir,
});
const first = readGeneratedConfig();
expect(first['oauth-model-alias']?.codex).toEqual([
{ name: 'existing-upstream', alias: 'existing-client' },
{ name: 'gpt-5.6-sol', alias: 'gpt-5.6-sol-fast', fork: true },
]);
expect(first['oauth-model-alias']?.['future-provider']).toEqual([
{ name: 'future-upstream', alias: 'future-client' },
]);
expect(first.payload?.default).toBeDefined();
expect(first.payload?.override).toEqual([
{
models: [{ name: 'gpt-5.6-sol-fast', protocol: 'codex' }],
params: { service_tier: 'priority' },
},
{
models: [{ name: 'manual-client', protocol: 'codex' }],
params: { custom_flag: 'preserved' },
},
]);
expect(configNeedsRegeneration()).toBe(false);
regenerateConfig(8317, {
configPath: generatedConfigPath,
authDir: generatedAuthDir,
});
expect(readGeneratedConfig()).toEqual(first);
});
});
@@ -0,0 +1,151 @@
import { describe, expect, it } from 'bun:test';
import {
mergeOAuthModelAliases,
parseOAuthModelAliasSection,
serializeOAuthModelAliasBody,
} from '../oauth-model-alias-config';
import {
mergePayloadConfig,
parsePayloadSection,
serializePayloadSection,
} from '../payload-rule-config';
describe('CLIProxy user routing config', () => {
it('parses and round-trips provider aliases without dropping provider sections', () => {
const body = ` codex:
- name: gpt-5.6-sol
alias: gpt-5.6-sol-fast
fork: true
future-provider:
- name: upstream-model
alias: client-model
`;
const parsed = parseOAuthModelAliasSection(body);
expect(parsed.codex).toEqual([{ name: 'gpt-5.6-sol', alias: 'gpt-5.6-sol-fast', fork: true }]);
expect(parsed['future-provider']).toEqual([{ name: 'upstream-model', alias: 'client-model' }]);
expect(parseOAuthModelAliasSection(serializeOAuthModelAliasBody(parsed))).toEqual(parsed);
});
it('preserves insertion order and deduplicates exact aliases while promoting fork', () => {
const merged = mergeOAuthModelAliases(
{
codex: [
{ name: 'first-upstream', alias: 'first-client' },
{ name: 'gpt-5.6-sol', alias: 'gpt-5.6-sol-fast' },
],
},
{
codex: [
{ name: 'gpt-5.6-sol', alias: 'gpt-5.6-sol-fast', fork: true },
{ name: 'last-upstream', alias: 'last-client' },
],
}
);
expect(merged.codex).toEqual([
{ name: 'first-upstream', alias: 'first-client' },
{ name: 'gpt-5.6-sol', alias: 'gpt-5.6-sol-fast', fork: true },
{ name: 'last-upstream', alias: 'last-client' },
]);
});
it('lets configured aliases replace an existing provider client alias in place', () => {
const merged = mergeOAuthModelAliases(
{
codex: [
{ name: 'first-upstream', alias: 'first-client' },
{ name: 'old-upstream', alias: 'gpt-5.6-sol-fast', fork: true },
{ name: 'last-upstream', alias: 'last-client' },
],
},
{
codex: [{ name: 'gpt-5.6-sol', alias: 'gpt-5.6-sol-fast' }],
}
);
expect(merged.codex).toEqual([
{ name: 'first-upstream', alias: 'first-client' },
{ name: 'gpt-5.6-sol', alias: 'gpt-5.6-sol-fast' },
{ name: 'last-upstream', alias: 'last-client' },
]);
});
it('round-trips unknown payload subsections and replaces matching scoped rules', () => {
const existing = parsePayloadSection(` default:
- models:
- name: keep-default
params:
temperature: 0.2
override:
- models:
- name: gpt-5.6-sol-fast
protocol: codex
params:
service_tier: standard
`);
const configured = {
override: [
{
models: [{ name: 'gpt-5.6-sol-fast', protocol: 'codex' }],
params: { service_tier: 'priority' },
},
],
};
const merged = mergePayloadConfig(existing, configured);
expect(merged?.default).toBeDefined();
expect(merged?.override).toEqual(configured.override);
expect(parsePayloadSection(serializePayloadSection(merged).replace(/^payload:\n/, ''))).toEqual(
merged
);
});
it('preserves distinct full predicates that select the same model', () => {
const existing = {
override: [
{
models: [
{
name: 'gpt-5.6-sol-fast',
protocol: 'codex',
'from-protocol': 'openai',
},
],
headers: { 'x-tenant': 'alpha' },
match: { project: '^priority-' },
params: { service_tier: 'standard' },
},
{
models: [{ name: 'gpt-5.6-sol-fast', protocol: 'codex' }],
headers: { 'x-tenant': 'beta' },
'not-match': { project: '^disabled-' },
exist: ['metadata.project'],
params: { service_tier: 'standard' },
},
],
};
const configured = {
override: [
{
models: [
{
protocol: 'codex',
'from-protocol': 'openai',
name: 'gpt-5.6-sol-fast',
},
],
match: { project: '^priority-' },
headers: { 'x-tenant': 'alpha' },
params: { service_tier: 'priority' },
},
],
};
const merged = mergePayloadConfig(existing, configured);
expect(merged?.override).toEqual([configured.override[0], existing.override[1]]);
expect(parsePayloadSection(serializePayloadSection(merged).replace(/^payload:\n/, ''))).toEqual(
merged
);
});
});
@@ -0,0 +1,37 @@
import { describe, expect, it } from 'bun:test';
import * as yaml from 'js-yaml';
import { mergeWithDefaults } from '../../loader/defaults-merger';
import { generateYamlWithComments } from '../../loader/yaml-serializer';
import type { UnifiedConfig } from '../unified-config';
describe('CLIProxy user routing schema', () => {
it('survives default merging and YAML serialization', () => {
const configuredRouting = {
oauth_model_alias: {
codex: [
{
name: 'gpt-5.6-sol',
alias: 'gpt-5.6-sol-fast',
fork: true,
},
],
},
payload: {
override: [
{
models: [{ name: 'gpt-5.6-sol-fast', protocol: 'codex' }],
params: { service_tier: 'priority' },
},
],
},
};
const merged = mergeWithDefaults({
version: 14,
cliproxy: configuredRouting,
} as Partial<UnifiedConfig>);
const serialized = yaml.load(generateYamlWithComments(merged)) as UnifiedConfig;
expect(serialized.cliproxy.oauth_model_alias).toEqual(configuredRouting.oauth_model_alias);
expect(serialized.cliproxy.payload).toEqual(configuredRouting.payload);
});
});
@@ -33,6 +33,13 @@ import type {
CLIProxyLoggingConfig,
CLIProxySafetyConfig,
CLIProxyRoutingConfig,
CLIProxyRetryConfig,
CLIProxyPoolRoutingConfig,
CLIProxyOAuthModelAliasEntry,
CLIProxyOAuthModelAliasConfig,
CLIProxyPayloadModelSelector,
CLIProxyPayloadOverrideRule,
CLIProxyPayloadConfig,
CLIProxyConfig,
AutoQuotaConfig,
RuntimeMonitorConfig,
@@ -77,12 +84,40 @@ import type {
} from '../../unified-config-types';
describe('config schemas backward compatibility', () => {
it('re-exports structured CLIProxy user routing types', () => {
const alias: CLIProxyOAuthModelAliasEntry = {
name: 'gpt-5.6-sol',
alias: 'gpt-5.6-sol-fast',
};
const aliases: CLIProxyOAuthModelAliasConfig = { codex: [alias] };
const selector: CLIProxyPayloadModelSelector = {
name: alias.alias,
protocol: 'codex',
'from-protocol': 'openai',
};
const rule: CLIProxyPayloadOverrideRule = {
models: [selector],
params: { service_tier: 'priority' },
headers: { 'x-tenant': 'alpha' },
};
const payload: CLIProxyPayloadConfig = { override: [rule] };
const retry: CLIProxyRetryConfig = { request_retry: 0 };
const pool: CLIProxyPoolRoutingConfig = { enabled: false };
expect({ aliases, payload, retry, pool }).toEqual({
aliases: { codex: [alias] },
payload: { override: [rule] },
retry: { request_retry: 0 },
pool: { enabled: false },
});
});
// -------------------------------------------------------------------------
// Constants
// -------------------------------------------------------------------------
it('re-exports UNIFIED_CONFIG_VERSION', () => {
expect(barrel.UNIFIED_CONFIG_VERSION).toBe(13);
expect(schemas.UNIFIED_CONFIG_VERSION).toBe(13);
expect(barrel.UNIFIED_CONFIG_VERSION).toBe(14);
expect(schemas.UNIFIED_CONFIG_VERSION).toBe(14);
});
it('re-exports CLIPROXY_SUPPORTED_PROVIDERS', () => {
@@ -129,7 +164,7 @@ describe('config schemas backward compatibility', () => {
expect(typeof schemas.createEmptyUnifiedConfig).toBe('function');
const config = barrel.createEmptyUnifiedConfig();
expect(config.version).toBe(13);
expect(config.version).toBe(14);
expect(config.accounts).toEqual({});
expect(config.profiles).toEqual({});
expect(config.cliproxy).toBeDefined();