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
This commit is contained in:
B1GGersnow
2026-04-25 03:20:29 +08:00
parent 8ccf193fd4
commit 2915ab0c40
8 changed files with 97 additions and 9 deletions
+14 -5
View File
@@ -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);
}
+15
View File
@@ -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;
+5 -1
View File
@@ -493,7 +493,7 @@ export async function handleApiCreateCommand(args: string[]): Promise<void> {
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<void> {
` 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) {
+17
View File
@@ -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'],
+16 -3
View File
@@ -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,
+12
View File
@@ -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 ||
@@ -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) {
</p>
</div>
<div>
<Label htmlFor="extraModels">Extra Models</Label>
<Input
id="extraModels"
{...register('extraModels')}
placeholder="model-a,model-b,model-c"
/>
<p className="text-xs text-muted-foreground mt-1">
Comma-separated list of additional models available via this provider. These are
synced to CLIProxy alongside the default model.
</p>
</div>
{/* Model Mapping Section */}
<div className="border rounded-md">
<button
+2
View File
@@ -336,6 +336,7 @@ export interface CreateProfile {
baseUrl: string;
apiKey: string;
model?: string;
extraModels?: string;
opusModel?: string;
sonnetModel?: string;
haikuModel?: string;
@@ -346,6 +347,7 @@ export interface UpdateProfile {
baseUrl?: string;
apiKey?: string;
model?: string;
extraModels?: string;
opusModel?: string;
sonnetModel?: string;
haikuModel?: string;