fix(cliproxy): address 4 functional regressions in composite variants

- P1: Remote composite env now uses remote endpoint/auth
  Add CompositeRemoteConfig parameter to getCompositeEnvVars()
  Pass remote config when useRemoteProxy is true

- P2: Tier thinking uses tier-specific provider
  Extract tier provider from compositeTiers for supportsThinking()
  Fix mixed-provider composite thinking config

- P2: Force variant removal checks result
  Fail fast if removeVariant fails in --force mode

- P2: Guard composite validation against missing tiers
  Add null check before Object.entries(variant.tiers)
This commit is contained in:
Tam Nhu Tran
2026-02-12 12:08:20 +07:00
parent a99180fa22
commit 27d22e836f
5 changed files with 80 additions and 12 deletions
+28 -4
View File
@@ -415,6 +415,14 @@ export function getRemoteEnvVars(
return env;
}
/** Remote config for composite variant (passed from env-resolver) */
export interface CompositeRemoteConfig {
host: string;
port: number;
protocol: 'http' | 'https';
authToken?: string;
}
/**
* Get environment variables for composite variant.
* Uses root URL (no /api/provider/ path) for model-based routing.
@@ -422,14 +430,16 @@ export function getRemoteEnvVars(
*
* @param tiers Per-tier provider+model mappings
* @param defaultTier Which tier ANTHROPIC_MODEL equals
* @param port Local CLIProxy port
* @param port Local CLIProxy port (ignored if remoteConfig provided)
* @param customSettingsPath Optional path to user's custom settings file
* @param remoteConfig Optional remote proxy config (overrides localhost URL/auth)
*/
export function getCompositeEnvVars(
tiers: { opus: CompositeTierConfig; sonnet: CompositeTierConfig; haiku: CompositeTierConfig },
defaultTier: 'opus' | 'sonnet' | 'haiku',
port: number = CLIPROXY_DEFAULT_PORT,
customSettingsPath?: string
customSettingsPath?: string,
remoteConfig?: CompositeRemoteConfig
): Record<string, string> {
const globalEnv = getGlobalEnvVars();
@@ -473,12 +483,26 @@ export function getCompositeEnvVars(
throw new Error(`Missing model for default tier '${defaultTier}'`);
}
// Determine base URL and auth token based on remote vs local mode
const baseUrl = remoteConfig
? (() => {
const normalizedProtocol = normalizeProtocol(remoteConfig.protocol);
const effectivePort =
validateRemotePort(remoteConfig.port) ?? getRemoteDefaultPort(normalizedProtocol);
const standardWebPort = normalizedProtocol === 'https' ? 443 : 80;
const portSuffix = effectivePort === standardWebPort ? '' : `:${effectivePort}`;
return `${normalizedProtocol}://${remoteConfig.host}${portSuffix}`;
})()
: `http://127.0.0.1:${validPort}`;
const authToken = remoteConfig?.authToken ?? getEffectiveApiKey();
const env: Record<string, string> = {
...globalEnv,
...additionalEnvVars,
// Root URL — CLIProxyAPI routes based on model name in request body
ANTHROPIC_BASE_URL: `http://127.0.0.1:${validPort}`,
ANTHROPIC_AUTH_TOKEN: getEffectiveApiKey(),
ANTHROPIC_BASE_URL: baseUrl,
ANTHROPIC_AUTH_TOKEN: authToken,
ANTHROPIC_MODEL: defaultModel,
};
+26 -4
View File
@@ -67,14 +67,22 @@ export function getThinkingValueForTier(
return thinkingConfig.tier_defaults?.[tier] ?? DEFAULT_THINKING_TIER_DEFAULTS[tier];
}
/**
* Composite tier config for provider lookup (subset of full CompositeTierConfig)
*/
interface CompositeTierProvider {
provider?: CLIProxyProvider;
}
/**
* Apply thinking configuration to env vars.
* Modifies ANTHROPIC_MODEL and tier models with thinking suffixes.
*
* @param envVars - Environment variables to modify
* @param provider - CLIProxy provider
* @param provider - CLIProxy provider (default provider for base model)
* @param thinkingOverride - Optional CLI override (takes priority over config)
* @param compositeTierThinking - Optional per-tier thinking overrides for composite variants
* @param compositeTiers - Optional per-tier provider config for composite variants
* @returns Modified env vars with thinking suffixes applied
*/
export function applyThinkingConfig(
@@ -85,6 +93,11 @@ export function applyThinkingConfig(
opus?: string;
sonnet?: string;
haiku?: string;
},
compositeTiers?: {
opus?: CompositeTierProvider;
sonnet?: CompositeTierProvider;
haiku?: CompositeTierProvider;
}
): NodeJS.ProcessEnv {
const thinkingConfig = getThinkingConfig();
@@ -158,7 +171,7 @@ export function applyThinkingConfig(
for (const tierVar of tierModels) {
const model = result[tierVar];
if (model && supportsThinking(provider, model)) {
if (model) {
// Get tier-specific thinking value
const tier = tierVar.includes('OPUS')
? 'opus'
@@ -166,6 +179,15 @@ export function applyThinkingConfig(
? 'sonnet'
: 'haiku';
// P2 FIX: Use tier-specific provider from compositeTiers for mixed-provider composites
// Falls back to the default provider if not a composite or tier not specified
const tierProvider = compositeTiers?.[tier]?.provider ?? provider;
// Check if this tier's model supports thinking (using tier-specific provider)
if (!supportsThinking(tierProvider, model)) {
continue;
}
// Priority chain: CLI --thinking > per-tier config > global config > defaults
let tierThinkingValue: string | number;
if (thinkingOverride !== undefined) {
@@ -177,8 +199,8 @@ export function applyThinkingConfig(
// Per-tier config from composite variant
tierThinkingValue = perTierValue;
} else {
// Global config or defaults
tierThinkingValue = getThinkingValueForTier(tier, provider, thinkingConfig);
// Global config or defaults (use tier-specific provider for provider overrides)
tierThinkingValue = getThinkingValueForTier(tier, tierProvider, thinkingConfig);
}
}
+15 -3
View File
@@ -92,7 +92,13 @@ export function buildClaudeEnvironment(config: ProxyChainConfig): Record<string,
compositeTiers,
compositeDefaultTier,
tunnelPort,
customSettingsPath
customSettingsPath,
{
host: '127.0.0.1',
port: tunnelPort,
protocol: 'http',
authToken: remoteConfig.authToken,
}
);
} else {
envVars = getRemoteEnvVars(
@@ -113,7 +119,13 @@ export function buildClaudeEnvironment(config: ProxyChainConfig): Record<string,
compositeTiers,
compositeDefaultTier,
remoteConfig.port,
customSettingsPath
customSettingsPath,
{
host: remoteConfig.host,
port: remoteConfig.port,
protocol: remoteConfig.protocol,
authToken: remoteConfig.authToken,
}
);
} else {
envVars = getRemoteEnvVars(
@@ -164,7 +176,7 @@ export function buildClaudeEnvironment(config: ProxyChainConfig): Record<string,
}
// Apply thinking configuration to model (auto tier-based or manual override)
applyThinkingConfig(envVars, provider, thinkingOverride, compositeTierThinking);
applyThinkingConfig(envVars, provider, thinkingOverride, compositeTierThinking, compositeTiers);
// Apply extended context suffix for 1M token context window
// Auto-enabled for Gemini, opt-in for Claude (--1m flag)
+5 -1
View File
@@ -170,7 +170,11 @@ export async function handleCreate(
// Clean up old variant if --force is set
if (variantExists(name) && parsedArgs.force) {
removeVariant(name);
const removeResult = removeVariant(name);
if (!removeResult.success) {
console.log(fail(`Cannot overwrite variant '${name}': ${removeResult.error}`));
process.exit(1);
}
}
// Composite mode: select provider+model per tier
+6
View File
@@ -216,6 +216,12 @@ function validateCompositeVariants(config: UnifiedConfig): void {
for (const [name, variant] of Object.entries(variants)) {
if ('type' in variant && variant.type === 'composite') {
// Guard against malformed composite variants
if (!variant.tiers || typeof variant.tiers !== 'object') {
console.warn(`[!] Composite variant '${name}' missing tiers object, skipping validation`);
continue;
}
for (const [tier, tierConfig] of Object.entries(variant.tiers)) {
if (!validProviders.has(tierConfig.provider)) {
console.warn(