mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 02:11:28 +00:00
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
This commit is contained in:
@@ -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<OAuthModelAliasEntry, 'name' | 'alias'>): 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<string, number>();
|
||||
|
||||
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<string, number>();
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
|
||||
Reference in New Issue
Block a user