mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 18:15:23 +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:
1 parent
c35de7ec9a
commit
61f46e738d
3 files changed
+68
-50
No files matched your search
@@ -81,55 +81,74 @@ export function buildClaudeEnvironment(config: ProxyChainConfig): Record<string,
|
|||||||
compositeDefaultTier,
|
compositeDefaultTier,
|
||||||
} = config;
|
} = config;
|
||||||
|
|
||||||
// Build base env vars - composite, remote, or local
|
// Build base env vars - check remote mode first
|
||||||
let envVars: NodeJS.ProcessEnv;
|
let envVars: NodeJS.ProcessEnv;
|
||||||
|
|
||||||
if (isComposite && compositeTiers && compositeDefaultTier) {
|
if (useRemoteProxy && remoteConfig) {
|
||||||
// Composite variant: root URL, per-tier models from different providers
|
|
||||||
envVars = getCompositeEnvVars(
|
|
||||||
compositeTiers,
|
|
||||||
compositeDefaultTier,
|
|
||||||
localPort,
|
|
||||||
customSettingsPath
|
|
||||||
);
|
|
||||||
} else if (useRemoteProxy && remoteConfig) {
|
|
||||||
if (httpsTunnel && tunnelPort) {
|
if (httpsTunnel && tunnelPort) {
|
||||||
// HTTPS remote via local tunnel - use HTTP to tunnel
|
// HTTPS remote via local tunnel
|
||||||
envVars = getRemoteEnvVars(
|
if (isComposite && compositeTiers && compositeDefaultTier) {
|
||||||
provider,
|
envVars = getCompositeEnvVars(
|
||||||
{
|
compositeTiers,
|
||||||
host: '127.0.0.1',
|
compositeDefaultTier,
|
||||||
port: tunnelPort,
|
tunnelPort,
|
||||||
protocol: 'http', // Tunnel speaks HTTP locally
|
customSettingsPath
|
||||||
authToken: remoteConfig.authToken,
|
);
|
||||||
},
|
} else {
|
||||||
customSettingsPath
|
envVars = getRemoteEnvVars(
|
||||||
);
|
provider,
|
||||||
|
{
|
||||||
|
host: '127.0.0.1',
|
||||||
|
port: tunnelPort,
|
||||||
|
protocol: 'http', // Tunnel speaks HTTP locally
|
||||||
|
authToken: remoteConfig.authToken,
|
||||||
|
},
|
||||||
|
customSettingsPath
|
||||||
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// HTTP remote - direct connection
|
// HTTP remote - direct connection
|
||||||
envVars = getRemoteEnvVars(
|
if (isComposite && compositeTiers && compositeDefaultTier) {
|
||||||
provider,
|
envVars = getCompositeEnvVars(
|
||||||
{
|
compositeTiers,
|
||||||
host: remoteConfig.host,
|
compositeDefaultTier,
|
||||||
port: remoteConfig.port,
|
remoteConfig.port,
|
||||||
protocol: remoteConfig.protocol,
|
customSettingsPath
|
||||||
authToken: remoteConfig.authToken,
|
);
|
||||||
},
|
} else {
|
||||||
customSettingsPath
|
envVars = getRemoteEnvVars(
|
||||||
);
|
provider,
|
||||||
|
{
|
||||||
|
host: remoteConfig.host,
|
||||||
|
port: remoteConfig.port,
|
||||||
|
protocol: remoteConfig.protocol,
|
||||||
|
authToken: remoteConfig.authToken,
|
||||||
|
},
|
||||||
|
customSettingsPath
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Local proxy mode
|
// Local proxy mode
|
||||||
const remoteRewriteConfig = remoteConfig
|
if (isComposite && compositeTiers && compositeDefaultTier) {
|
||||||
? {
|
envVars = getCompositeEnvVars(
|
||||||
host: remoteConfig.host,
|
compositeTiers,
|
||||||
port: remoteConfig.port,
|
compositeDefaultTier,
|
||||||
protocol: remoteConfig.protocol,
|
localPort,
|
||||||
authToken: remoteConfig.authToken,
|
customSettingsPath
|
||||||
}
|
);
|
||||||
: undefined;
|
} 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
|
// 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` };
|
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 = {
|
const mergedTiers = {
|
||||||
opus: updates.tiers?.opus ?? existing.tiers.opus,
|
opus: { ...existing.tiers.opus, ...updates.tiers?.opus },
|
||||||
sonnet: updates.tiers?.sonnet ?? existing.tiers.sonnet,
|
sonnet: { ...existing.tiers.sonnet, ...updates.tiers?.sonnet },
|
||||||
haiku: updates.tiers?.haiku ?? existing.tiers.haiku,
|
haiku: { ...existing.tiers.haiku, ...updates.tiers?.haiku },
|
||||||
};
|
};
|
||||||
|
|
||||||
// Validate all tier providers against backend compatibility
|
// Validate all tier providers against backend compatibility
|
||||||
|
|||||||
@@ -31,13 +31,12 @@ function validateCompositeTiers(
|
|||||||
if (defaultTier && !VALID_TIERS.includes(defaultTier as (typeof VALID_TIERS)[number])) {
|
if (defaultTier && !VALID_TIERS.includes(defaultTier as (typeof VALID_TIERS)[number])) {
|
||||||
return `Invalid default_tier '${defaultTier}': must be one of ${VALID_TIERS.join(', ')}`;
|
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) {
|
for (const tier of VALID_TIERS) {
|
||||||
if (
|
// Skip validation for tiers not present in the request (partial updates allowed)
|
||||||
!tiers[tier] ||
|
if (tiers[tier] === undefined) continue;
|
||||||
typeof tiers[tier].provider !== 'string' ||
|
|
||||||
typeof tiers[tier].model !== 'string'
|
if (typeof tiers[tier].provider !== 'string' || typeof tiers[tier].model !== 'string') {
|
||||||
) {
|
|
||||||
return `Invalid tier config for '${tier}': requires 'provider' and 'model' strings`;
|
return `Invalid tier config for '${tier}': requires 'provider' and 'model' strings`;
|
||||||
}
|
}
|
||||||
// Validate non-empty model string (whitespace-only is also invalid)
|
// Validate non-empty model string (whitespace-only is also invalid)
|
||||||
|
|||||||
Reference in new issue
Block a user