mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-11 12:09:03 +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,
|
CLIProxyLoggingConfig,
|
||||||
CLIProxySafetyConfig,
|
CLIProxySafetyConfig,
|
||||||
CLIProxyRoutingConfig,
|
CLIProxyRoutingConfig,
|
||||||
|
CLIProxyRetryConfig,
|
||||||
|
CLIProxyPoolRoutingConfig,
|
||||||
|
CLIProxyOAuthModelAliasEntry,
|
||||||
|
CLIProxyOAuthModelAliasConfig,
|
||||||
|
CLIProxyPayloadModelSelector,
|
||||||
|
CLIProxyPayloadOverrideRule,
|
||||||
|
CLIProxyPayloadConfig,
|
||||||
CLIProxyConfig,
|
CLIProxyConfig,
|
||||||
AutoQuotaConfig,
|
AutoQuotaConfig,
|
||||||
RuntimeMonitorConfig,
|
RuntimeMonitorConfig,
|
||||||
@@ -77,12 +84,40 @@ import type {
|
|||||||
} from '../../unified-config-types';
|
} from '../../unified-config-types';
|
||||||
|
|
||||||
describe('config schemas backward compatibility', () => {
|
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
|
// Constants
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
it('re-exports UNIFIED_CONFIG_VERSION', () => {
|
it('re-exports UNIFIED_CONFIG_VERSION', () => {
|
||||||
expect(barrel.UNIFIED_CONFIG_VERSION).toBe(13);
|
expect(barrel.UNIFIED_CONFIG_VERSION).toBe(14);
|
||||||
expect(schemas.UNIFIED_CONFIG_VERSION).toBe(13);
|
expect(schemas.UNIFIED_CONFIG_VERSION).toBe(14);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('re-exports CLIPROXY_SUPPORTED_PROVIDERS', () => {
|
it('re-exports CLIPROXY_SUPPORTED_PROVIDERS', () => {
|
||||||
@@ -129,7 +164,7 @@ describe('config schemas backward compatibility', () => {
|
|||||||
expect(typeof schemas.createEmptyUnifiedConfig).toBe('function');
|
expect(typeof schemas.createEmptyUnifiedConfig).toBe('function');
|
||||||
|
|
||||||
const config = barrel.createEmptyUnifiedConfig();
|
const config = barrel.createEmptyUnifiedConfig();
|
||||||
expect(config.version).toBe(13);
|
expect(config.version).toBe(14);
|
||||||
expect(config.accounts).toEqual({});
|
expect(config.accounts).toEqual({});
|
||||||
expect(config.profiles).toEqual({});
|
expect(config.profiles).toEqual({});
|
||||||
expect(config.cliproxy).toBeDefined();
|
expect(config.cliproxy).toBeDefined();
|
||||||
|
|||||||
Reference in new issue
Block a user