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:
Tam Nhu Tran committed 2026-02-12 11:22:43 +07:00
1 parent c35de7ec9a
commit 61f46e738d
3 files changed
+68 -50

No files matched your search

+59 -40
View File
@@ -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
+4 -4
View File
@@ -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
+5 -6
View File
@@ -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)