mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 08:15:17 +00:00
feat(cliproxy): add composite variant CLI wizard
- Add --composite flag to 'ccs cliproxy variant create' - Implement selectTierConfig() interactive prompt per tier - Add default tier selection after all tiers configured - Display composite label in auth list and variant remove - Show per-tier details in remove confirmation - Update help text with --composite usage Refs #506
This commit is contained in:
1 parent
44b30b8753
commit
4926c200f2
3 files changed
+158
-3
No files matched your search
@@ -43,8 +43,9 @@ export async function handleList(): Promise<void> {
|
|||||||
console.log(subheader('Custom Variants'));
|
console.log(subheader('Custom Variants'));
|
||||||
const rows = variantNames.map((name) => {
|
const rows = variantNames.map((name) => {
|
||||||
const variant = variants[name];
|
const variant = variants[name];
|
||||||
|
const providerDisplay = variant.type === 'composite' ? 'composite' : variant.provider;
|
||||||
const portStr = variant.port ? String(variant.port) : '-';
|
const portStr = variant.port ? String(variant.port) : '-';
|
||||||
return [name, variant.provider, portStr, variant.settings || '-'];
|
return [name, providerDisplay, portStr, variant.settings || '-'];
|
||||||
});
|
});
|
||||||
console.log(
|
console.log(
|
||||||
table(rows, { head: ['Variant', 'Provider', 'Port', 'Settings'], colWidths: [15, 12, 8, 30] })
|
table(rows, { head: ['Variant', 'Provider', 'Port', 'Settings'], colWidths: [15, 12, 8, 30] })
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export async function showHelp(): Promise<void> {
|
|||||||
'Profile Commands:',
|
'Profile Commands:',
|
||||||
[
|
[
|
||||||
['create [name]', 'Create new CLIProxy variant profile'],
|
['create [name]', 'Create new CLIProxy variant profile'],
|
||||||
|
['create --composite', 'Create composite variant (mix providers per tier)'],
|
||||||
['list', 'List all CLIProxy variant profiles'],
|
['list', 'List all CLIProxy variant profiles'],
|
||||||
['remove <name>', 'Remove a CLIProxy variant profile'],
|
['remove <name>', 'Remove a CLIProxy variant profile'],
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -20,9 +20,11 @@ import {
|
|||||||
variantExists,
|
variantExists,
|
||||||
listVariants,
|
listVariants,
|
||||||
createVariant,
|
createVariant,
|
||||||
|
createCompositeVariant,
|
||||||
removeVariant,
|
removeVariant,
|
||||||
} from '../../cliproxy/services';
|
} from '../../cliproxy/services';
|
||||||
import { DEFAULT_BACKEND } from '../../cliproxy/platform-detector';
|
import { DEFAULT_BACKEND } from '../../cliproxy/platform-detector';
|
||||||
|
import { CompositeTierConfig } from '../../config/unified-config-types';
|
||||||
|
|
||||||
interface CliproxyProfileArgs {
|
interface CliproxyProfileArgs {
|
||||||
name?: string;
|
name?: string;
|
||||||
@@ -31,6 +33,7 @@ interface CliproxyProfileArgs {
|
|||||||
account?: string;
|
account?: string;
|
||||||
force?: boolean;
|
force?: boolean;
|
||||||
yes?: boolean;
|
yes?: boolean;
|
||||||
|
composite?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseProfileArgs(args: string[]): CliproxyProfileArgs {
|
function parseProfileArgs(args: string[]): CliproxyProfileArgs {
|
||||||
@@ -47,6 +50,8 @@ function parseProfileArgs(args: string[]): CliproxyProfileArgs {
|
|||||||
result.force = true;
|
result.force = true;
|
||||||
} else if (arg === '--yes' || arg === '-y') {
|
} else if (arg === '--yes' || arg === '-y') {
|
||||||
result.yes = true;
|
result.yes = true;
|
||||||
|
} else if (arg === '--composite') {
|
||||||
|
result.composite = true;
|
||||||
} else if (!arg.startsWith('-') && !result.name) {
|
} else if (!arg.startsWith('-') && !result.name) {
|
||||||
result.name = arg;
|
result.name = arg;
|
||||||
}
|
}
|
||||||
@@ -68,6 +73,71 @@ function getBackendLabel(backend: CLIProxyBackend): string {
|
|||||||
return backend === 'plus' ? 'CLIProxy Plus' : 'CLIProxy';
|
return backend === 'plus' ? 'CLIProxy Plus' : 'CLIProxy';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interactive prompt to select provider + model for a single tier.
|
||||||
|
* Returns a CompositeTierConfig, or null if user cancelled auth.
|
||||||
|
*/
|
||||||
|
async function selectTierConfig(
|
||||||
|
tierName: string,
|
||||||
|
verbose: boolean
|
||||||
|
): Promise<CompositeTierConfig | null> {
|
||||||
|
console.log(header(`${tierName.charAt(0).toUpperCase() + tierName.slice(1)} Tier`));
|
||||||
|
|
||||||
|
// Select provider
|
||||||
|
const providerOptions = CLIPROXY_PROFILES.map((p) => ({
|
||||||
|
id: p,
|
||||||
|
label: p.charAt(0).toUpperCase() + p.slice(1),
|
||||||
|
}));
|
||||||
|
const provider = (await InteractivePrompt.selectFromList(
|
||||||
|
`Provider for ${tierName}:`,
|
||||||
|
providerOptions
|
||||||
|
)) as CLIProxyProfileName;
|
||||||
|
|
||||||
|
// Check auth
|
||||||
|
const providerAccounts = getProviderAccounts(provider as CLIProxyProvider);
|
||||||
|
if (providerAccounts.length === 0) {
|
||||||
|
console.log('');
|
||||||
|
console.log(warn(`No accounts authenticated for ${provider}`));
|
||||||
|
const shouldAuth = await InteractivePrompt.confirm(`Authenticate with ${provider} now?`, {
|
||||||
|
default: true,
|
||||||
|
});
|
||||||
|
if (!shouldAuth) {
|
||||||
|
console.log(info(`Skipping auth. Run: ${color(`ccs ${provider} --auth`, 'command')}`));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const newAccount = await triggerOAuth(provider as CLIProxyProvider, {
|
||||||
|
add: true,
|
||||||
|
verbose,
|
||||||
|
});
|
||||||
|
if (!newAccount) {
|
||||||
|
console.log(fail('Authentication failed'));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
console.log(ok(`Authenticated as ${newAccount.email || newAccount.id}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select model
|
||||||
|
let model: string | undefined;
|
||||||
|
if (supportsModelConfig(provider as CLIProxyProvider)) {
|
||||||
|
const catalog = getProviderCatalog(provider as CLIProxyProvider);
|
||||||
|
if (catalog) {
|
||||||
|
const modelOptions = catalog.models.map((m) => ({ id: m.id, label: formatModelOption(m) }));
|
||||||
|
const defaultIdx = catalog.models.findIndex((m) => m.id === catalog.defaultModel);
|
||||||
|
model = await InteractivePrompt.selectFromList(`Model for ${tierName}:`, modelOptions, {
|
||||||
|
defaultIndex: defaultIdx >= 0 ? defaultIdx : 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!model) {
|
||||||
|
model = await InteractivePrompt.input(`Model name for ${tierName}`, {
|
||||||
|
validate: (val) => (val ? null : 'Model is required'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
return { provider, model };
|
||||||
|
}
|
||||||
|
|
||||||
export async function handleCreate(
|
export async function handleCreate(
|
||||||
args: string[],
|
args: string[],
|
||||||
backend: CLIProxyBackend = DEFAULT_BACKEND
|
backend: CLIProxyBackend = DEFAULT_BACKEND
|
||||||
@@ -97,6 +167,76 @@ export async function handleCreate(
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clean up old variant if --force is set
|
||||||
|
if (variantExists(name) && parsedArgs.force) {
|
||||||
|
removeVariant(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Composite mode: select provider+model per tier
|
||||||
|
if (parsedArgs.composite) {
|
||||||
|
console.log(info('Composite variant — select provider and model for each tier'));
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
const verbose = args.includes('--verbose');
|
||||||
|
const opus = await selectTierConfig('opus', verbose);
|
||||||
|
if (!opus) {
|
||||||
|
return; // User cancelled auth
|
||||||
|
}
|
||||||
|
const sonnet = await selectTierConfig('sonnet', verbose);
|
||||||
|
if (!sonnet) {
|
||||||
|
return; // User cancelled auth
|
||||||
|
}
|
||||||
|
const haiku = await selectTierConfig('haiku', verbose);
|
||||||
|
if (!haiku) {
|
||||||
|
return; // User cancelled auth
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select default tier
|
||||||
|
const tierOptions = [
|
||||||
|
{ id: 'opus' as const, label: `Opus (${opus.provider}: ${opus.model})` },
|
||||||
|
{ id: 'sonnet' as const, label: `Sonnet (${sonnet.provider}: ${sonnet.model})` },
|
||||||
|
{ id: 'haiku' as const, label: `Haiku (${haiku.provider}: ${haiku.model})` },
|
||||||
|
];
|
||||||
|
const defaultTier = (await InteractivePrompt.selectFromList(
|
||||||
|
'Default tier (ANTHROPIC_MODEL):',
|
||||||
|
tierOptions
|
||||||
|
)) as 'opus' | 'sonnet' | 'haiku';
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
console.log(info(`Creating composite ${getBackendLabel(backend)} variant...`));
|
||||||
|
const result = createCompositeVariant({
|
||||||
|
name,
|
||||||
|
defaultTier,
|
||||||
|
tiers: { opus, sonnet, haiku },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
console.log(fail(`Failed to create composite variant: ${result.error}`));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
const tiers = result.variant?.tiers;
|
||||||
|
const tierSummary = tiers
|
||||||
|
? `Opus: ${tiers.opus.provider} / ${tiers.opus.model}\n` +
|
||||||
|
`Sonnet: ${tiers.sonnet.provider} / ${tiers.sonnet.model}\n` +
|
||||||
|
`Haiku: ${tiers.haiku.provider} / ${tiers.haiku.model}\n` +
|
||||||
|
`Default: ${defaultTier}`
|
||||||
|
: '';
|
||||||
|
const portInfo = result.variant?.port ? `\nPort: ${result.variant.port}` : '';
|
||||||
|
console.log(
|
||||||
|
infoBox(
|
||||||
|
`Variant: ${name} (composite)\n${tierSummary}${portInfo}\nConfig: ~/.ccs/config.yaml`,
|
||||||
|
'Composite Variant Created'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
console.log('');
|
||||||
|
console.log(header('Usage'));
|
||||||
|
console.log(` ${color(`ccs ${name} "your prompt"`, 'command')}`);
|
||||||
|
console.log('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Step 2: Provider selection
|
// Step 2: Provider selection
|
||||||
let provider = parsedArgs.provider;
|
let provider = parsedArgs.provider;
|
||||||
if (!provider) {
|
if (!provider) {
|
||||||
@@ -260,7 +400,11 @@ export async function handleRemove(args: string[]): Promise<void> {
|
|||||||
console.log(header('Remove CLIProxy Variant'));
|
console.log(header('Remove CLIProxy Variant'));
|
||||||
console.log('');
|
console.log('');
|
||||||
console.log('Available variants:');
|
console.log('Available variants:');
|
||||||
variantNames.forEach((n, i) => console.log(` ${i + 1}. ${n} (${variants[n].provider})`));
|
variantNames.forEach((n, i) => {
|
||||||
|
const v = variants[n];
|
||||||
|
const label = v.type === 'composite' ? 'composite' : v.provider;
|
||||||
|
console.log(` ${i + 1}. ${n} (${label})`);
|
||||||
|
});
|
||||||
console.log('');
|
console.log('');
|
||||||
name = await InteractivePrompt.input('Variant name to remove', {
|
name = await InteractivePrompt.input('Variant name to remove', {
|
||||||
validate: (val) => {
|
validate: (val) => {
|
||||||
@@ -282,7 +426,16 @@ export async function handleRemove(args: string[]): Promise<void> {
|
|||||||
const variant = variants[name];
|
const variant = variants[name];
|
||||||
console.log('');
|
console.log('');
|
||||||
console.log(`Variant '${color(name, 'command')}' will be removed.`);
|
console.log(`Variant '${color(name, 'command')}' will be removed.`);
|
||||||
console.log(` Provider: ${variant.provider}`);
|
if (variant.type === 'composite') {
|
||||||
|
console.log(` Type: composite`);
|
||||||
|
if (variant.tiers) {
|
||||||
|
console.log(` Opus: ${variant.tiers.opus.provider} / ${variant.tiers.opus.model}`);
|
||||||
|
console.log(` Sonnet: ${variant.tiers.sonnet.provider} / ${variant.tiers.sonnet.model}`);
|
||||||
|
console.log(` Haiku: ${variant.tiers.haiku.provider} / ${variant.tiers.haiku.model}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(` Provider: ${variant.provider}`);
|
||||||
|
}
|
||||||
if (variant.port) {
|
if (variant.port) {
|
||||||
console.log(` Port: ${variant.port}`);
|
console.log(` Port: ${variant.port}`);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user