From 061cd9679fc56a8a0711877f57eebcd998df10b6 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Sun, 29 Mar 2026 13:30:53 -0400 Subject: [PATCH] fix(cliproxy): prune stale control panel alias clusters - prune stale generated Gemini preview clusters during config regeneration - preserve manual preview overrides on built-in Gemini alias names - add regression coverage for stale cluster cleanup and manual pins --- src/cliproxy/config/generator.ts | 126 +++++++++++++++++- tests/unit/cliproxy/config-generator.test.js | 129 +++++++++++++++++++ 2 files changed, 252 insertions(+), 3 deletions(-) diff --git a/src/cliproxy/config/generator.ts b/src/cliproxy/config/generator.ts index 61b565a5..cae36da7 100644 --- a/src/cliproxy/config/generator.ts +++ b/src/cliproxy/config/generator.ts @@ -36,8 +36,9 @@ export const CCS_CONTROL_PANEL_SECRET = 'ccs'; * v12: Removed denylisted Antigravity Claude 4.5 aliases * v13: Removed aggressive Gemini alias expansion to reduce model list noise in Control Panel * v14: Added Gemini 3.1 Flash Antigravity aliases for upcoming rollout compatibility + * v15: Prune stale generated Antigravity Gemini preview aliases during regeneration */ -export const CLIPROXY_CONFIG_VERSION = 14; +export const CLIPROXY_CONFIG_VERSION = 15; interface OAuthModelAliasEntry { name: string; @@ -69,6 +70,12 @@ const DEFAULT_ANTIGRAVITY_ALIASES: OAuthModelAliasEntry[] = [ { name: 'claude-opus-4-6-thinking', alias: 'claude-opus-4-6-thinking', fork: true }, ]; +const BUILT_IN_GEMINI_ALIAS_NAMES = new Set( + DEFAULT_ANTIGRAVITY_ALIASES.filter((entry) => entry.alias.startsWith('gemini-3')).map( + (entry) => entry.name + ) +); + /** * Get provider configuration * Model mappings are loaded from config/base-{provider}.settings.json @@ -148,6 +155,10 @@ function addAliasEntry( entries.push(normalized); } +function buildAntigravityAliasKey(entry: Pick): string { + return `${sanitizeYamlScalar(entry.name)}\u0000${normalizeAntigravityAlias(entry.alias)}`; +} + function parseExistingAntigravityAliases(existingAliases: string): OAuthModelAliasEntry[] { const entries: OAuthModelAliasEntry[] = []; const lines = existingAliases.replace(/\r\n/g, '\n').split('\n'); @@ -267,6 +278,113 @@ function getCompatibilityAliases(entries: OAuthModelAliasEntry[]): OAuthModelAli return compatibilityAliases; } +function buildGeneratedAntigravityAliases(): OAuthModelAliasEntry[] { + const generatedAliases: OAuthModelAliasEntry[] = []; + const generatedAliasIndex = new Map(); + + for (const alias of DEFAULT_ANTIGRAVITY_ALIASES) { + addAliasEntry(generatedAliases, generatedAliasIndex, alias); + } + + for (const alias of getCompatibilityAliases(generatedAliases)) { + addAliasEntry(generatedAliases, generatedAliasIndex, alias); + } + + return generatedAliases; +} + +const GENERATED_ANTIGRAVITY_ALIASES = buildGeneratedAntigravityAliases(); +const GENERATED_ANTIGRAVITY_ALIAS_MAP = new Map( + GENERATED_ANTIGRAVITY_ALIASES.map((entry) => [buildAntigravityAliasKey(entry), entry] as const) +); + +function serializeAntigravityAliases(entries: OAuthModelAliasEntry[]): string { + if (entries.length === 0) { + return ''; + } + + const lines = [' antigravity:']; + for (const entry of entries) { + lines.push(` - name: ${entry.name}`); + lines.push(` alias: ${entry.alias}`); + if (entry.fork) { + lines.push(' fork: true'); + } + } + + return lines.join('\n'); +} + +function getLegacyGeneratedGeminiPreviewClusterKey(entry: OAuthModelAliasEntry): string | null { + if (!BUILT_IN_GEMINI_ALIAS_NAMES.has(entry.name)) { + return null; + } + + const normalizedAlias = normalizeAntigravityAlias(entry.alias).toLowerCase(); + if (!normalizedAlias.startsWith('gemini-3') || !normalizedAlias.includes('-preview')) { + return null; + } + + const aliasWithoutCustomtools = normalizedAlias.endsWith('-customtools') + ? normalizedAlias.slice(0, -'-customtools'.length) + : normalizedAlias; + const clusterAlias = + toDottedGeminiVersionAlias(aliasWithoutCustomtools) ?? aliasWithoutCustomtools; + + return `${sanitizeYamlScalar(entry.name)}\u0000${clusterAlias}`; +} + +function extractPreservedAntigravityAliases(existingAliases: string): string { + if (!existingAliases.trim()) { + return ''; + } + + const parsedEntries = parseExistingAntigravityAliases(existingAliases); + const legacyGeneratedGeminiClusterCounts = new Map(); + + for (const entry of parsedEntries) { + if (entry.fork) { + continue; + } + + const generatedEntry = GENERATED_ANTIGRAVITY_ALIAS_MAP.get(buildAntigravityAliasKey(entry)); + if (generatedEntry) { + continue; + } + + const clusterKey = getLegacyGeneratedGeminiPreviewClusterKey(entry); + if (!clusterKey) { + continue; + } + + legacyGeneratedGeminiClusterCounts.set( + clusterKey, + (legacyGeneratedGeminiClusterCounts.get(clusterKey) ?? 0) + 1 + ); + } + + const preservedEntries = parsedEntries.filter((entry) => { + const generatedEntry = GENERATED_ANTIGRAVITY_ALIAS_MAP.get(buildAntigravityAliasKey(entry)); + + if (generatedEntry) { + return Boolean(entry.fork) && !generatedEntry.fork; + } + + if (entry.fork) { + return true; + } + + const clusterKey = getLegacyGeneratedGeminiPreviewClusterKey(entry); + if (!clusterKey) { + return true; + } + + return (legacyGeneratedGeminiClusterCounts.get(clusterKey) ?? 0) < 3; + }); + + return serializeAntigravityAliases(preservedEntries); +} + /** * Generate oauth-model-alias YAML section. * Merges default Antigravity aliases with any user-added custom aliases. @@ -543,8 +661,10 @@ export function regenerateConfig(port: number = CLIPROXY_DEFAULT_PORT): string { // Preserve claude-api-key section (managed via dashboard/API) claudeApiKeySection = extractYamlSection(content, 'claude-api-key'); - // Preserve existing oauth-model-alias user customizations - existingAliases = extractYamlSection(content, 'oauth-model-alias'); + // Preserve user customizations while pruning legacy generated Gemini preview noise. + existingAliases = extractPreservedAntigravityAliases( + extractYamlSection(content, 'oauth-model-alias') + ); } catch { // Use defaults if reading fails } diff --git a/tests/unit/cliproxy/config-generator.test.js b/tests/unit/cliproxy/config-generator.test.js index 4445c807..b1608958 100644 --- a/tests/unit/cliproxy/config-generator.test.js +++ b/tests/unit/cliproxy/config-generator.test.js @@ -753,6 +753,135 @@ oauth-model-alias: ); }); + it('drops stale generated Gemini preview clusters while preserving true custom aliases', () => { + const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy'); + fs.mkdirSync(cliproxyDir, { recursive: true }); + + const initialConfig = `# CLIProxyAPI config generated by CCS v12 +port: 8317 +api-keys: + - "ccs-internal-managed" +auth-dir: "${cliproxyDir.replace(/\\/g, '/')}/auth" +oauth-model-alias: + antigravity: + - name: gemini-3-pro-high + alias: gemini-3.9-pro-preview + - name: gemini-3-pro-high + alias: gemini-3.9-pro-preview-customtools + - name: gemini-3-pro-high + alias: gemini-3-9-pro-preview + - name: gemini-3-pro-high + alias: gemini-3-9-pro-preview-customtools + - name: gemini-3-flash + alias: gemini-3.9-flash-preview + - name: gemini-3-flash + alias: gemini-3.9-flash-preview-customtools + - name: gemini-3-flash + alias: gemini-3-9-flash-preview + - name: gemini-3-flash + alias: gemini-3-9-flash-preview-customtools + - name: custom-model + alias: keep-me +`; + fs.writeFileSync(path.join(cliproxyDir, 'config.yaml'), initialConfig); + + regenerateConfig(); + + const newConfig = fs.readFileSync(path.join(cliproxyDir, 'config.yaml'), 'utf-8'); + assert( + !newConfig.includes('alias: gemini-3.9-pro-preview'), + 'Should remove stale generated Pro preview alias noise' + ); + assert( + !newConfig.includes('alias: gemini-3.9-pro-preview-customtools'), + 'Should remove stale generated Pro customtools alias noise' + ); + assert( + !newConfig.includes('alias: gemini-3-9-pro-preview'), + 'Should remove stale generated hyphenated Pro alias noise' + ); + assert( + !newConfig.includes('alias: gemini-3-9-pro-preview-customtools'), + 'Should remove stale generated hyphenated Pro customtools alias noise' + ); + assert( + !newConfig.includes('alias: gemini-3.9-flash-preview'), + 'Should remove stale generated Flash preview alias noise' + ); + assert( + !newConfig.includes('alias: gemini-3.9-flash-preview-customtools'), + 'Should remove stale generated Flash customtools alias noise' + ); + assert( + !newConfig.includes('alias: gemini-3-9-flash-preview'), + 'Should remove stale generated hyphenated Flash alias noise' + ); + assert( + !newConfig.includes('alias: gemini-3-9-flash-preview-customtools'), + 'Should remove stale generated Flash preview alias noise' + ); + assert(newConfig.includes('alias: keep-me'), 'Should preserve true custom aliases'); + assert( + newConfig.includes('alias: gemini-3-pro-preview'), + 'Should still keep current baseline Pro preview alias' + ); + assert( + newConfig.includes('alias: gemini-3.1-flash-preview-customtools'), + 'Should still keep current baseline Flash compatibility alias' + ); + }); + + it('preserves manual Gemini preview overrides on built-in alias names', () => { + const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy'); + fs.mkdirSync(cliproxyDir, { recursive: true }); + + const initialConfig = `# CLIProxyAPI config generated by CCS v12 +port: 8317 +api-keys: + - "ccs-internal-managed" +auth-dir: "${cliproxyDir.replace(/\\/g, '/')}/auth" +oauth-model-alias: + antigravity: + - name: gemini-3-pro-high + alias: gemini-3.2-pro-preview + - name: gemini-3-flash + alias: gemini-3.2-flash-preview + fork: true +`; + fs.writeFileSync(path.join(cliproxyDir, 'config.yaml'), initialConfig); + + regenerateConfig(); + + const newConfig = fs.readFileSync(path.join(cliproxyDir, 'config.yaml'), 'utf-8'); + assert( + newConfig.includes('alias: gemini-3.2-pro-preview'), + 'Should preserve manual Pro preview overrides on built-in alias names' + ); + assert( + newConfig.includes('alias: gemini-3.2-flash-preview'), + 'Should preserve manual Flash preview overrides on built-in alias names' + ); + assert( + newConfig.includes('alias: gemini-3.2-pro-preview-customtools'), + 'Should still expand compatibility aliases for preserved manual Pro override' + ); + assert( + newConfig.includes('alias: gemini-3.2-flash-preview-customtools'), + 'Should still expand compatibility aliases for preserved manual Flash override' + ); + + const lines = newConfig.split('\n'); + for (let i = 0; i < lines.length; i++) { + if (lines[i].includes('alias: gemini-3.2-flash-preview')) { + assert( + lines[i + 1] && lines[i + 1].trim() === 'fork: true', + 'Should preserve fork: true on manual Flash preview override' + ); + break; + } + } + }); + it('preserves user-added aliases with fork during regeneration', () => { const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy'); fs.mkdirSync(cliproxyDir, { recursive: true });