mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 14:16:43 +00:00
fix(cliproxy): address 3 functional regressions in composite variants
P1 (Critical): Remote composite execution now correctly uses remote endpoint - Reorder condition to check useRemoteProxy before isComposite - Composite variants now work with remote CLIProxy instances P2a (Medium): Tier deep-merge preserves optional fields during updates - Use spread merge instead of ?? to preserve fallback/thinking/account P2b (Medium): API accepts partial tier updates in PUT endpoint - Skip validation for tiers not present in request - Aligns with updateCompositeVariant's merge behavior
This commit is contained in:
@@ -81,55 +81,74 @@ export function buildClaudeEnvironment(config: ProxyChainConfig): Record<string,
|
||||
compositeDefaultTier,
|
||||
} = config;
|
||||
|
||||
// Build base env vars - composite, remote, or local
|
||||
// Build base env vars - check remote mode first
|
||||
let envVars: NodeJS.ProcessEnv;
|
||||
|
||||
if (isComposite && compositeTiers && compositeDefaultTier) {
|
||||
// Composite variant: root URL, per-tier models from different providers
|
||||
envVars = getCompositeEnvVars(
|
||||
compositeTiers,
|
||||
compositeDefaultTier,
|
||||
localPort,
|
||||
customSettingsPath
|
||||
);
|
||||
} else if (useRemoteProxy && remoteConfig) {
|
||||
if (useRemoteProxy && remoteConfig) {
|
||||
if (httpsTunnel && tunnelPort) {
|
||||
// HTTPS remote via local tunnel - use HTTP to tunnel
|
||||
envVars = getRemoteEnvVars(
|
||||
provider,
|
||||
{
|
||||
host: '127.0.0.1',
|
||||
port: tunnelPort,
|
||||
protocol: 'http', // Tunnel speaks HTTP locally
|
||||
authToken: remoteConfig.authToken,
|
||||
},
|
||||
customSettingsPath
|
||||
);
|
||||
// HTTPS remote via local tunnel
|
||||
if (isComposite && compositeTiers && compositeDefaultTier) {
|
||||
envVars = getCompositeEnvVars(
|
||||
compositeTiers,
|
||||
compositeDefaultTier,
|
||||
tunnelPort,
|
||||
customSettingsPath
|
||||
);
|
||||
} else {
|
||||
envVars = getRemoteEnvVars(
|
||||
provider,
|
||||
{
|
||||
host: '127.0.0.1',
|
||||
port: tunnelPort,
|
||||
protocol: 'http', // Tunnel speaks HTTP locally
|
||||
authToken: remoteConfig.authToken,
|
||||
},
|
||||
customSettingsPath
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// HTTP remote - direct connection
|
||||
envVars = getRemoteEnvVars(
|
||||
provider,
|
||||
{
|
||||
host: remoteConfig.host,
|
||||
port: remoteConfig.port,
|
||||
protocol: remoteConfig.protocol,
|
||||
authToken: remoteConfig.authToken,
|
||||
},
|
||||
customSettingsPath
|
||||
);
|
||||
if (isComposite && compositeTiers && compositeDefaultTier) {
|
||||
envVars = getCompositeEnvVars(
|
||||
compositeTiers,
|
||||
compositeDefaultTier,
|
||||
remoteConfig.port,
|
||||
customSettingsPath
|
||||
);
|
||||
} else {
|
||||
envVars = getRemoteEnvVars(
|
||||
provider,
|
||||
{
|
||||
host: remoteConfig.host,
|
||||
port: remoteConfig.port,
|
||||
protocol: remoteConfig.protocol,
|
||||
authToken: remoteConfig.authToken,
|
||||
},
|
||||
customSettingsPath
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Local proxy mode
|
||||
const remoteRewriteConfig = remoteConfig
|
||||
? {
|
||||
host: remoteConfig.host,
|
||||
port: remoteConfig.port,
|
||||
protocol: remoteConfig.protocol,
|
||||
authToken: remoteConfig.authToken,
|
||||
}
|
||||
: undefined;
|
||||
if (isComposite && compositeTiers && compositeDefaultTier) {
|
||||
envVars = getCompositeEnvVars(
|
||||
compositeTiers,
|
||||
compositeDefaultTier,
|
||||
localPort,
|
||||
customSettingsPath
|
||||
);
|
||||
} else {
|
||||
const remoteRewriteConfig = remoteConfig
|
||||
? {
|
||||
host: remoteConfig.host,
|
||||
port: remoteConfig.port,
|
||||
protocol: remoteConfig.protocol,
|
||||
authToken: remoteConfig.authToken,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
envVars = getEffectiveEnvVars(provider, localPort, customSettingsPath, remoteRewriteConfig);
|
||||
envVars = getEffectiveEnvVars(provider, localPort, customSettingsPath, remoteRewriteConfig);
|
||||
}
|
||||
}
|
||||
|
||||
// Extract per-tier thinking from composite config
|
||||
|
||||
@@ -380,11 +380,11 @@ export function updateCompositeVariant(
|
||||
return { success: false, error: `Variant '${name}' is not a composite variant` };
|
||||
}
|
||||
|
||||
// Merge tiers (keep unchanged tiers from existing)
|
||||
// Deep merge tiers to preserve optional fields (fallback, thinking, account)
|
||||
const mergedTiers = {
|
||||
opus: updates.tiers?.opus ?? existing.tiers.opus,
|
||||
sonnet: updates.tiers?.sonnet ?? existing.tiers.sonnet,
|
||||
haiku: updates.tiers?.haiku ?? existing.tiers.haiku,
|
||||
opus: { ...existing.tiers.opus, ...updates.tiers?.opus },
|
||||
sonnet: { ...existing.tiers.sonnet, ...updates.tiers?.sonnet },
|
||||
haiku: { ...existing.tiers.haiku, ...updates.tiers?.haiku },
|
||||
};
|
||||
|
||||
// Validate all tier providers against backend compatibility
|
||||
|
||||
@@ -31,13 +31,12 @@ function validateCompositeTiers(
|
||||
if (defaultTier && !VALID_TIERS.includes(defaultTier as (typeof VALID_TIERS)[number])) {
|
||||
return `Invalid default_tier '${defaultTier}': must be one of ${VALID_TIERS.join(', ')}`;
|
||||
}
|
||||
// Validate each tier
|
||||
// Validate each tier (partial updates allowed - only validate tiers present in request)
|
||||
for (const tier of VALID_TIERS) {
|
||||
if (
|
||||
!tiers[tier] ||
|
||||
typeof tiers[tier].provider !== 'string' ||
|
||||
typeof tiers[tier].model !== 'string'
|
||||
) {
|
||||
// Skip validation for tiers not present in the request (partial updates allowed)
|
||||
if (tiers[tier] === undefined) continue;
|
||||
|
||||
if (typeof tiers[tier].provider !== 'string' || typeof tiers[tier].model !== 'string') {
|
||||
return `Invalid tier config for '${tier}': requires 'provider' and 'model' strings`;
|
||||
}
|
||||
// Validate non-empty model string (whitespace-only is also invalid)
|
||||
|
||||
Reference in New Issue
Block a user