mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-03 20:13:02 +00:00
test(cliproxy): cover routing rule preservation
This commit is contained in:
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();
|
||||
|
||||
Reference in new issue
Block a user