From 061cd9679fc56a8a0711877f57eebcd998df10b6 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Sun, 29 Mar 2026 13:30:53 -0400 Subject: [PATCH 1/3] 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 }); From c4428e4176bb7244d597b5f75240aa39bf536bbf Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Sun, 29 Mar 2026 14:07:16 -0400 Subject: [PATCH 2/3] fix(cliproxy): narrow legacy gemini alias cleanup --- src/cliproxy/config/generator.ts | 164 +++++++-- tests/unit/cliproxy/config-generator.test.js | 367 ++++++++++++++++--- 2 files changed, 445 insertions(+), 86 deletions(-) diff --git a/src/cliproxy/config/generator.ts b/src/cliproxy/config/generator.ts index cae36da7..1dfdd46e 100644 --- a/src/cliproxy/config/generator.ts +++ b/src/cliproxy/config/generator.ts @@ -37,8 +37,9 @@ export const CCS_CONTROL_PANEL_SECRET = 'ccs'; * 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 + * v16: Narrow stale Gemini alias cleanup to broad multi-version guessed ranges */ -export const CLIPROXY_CONFIG_VERSION = 15; +export const CLIPROXY_CONFIG_VERSION = 16; interface OAuthModelAliasEntry { name: string; @@ -46,6 +47,11 @@ interface OAuthModelAliasEntry { fork?: boolean; } +interface PreservedAntigravityAliasesResult { + yaml: string; + prunedLegacyAliasCount: number; +} + const DEPRECATED_ANTIGRAVITY_ALIAS_PREFIX = 'gemini-claude-'; const UPSTREAM_CLAUDE_ALIAS_PREFIX = 'claude-'; const DEPRECATED_ANTIGRAVITY_SONNET_46_THINKING_REGEX = /^claude-sonnet-4(?:[.-])6-thinking$/i; @@ -75,6 +81,10 @@ const BUILT_IN_GEMINI_ALIAS_NAMES = new Set( (entry) => entry.name ) ); +const MIN_STALE_GUESSED_GEMINI_MINOR_VERSIONS = 2; +const MIN_STALE_GUESSED_GEMINI_AVERAGE_VARIANTS_PER_MINOR = 2; +const LEGACY_GEMINI_STALE_ALIAS_MIGRATION_VERSION = 16; +const MAX_LEGACY_MANUAL_GEMINI_MINOR_VERSION = 2; /** * Get provider configuration @@ -215,6 +225,16 @@ function parseExistingAntigravityAliases(existingAliases: string): OAuthModelAli return entries; } +function getConfigVersionFromContent(content: string): number | null { + const versionMatch = content.match(/CCS v(\d+)/); + if (!versionMatch) { + return null; + } + + const parsedVersion = Number.parseInt(versionMatch[1], 10); + return Number.isNaN(parsedVersion) ? null : parsedVersion; +} + function toDottedGeminiVersionAlias(alias: string): string | null { const match = alias.match(/^(gemini-\d+)-(\d+)(-.+)$/); if (!match) return null; @@ -315,7 +335,9 @@ function serializeAntigravityAliases(entries: OAuthModelAliasEntry[]): string { return lines.join('\n'); } -function getLegacyGeneratedGeminiPreviewClusterKey(entry: OAuthModelAliasEntry): string | null { +function getLegacyGeneratedGeminiPreviewInfo( + entry: OAuthModelAliasEntry +): { nameKey: string; minorVersion: string } | null { if (!BUILT_IN_GEMINI_ALIAS_NAMES.has(entry.name)) { return null; } @@ -328,43 +350,100 @@ function getLegacyGeneratedGeminiPreviewClusterKey(entry: OAuthModelAliasEntry): const aliasWithoutCustomtools = normalizedAlias.endsWith('-customtools') ? normalizedAlias.slice(0, -'-customtools'.length) : normalizedAlias; - const clusterAlias = + const canonicalAlias = toDottedGeminiVersionAlias(aliasWithoutCustomtools) ?? aliasWithoutCustomtools; - - return `${sanitizeYamlScalar(entry.name)}\u0000${clusterAlias}`; -} - -function extractPreservedAntigravityAliases(existingAliases: string): string { - if (!existingAliases.trim()) { - return ''; + const minorVersionMatch = canonicalAlias.match(/^gemini-3\.(\d+)(-.+-preview)$/); + if (!minorVersionMatch) { + return null; } - const parsedEntries = parseExistingAntigravityAliases(existingAliases); - const legacyGeneratedGeminiClusterCounts = new Map(); + return { + nameKey: sanitizeYamlScalar(entry.name), + minorVersion: minorVersionMatch[1], + }; +} + +function buildLegacyGeneratedGeminiAliasPruneSet( + parsedEntries: OAuthModelAliasEntry[] +): Set { + const aliasKeysByNameAndMinor = new Map>(); for (const entry of parsedEntries) { if (entry.fork) { continue; } - const generatedEntry = GENERATED_ANTIGRAVITY_ALIAS_MAP.get(buildAntigravityAliasKey(entry)); - if (generatedEntry) { + const aliasKey = buildAntigravityAliasKey(entry); + if (GENERATED_ANTIGRAVITY_ALIAS_MAP.has(aliasKey)) { continue; } - const clusterKey = getLegacyGeneratedGeminiPreviewClusterKey(entry); - if (!clusterKey) { + const previewInfo = getLegacyGeneratedGeminiPreviewInfo(entry); + if (!previewInfo) { continue; } - legacyGeneratedGeminiClusterCounts.set( - clusterKey, - (legacyGeneratedGeminiClusterCounts.get(clusterKey) ?? 0) + 1 - ); + const aliasKeysByMinor = aliasKeysByNameAndMinor.get(previewInfo.nameKey) ?? new Map(); + const aliasKeys = aliasKeysByMinor.get(previewInfo.minorVersion) ?? []; + aliasKeys.push(aliasKey); + aliasKeysByMinor.set(previewInfo.minorVersion, aliasKeys); + aliasKeysByNameAndMinor.set(previewInfo.nameKey, aliasKeysByMinor); } + const staleAliasKeys = new Set(); + + for (const aliasKeysByMinor of aliasKeysByNameAndMinor.values()) { + const sortedMinorVersions = [...aliasKeysByMinor.keys()].sort((left, right) => { + return Number(left) - Number(right); + }); + const totalAliasCount = sortedMinorVersions.reduce((total, minorVersion) => { + return total + (aliasKeysByMinor.get(minorVersion)?.length ?? 0); + }, 0); + if (sortedMinorVersions.length < MIN_STALE_GUESSED_GEMINI_MINOR_VERSIONS) { + continue; + } + if ( + totalAliasCount < + sortedMinorVersions.length * MIN_STALE_GUESSED_GEMINI_AVERAGE_VARIANTS_PER_MINOR + ) { + continue; + } + + const preservedMinorVersion = + Number(sortedMinorVersions[0]) <= MAX_LEGACY_MANUAL_GEMINI_MINOR_VERSION + ? sortedMinorVersions[0] + : null; + for (const minorVersion of sortedMinorVersions) { + if (minorVersion === preservedMinorVersion) { + continue; + } + + const aliasKeys = aliasKeysByMinor.get(minorVersion) ?? []; + for (const aliasKey of aliasKeys) { + staleAliasKeys.add(aliasKey); + } + } + } + + return staleAliasKeys; +} + +function extractPreservedAntigravityAliases( + existingAliases: string, + options?: { enableLegacyGeminiStaleCleanup?: boolean } +): PreservedAntigravityAliasesResult { + if (!existingAliases.trim()) { + return { yaml: '', prunedLegacyAliasCount: 0 }; + } + + const parsedEntries = parseExistingAntigravityAliases(existingAliases); + const staleAliasKeys = options?.enableLegacyGeminiStaleCleanup + ? buildLegacyGeneratedGeminiAliasPruneSet(parsedEntries) + : new Set(); + const preservedEntries = parsedEntries.filter((entry) => { - const generatedEntry = GENERATED_ANTIGRAVITY_ALIAS_MAP.get(buildAntigravityAliasKey(entry)); + const aliasKey = buildAntigravityAliasKey(entry); + const generatedEntry = GENERATED_ANTIGRAVITY_ALIAS_MAP.get(aliasKey); if (generatedEntry) { return Boolean(entry.fork) && !generatedEntry.fork; @@ -374,15 +453,24 @@ function extractPreservedAntigravityAliases(existingAliases: string): string { return true; } - const clusterKey = getLegacyGeneratedGeminiPreviewClusterKey(entry); - if (!clusterKey) { - return true; - } - - return (legacyGeneratedGeminiClusterCounts.get(clusterKey) ?? 0) < 3; + return !staleAliasKeys.has(aliasKey); }); - return serializeAntigravityAliases(preservedEntries); + return { + yaml: serializeAntigravityAliases(preservedEntries), + prunedLegacyAliasCount: parsedEntries.filter((entry) => + staleAliasKeys.has(buildAntigravityAliasKey(entry)) + ).length, + }; +} + +function writeLegacyGeminiAliasCleanupBackup(configPath: string, existingContent: string): void { + const backupPath = `${configPath}.pre-v16-gemini-alias-cleanup.bak`; + if (fs.existsSync(backupPath)) { + return; + } + + fs.writeFileSync(backupPath, existingContent, { mode: 0o600 }); } /** @@ -662,9 +750,19 @@ export function regenerateConfig(port: number = CLIPROXY_DEFAULT_PORT): string { claudeApiKeySection = extractYamlSection(content, 'claude-api-key'); // Preserve user customizations while pruning legacy generated Gemini preview noise. - existingAliases = extractPreservedAntigravityAliases( - extractYamlSection(content, 'oauth-model-alias') + const existingConfigVersion = getConfigVersionFromContent(content); + const preservedAliases = extractPreservedAntigravityAliases( + extractYamlSection(content, 'oauth-model-alias'), + { + enableLegacyGeminiStaleCleanup: + existingConfigVersion === null || + existingConfigVersion < LEGACY_GEMINI_STALE_ALIAS_MIGRATION_VERSION, + } ); + existingAliases = preservedAliases.yaml; + if (preservedAliases.prunedLegacyAliasCount > 0) { + writeLegacyGeminiAliasCleanupBackup(configPath, content); + } } catch { // Use defaults if reading fails } @@ -703,12 +801,10 @@ export function configNeedsRegeneration(): boolean { const content = fs.readFileSync(configPath, 'utf-8'); // Check for version marker - const versionMatch = content.match(/CCS v(\d+)/); - if (!versionMatch) { + const configVersion = getConfigVersionFromContent(content); + if (configVersion === null) { return true; // No version marker = old config } - - const configVersion = parseInt(versionMatch[1], 10); return configVersion < CLIPROXY_CONFIG_VERSION; } catch { return true; // Error reading = regenerate diff --git a/tests/unit/cliproxy/config-generator.test.js b/tests/unit/cliproxy/config-generator.test.js index b1608958..cbf16e2b 100644 --- a/tests/unit/cliproxy/config-generator.test.js +++ b/tests/unit/cliproxy/config-generator.test.js @@ -753,7 +753,202 @@ oauth-model-alias: ); }); - it('drops stale generated Gemini preview clusters while preserving true custom aliases', () => { + it('drops stale multi-version Gemini preview ranges while preserving true custom aliases', () => { + const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy'); + fs.mkdirSync(cliproxyDir, { recursive: true }); + const guessedMinors = ['3', '4', '5', '6']; + const buildGuessedRange = (name, family) => + guessedMinors + .flatMap((minor) => [ + ` - name: ${name}`, + ` alias: gemini-3.${minor}-${family}-preview`, + ` - name: ${name}`, + ` alias: gemini-3.${minor}-${family}-preview-customtools`, + ` - name: ${name}`, + ` alias: gemini-3-${minor}-${family}-preview`, + ` - name: ${name}`, + ` alias: gemini-3-${minor}-${family}-preview-customtools`, + ]) + .join('\n'); + + 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: +${buildGuessedRange('gemini-3-pro-high', 'pro')} +${buildGuessedRange('gemini-3-flash', 'flash')} + - 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'); + for (const minor of guessedMinors) { + assert( + !newConfig.includes(`alias: gemini-3.${minor}-pro-preview`), + `Should remove stale generated Pro aliases for 3.${minor}` + ); + assert( + !newConfig.includes(`alias: gemini-3-${minor}-flash-preview-customtools`), + `Should remove stale generated Flash aliases for 3.${minor}` + ); + } + 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 sparse manual minor-version overrides when a stale guessed range is pruned', () => { + const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy'); + fs.mkdirSync(cliproxyDir, { recursive: true }); + const guessedMinors = ['3', '4', '5', '6']; + const staleRange = guessedMinors + .flatMap((minor) => [ + ` - name: gemini-3-pro-high`, + ` alias: gemini-3.${minor}-pro-preview`, + ` - name: gemini-3-pro-high`, + ` alias: gemini-3.${minor}-pro-preview-customtools`, + ` - name: gemini-3-pro-high`, + ` alias: gemini-3-${minor}-pro-preview`, + ` - name: gemini-3-pro-high`, + ` alias: gemini-3-${minor}-pro-preview-customtools`, + ]) + .join('\n'); + + 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 +${staleRange} +`; + 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 sparse manual minor-version override' + ); + assert( + newConfig.includes('alias: gemini-3.2-pro-preview-customtools'), + 'Should still expand compatibility aliases for preserved sparse manual override' + ); + for (const minor of guessedMinors) { + assert( + !newConfig.includes(`alias: gemini-3.${minor}-pro-preview`), + `Should prune stale guessed aliases for 3.${minor}` + ); + } + }); + + it('preserves a manual 3.2 compatibility cluster while pruning higher stale guessed minors', () => { + const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy'); + fs.mkdirSync(cliproxyDir, { recursive: true }); + const guessedMinors = ['3', '4', '5', '6']; + const staleRange = guessedMinors + .flatMap((minor) => [ + ` - name: gemini-3-pro-high`, + ` alias: gemini-3.${minor}-pro-preview`, + ` - name: gemini-3-pro-high`, + ` alias: gemini-3.${minor}-pro-preview-customtools`, + ` - name: gemini-3-pro-high`, + ` alias: gemini-3-${minor}-pro-preview`, + ` - name: gemini-3-pro-high`, + ` alias: gemini-3-${minor}-pro-preview-customtools`, + ]) + .join('\n'); + + 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-pro-high + alias: gemini-3.2-pro-preview-customtools + - name: gemini-3-pro-high + alias: gemini-3-2-pro-preview + - name: gemini-3-pro-high + alias: gemini-3-2-pro-preview-customtools +${staleRange} +`; + 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 the manual 3.2 compatibility cluster during legacy cleanup' + ); + assert( + newConfig.includes('alias: gemini-3-2-pro-preview-customtools'), + 'Should preserve hyphenated manual compatibility aliases for 3.2' + ); + for (const minor of guessedMinors) { + assert( + !newConfig.includes(`alias: gemini-3.${minor}-pro-preview`), + `Should prune higher stale guessed aliases for 3.${minor}` + ); + } + }); + + it('prunes partially retained guessed ranges during legacy cleanup', () => { + const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy'); + fs.mkdirSync(cliproxyDir, { recursive: true }); + const guessedMinors = ['3', '4']; + const partialRange = guessedMinors + .flatMap((minor) => [ + ` - name: gemini-3-pro-high`, + ` alias: gemini-3.${minor}-pro-preview`, + ` - name: gemini-3-pro-high`, + ` alias: gemini-3.${minor}-pro-preview-customtools`, + ]) + .join('\n'); + + 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: +${partialRange} +`; + fs.writeFileSync(path.join(cliproxyDir, 'config.yaml'), initialConfig); + + regenerateConfig(); + + const newConfig = fs.readFileSync(path.join(cliproxyDir, 'config.yaml'), 'utf-8'); + for (const minor of guessedMinors) { + assert( + !newConfig.includes(`alias: gemini-3.${minor}-pro-preview`), + `Should prune partially retained guessed aliases for 3.${minor}` + ); + } + }); + + it('preserves single-minor high-version aliases when they are not part of a guessed range', () => { const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy'); fs.mkdirSync(cliproxyDir, { recursive: true }); @@ -765,23 +960,9 @@ auth-dir: "${cliproxyDir.replace(/\\/g, '/')}/auth" oauth-model-alias: antigravity: - name: gemini-3-pro-high - alias: gemini-3.9-pro-preview + alias: gemini-3.11-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 + alias: gemini-3.11-pro-preview-customtools `; fs.writeFileSync(path.join(cliproxyDir, 'config.yaml'), initialConfig); @@ -789,45 +970,47 @@ oauth-model-alias: 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' + newConfig.includes('alias: gemini-3.11-pro-preview'), + 'Should preserve lone manual high-version aliases during legacy cleanup' ); assert( - !newConfig.includes('alias: gemini-3.9-pro-preview-customtools'), - 'Should remove stale generated Pro customtools alias noise' + newConfig.includes('alias: gemini-3.11-pro-preview-customtools'), + 'Should still expand compatibility aliases for preserved high-version aliases' ); + }); + + it('writes a one-time backup before pruning legacy Gemini aliases during migration', () => { + const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy'); + fs.mkdirSync(cliproxyDir, { recursive: true }); + const configPath = path.join(cliproxyDir, 'config.yaml'); + const guessedMinors = ['3', '4', '5']; + const staleRange = guessedMinors + .flatMap((minor) => [ + ` - name: gemini-3-pro-high`, + ` alias: gemini-3.${minor}-pro-preview`, + ` - name: gemini-3-pro-high`, + ` alias: gemini-3.${minor}-pro-preview-customtools`, + ]) + .join('\n'); + + 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: +${staleRange} +`; + fs.writeFileSync(configPath, initialConfig); + + regenerateConfig(); + + const backupPath = `${configPath}.pre-v16-gemini-alias-cleanup.bak`; + assert(fs.existsSync(backupPath), 'Should keep a backup of the legacy config before pruning'); 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' + fs.readFileSync(backupPath, 'utf-8').includes('gemini-3.5-pro-preview'), + 'Should preserve the pruned legacy alias in the migration backup' ); }); @@ -882,6 +1065,86 @@ oauth-model-alias: } }); + it('preserves manual Gemini preview compatibility clusters 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-pro-high + alias: gemini-3.2-pro-preview-customtools + - name: gemini-3-pro-high + alias: gemini-3-2-pro-preview + - name: gemini-3-pro-high + alias: gemini-3-2-pro-preview-customtools +`; + 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 dotted Pro preview override' + ); + assert( + newConfig.includes('alias: gemini-3.2-pro-preview-customtools'), + 'Should preserve manual dotted Pro customtools override' + ); + assert( + newConfig.includes('alias: gemini-3-2-pro-preview'), + 'Should preserve manual hyphenated Pro preview override' + ); + assert( + newConfig.includes('alias: gemini-3-2-pro-preview-customtools'), + 'Should preserve manual hyphenated Pro customtools override' + ); + }); + + it('does not re-prune multi-version manual clusters once the config is already migrated', () => { + const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy'); + fs.mkdirSync(cliproxyDir, { recursive: true }); + + const initialConfig = `# CLIProxyAPI config generated by CCS v16 +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-pro-high + alias: gemini-3.3-pro-preview + - name: gemini-3-pro-high + alias: gemini-3.4-pro-preview +`; + 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 aliases after legacy migration has already run' + ); + assert( + newConfig.includes('alias: gemini-3.3-pro-preview'), + 'Should not re-prune later manual version clusters on v16+ configs' + ); + assert( + newConfig.includes('alias: gemini-3.4-pro-preview'), + 'Should leave already-migrated multi-version manual aliases untouched' + ); + }); + it('preserves user-added aliases with fork during regeneration', () => { const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy'); fs.mkdirSync(cliproxyDir, { recursive: true }); From 6707b7d5b470375c3068950894fb7b96b8fa4bf4 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Sun, 29 Mar 2026 14:13:23 -0400 Subject: [PATCH 3/3] fix(cliproxy): preserve manual high-minor alias clusters --- src/cliproxy/config/generator.ts | 14 ++++--- tests/unit/cliproxy/config-generator.test.js | 39 +++++++++++++++++++- 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/src/cliproxy/config/generator.ts b/src/cliproxy/config/generator.ts index 1dfdd46e..fc79958e 100644 --- a/src/cliproxy/config/generator.ts +++ b/src/cliproxy/config/generator.ts @@ -82,6 +82,7 @@ const BUILT_IN_GEMINI_ALIAS_NAMES = new Set( ) ); const MIN_STALE_GUESSED_GEMINI_MINOR_VERSIONS = 2; +const MIN_STALE_HIGH_ONLY_GEMINI_MINOR_VERSIONS = 3; const MIN_STALE_GUESSED_GEMINI_AVERAGE_VARIANTS_PER_MINOR = 2; const LEGACY_GEMINI_STALE_ALIAS_MIGRATION_VERSION = 16; const MAX_LEGACY_MANUAL_GEMINI_MINOR_VERSION = 2; @@ -399,7 +400,14 @@ function buildLegacyGeneratedGeminiAliasPruneSet( const totalAliasCount = sortedMinorVersions.reduce((total, minorVersion) => { return total + (aliasKeysByMinor.get(minorVersion)?.length ?? 0); }, 0); - if (sortedMinorVersions.length < MIN_STALE_GUESSED_GEMINI_MINOR_VERSIONS) { + const preservedMinorVersion = + Number(sortedMinorVersions[0]) <= MAX_LEGACY_MANUAL_GEMINI_MINOR_VERSION + ? sortedMinorVersions[0] + : null; + const minimumMinorVersionsToPrune = preservedMinorVersion + ? MIN_STALE_GUESSED_GEMINI_MINOR_VERSIONS + : MIN_STALE_HIGH_ONLY_GEMINI_MINOR_VERSIONS; + if (sortedMinorVersions.length < minimumMinorVersionsToPrune) { continue; } if ( @@ -409,10 +417,6 @@ function buildLegacyGeneratedGeminiAliasPruneSet( continue; } - const preservedMinorVersion = - Number(sortedMinorVersions[0]) <= MAX_LEGACY_MANUAL_GEMINI_MINOR_VERSION - ? sortedMinorVersions[0] - : null; for (const minorVersion of sortedMinorVersions) { if (minorVersion === preservedMinorVersion) { continue; diff --git a/tests/unit/cliproxy/config-generator.test.js b/tests/unit/cliproxy/config-generator.test.js index cbf16e2b..0e55d717 100644 --- a/tests/unit/cliproxy/config-generator.test.js +++ b/tests/unit/cliproxy/config-generator.test.js @@ -913,10 +913,10 @@ ${staleRange} } }); - it('prunes partially retained guessed ranges during legacy cleanup', () => { + it('prunes partially retained broad guessed ranges during legacy cleanup', () => { const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy'); fs.mkdirSync(cliproxyDir, { recursive: true }); - const guessedMinors = ['3', '4']; + const guessedMinors = ['3', '4', '5']; const partialRange = guessedMinors .flatMap((minor) => [ ` - name: gemini-3-pro-high`, @@ -948,6 +948,41 @@ ${partialRange} } }); + it('preserves manual high-only multi-version clusters that are not broad guessed ranges', () => { + 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.3-pro-preview + - name: gemini-3-pro-high + alias: gemini-3.3-pro-preview-customtools + - name: gemini-3-pro-high + alias: gemini-3.4-pro-preview + - name: gemini-3-pro-high + alias: gemini-3.4-pro-preview-customtools +`; + 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.3-pro-preview'), + 'Should preserve manual 3.3 aliases when the cluster is not broad enough to infer stale fan-out' + ); + assert( + newConfig.includes('alias: gemini-3.4-pro-preview-customtools'), + 'Should preserve manual 3.4 compatibility aliases when provenance is ambiguous' + ); + }); + it('preserves single-minor high-version aliases when they are not part of a guessed range', () => { const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy'); fs.mkdirSync(cliproxyDir, { recursive: true });