From 2915ab0c40e8c22027884ff95f14a7df78ee275b Mon Sep 17 00:00:00 2001 From: B1GGersnow <2642448440@qq.com> Date: Sat, 25 Apr 2026 03:20:29 +0800 Subject: [PATCH 1/7] feat: add extra models support for API profiles Add ANTHROPIC_EXTRA_MODELS env var to allow each API profile to configure additional models alongside the primary ANTHROPIC_MODEL. These are synced to CLIProxy config.yaml during ccs cliproxy sync. - profile-mapper.ts: parse ANTHROPIC_EXTRA_MODELS on sync - profile-writer.ts: write extra models to settings.json - shared.ts + create-command.ts: --extra-models CLI flag - profile-routes.ts + route-helpers.ts: REST API support - profile-dialog.tsx + api-client.ts: frontend input --- src/api/services/profile-writer.ts | 19 ++++++++++++++----- src/cliproxy/sync/profile-mapper.ts | 15 +++++++++++++++ src/commands/api-command/create-command.ts | 6 +++++- src/commands/api-command/shared.ts | 17 +++++++++++++++++ src/web-server/routes/profile-routes.ts | 19 ++++++++++++++++--- src/web-server/routes/route-helpers.ts | 12 ++++++++++++ ui/src/components/profiles/profile-dialog.tsx | 16 ++++++++++++++++ ui/src/lib/api-client.ts | 2 ++ 8 files changed, 97 insertions(+), 9 deletions(-) diff --git a/src/api/services/profile-writer.ts b/src/api/services/profile-writer.ts index a39fdce9..b7c00ac7 100644 --- a/src/api/services/profile-writer.ts +++ b/src/api/services/profile-writer.ts @@ -91,7 +91,8 @@ function createSettingsFile( baseUrl: string, apiKey: string, models: ModelMapping, - provider?: string + provider?: string, + extraModels?: string[] ): string { const ccsDir = getCcsDir(); const settingsPath = path.join(ccsDir, `${name}.settings.json`); @@ -117,6 +118,9 @@ function createSettingsFile( ANTHROPIC_DEFAULT_OPUS_MODEL: models.opus, ANTHROPIC_DEFAULT_SONNET_MODEL: models.sonnet, ANTHROPIC_DEFAULT_HAIKU_MODEL: models.haiku, + ...(extraModels && extraModels.length > 0 + ? { ANTHROPIC_EXTRA_MODELS: extraModels.join(',') } + : {}), CCS_DROID_PROVIDER: droidProvider, }, }; @@ -179,7 +183,8 @@ function createApiProfileUnified( apiKey: string, models: ModelMapping, target: TargetType = 'claude', - provider?: string + provider?: string, + extraModels?: string[] ): void { const ccsDir = getCcsDir(); const settingsFile = `${name}.settings.json`; @@ -204,6 +209,9 @@ function createApiProfileUnified( ANTHROPIC_DEFAULT_OPUS_MODEL: models.opus, ANTHROPIC_DEFAULT_SONNET_MODEL: models.sonnet, ANTHROPIC_DEFAULT_HAIKU_MODEL: models.haiku, + ...(extraModels && extraModels.length > 0 + ? { ANTHROPIC_EXTRA_MODELS: extraModels.join(',') } + : {}), CCS_DROID_PROVIDER: droidProvider, }, }; @@ -240,7 +248,8 @@ export function createApiProfile( apiKey: string, models: ModelMapping, target: TargetType = 'claude', - provider?: string + provider?: string, + extraModels?: string[] ): CreateApiProfileResult { try { const deniedReason = getDeniedModelReason(baseUrl, models); @@ -251,9 +260,9 @@ export function createApiProfile( const settingsFile = `~/.ccs/${name}.settings.json`; if (isUnifiedMode()) { - createApiProfileUnified(name, baseUrl, apiKey, models, target, provider); + createApiProfileUnified(name, baseUrl, apiKey, models, target, provider, extraModels); } else { - createSettingsFile(name, baseUrl, apiKey, models, provider); + createSettingsFile(name, baseUrl, apiKey, models, provider, extraModels); updateLegacyConfig(name, target); } diff --git a/src/cliproxy/sync/profile-mapper.ts b/src/cliproxy/sync/profile-mapper.ts index 40583fa4..cb4b7a10 100644 --- a/src/cliproxy/sync/profile-mapper.ts +++ b/src/cliproxy/sync/profile-mapper.ts @@ -149,6 +149,21 @@ export function mapProfileToClaudeKey(profile: SyncableProfile): ClaudeKey | nul alias: '', }, ]; + + // Append extra models from ANTHROPIC_EXTRA_MODELS env var (comma-separated) + const extraModelsRaw = env.ANTHROPIC_EXTRA_MODELS; + if (extraModelsRaw && extraModelsRaw.trim()) { + const extraModels = extraModelsRaw + .split(',') + .map((m) => m.trim()) + .filter((m) => m.length > 0); + for (const extra of extraModels) { + claudeKey.models.push({ + name: extra, + alias: '', + }); + } + } } return claudeKey; diff --git a/src/commands/api-command/create-command.ts b/src/commands/api-command/create-command.ts index 1909c2dc..c7e6f782 100644 --- a/src/commands/api-command/create-command.ts +++ b/src/commands/api-command/create-command.ts @@ -493,7 +493,7 @@ export async function handleApiCreateCommand(args: string[]): Promise { console.log(''); console.log(info('Creating API profile...')); - const result = createApiProfile(name, baseUrl || '', apiKey, finalModels, target); + const result = createApiProfile(name, baseUrl || '', apiKey, finalModels, target, undefined, parsedArgs.extraModels); if (!result.success) { console.log(fail(`Failed to create API profile: ${result.error}`)); process.exit(1); @@ -534,6 +534,10 @@ export async function handleApiCreateCommand(args: string[]): Promise { ` Haiku: ${finalModels.haiku}`; } + if (parsedArgs.extraModels && parsedArgs.extraModels.length > 0) { + details += `\nExtraModels: ${parsedArgs.extraModels.join(', ')}`; + } + console.log(''); console.log(infoBox(details, 'API Profile Created')); if (hasClaudeMappings) { diff --git a/src/commands/api-command/shared.ts b/src/commands/api-command/shared.ts index 37dfe35f..657cbb3d 100644 --- a/src/commands/api-command/shared.ts +++ b/src/commands/api-command/shared.ts @@ -17,6 +17,7 @@ export interface ApiCommandArgs { baseUrl?: string; apiKey?: string; model?: string; + extraModels?: string[]; preset?: string; cliproxyProvider?: string; target?: TargetType; @@ -33,6 +34,7 @@ export const API_VALUE_FLAGS = [ '--base-url', '--api-key', '--model', + '--extra-models', '--preset', '--cliproxy-provider', '--target', @@ -222,6 +224,21 @@ export function parseApiCommandArgs( true ); + remaining = applyRepeatedOption( + remaining, + ['--extra-models'], + (value) => { + result.extraModels = value + .split(',') + .map((m) => m.trim()) + .filter((m) => m.length > 0); + }, + () => { + result.errors.push('Missing value for --extra-models'); + }, + false + ); + remaining = applyRepeatedOption( remaining, ['--preset'], diff --git a/src/web-server/routes/profile-routes.ts b/src/web-server/routes/profile-routes.ts index 84752b1d..de22ba4b 100644 --- a/src/web-server/routes/profile-routes.ts +++ b/src/web-server/routes/profile-routes.ts @@ -144,6 +144,7 @@ router.post('/', (req: Request, res: Response): void => { 'baseUrl', 'apiKey', 'model', + 'extraModels', 'opusModel', 'sonnetModel', 'haikuModel', @@ -156,7 +157,7 @@ router.post('/', (req: Request, res: Response): void => { return; } - const { name, baseUrl, apiKey, model, opusModel, sonnetModel, haikuModel, target } = req.body; + const { name, baseUrl, apiKey, model, extraModels, opusModel, sonnetModel, haikuModel, target } = req.body; const providerHint = req.body?.droidProvider ?? req.body?.provider; const parsedProvider = normalizeDroidProvider(providerHint); const normalizedBaseUrl = typeof baseUrl === 'string' ? baseUrl.trim() : ''; @@ -198,6 +199,14 @@ router.post('/', (req: Request, res: Response): void => { } // Create profile using unified-config-aware service + const parsedExtraModels: string[] | undefined = + typeof extraModels === 'string' && extraModels.trim() + ? extraModels + .split(',') + .map((m: string) => m.trim()) + .filter((m: string) => m.length > 0) + : undefined; + const result = createApiProfile( name, normalizedBaseUrl, @@ -209,7 +218,8 @@ router.post('/', (req: Request, res: Response): void => { haiku: haikuModel || model || '', }, parsedTarget || 'claude', - parsedProvider || undefined + parsedProvider || undefined, + parsedExtraModels ); if (!result.success) { @@ -409,6 +419,7 @@ router.put('/:name', (req: Request, res: Response): void => { 'baseUrl', 'apiKey', 'model', + 'extraModels', 'opusModel', 'sonnetModel', 'haikuModel', @@ -422,7 +433,7 @@ router.put('/:name', (req: Request, res: Response): void => { } const { name } = req.params; - const { baseUrl, apiKey, model, opusModel, sonnetModel, haikuModel, target } = req.body; + const { baseUrl, apiKey, model, extraModels, opusModel, sonnetModel, haikuModel, target } = req.body; const providerHint = req.body?.droidProvider ?? req.body?.provider; const parsedProvider = normalizeDroidProvider(providerHint); const normalizedBaseUrl = typeof baseUrl === 'string' ? baseUrl.trim() : baseUrl; @@ -457,6 +468,7 @@ router.put('/:name', (req: Request, res: Response): void => { baseUrl !== undefined || apiKey !== undefined || model !== undefined || + extraModels !== undefined || opusModel !== undefined || sonnetModel !== undefined || haikuModel !== undefined || @@ -473,6 +485,7 @@ router.put('/:name', (req: Request, res: Response): void => { baseUrl: normalizedBaseUrl, apiKey: normalizedApiKey, model, + extraModels, opusModel, sonnetModel, haikuModel, diff --git a/src/web-server/routes/route-helpers.ts b/src/web-server/routes/route-helpers.ts index b45f762a..3836711f 100644 --- a/src/web-server/routes/route-helpers.ts +++ b/src/web-server/routes/route-helpers.ts @@ -218,6 +218,7 @@ export function updateSettingsFile( baseUrl?: string; apiKey?: string; model?: string; + extraModels?: string; opusModel?: string; sonnetModel?: string; haikuModel?: string; @@ -336,6 +337,17 @@ export function updateSettingsFile( } } + // Handle extra models (comma-separated string) + if (updates.extraModels !== undefined) { + settings.env = settings.env || {}; + const normalized = typeof updates.extraModels === 'string' ? updates.extraModels.trim() : ''; + if (normalized.length > 0) { + settings.env.ANTHROPIC_EXTRA_MODELS = normalized; + } else { + delete settings.env.ANTHROPIC_EXTRA_MODELS; + } + } + if ( updates.provider !== undefined || updates.baseUrl !== undefined || diff --git a/ui/src/components/profiles/profile-dialog.tsx b/ui/src/components/profiles/profile-dialog.tsx index cdd03d69..d16caf27 100644 --- a/ui/src/components/profiles/profile-dialog.tsx +++ b/ui/src/components/profiles/profile-dialog.tsx @@ -34,6 +34,7 @@ const schema = z.object({ baseUrl: optionalUrlSchema, apiKey: z.string().min(10, 'API key must be at least 10 characters'), model: z.string().optional(), + extraModels: z.string().optional(), opusModel: z.string().optional(), sonnetModel: z.string().optional(), haikuModel: z.string().optional(), @@ -67,6 +68,7 @@ export function ProfileDialog({ open, onClose, profile }: ProfileDialogProps) { baseUrl: '', apiKey: '', model: '', + extraModels: '', opusModel: '', sonnetModel: '', haikuModel: '', @@ -102,6 +104,7 @@ export function ProfileDialog({ open, onClose, profile }: ProfileDialogProps) { baseUrl: data.baseUrl, apiKey: data.apiKey, model: data.model, + extraModels: data.extraModels, opusModel: data.opusModel, sonnetModel: data.sonnetModel, haikuModel: data.haikuModel, @@ -165,6 +168,19 @@ export function ProfileDialog({ open, onClose, profile }: ProfileDialogProps) {

+
+ + +

+ Comma-separated list of additional models available via this provider. These are + synced to CLIProxy alongside the default model. +

+
+ {/* Model Mapping Section */}