mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 02:11:28 +00:00
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:
@@ -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'
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user