Merge pull request #1678 from kaitranntt/kai/feat/1662-codex-fast-alias

feat(cliproxy): preserve scoped routing rules
This commit is contained in:
Kai (Tam Nhu) Tran authored and GitHub committed 2026-07-29 14:18:51 -04:00
commit bd90056f9b
13 files changed
+827 -32

No files matched your search

+8 -8
View File
@@ -1,8 +1,8 @@
{
"scope": "src/**/*.{ts,tsx,js,jsx,mjs,cjs}",
"syncFs": {
"totalOccurrences": 2382,
"filesAffected": 256,
"totalOccurrences": 2388,
"filesAffected": 257,
"hotpathOccurrences": 1103,
"hotpathFilesAffected": 151,
"topHotpathFiles": [
@@ -573,8 +573,8 @@
},
"loggerCoverage": {
"filesWithCreateLogger": 65,
"totalSourceFiles": 754,
"coverageRatio": 0.0862,
"totalSourceFiles": 756,
"coverageRatio": 0.086,
"subdomainsWithZeroCreateLogger": [
"api",
"bin",
@@ -737,6 +737,10 @@
"file": "src/web-server/model-pricing.ts",
"loc": 1127
},
{
"file": "src/cliproxy/config/generator.ts",
"loc": 1109
},
{
"file": "src/cliproxy/auth/oauth-process.ts",
"loc": 1048
@@ -749,10 +753,6 @@
"file": "src/web-server/routes/settings-routes.ts",
"loc": 1042
},
{
"file": "src/cliproxy/config/generator.ts",
"loc": 1034
},
{
"file": "src/cliproxy/proxy/tool-sanitization-proxy.ts",
"loc": 1020
+4 -4
View File
@@ -6,8 +6,8 @@ Scope: `src/**/*.{ts,tsx,js,jsx,mjs,cjs}`
| Metric | Value |
|---|---:|
| Sync fs occurrences (all) | 2382 |
| Sync fs files affected (all) | 256 |
| Sync fs occurrences (all) | 2388 |
| Sync fs files affected (all) | 257 |
| Sync fs occurrences (runtime hotpaths) | 1103 |
| Sync fs files affected (runtime hotpaths) | 151 |
| Legacy shim markers | 456 |
@@ -60,7 +60,7 @@ Scope: `src/**/*.{ts,tsx,js,jsx,mjs,cjs}`
| typed-error adoption (P4 locked subdomains) | 91.7% (22/24), target 40% |
| hotpath console.error/warn occurrences | 266 (571 total, 305 CLI-UX exempt) |
| hotpath console.error/warn files | 82 |
| files with createLogger | 65/754 |
| files with createLogger | 65/756 |
| subdomains with zero createLogger | 15 (api, bin, channels, cliproxy, cliproxy/accounts, cliproxy/ai-providers, cliproxy/binary, cliproxy/config, cliproxy/management, cliproxy/sync, cliproxy/types, config, dispatcher, shared, types) |
| files > 400 LOC | 91 |
| files > 600 LOC | 42 |
@@ -94,10 +94,10 @@ Scope: `src/**/*.{ts,tsx,js,jsx,mjs,cjs}`
| `src/cliproxy/auth/oauth-handler.ts` | 1467 |
| `src/cursor/cursor-executor.ts` | 1234 |
| `src/web-server/model-pricing.ts` | 1127 |
| `src/cliproxy/config/generator.ts` | 1109 |
| `src/cliproxy/auth/oauth-process.ts` | 1048 |
| `src/cliproxy/config/env-builder.ts` | 1045 |
| `src/web-server/routes/settings-routes.ts` | 1042 |
| `src/cliproxy/config/generator.ts` | 1034 |
| `src/cliproxy/proxy/tool-sanitization-proxy.ts` | 1020 |
| `src/commands/cliproxy/variant-subcommand.ts` | 997 |
| `src/cliproxy/quota/quota-manager.ts` | 954 |
@@ -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
);
});
});
+91 -16
View File
@@ -17,6 +17,17 @@ import { CLIPROXY_DEFAULT_PORT } from './port-manager';
import { loadOrCreateUnifiedConfig } from '../../config/config-loader-facade';
import { getActiveDockerLegacyApiKeys } from '../../docker/docker-key-rotation';
import { isValidCliproxyRetryValue } from '../../config/schemas/cliproxy';
import type { CLIProxyOAuthModelAliasConfig } from '../../config/schemas/cliproxy';
import {
mergeOAuthModelAliases,
parseOAuthModelAliasSection,
serializeOAuthModelAliasBody,
} from './oauth-model-alias-config';
import {
mergePayloadConfig,
parsePayloadSection,
serializePayloadSection,
} from './payload-rule-config';
/** Internal API key for CCS-managed requests */
export const CCS_INTERNAL_API_KEY = 'ccs-internal-managed';
@@ -46,8 +57,9 @@ export const CCS_CONTROL_PANEL_SECRET = 'ccs';
* v18: Persist routing.session-affinity and routing.session-affinity-ttl from CCS unified config
* v19: Persist backend-aware management panel repository from CCS unified config
* v20: Pool-gated cooling/routing/retry-cap block; disable-cooling flips to false for pool users
* v21: Persist user-defined OAuth aliases and scoped payload override rules
*/
export const CLIPROXY_CONFIG_VERSION = 20;
export const CLIPROXY_CONFIG_VERSION = 21;
export const ORIGINAL_MANAGEMENT_PANEL_REPOSITORY =
'https://github.com/router-for-me/Cli-Proxy-API-Management-Center';
@@ -296,7 +308,7 @@ function addAliasEntry(
if (getDeniedModelIdReasonForProvider(normalized.name, 'agy')) return;
if (getDeniedModelIdReasonForProvider(normalized.alias, 'agy')) return;
const key = `${normalized.name}\u0000${normalized.alias}`;
const key = normalized.alias;
const existingIndex = indexByKey.get(key);
if (existingIndex !== undefined) {
if (normalized.fork) entries[existingIndex].fork = true;
@@ -307,6 +319,33 @@ function addAliasEntry(
entries.push(normalized);
}
function upsertConfiguredAliasEntry(
entries: OAuthModelAliasEntry[],
indexByKey: Map<string, number>,
entry: OAuthModelAliasEntry
): void {
const normalized: OAuthModelAliasEntry = {
name: sanitizeYamlScalar(entry.name),
alias: normalizeAntigravityAlias(entry.alias),
fork: entry.fork || undefined,
};
if (!normalized.name || !normalized.alias) return;
if (getDeniedModelIdReasonForProvider(normalized.name, 'agy')) return;
if (getDeniedModelIdReasonForProvider(normalized.alias, 'agy')) return;
const existingIndex = entries.findIndex((candidate) => candidate.alias === normalized.alias);
if (existingIndex === -1) {
addAliasEntry(entries, indexByKey, normalized);
return;
}
entries[existingIndex] = normalized;
indexByKey.clear();
entries.forEach((candidate, index) => {
indexByKey.set(candidate.alias, index);
});
}
function buildAntigravityAliasKey(entry: Pick<OAuthModelAliasEntry, 'name' | 'alias'>): string {
return `${sanitizeYamlScalar(entry.name)}\u0000${normalizeAntigravityAlias(entry.alias)}`;
}
@@ -622,9 +661,13 @@ function writeLegacyGeminiAliasCleanupBackup(configPath: string, existingContent
* Generate oauth-model-alias YAML section.
* Merges default Antigravity aliases with any user-added custom aliases.
*/
function generateOAuthModelAliasSection(existingAliases?: string): string {
function generateOAuthModelAliasSection(
existingAliases?: string,
configuredAliases?: CLIProxyOAuthModelAliasConfig
): string {
const aliasEntries: OAuthModelAliasEntry[] = [];
const aliasIndexByKey = new Map<string, number>();
const normalizedConfiguredAliases = mergeOAuthModelAliases({}, configuredAliases);
// Start with default aliases.
for (const alias of DEFAULT_ANTIGRAVITY_ALIASES) {
@@ -639,6 +682,11 @@ function generateOAuthModelAliasSection(existingAliases?: string): string {
}
}
// CCS-owned structured aliases replace the same client alias in place.
for (const alias of normalizedConfiguredAliases.antigravity ?? []) {
upsertConfiguredAliasEntry(aliasEntries, aliasIndexByKey, alias);
}
// Expand lightweight compatibility aliases (dot/hyphen + customtools toggle).
for (const alias of getCompatibilityAliases(aliasEntries)) {
addAliasEntry(aliasEntries, aliasIndexByKey, alias);
@@ -652,7 +700,15 @@ function generateOAuthModelAliasSection(existingAliases?: string): string {
})
.join('\n');
return `oauth-model-alias:\n antigravity:\n${entries}`;
const existingAliasConfig = parseOAuthModelAliasSection(existingAliases ?? '');
const mergedAliasConfig = mergeOAuthModelAliases(
existingAliasConfig,
normalizedConfiguredAliases
);
delete mergedAliasConfig.antigravity;
const otherProviders = serializeOAuthModelAliasBody(mergedAliasConfig);
return `oauth-model-alias:\n antigravity:\n${entries}${otherProviders ? `\n${otherProviders}` : ''}`;
}
/**
@@ -663,11 +719,13 @@ function generateOAuthModelAliasSection(existingAliases?: string): string {
* @param port - Server port (default: 8317)
* @param userApiKeys - User-added API keys to preserve (default: [])
* @param existingAliases - Existing oauth-model-alias content to merge with defaults
* @param existingPayload - Existing payload content to merge with structured CCS rules
*/
function generateUnifiedConfigContent(
port: number = CLIPROXY_DEFAULT_PORT,
userApiKeys: string[] = [],
existingAliases?: string
existingAliases?: string,
existingPayload?: string
): string {
const authDir = getAuthDir(); // Base auth dir - CLIProxyAPI scans subdirectories
// Convert Windows backslashes to forward slashes for YAML compatibility
@@ -682,6 +740,10 @@ function generateUnifiedConfigContent(
const requestRetry = getRequestRetry();
const maxRetryInterval = getMaxRetryInterval();
const managementPanelRepository = getManagementPanelRepository();
const userRoutingConfig = loadOrCreateUnifiedConfig().cliproxy;
const payloadSection = serializePayloadSection(
mergePayloadConfig(parsePayloadSection(existingPayload ?? ''), userRoutingConfig.payload)
);
// Get effective auth tokens (respects user customization)
const effectiveApiKey = getEffectiveApiKey();
@@ -795,7 +857,8 @@ ${apiKeysYaml}
# OAuth tokens directory (auto-discovered by CLIProxyAPI)
auth-dir: "${authDirNormalized}"
${generateOAuthModelAliasSection(existingAliases)}
${generateOAuthModelAliasSection(existingAliases, userRoutingConfig.oauth_model_alias)}
${payloadSection ? `${payloadSection}\n` : ''}
`;
return config;
@@ -917,6 +980,7 @@ export function regenerateConfig(
let effectivePort = port;
let userApiKeys: string[] = [];
let existingAliases = '';
let existingPayload = '';
const preservedSections: PreservedYamlSection[] = [];
if (fs.existsSync(configPath)) {
@@ -945,15 +1009,21 @@ export function regenerateConfig(
// Preserve user customizations while pruning legacy generated Gemini preview noise.
const existingConfigVersion = getConfigVersionFromContent(content);
const preservedAliases = extractPreservedAntigravityAliases(
extractYamlSection(content, 'oauth-model-alias'),
{
enableLegacyGeminiStaleCleanup:
existingConfigVersion === null ||
existingConfigVersion < LEGACY_GEMINI_STALE_ALIAS_MIGRATION_VERSION,
}
);
existingAliases = preservedAliases.yaml;
const existingAliasBody = extractYamlSection(content, 'oauth-model-alias');
const preservedAliases = extractPreservedAntigravityAliases(existingAliasBody, {
enableLegacyGeminiStaleCleanup:
existingConfigVersion === null ||
existingConfigVersion < LEGACY_GEMINI_STALE_ALIAS_MIGRATION_VERSION,
});
const preservedAliasConfig = parseOAuthModelAliasSection(existingAliasBody);
const preservedAntigravityConfig = parseOAuthModelAliasSection(preservedAliases.yaml);
if (preservedAntigravityConfig.antigravity) {
preservedAliasConfig.antigravity = preservedAntigravityConfig.antigravity;
} else {
delete preservedAliasConfig.antigravity;
}
existingAliases = serializeOAuthModelAliasBody(preservedAliasConfig);
existingPayload = extractYamlSection(content, 'payload');
if (preservedAliases.prunedLegacyAliasCount > 0) {
writeLegacyGeminiAliasCleanupBackup(configPath, content);
}
@@ -969,7 +1039,12 @@ export function regenerateConfig(
fs.mkdirSync(authDir, { recursive: true, mode: 0o700 });
// Generate fresh config with preserved user API keys and aliases
let configContent = generateUnifiedConfigContent(effectivePort, userApiKeys, existingAliases);
let configContent = generateUnifiedConfigContent(
effectivePort,
userApiKeys,
existingAliases,
existingPayload
);
// Re-append managed top-level sections that are not part of the generated defaults.
for (const section of preservedSections) {
@@ -0,0 +1,130 @@
/** Parse, merge, and serialize user-owned CLIProxy OAuth model aliases. */
import * as yaml from 'js-yaml';
import type {
CLIProxyOAuthModelAliasConfig,
CLIProxyOAuthModelAliasEntry,
} from '../../config/schemas/cliproxy';
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function normalizeAliasEntry(value: unknown): CLIProxyOAuthModelAliasEntry | null {
if (!isRecord(value) || typeof value.name !== 'string' || typeof value.alias !== 'string') {
return null;
}
const name = value.name.trim();
const alias = value.alias.trim();
if (!name || !alias) {
return null;
}
return {
name,
alias,
...(value.fork === true ? { fork: true } : {}),
};
}
function aliasKey(entry: CLIProxyOAuthModelAliasEntry): string {
return entry.alias;
}
export function parseOAuthModelAliasSection(body: string): CLIProxyOAuthModelAliasConfig {
if (!body.trim()) {
return {};
}
try {
const parsed = yaml.load(`oauth-model-alias:\n${body}`);
if (!isRecord(parsed) || !isRecord(parsed['oauth-model-alias'])) {
return {};
}
const result = Object.create(null) as CLIProxyOAuthModelAliasConfig;
for (const [provider, entries] of Object.entries(parsed['oauth-model-alias'])) {
if (!provider.trim() || !Array.isArray(entries)) {
continue;
}
const normalized = entries
.map(normalizeAliasEntry)
.filter((entry): entry is CLIProxyOAuthModelAliasEntry => entry !== null);
if (normalized.length > 0) {
result[provider] = normalized;
}
}
return result;
} catch {
return {};
}
}
export function mergeOAuthModelAliases(
existing: CLIProxyOAuthModelAliasConfig,
configured?: CLIProxyOAuthModelAliasConfig
): CLIProxyOAuthModelAliasConfig {
const merged = Object.create(null) as CLIProxyOAuthModelAliasConfig;
for (const [sourceIndex, source] of [existing, configured].entries()) {
if (!isRecord(source)) {
continue;
}
const replaceMatching = sourceIndex === 1;
for (const [provider, rawEntries] of Object.entries(source)) {
if (!provider.trim() || !Array.isArray(rawEntries)) {
continue;
}
const entries = merged[provider] ?? [];
const indexByKey = new Map(entries.map((entry, index) => [aliasKey(entry), index]));
for (const rawEntry of rawEntries) {
const entry = normalizeAliasEntry(rawEntry);
if (!entry) {
continue;
}
const key = aliasKey(entry);
const existingIndex = indexByKey.get(key);
if (existingIndex !== undefined) {
if (replaceMatching) {
entries[existingIndex] = entry;
} else if (entry.fork) {
entries[existingIndex] = { ...entries[existingIndex], fork: true };
}
continue;
}
indexByKey.set(key, entries.length);
entries.push(entry);
}
if (entries.length > 0) {
merged[provider] = entries;
}
}
}
return merged;
}
export function serializeOAuthModelAliasBody(config: CLIProxyOAuthModelAliasConfig): string {
if (Object.keys(config).length === 0) {
return '';
}
return yaml
.dump(config, {
indent: 2,
lineWidth: -1,
noRefs: true,
noCompatMode: true,
quotingType: '"',
})
.trimEnd()
.split('\n')
.map((line) => ` ${line}`)
.join('\n');
}
+141
View File
@@ -0,0 +1,141 @@
/** Parse, merge, and serialize user-owned CLIProxy payload rules. */
import * as yaml from 'js-yaml';
import type {
CLIProxyPayloadConfig,
CLIProxyPayloadModelSelector,
CLIProxyPayloadOverrideRule,
} from '../../config/schemas/cliproxy';
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function isValidModelSelector(value: unknown): value is CLIProxyPayloadModelSelector {
if (!isRecord(value) || typeof value.name !== 'string') {
return false;
}
return value.name.trim().length > 0;
}
function cloneOverrideRule(value: unknown): CLIProxyPayloadOverrideRule | null {
if (!isRecord(value) || !Array.isArray(value.models) || !isRecord(value.params)) {
return null;
}
if (value.models.length === 0 || !value.models.every(isValidModelSelector)) {
return null;
}
return structuredClone(value) as CLIProxyPayloadOverrideRule;
}
function canonicalizeForKey(value: unknown): unknown {
if (Array.isArray(value)) {
return value.map(canonicalizeForKey);
}
if (!isRecord(value)) {
return value;
}
return Object.fromEntries(
Object.keys(value)
.sort()
.map((key) => [key, canonicalizeForKey(value[key])])
);
}
function predicateKey(rule: CLIProxyPayloadOverrideRule): string {
const predicate = Object.fromEntries(Object.entries(rule).filter(([key]) => key !== 'params'));
return JSON.stringify(canonicalizeForKey(predicate));
}
export function parsePayloadSection(body: string): CLIProxyPayloadConfig | undefined {
if (!body.trim()) {
return undefined;
}
try {
const parsed = yaml.load(`payload:\n${body}`);
if (!isRecord(parsed) || !isRecord(parsed.payload)) {
return undefined;
}
return parsed.payload as CLIProxyPayloadConfig;
} catch {
return undefined;
}
}
export function mergePayloadConfig(
existing?: CLIProxyPayloadConfig,
configured?: CLIProxyPayloadConfig
): CLIProxyPayloadConfig | undefined {
const existingConfig = isRecord(existing) ? (existing as CLIProxyPayloadConfig) : undefined;
const configuredConfig = isRecord(configured) ? (configured as CLIProxyPayloadConfig) : undefined;
if (!existingConfig && !configuredConfig) {
return undefined;
}
const merged: CLIProxyPayloadConfig = {
...(existingConfig ?? {}),
...(configuredConfig ?? {}),
};
const rules: CLIProxyPayloadOverrideRule[] = [];
const indexByPredicate = new Map<string, number>();
const addRules = (rawRules: unknown, replaceMatching: boolean) => {
if (!Array.isArray(rawRules)) {
return;
}
for (const rawRule of rawRules) {
const rule = cloneOverrideRule(rawRule);
if (!rule) {
continue;
}
const key = predicateKey(rule);
const existingIndex = indexByPredicate.get(key);
if (existingIndex !== undefined) {
if (replaceMatching) {
rules[existingIndex] = rule;
}
continue;
}
indexByPredicate.set(key, rules.length);
rules.push(rule);
}
};
addRules(existingConfig?.override, false);
addRules(configuredConfig?.override, true);
if (rules.length > 0) {
merged.override = rules;
} else {
delete merged.override;
}
return Object.keys(merged).length > 0 ? merged : undefined;
}
export function serializePayloadSection(config?: CLIProxyPayloadConfig): string {
if (!config || Object.keys(config).length === 0) {
return '';
}
return yaml
.dump(
{ payload: config },
{
indent: 2,
lineWidth: -1,
noRefs: true,
noCompatMode: true,
quotingType: '"',
}
)
.trimEnd();
}
+6
View File
@@ -3,6 +3,10 @@
*/
import type { CompositeTierConfig } from '../../config/unified-config-types';
import type {
CLIProxyOAuthModelAliasConfig,
CLIProxyPayloadConfig,
} from '../../config/schemas/cliproxy';
import type { CliproxyRoutingStrategy } from './provider-types';
/** CLIProxy config.yaml structure (minimal) */
@@ -11,6 +15,8 @@ export interface CLIProxyConfig {
'api-keys': string[];
'auth-dir': string;
debug: boolean;
'oauth-model-alias'?: CLIProxyOAuthModelAliasConfig;
payload?: CLIProxyPayloadConfig;
routing?: {
strategy?: CliproxyRoutingStrategy;
'session-affinity'?: boolean;
@@ -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();
+48
View File
@@ -135,6 +135,50 @@ export interface CLIProxyRetryConfig {
max_retry_interval?: number;
}
/** One OAuth model alias exposed by CLIProxy to compatible clients. */
export interface CLIProxyOAuthModelAliasEntry {
/** Upstream model ID used after alias resolution. */
name: string;
/** Client-visible model ID accepted by CLIProxy. */
alias: string;
/** Keep both the upstream name and alias visible when supported upstream. */
fork?: boolean;
}
/** Provider/protocol keyed OAuth model aliases (for example `codex`). */
export type CLIProxyOAuthModelAliasConfig = Record<string, CLIProxyOAuthModelAliasEntry[]>;
/** Model selector for a CLIProxy payload rule. */
export interface CLIProxyPayloadModelSelector {
/** Client-visible model ID to match before OAuth alias resolution. */
name: string;
/** Optional protocol boundary, such as `codex`. */
protocol?: string;
/** Additional CLIProxy selector constraints retained across regeneration. */
[constraint: string]: unknown;
}
/** A scoped CLIProxy payload override rule. */
export interface CLIProxyPayloadOverrideRule {
/** All client model/protocol selectors covered by this rule. */
models: CLIProxyPayloadModelSelector[];
/** Upstream request parameters added when a selector matches. */
params: Record<string, unknown>;
/** Additional CLIProxy match constraints retained across regeneration. */
[constraint: string]: unknown;
}
/**
* User-owned CLIProxy payload configuration.
*
* Unknown supported payload subsections are retained so CCS regeneration does
* not erase options introduced by newer CLIProxy releases.
*/
export interface CLIProxyPayloadConfig {
override?: CLIProxyPayloadOverrideRule[];
[section: string]: unknown;
}
/** Lower bound (inclusive) accepted for CLIProxy retry config fields. */
export const CLIPROXY_RETRY_MIN_VALUE = 0;
@@ -217,4 +261,8 @@ export interface CLIProxyConfig {
pool_routing?: CLIProxyPoolRoutingConfig;
/** Request-retry behavior for transient upstream errors (opt-in, default: disabled) */
retry?: CLIProxyRetryConfig;
/** User-defined OAuth model aliases emitted into managed CLIProxy config. */
oauth_model_alias?: CLIProxyOAuthModelAliasConfig;
/** Scoped payload rules emitted into managed CLIProxy config. */
payload?: CLIProxyPayloadConfig;
}
+7
View File
@@ -29,6 +29,13 @@ export type {
CLIProxyLoggingConfig,
CLIProxySafetyConfig,
CLIProxyRoutingConfig,
CLIProxyRetryConfig,
CLIProxyPoolRoutingConfig,
CLIProxyOAuthModelAliasEntry,
CLIProxyOAuthModelAliasConfig,
CLIProxyPayloadModelSelector,
CLIProxyPayloadOverrideRule,
CLIProxyPayloadConfig,
CLIProxyConfig,
} from './cliproxy';
+2 -1
View File
@@ -19,5 +19,6 @@
* Version 11 = Discord Channels runtime auto-enable preferences
* Version 12 = Official Channels multi-provider support (Telegram, Discord, iMessage)
* Version 13 = Browser automation defaults to safe manual/off exposure
* Version 14 = User-defined CLIProxy OAuth aliases and scoped payload rules
*/
export const UNIFIED_CONFIG_VERSION = 13;
export const UNIFIED_CONFIG_VERSION = 14;