fix(cliproxy): add fork:true for Claude model aliases in config generator (#523)

Config generator now outputs fork:true for Claude model alias entries,
ensuring both upstream (claude-*) and aliased (gemini-claude-*) model
names appear in /v1/models listings. Also preserves fork flag when
parsing user-added aliases during config regeneration.

Bumps config version to v7 to trigger regeneration on next ccs doctor.

Closes #522
This commit is contained in:
Kai (Tam Nhu) Tran
2026-02-11 19:04:41 +07:00
committed by GitHub
parent 8244162ff3
commit 4065399d8a
2 changed files with 139 additions and 11 deletions
+35 -11
View File
@@ -26,23 +26,24 @@ export const CCS_CONTROL_PANEL_SECRET = 'ccs';
* v4: Added Kiro (AWS) and GitHub Copilot providers
* v5: Added disable-cooling: true for stability
* v6: Added oauth-model-alias with Opus 4.6 support
* v7: Added fork:true for Claude model aliases (keep both upstream and alias names)
*/
export const CLIPROXY_CONFIG_VERSION = 6;
export const CLIPROXY_CONFIG_VERSION = 7;
/**
* Default Antigravity oauth-model-alias entries.
* Maps user-facing model names to Antigravity internal model names.
* Must stay in sync with CLIProxyAPIPlus defaultAntigravityAliases().
*/
const DEFAULT_ANTIGRAVITY_ALIASES: Array<{ name: string; alias: string }> = [
const DEFAULT_ANTIGRAVITY_ALIASES: Array<{ name: string; alias: string; fork?: boolean }> = [
{ name: 'rev19-uic3-1p', alias: 'gemini-2.5-computer-use-preview-10-2025' },
{ name: 'gemini-3-pro-image', alias: 'gemini-3-pro-image-preview' },
{ name: 'gemini-3-pro-high', alias: 'gemini-3-pro-preview' },
{ name: 'gemini-3-flash', alias: 'gemini-3-flash-preview' },
{ name: 'claude-sonnet-4-5', alias: 'gemini-claude-sonnet-4-5' },
{ name: 'claude-sonnet-4-5-thinking', alias: 'gemini-claude-sonnet-4-5-thinking' },
{ name: 'claude-opus-4-5-thinking', alias: 'gemini-claude-opus-4-5-thinking' },
{ name: 'claude-opus-4-6-thinking', alias: 'gemini-claude-opus-4-6-thinking' },
{ name: 'claude-sonnet-4-5', alias: 'gemini-claude-sonnet-4-5', fork: true },
{ name: 'claude-sonnet-4-5-thinking', alias: 'gemini-claude-sonnet-4-5-thinking', fork: true },
{ name: 'claude-opus-4-5-thinking', alias: 'gemini-claude-opus-4-5-thinking', fork: true },
{ name: 'claude-opus-4-6-thinking', alias: 'gemini-claude-opus-4-6-thinking', fork: true },
];
/** Provider display names (static metadata) */
@@ -103,21 +104,44 @@ function generateOAuthModelAliasSection(existingAliases?: string): string {
const existingNames = new Set(aliasEntries.map((a) => a.name));
const lines = existingAliases.split('\n');
let currentName = '';
let currentAlias = '';
let currentFork = false;
for (const line of lines) {
const nameMatch = line.match(/^\s+-\s*name:\s*(.+)/);
const aliasMatch = line.match(/^\s+alias:\s*(.+)/);
const forkMatch = line.match(/^\s+fork:\s*(.+)/);
if (nameMatch) {
// Flush previous entry if complete
if (currentName && currentAlias && !existingNames.has(currentName)) {
aliasEntries.push({
name: currentName,
alias: currentAlias,
fork: currentFork || undefined,
});
existingNames.add(currentName);
}
currentName = nameMatch[1].trim();
} else if (aliasMatch && currentName && !existingNames.has(currentName)) {
aliasEntries.push({ name: currentName, alias: aliasMatch[1].trim() });
existingNames.add(currentName);
currentName = '';
currentAlias = '';
currentFork = false;
} else if (aliasMatch) {
currentAlias = aliasMatch[1].trim();
} else if (forkMatch) {
currentFork = forkMatch[1].trim().toLowerCase() === 'true';
}
}
// Flush last entry
if (currentName && currentAlias && !existingNames.has(currentName)) {
aliasEntries.push({ name: currentName, alias: currentAlias, fork: currentFork || undefined });
existingNames.add(currentName);
}
}
const entries = aliasEntries
.map((a) => ` - name: ${a.name}\n alias: ${a.alias}`)
.map((a) => {
let entry = ` - name: ${a.name}\n alias: ${a.alias}`;
if (a.fork) entry += '\n fork: true';
return entry;
})
.join('\n');
return `oauth-model-alias:\n antigravity:\n${entries}`;
@@ -552,4 +552,108 @@ auth-dir: "${cliproxyDir.replace(/\\/g, '/')}/auth"
});
});
});
describe('oauth-model-alias fork:true', () => {
const fs = require('fs');
const os = require('os');
const path = require('path');
let testDir;
let originalCcsHome;
let regenerateConfig;
beforeEach(() => {
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-test-fork-'));
originalCcsHome = process.env.CCS_HOME;
process.env.CCS_HOME = testDir;
delete require.cache[require.resolve('../../../dist/cliproxy/config-generator')];
delete require.cache[require.resolve('../../../dist/utils/config-manager')];
const configGenerator = require('../../../dist/cliproxy/config-generator');
regenerateConfig = configGenerator.regenerateConfig;
});
afterEach(() => {
process.env.CCS_HOME = originalCcsHome;
if (testDir && fs.existsSync(testDir)) {
fs.rmSync(testDir, { recursive: true, force: true });
}
});
it('generates fork:true for Claude model aliases', () => {
regenerateConfig();
const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy');
const config = fs.readFileSync(path.join(cliproxyDir, 'config.yaml'), 'utf-8');
// Claude aliases should have fork: true
assert(config.includes('claude-sonnet-4-5'), 'Should include Claude sonnet model');
assert(config.includes('fork: true'), 'Should include fork: true for Claude aliases');
// Verify fork: true appears after each Claude alias entry
const lines = config.split('\n');
for (let i = 0; i < lines.length; i++) {
if (lines[i].includes('alias: gemini-claude-')) {
assert(
lines[i + 1] && lines[i + 1].trim() === 'fork: true',
`fork: true should follow Claude alias at line ${i}: ${lines[i]}`
);
}
}
});
it('does not generate fork:true for non-Claude aliases', () => {
regenerateConfig();
const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy');
const config = fs.readFileSync(path.join(cliproxyDir, 'config.yaml'), 'utf-8');
// Gemini aliases should NOT have fork: true
const lines = config.split('\n');
for (let i = 0; i < lines.length; i++) {
if (lines[i].includes('alias: gemini-3-') || lines[i].includes('alias: gemini-2.5-')) {
const nextLine = lines[i + 1] || '';
assert(
!nextLine.trim().startsWith('fork:'),
`Gemini alias should not have fork: ${lines[i]}`
);
}
}
});
it('preserves user-added aliases with fork during regeneration', () => {
const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy');
fs.mkdirSync(cliproxyDir, { recursive: true });
const initialConfig = `# CLIProxyAPI config generated by CCS v6
port: 8317
api-keys:
- "ccs-internal-managed"
auth-dir: "${cliproxyDir.replace(/\\/g, '/')}/auth"
oauth-model-alias:
antigravity:
- name: custom-model
alias: my-custom-alias
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('custom-model'), 'Should preserve custom alias name');
assert(newConfig.includes('my-custom-alias'), 'Should preserve custom alias');
// Check fork is preserved for user alias
const lines = newConfig.split('\n');
for (let i = 0; i < lines.length; i++) {
if (lines[i].includes('alias: my-custom-alias')) {
assert(
lines[i + 1] && lines[i + 1].trim() === 'fork: true',
'Should preserve fork: true for user-added alias'
);
}
}
});
});
});