mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-02 16:19:27 +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
|
* v12: Removed denylisted Antigravity Claude 4.5 aliases
|
||||||
* v13: Removed aggressive Gemini alias expansion to reduce model list noise in Control Panel
|
* 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
|
* 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 {
|
interface OAuthModelAliasEntry {
|
||||||
name: string;
|
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 },
|
{ 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
|
* Get provider configuration
|
||||||
* Model mappings are loaded from config/base-{provider}.settings.json
|
* Model mappings are loaded from config/base-{provider}.settings.json
|
||||||
@@ -148,6 +155,10 @@ function addAliasEntry(
|
|||||||
entries.push(normalized);
|
entries.push(normalized);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildAntigravityAliasKey(entry: Pick<OAuthModelAliasEntry, 'name' | 'alias'>): string {
|
||||||
|
return `${sanitizeYamlScalar(entry.name)}\u0000${normalizeAntigravityAlias(entry.alias)}`;
|
||||||
|
}
|
||||||
|
|
||||||
function parseExistingAntigravityAliases(existingAliases: string): OAuthModelAliasEntry[] {
|
function parseExistingAntigravityAliases(existingAliases: string): OAuthModelAliasEntry[] {
|
||||||
const entries: OAuthModelAliasEntry[] = [];
|
const entries: OAuthModelAliasEntry[] = [];
|
||||||
const lines = existingAliases.replace(/\r\n/g, '\n').split('\n');
|
const lines = existingAliases.replace(/\r\n/g, '\n').split('\n');
|
||||||
@@ -267,6 +278,113 @@ function getCompatibilityAliases(entries: OAuthModelAliasEntry[]): OAuthModelAli
|
|||||||
return compatibilityAliases;
|
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.
|
* Generate oauth-model-alias YAML section.
|
||||||
* Merges default Antigravity aliases with any user-added custom aliases.
|
* 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)
|
// Preserve claude-api-key section (managed via dashboard/API)
|
||||||
claudeApiKeySection = extractYamlSection(content, 'claude-api-key');
|
claudeApiKeySection = extractYamlSection(content, 'claude-api-key');
|
||||||
|
|
||||||
// Preserve existing oauth-model-alias user customizations
|
// Preserve user customizations while pruning legacy generated Gemini preview noise.
|
||||||
existingAliases = extractYamlSection(content, 'oauth-model-alias');
|
existingAliases = extractPreservedAntigravityAliases(
|
||||||
|
extractYamlSection(content, 'oauth-model-alias')
|
||||||
|
);
|
||||||
} catch {
|
} catch {
|
||||||
// Use defaults if reading fails
|
// 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', () => {
|
it('preserves user-added aliases with fork during regeneration', () => {
|
||||||
const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy');
|
const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy');
|
||||||
fs.mkdirSync(cliproxyDir, { recursive: true });
|
fs.mkdirSync(cliproxyDir, { recursive: true });
|
||||||
|
|||||||
Reference in New Issue
Block a user