fix(cliproxy): harden composite variant routing and validation

This commit is contained in:
Tam Nhu Tran
2026-02-12 15:18:33 +07:00
parent e914fe9778
commit 0a8adb22fc
4 changed files with 115 additions and 33 deletions
+4 -3
View File
@@ -608,7 +608,7 @@ export async function execClaudeWithCLIProxy(
}
// 4. First-run model configuration
if (supportsModelConfig(provider) && !skipLocalAuth) {
if (!cfg.isComposite && supportsModelConfig(provider) && !skipLocalAuth) {
await configureProviderModel(provider, false, cfg.customSettingsPath);
}
@@ -770,11 +770,12 @@ export async function execClaudeWithCLIProxy(
? `http://127.0.0.1:${toolSanitizationPort}`
: initialEnvVars.ANTHROPIC_BASE_URL;
// 10. Setup Codex reasoning proxy (Codex only)
// 10. Setup Codex reasoning proxy (single-provider Codex only)
let codexReasoningProxy: CodexReasoningProxy | null = null;
let codexReasoningPort: number | null = null;
if (provider === 'codex') {
// Composite variants require root model-routed endpoints, never provider-pinned codex endpoints.
if (provider === 'codex' && !cfg.isComposite) {
if (!postSanitizationBaseUrl) {
log('ANTHROPIC_BASE_URL not set for Codex, reasoning proxy disabled');
} else {
+58 -26
View File
@@ -98,33 +98,65 @@ export function listVariantsFromConfig(): Record<string, VariantConfig> {
const unifiedConfig = loadOrCreateUnifiedConfig();
const variants = unifiedConfig.cliproxy?.variants || {};
const result: Record<string, VariantConfig> = {};
for (const name of Object.keys(variants)) {
const v = variants[name];
if ('type' in v && v.type === 'composite') {
const composite = v as CompositeVariantConfig;
const tiers = composite.tiers;
const hasFallback = !!(
tiers.opus.fallback ||
tiers.sonnet.fallback ||
tiers.haiku.fallback
for (const [name, variantConfig] of Object.entries(variants)) {
try {
if ('type' in variantConfig && variantConfig.type === 'composite') {
const composite = variantConfig as CompositeVariantConfig;
const tiers = composite.tiers as Partial<{
opus: CompositeTierConfig;
sonnet: CompositeTierConfig;
haiku: CompositeTierConfig;
}> | null;
if (!tiers || !tiers.opus || !tiers.sonnet || !tiers.haiku || !composite.default_tier) {
console.warn(
`[!] Skipping malformed composite variant '${name}': missing required tier configuration`
);
continue;
}
const defaultTierConfig = tiers[composite.default_tier];
if (!defaultTierConfig) {
console.warn(
`[!] Skipping malformed composite variant '${name}': missing default tier '${composite.default_tier}'`
);
continue;
}
const normalizedTiers = {
opus: tiers.opus,
sonnet: tiers.sonnet,
haiku: tiers.haiku,
};
const hasFallback = !!(
normalizedTiers.opus.fallback ||
normalizedTiers.sonnet.fallback ||
normalizedTiers.haiku.fallback
);
result[name] = {
provider: defaultTierConfig.provider,
settings: composite.settings,
port: composite.port,
type: 'composite',
default_tier: composite.default_tier,
tiers: normalizedTiers,
hasFallback,
};
} else {
const single = variantConfig as CLIProxyVariantConfig;
result[name] = {
provider: single.provider,
settings: single.settings,
account: single.account,
port: single.port,
};
}
} catch (error) {
console.warn(
`[!] Skipping malformed variant '${name}': ${(error as Error).message || 'invalid config'}`
);
result[name] = {
provider: tiers[composite.default_tier].provider,
settings: composite.settings,
port: composite.port,
type: 'composite',
default_tier: composite.default_tier,
tiers,
hasFallback,
};
} else {
const single = v as CLIProxyVariantConfig;
result[name] = {
provider: single.provider,
settings: single.settings,
account: single.account,
port: single.port,
};
}
}
return result;
+12 -4
View File
@@ -210,7 +210,7 @@ export function loadUnifiedConfig(): UnifiedConfig | null {
* Warns about invalid providers in composite variant configurations.
*/
function validateCompositeVariants(config: UnifiedConfig): void {
const validProviders = new Set(CLIPROXY_SUPPORTED_PROVIDERS);
const validProviders = new Set<string>(CLIPROXY_SUPPORTED_PROVIDERS);
const variants = config.cliproxy?.variants;
if (!variants) return;
@@ -222,10 +222,18 @@ function validateCompositeVariants(config: UnifiedConfig): void {
continue;
}
for (const [tier, tierConfig] of Object.entries(variant.tiers)) {
if (!validProviders.has(tierConfig.provider)) {
for (const [tier, tierConfig] of Object.entries(variant.tiers as Record<string, unknown>)) {
if (!tierConfig || typeof tierConfig !== 'object') {
console.warn(
`[!] Variant '${name}': invalid provider '${tierConfig.provider}' in ${tier} tier`
`[!] Variant '${name}': invalid config in ${tier} tier (expected object, got ${tierConfig === null ? 'null' : typeof tierConfig})`
);
continue;
}
const provider = (tierConfig as { provider?: unknown }).provider;
if (typeof provider !== 'string' || !validProviders.has(provider)) {
console.warn(
`[!] Variant '${name}': invalid provider '${String(provider)}' in ${tier} tier`
);
}
}
@@ -435,4 +435,45 @@ cliproxy:
expect(variants.test).toBeDefined();
expect(variants.test.hasFallback).toBe(false);
});
it('should skip malformed composite variant and keep valid variants', () => {
const configPath = path.join(tmpDir, 'config.yaml');
const yamlContent = `version: 2
accounts: {}
profiles: {}
preferences:
theme: system
telemetry: false
auto_update: true
cliproxy:
oauth_accounts: {}
providers:
- gemini
- codex
- agy
variants:
bad:
type: composite
default_tier: sonnet
tiers:
opus:
provider: agy
model: claude-opus-4-6-thinking
sonnet:
provider: agy
model: claude-sonnet-4-5-thinking
settings: cliproxy/bad.settings.json
port: 8318
good:
provider: gemini
settings: cliproxy/good.settings.json
port: 8319
`;
fs.writeFileSync(configPath, yamlContent, 'utf-8');
const variants = listVariantsFromConfig();
expect(variants.bad).toBeUndefined();
expect(variants.good).toBeDefined();
expect(variants.good.provider).toBe('gemini');
});
});