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] 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 */}