From 0a8adb22fcdffc993b06ed577217ccb8e55253c9 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Thu, 12 Feb 2026 15:18:33 +0700 Subject: [PATCH] fix(cliproxy): harden composite variant routing and validation --- src/cliproxy/executor/index.ts | 7 +- .../services/variant-config-adapter.ts | 84 +++++++++++++------ src/config/unified-config-loader.ts | 16 +++- .../composite-variant-service.test.ts | 41 +++++++++ 4 files changed, 115 insertions(+), 33 deletions(-) diff --git a/src/cliproxy/executor/index.ts b/src/cliproxy/executor/index.ts index 0c6630e4..26abbf81 100644 --- a/src/cliproxy/executor/index.ts +++ b/src/cliproxy/executor/index.ts @@ -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 { diff --git a/src/cliproxy/services/variant-config-adapter.ts b/src/cliproxy/services/variant-config-adapter.ts index 3f6a869c..1b1b3f15 100644 --- a/src/cliproxy/services/variant-config-adapter.ts +++ b/src/cliproxy/services/variant-config-adapter.ts @@ -98,33 +98,65 @@ export function listVariantsFromConfig(): Record { const unifiedConfig = loadOrCreateUnifiedConfig(); const variants = unifiedConfig.cliproxy?.variants || {}; const result: Record = {}; - 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; diff --git a/src/config/unified-config-loader.ts b/src/config/unified-config-loader.ts index 9e1457fc..0fecb6ff 100644 --- a/src/config/unified-config-loader.ts +++ b/src/config/unified-config-loader.ts @@ -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(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)) { + 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` ); } } diff --git a/tests/unit/cliproxy/composite-variant-service.test.ts b/tests/unit/cliproxy/composite-variant-service.test.ts index a5e91980..93745c25 100644 --- a/tests/unit/cliproxy/composite-variant-service.test.ts +++ b/tests/unit/cliproxy/composite-variant-service.test.ts @@ -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'); + }); });