feat(cli): add --preset option to ccs api create command

- add provider-presets.ts with OpenRouter/GLM/GLMT/Kimi configs
- add --preset flag for quick profile creation
- auto-fill name, base URL, model from preset
- show preset info and API key hints
- update help with preset documentation and examples
This commit is contained in:
kaitranntt
2025-12-20 19:49:50 -05:00
parent 05380e21b4
commit 418d121577
3 changed files with 171 additions and 24 deletions
+10
View File
@@ -32,3 +32,13 @@ export { createApiProfile, removeApiProfile } from './profile-writer';
// OpenRouter catalog and picker // OpenRouter catalog and picker
export { isOpenRouterUrl, fetchOpenRouterModels, type OpenRouterModel } from './openrouter-catalog'; export { isOpenRouterUrl, fetchOpenRouterModels, type OpenRouterModel } from './openrouter-catalog';
export { pickOpenRouterModel, type OpenRouterSelection } from './openrouter-picker'; export { pickOpenRouterModel, type OpenRouterSelection } from './openrouter-picker';
// Provider presets for CLI
export {
PROVIDER_PRESETS,
OPENROUTER_BASE_URL,
getPresetById,
getPresetIds,
isValidPresetId,
type ProviderPreset,
} from './provider-presets';
+96
View File
@@ -0,0 +1,96 @@
/**
* Provider Presets for CLI
*
* Pre-configured templates for common API providers.
* Mirrors the UI presets in ui/src/lib/provider-presets.ts
*/
export interface ProviderPreset {
id: string;
name: string;
description: string;
baseUrl: string;
defaultProfileName: string;
defaultModel: string;
apiKeyPlaceholder: string;
apiKeyHint: string;
/** Additional env vars for thinking mode, etc. */
extraEnv?: Record<string, string>;
/** Enable always thinking mode */
alwaysThinkingEnabled?: boolean;
}
export const OPENROUTER_BASE_URL = 'https://openrouter.ai/api/v1';
/**
* Provider presets available via CLI and UI
*
* NOTE: Keep in sync with ui/src/lib/provider-presets.ts
*/
export const PROVIDER_PRESETS: ProviderPreset[] = [
{
id: 'openrouter',
name: 'OpenRouter',
description: '349+ models from OpenAI, Anthropic, Google, Meta',
baseUrl: OPENROUTER_BASE_URL,
defaultProfileName: 'openrouter',
defaultModel: 'anthropic/claude-sonnet-4',
apiKeyPlaceholder: 'sk-or-...',
apiKeyHint: 'Get your API key at openrouter.ai/keys',
},
{
id: 'glm',
name: 'GLM',
description: 'Claude via Z.AI (GitHub Copilot)',
baseUrl: 'https://api.z.ai/api/anthropic',
defaultProfileName: 'glm',
defaultModel: 'glm-4.6',
apiKeyPlaceholder: 'ghp_...',
apiKeyHint: 'Get your API key from Z.AI',
},
{
id: 'glmt',
name: 'GLMT',
description: 'GLM with Thinking mode support',
baseUrl: 'https://api.z.ai/api/coding/paas/v4/chat/completions',
defaultProfileName: 'glmt',
defaultModel: 'glm-4.6',
apiKeyPlaceholder: 'ghp_...',
apiKeyHint: 'Same API key as GLM',
extraEnv: {
ANTHROPIC_TEMPERATURE: '0.2',
ANTHROPIC_MAX_TOKENS: '65536',
MAX_THINKING_TOKENS: '32768',
ENABLE_STREAMING: 'true',
ANTHROPIC_SAFE_MODE: 'false',
API_TIMEOUT_MS: '3000000',
},
alwaysThinkingEnabled: true,
},
{
id: 'kimi',
name: 'Kimi',
description: 'Moonshot AI - Fast reasoning model',
baseUrl: 'https://api.kimi.com/coding/',
defaultProfileName: 'kimi',
defaultModel: 'kimi-k2-thinking-turbo',
apiKeyPlaceholder: 'sk-...',
apiKeyHint: 'Get your API key from Moonshot AI',
alwaysThinkingEnabled: true,
},
];
/** Get preset by ID */
export function getPresetById(id: string): ProviderPreset | undefined {
return PROVIDER_PRESETS.find((p) => p.id === id.toLowerCase());
}
/** Get all preset IDs */
export function getPresetIds(): string[] {
return PROVIDER_PRESETS.map((p) => p.id);
}
/** Check if preset ID is valid */
export function isValidPresetId(id: string): boolean {
return getPresetById(id) !== undefined;
}
+65 -24
View File
@@ -35,6 +35,8 @@ import {
isUsingUnifiedConfig, isUsingUnifiedConfig,
isOpenRouterUrl, isOpenRouterUrl,
pickOpenRouterModel, pickOpenRouterModel,
getPresetById,
getPresetIds,
type ModelMapping, type ModelMapping,
} from '../api/services'; } from '../api/services';
@@ -43,6 +45,7 @@ interface ApiCommandArgs {
baseUrl?: string; baseUrl?: string;
apiKey?: string; apiKey?: string;
model?: string; model?: string;
preset?: string;
force?: boolean; force?: boolean;
yes?: boolean; yes?: boolean;
} }
@@ -60,6 +63,8 @@ function parseArgs(args: string[]): ApiCommandArgs {
result.apiKey = args[++i]; result.apiKey = args[++i];
} else if (arg === '--model' && args[i + 1]) { } else if (arg === '--model' && args[i + 1]) {
result.model = args[++i]; result.model = args[++i];
} else if (arg === '--preset' && args[i + 1]) {
result.preset = args[++i];
} else if (arg === '--force') { } else if (arg === '--force') {
result.force = true; result.force = true;
} else if (arg === '--yes' || arg === '-y') { } else if (arg === '--yes' || arg === '-y') {
@@ -80,8 +85,18 @@ async function handleCreate(args: string[]): Promise<void> {
console.log(header('Create API Profile')); console.log(header('Create API Profile'));
console.log(''); console.log('');
// Step 1: API name // Handle --preset option for quick provider setup
let name = parsedArgs.name; const preset = parsedArgs.preset ? getPresetById(parsedArgs.preset) : null;
if (parsedArgs.preset && !preset) {
console.log(fail(`Unknown preset: ${parsedArgs.preset}`));
console.log('');
console.log('Available presets:');
getPresetIds().forEach((id) => console.log(` - ${id}`));
process.exit(1);
}
// Step 1: API name (use preset default if --preset provided)
let name = parsedArgs.name || preset?.defaultProfileName;
if (!name) { if (!name) {
name = await InteractivePrompt.input('API name', { name = await InteractivePrompt.input('API name', {
validate: validateApiName, validate: validateApiName,
@@ -101,14 +116,15 @@ async function handleCreate(args: string[]): Promise<void> {
process.exit(1); process.exit(1);
} }
// Step 2: Base URL // Step 2: Base URL (use preset if provided)
let baseUrl = parsedArgs.baseUrl; let baseUrl = parsedArgs.baseUrl || preset?.baseUrl;
if (!baseUrl) { if (!baseUrl) {
baseUrl = await InteractivePrompt.input( baseUrl = await InteractivePrompt.input(
'API Base URL (e.g., https://api.example.com/v1 - without /chat/completions)', 'API Base URL (e.g., https://api.example.com/v1 - without /chat/completions)',
{ validate: validateUrl } { validate: validateUrl }
); );
} else { } else if (!preset) {
// Only validate custom URLs, not preset URLs
const error = validateUrl(baseUrl); const error = validateUrl(baseUrl);
if (error) { if (error) {
console.log(fail(error)); console.log(fail(error));
@@ -116,20 +132,28 @@ async function handleCreate(args: string[]): Promise<void> {
} }
} }
// Check for common URL mistakes and warn // Check for common URL mistakes and warn (skip for presets)
const urlWarning = getUrlWarning(baseUrl); if (!preset) {
if (urlWarning) { const urlWarning = getUrlWarning(baseUrl);
console.log(''); if (urlWarning) {
console.log(warn(urlWarning)); console.log('');
const continueAnyway = await InteractivePrompt.confirm('Continue with this URL anyway?', { console.log(warn(urlWarning));
default: false, const continueAnyway = await InteractivePrompt.confirm('Continue with this URL anyway?', {
}); default: false,
if (!continueAnyway) {
baseUrl = await InteractivePrompt.input('API Base URL', {
validate: validateUrl,
default: sanitizeBaseUrl(baseUrl),
}); });
if (!continueAnyway) {
baseUrl = await InteractivePrompt.input('API Base URL', {
validate: validateUrl,
default: sanitizeBaseUrl(baseUrl),
});
}
} }
} else {
// Show preset info
console.log(info(`Using preset: ${preset.name}`));
console.log(dim(` ${preset.description}`));
console.log(dim(` Base URL: ${preset.baseUrl}`));
console.log('');
} }
// OpenRouter detection: offer interactive model picker // OpenRouter detection: offer interactive model picker
@@ -160,31 +184,33 @@ async function handleCreate(args: string[]): Promise<void> {
// Step 3: API Key // Step 3: API Key
let apiKey = parsedArgs.apiKey; let apiKey = parsedArgs.apiKey;
if (!apiKey) { if (!apiKey) {
apiKey = await InteractivePrompt.password('API Key'); const keyPrompt = preset?.apiKeyHint ? `API Key (${preset.apiKeyHint})` : 'API Key';
apiKey = await InteractivePrompt.password(keyPrompt);
if (!apiKey) { if (!apiKey) {
console.log(fail('API key is required')); console.log(fail('API key is required'));
process.exit(1); process.exit(1);
} }
} }
// Step 4: Model configuration // Step 4: Model configuration (use preset default if available)
const defaultModel = 'claude-sonnet-4-5-20250929'; const defaultModel = preset?.defaultModel || 'claude-sonnet-4-5-20250929';
let model = parsedArgs.model || openRouterModel; let model = parsedArgs.model || openRouterModel || preset?.defaultModel;
if (!model && !parsedArgs.yes) { if (!model && !parsedArgs.yes && !preset) {
model = await InteractivePrompt.input('Default model (ANTHROPIC_MODEL)', { model = await InteractivePrompt.input('Default model (ANTHROPIC_MODEL)', {
default: defaultModel, default: defaultModel,
}); });
} }
model = model || defaultModel; model = model || defaultModel;
// Step 5: Model mapping for Opus/Sonnet/Haiku // Step 5: Model mapping for Opus/Sonnet/Haiku (skip prompt for presets with --yes)
let opusModel = openRouterTierMapping?.opus || model; let opusModel = openRouterTierMapping?.opus || model;
let sonnetModel = openRouterTierMapping?.sonnet || model; let sonnetModel = openRouterTierMapping?.sonnet || model;
let haikuModel = openRouterTierMapping?.haiku || model; let haikuModel = openRouterTierMapping?.haiku || model;
const isCustomModel = model !== defaultModel; const isCustomModel = model !== defaultModel;
const hasOpenRouterTierMapping = openRouterTierMapping !== undefined; const hasOpenRouterTierMapping = openRouterTierMapping !== undefined;
const hasPreset = preset !== null;
if (!parsedArgs.yes && !hasOpenRouterTierMapping) { if (!parsedArgs.yes && !hasOpenRouterTierMapping && !hasPreset) {
let wantCustomMapping = isCustomModel; let wantCustomMapping = isCustomModel;
if (!isCustomModel) { if (!isCustomModel) {
@@ -401,16 +427,31 @@ async function showHelp(): Promise<void> {
console.log(` ${color('remove <name>', 'command')} Remove an API profile`); console.log(` ${color('remove <name>', 'command')} Remove an API profile`);
console.log(''); console.log('');
console.log(subheader('Options')); console.log(subheader('Options'));
console.log(
` ${color('--preset <id>', 'command')} Use provider preset (openrouter, glm, glmt, kimi)`
);
console.log(` ${color('--base-url <url>', 'command')} API base URL (create)`); console.log(` ${color('--base-url <url>', 'command')} API base URL (create)`);
console.log(` ${color('--api-key <key>', 'command')} API key (create)`); console.log(` ${color('--api-key <key>', 'command')} API key (create)`);
console.log(` ${color('--model <model>', 'command')} Default model (create)`); console.log(` ${color('--model <model>', 'command')} Default model (create)`);
console.log(` ${color('--force', 'command')} Overwrite existing (create)`); console.log(` ${color('--force', 'command')} Overwrite existing (create)`);
console.log(` ${color('--yes, -y', 'command')} Skip confirmation prompts`); console.log(` ${color('--yes, -y', 'command')} Skip confirmation prompts`);
console.log(''); console.log('');
console.log(subheader('Provider Presets'));
console.log(
` ${color('openrouter', 'command')} OpenRouter - 349+ models (Claude, GPT, Gemini, Llama)`
);
console.log(` ${color('glm', 'command')} GLM - Claude via Z.AI (GitHub Copilot)`);
console.log(` ${color('glmt', 'command')} GLMT - GLM with Thinking mode`);
console.log(` ${color('kimi', 'command')} Kimi - Moonshot AI reasoning model`);
console.log('');
console.log(subheader('Examples')); console.log(subheader('Examples'));
console.log(` ${dim('# Interactive wizard')}`); console.log(` ${dim('# Interactive wizard')}`);
console.log(` ${color('ccs api create', 'command')}`); console.log(` ${color('ccs api create', 'command')}`);
console.log(''); console.log('');
console.log(` ${dim('# Quick setup with preset')}`);
console.log(` ${color('ccs api create --preset openrouter', 'command')}`);
console.log(` ${color('ccs api create --preset glm', 'command')}`);
console.log('');
console.log(` ${dim('# Create with name')}`); console.log(` ${dim('# Create with name')}`);
console.log(` ${color('ccs api create myapi', 'command')}`); console.log(` ${color('ccs api create myapi', 'command')}`);
console.log(''); console.log('');