mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-17 00:16:46 +00:00
fix(cliproxy): harden composite variant routing and validation
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user