diff --git a/src/cliproxy/config/generator.ts b/src/cliproxy/config/generator.ts index f2cd8cb4..dd1799bc 100644 --- a/src/cliproxy/config/generator.ts +++ b/src/cliproxy/config/generator.ts @@ -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}`; diff --git a/tests/unit/cliproxy/config-generator.test.js b/tests/unit/cliproxy/config-generator.test.js index fc8aa340..22b28984 100644 --- a/tests/unit/cliproxy/config-generator.test.js +++ b/tests/unit/cliproxy/config-generator.test.js @@ -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' + ); + } + } + }); + }); });