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
2026-02-12 11:22:43 +07:00
parent c35de7ec9a
commit 61f46e738d
3 changed files with 68 additions and 50 deletions
+59 -40
View File
@@ -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
+4 -4
View File
@@ -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
+5 -6
View File
@@ -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)