mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-03 18:16:29 +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
|
// 4. First-run model configuration
|
||||||
if (supportsModelConfig(provider) && !skipLocalAuth) {
|
if (!cfg.isComposite && supportsModelConfig(provider) && !skipLocalAuth) {
|
||||||
await configureProviderModel(provider, false, cfg.customSettingsPath);
|
await configureProviderModel(provider, false, cfg.customSettingsPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -770,11 +770,12 @@ export async function execClaudeWithCLIProxy(
|
|||||||
? `http://127.0.0.1:${toolSanitizationPort}`
|
? `http://127.0.0.1:${toolSanitizationPort}`
|
||||||
: initialEnvVars.ANTHROPIC_BASE_URL;
|
: 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 codexReasoningProxy: CodexReasoningProxy | null = null;
|
||||||
let codexReasoningPort: number | 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) {
|
if (!postSanitizationBaseUrl) {
|
||||||
log('ANTHROPIC_BASE_URL not set for Codex, reasoning proxy disabled');
|
log('ANTHROPIC_BASE_URL not set for Codex, reasoning proxy disabled');
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -98,33 +98,65 @@ export function listVariantsFromConfig(): Record<string, VariantConfig> {
|
|||||||
const unifiedConfig = loadOrCreateUnifiedConfig();
|
const unifiedConfig = loadOrCreateUnifiedConfig();
|
||||||
const variants = unifiedConfig.cliproxy?.variants || {};
|
const variants = unifiedConfig.cliproxy?.variants || {};
|
||||||
const result: Record<string, VariantConfig> = {};
|
const result: Record<string, VariantConfig> = {};
|
||||||
for (const name of Object.keys(variants)) {
|
for (const [name, variantConfig] of Object.entries(variants)) {
|
||||||
const v = variants[name];
|
try {
|
||||||
if ('type' in v && v.type === 'composite') {
|
if ('type' in variantConfig && variantConfig.type === 'composite') {
|
||||||
const composite = v as CompositeVariantConfig;
|
const composite = variantConfig as CompositeVariantConfig;
|
||||||
const tiers = composite.tiers;
|
const tiers = composite.tiers as Partial<{
|
||||||
const hasFallback = !!(
|
opus: CompositeTierConfig;
|
||||||
tiers.opus.fallback ||
|
sonnet: CompositeTierConfig;
|
||||||
tiers.sonnet.fallback ||
|
haiku: CompositeTierConfig;
|
||||||
tiers.haiku.fallback
|
}> | 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;
|
return result;
|
||||||
|
|||||||
@@ -210,7 +210,7 @@ export function loadUnifiedConfig(): UnifiedConfig | null {
|
|||||||
* Warns about invalid providers in composite variant configurations.
|
* Warns about invalid providers in composite variant configurations.
|
||||||
*/
|
*/
|
||||||
function validateCompositeVariants(config: UnifiedConfig): void {
|
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;
|
const variants = config.cliproxy?.variants;
|
||||||
if (!variants) return;
|
if (!variants) return;
|
||||||
|
|
||||||
@@ -222,10 +222,18 @@ function validateCompositeVariants(config: UnifiedConfig): void {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [tier, tierConfig] of Object.entries(variant.tiers)) {
|
for (const [tier, tierConfig] of Object.entries(variant.tiers as Record<string, unknown>)) {
|
||||||
if (!validProviders.has(tierConfig.provider)) {
|
if (!tierConfig || typeof tierConfig !== 'object') {
|
||||||
console.warn(
|
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).toBeDefined();
|
||||||
expect(variants.test.hasFallback).toBe(false);
|
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