mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 02:14:32 +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:
1 parent
8ccf193fd4
commit
2915ab0c40
8 files changed
+97
-9
No files matched your search
@@ -91,7 +91,8 @@ function createSettingsFile(
|
|||||||
baseUrl: string,
|
baseUrl: string,
|
||||||
apiKey: string,
|
apiKey: string,
|
||||||
models: ModelMapping,
|
models: ModelMapping,
|
||||||
provider?: string
|
provider?: string,
|
||||||
|
extraModels?: string[]
|
||||||
): string {
|
): string {
|
||||||
const ccsDir = getCcsDir();
|
const ccsDir = getCcsDir();
|
||||||
const settingsPath = path.join(ccsDir, `${name}.settings.json`);
|
const settingsPath = path.join(ccsDir, `${name}.settings.json`);
|
||||||
@@ -117,6 +118,9 @@ function createSettingsFile(
|
|||||||
ANTHROPIC_DEFAULT_OPUS_MODEL: models.opus,
|
ANTHROPIC_DEFAULT_OPUS_MODEL: models.opus,
|
||||||
ANTHROPIC_DEFAULT_SONNET_MODEL: models.sonnet,
|
ANTHROPIC_DEFAULT_SONNET_MODEL: models.sonnet,
|
||||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: models.haiku,
|
ANTHROPIC_DEFAULT_HAIKU_MODEL: models.haiku,
|
||||||
|
...(extraModels && extraModels.length > 0
|
||||||
|
? { ANTHROPIC_EXTRA_MODELS: extraModels.join(',') }
|
||||||
|
: {}),
|
||||||
CCS_DROID_PROVIDER: droidProvider,
|
CCS_DROID_PROVIDER: droidProvider,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -179,7 +183,8 @@ function createApiProfileUnified(
|
|||||||
apiKey: string,
|
apiKey: string,
|
||||||
models: ModelMapping,
|
models: ModelMapping,
|
||||||
target: TargetType = 'claude',
|
target: TargetType = 'claude',
|
||||||
provider?: string
|
provider?: string,
|
||||||
|
extraModels?: string[]
|
||||||
): void {
|
): void {
|
||||||
const ccsDir = getCcsDir();
|
const ccsDir = getCcsDir();
|
||||||
const settingsFile = `${name}.settings.json`;
|
const settingsFile = `${name}.settings.json`;
|
||||||
@@ -204,6 +209,9 @@ function createApiProfileUnified(
|
|||||||
ANTHROPIC_DEFAULT_OPUS_MODEL: models.opus,
|
ANTHROPIC_DEFAULT_OPUS_MODEL: models.opus,
|
||||||
ANTHROPIC_DEFAULT_SONNET_MODEL: models.sonnet,
|
ANTHROPIC_DEFAULT_SONNET_MODEL: models.sonnet,
|
||||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: models.haiku,
|
ANTHROPIC_DEFAULT_HAIKU_MODEL: models.haiku,
|
||||||
|
...(extraModels && extraModels.length > 0
|
||||||
|
? { ANTHROPIC_EXTRA_MODELS: extraModels.join(',') }
|
||||||
|
: {}),
|
||||||
CCS_DROID_PROVIDER: droidProvider,
|
CCS_DROID_PROVIDER: droidProvider,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -240,7 +248,8 @@ export function createApiProfile(
|
|||||||
apiKey: string,
|
apiKey: string,
|
||||||
models: ModelMapping,
|
models: ModelMapping,
|
||||||
target: TargetType = 'claude',
|
target: TargetType = 'claude',
|
||||||
provider?: string
|
provider?: string,
|
||||||
|
extraModels?: string[]
|
||||||
): CreateApiProfileResult {
|
): CreateApiProfileResult {
|
||||||
try {
|
try {
|
||||||
const deniedReason = getDeniedModelReason(baseUrl, models);
|
const deniedReason = getDeniedModelReason(baseUrl, models);
|
||||||
@@ -251,9 +260,9 @@ export function createApiProfile(
|
|||||||
const settingsFile = `~/.ccs/${name}.settings.json`;
|
const settingsFile = `~/.ccs/${name}.settings.json`;
|
||||||
|
|
||||||
if (isUnifiedMode()) {
|
if (isUnifiedMode()) {
|
||||||
createApiProfileUnified(name, baseUrl, apiKey, models, target, provider);
|
createApiProfileUnified(name, baseUrl, apiKey, models, target, provider, extraModels);
|
||||||
} else {
|
} else {
|
||||||
createSettingsFile(name, baseUrl, apiKey, models, provider);
|
createSettingsFile(name, baseUrl, apiKey, models, provider, extraModels);
|
||||||
updateLegacyConfig(name, target);
|
updateLegacyConfig(name, target);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -149,6 +149,21 @@ export function mapProfileToClaudeKey(profile: SyncableProfile): ClaudeKey | nul
|
|||||||
alias: '',
|
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;
|
return claudeKey;
|
||||||
|
|||||||
@@ -493,7 +493,7 @@ export async function handleApiCreateCommand(args: string[]): Promise<void> {
|
|||||||
|
|
||||||
console.log('');
|
console.log('');
|
||||||
console.log(info('Creating API profile...'));
|
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) {
|
if (!result.success) {
|
||||||
console.log(fail(`Failed to create API profile: ${result.error}`));
|
console.log(fail(`Failed to create API profile: ${result.error}`));
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
@@ -534,6 +534,10 @@ export async function handleApiCreateCommand(args: string[]): Promise<void> {
|
|||||||
` Haiku: ${finalModels.haiku}`;
|
` Haiku: ${finalModels.haiku}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (parsedArgs.extraModels && parsedArgs.extraModels.length > 0) {
|
||||||
|
details += `\nExtraModels: ${parsedArgs.extraModels.join(', ')}`;
|
||||||
|
}
|
||||||
|
|
||||||
console.log('');
|
console.log('');
|
||||||
console.log(infoBox(details, 'API Profile Created'));
|
console.log(infoBox(details, 'API Profile Created'));
|
||||||
if (hasClaudeMappings) {
|
if (hasClaudeMappings) {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export interface ApiCommandArgs {
|
|||||||
baseUrl?: string;
|
baseUrl?: string;
|
||||||
apiKey?: string;
|
apiKey?: string;
|
||||||
model?: string;
|
model?: string;
|
||||||
|
extraModels?: string[];
|
||||||
preset?: string;
|
preset?: string;
|
||||||
cliproxyProvider?: string;
|
cliproxyProvider?: string;
|
||||||
target?: TargetType;
|
target?: TargetType;
|
||||||
@@ -33,6 +34,7 @@ export const API_VALUE_FLAGS = [
|
|||||||
'--base-url',
|
'--base-url',
|
||||||
'--api-key',
|
'--api-key',
|
||||||
'--model',
|
'--model',
|
||||||
|
'--extra-models',
|
||||||
'--preset',
|
'--preset',
|
||||||
'--cliproxy-provider',
|
'--cliproxy-provider',
|
||||||
'--target',
|
'--target',
|
||||||
@@ -222,6 +224,21 @@ export function parseApiCommandArgs(
|
|||||||
true
|
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 = applyRepeatedOption(
|
||||||
remaining,
|
remaining,
|
||||||
['--preset'],
|
['--preset'],
|
||||||
|
|||||||
@@ -144,6 +144,7 @@ router.post('/', (req: Request, res: Response): void => {
|
|||||||
'baseUrl',
|
'baseUrl',
|
||||||
'apiKey',
|
'apiKey',
|
||||||
'model',
|
'model',
|
||||||
|
'extraModels',
|
||||||
'opusModel',
|
'opusModel',
|
||||||
'sonnetModel',
|
'sonnetModel',
|
||||||
'haikuModel',
|
'haikuModel',
|
||||||
@@ -156,7 +157,7 @@ router.post('/', (req: Request, res: Response): void => {
|
|||||||
return;
|
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 providerHint = req.body?.droidProvider ?? req.body?.provider;
|
||||||
const parsedProvider = normalizeDroidProvider(providerHint);
|
const parsedProvider = normalizeDroidProvider(providerHint);
|
||||||
const normalizedBaseUrl = typeof baseUrl === 'string' ? baseUrl.trim() : '';
|
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
|
// 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(
|
const result = createApiProfile(
|
||||||
name,
|
name,
|
||||||
normalizedBaseUrl,
|
normalizedBaseUrl,
|
||||||
@@ -209,7 +218,8 @@ router.post('/', (req: Request, res: Response): void => {
|
|||||||
haiku: haikuModel || model || '',
|
haiku: haikuModel || model || '',
|
||||||
},
|
},
|
||||||
parsedTarget || 'claude',
|
parsedTarget || 'claude',
|
||||||
parsedProvider || undefined
|
parsedProvider || undefined,
|
||||||
|
parsedExtraModels
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
@@ -409,6 +419,7 @@ router.put('/:name', (req: Request, res: Response): void => {
|
|||||||
'baseUrl',
|
'baseUrl',
|
||||||
'apiKey',
|
'apiKey',
|
||||||
'model',
|
'model',
|
||||||
|
'extraModels',
|
||||||
'opusModel',
|
'opusModel',
|
||||||
'sonnetModel',
|
'sonnetModel',
|
||||||
'haikuModel',
|
'haikuModel',
|
||||||
@@ -422,7 +433,7 @@ router.put('/:name', (req: Request, res: Response): void => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { name } = req.params;
|
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 providerHint = req.body?.droidProvider ?? req.body?.provider;
|
||||||
const parsedProvider = normalizeDroidProvider(providerHint);
|
const parsedProvider = normalizeDroidProvider(providerHint);
|
||||||
const normalizedBaseUrl = typeof baseUrl === 'string' ? baseUrl.trim() : baseUrl;
|
const normalizedBaseUrl = typeof baseUrl === 'string' ? baseUrl.trim() : baseUrl;
|
||||||
@@ -457,6 +468,7 @@ router.put('/:name', (req: Request, res: Response): void => {
|
|||||||
baseUrl !== undefined ||
|
baseUrl !== undefined ||
|
||||||
apiKey !== undefined ||
|
apiKey !== undefined ||
|
||||||
model !== undefined ||
|
model !== undefined ||
|
||||||
|
extraModels !== undefined ||
|
||||||
opusModel !== undefined ||
|
opusModel !== undefined ||
|
||||||
sonnetModel !== undefined ||
|
sonnetModel !== undefined ||
|
||||||
haikuModel !== undefined ||
|
haikuModel !== undefined ||
|
||||||
@@ -473,6 +485,7 @@ router.put('/:name', (req: Request, res: Response): void => {
|
|||||||
baseUrl: normalizedBaseUrl,
|
baseUrl: normalizedBaseUrl,
|
||||||
apiKey: normalizedApiKey,
|
apiKey: normalizedApiKey,
|
||||||
model,
|
model,
|
||||||
|
extraModels,
|
||||||
opusModel,
|
opusModel,
|
||||||
sonnetModel,
|
sonnetModel,
|
||||||
haikuModel,
|
haikuModel,
|
||||||
|
|||||||
@@ -218,6 +218,7 @@ export function updateSettingsFile(
|
|||||||
baseUrl?: string;
|
baseUrl?: string;
|
||||||
apiKey?: string;
|
apiKey?: string;
|
||||||
model?: string;
|
model?: string;
|
||||||
|
extraModels?: string;
|
||||||
opusModel?: string;
|
opusModel?: string;
|
||||||
sonnetModel?: string;
|
sonnetModel?: string;
|
||||||
haikuModel?: 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 (
|
if (
|
||||||
updates.provider !== undefined ||
|
updates.provider !== undefined ||
|
||||||
updates.baseUrl !== undefined ||
|
updates.baseUrl !== undefined ||
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ const schema = z.object({
|
|||||||
baseUrl: optionalUrlSchema,
|
baseUrl: optionalUrlSchema,
|
||||||
apiKey: z.string().min(10, 'API key must be at least 10 characters'),
|
apiKey: z.string().min(10, 'API key must be at least 10 characters'),
|
||||||
model: z.string().optional(),
|
model: z.string().optional(),
|
||||||
|
extraModels: z.string().optional(),
|
||||||
opusModel: z.string().optional(),
|
opusModel: z.string().optional(),
|
||||||
sonnetModel: z.string().optional(),
|
sonnetModel: z.string().optional(),
|
||||||
haikuModel: z.string().optional(),
|
haikuModel: z.string().optional(),
|
||||||
@@ -67,6 +68,7 @@ export function ProfileDialog({ open, onClose, profile }: ProfileDialogProps) {
|
|||||||
baseUrl: '',
|
baseUrl: '',
|
||||||
apiKey: '',
|
apiKey: '',
|
||||||
model: '',
|
model: '',
|
||||||
|
extraModels: '',
|
||||||
opusModel: '',
|
opusModel: '',
|
||||||
sonnetModel: '',
|
sonnetModel: '',
|
||||||
haikuModel: '',
|
haikuModel: '',
|
||||||
@@ -102,6 +104,7 @@ export function ProfileDialog({ open, onClose, profile }: ProfileDialogProps) {
|
|||||||
baseUrl: data.baseUrl,
|
baseUrl: data.baseUrl,
|
||||||
apiKey: data.apiKey,
|
apiKey: data.apiKey,
|
||||||
model: data.model,
|
model: data.model,
|
||||||
|
extraModels: data.extraModels,
|
||||||
opusModel: data.opusModel,
|
opusModel: data.opusModel,
|
||||||
sonnetModel: data.sonnetModel,
|
sonnetModel: data.sonnetModel,
|
||||||
haikuModel: data.haikuModel,
|
haikuModel: data.haikuModel,
|
||||||
@@ -165,6 +168,19 @@ export function ProfileDialog({ open, onClose, profile }: ProfileDialogProps) {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</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 */}
|
{/* Model Mapping Section */}
|
||||||
<div className="border rounded-md">
|
<div className="border rounded-md">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -336,6 +336,7 @@ export interface CreateProfile {
|
|||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
model?: string;
|
model?: string;
|
||||||
|
extraModels?: string;
|
||||||
opusModel?: string;
|
opusModel?: string;
|
||||||
sonnetModel?: string;
|
sonnetModel?: string;
|
||||||
haikuModel?: string;
|
haikuModel?: string;
|
||||||
@@ -346,6 +347,7 @@ export interface UpdateProfile {
|
|||||||
baseUrl?: string;
|
baseUrl?: string;
|
||||||
apiKey?: string;
|
apiKey?: string;
|
||||||
model?: string;
|
model?: string;
|
||||||
|
extraModels?: string;
|
||||||
opusModel?: string;
|
opusModel?: string;
|
||||||
sonnetModel?: string;
|
sonnetModel?: string;
|
||||||
haikuModel?: string;
|
haikuModel?: string;
|
||||||
|
|||||||
Reference in new issue
Block a user