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