From 418d121577098722a35b060a37388ea2d267dffd Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Sat, 20 Dec 2025 19:49:50 -0500 Subject: [PATCH] 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 --- src/api/services/index.ts | 10 +++ src/api/services/provider-presets.ts | 96 ++++++++++++++++++++++++++++ src/commands/api-command.ts | 89 +++++++++++++++++++------- 3 files changed, 171 insertions(+), 24 deletions(-) create mode 100644 src/api/services/provider-presets.ts diff --git a/src/api/services/index.ts b/src/api/services/index.ts index f23cdcc9..c6ee337e 100644 --- a/src/api/services/index.ts +++ b/src/api/services/index.ts @@ -32,3 +32,13 @@ export { createApiProfile, removeApiProfile } from './profile-writer'; // OpenRouter catalog and picker export { isOpenRouterUrl, fetchOpenRouterModels, type OpenRouterModel } from './openrouter-catalog'; 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'; diff --git a/src/api/services/provider-presets.ts b/src/api/services/provider-presets.ts new file mode 100644 index 00000000..6b0411d2 --- /dev/null +++ b/src/api/services/provider-presets.ts @@ -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; + /** 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; +} diff --git a/src/commands/api-command.ts b/src/commands/api-command.ts index 165ecb1b..fee28347 100644 --- a/src/commands/api-command.ts +++ b/src/commands/api-command.ts @@ -35,6 +35,8 @@ import { isUsingUnifiedConfig, isOpenRouterUrl, pickOpenRouterModel, + getPresetById, + getPresetIds, type ModelMapping, } from '../api/services'; @@ -43,6 +45,7 @@ interface ApiCommandArgs { baseUrl?: string; apiKey?: string; model?: string; + preset?: string; force?: boolean; yes?: boolean; } @@ -60,6 +63,8 @@ function parseArgs(args: string[]): ApiCommandArgs { result.apiKey = args[++i]; } else if (arg === '--model' && args[i + 1]) { result.model = args[++i]; + } else if (arg === '--preset' && args[i + 1]) { + result.preset = args[++i]; } else if (arg === '--force') { result.force = true; } else if (arg === '--yes' || arg === '-y') { @@ -80,8 +85,18 @@ async function handleCreate(args: string[]): Promise { console.log(header('Create API Profile')); console.log(''); - // Step 1: API name - let name = parsedArgs.name; + // Handle --preset option for quick provider setup + 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) { name = await InteractivePrompt.input('API name', { validate: validateApiName, @@ -101,14 +116,15 @@ async function handleCreate(args: string[]): Promise { process.exit(1); } - // Step 2: Base URL - let baseUrl = parsedArgs.baseUrl; + // Step 2: Base URL (use preset if provided) + let baseUrl = parsedArgs.baseUrl || preset?.baseUrl; if (!baseUrl) { baseUrl = await InteractivePrompt.input( 'API Base URL (e.g., https://api.example.com/v1 - without /chat/completions)', { validate: validateUrl } ); - } else { + } else if (!preset) { + // Only validate custom URLs, not preset URLs const error = validateUrl(baseUrl); if (error) { console.log(fail(error)); @@ -116,20 +132,28 @@ async function handleCreate(args: string[]): Promise { } } - // Check for common URL mistakes and warn - const urlWarning = getUrlWarning(baseUrl); - if (urlWarning) { - console.log(''); - console.log(warn(urlWarning)); - 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), + // Check for common URL mistakes and warn (skip for presets) + if (!preset) { + const urlWarning = getUrlWarning(baseUrl); + if (urlWarning) { + console.log(''); + console.log(warn(urlWarning)); + 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), + }); + } } + } 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 @@ -160,31 +184,33 @@ async function handleCreate(args: string[]): Promise { // Step 3: API Key let apiKey = parsedArgs.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) { console.log(fail('API key is required')); process.exit(1); } } - // Step 4: Model configuration - const defaultModel = 'claude-sonnet-4-5-20250929'; - let model = parsedArgs.model || openRouterModel; - if (!model && !parsedArgs.yes) { + // Step 4: Model configuration (use preset default if available) + const defaultModel = preset?.defaultModel || 'claude-sonnet-4-5-20250929'; + let model = parsedArgs.model || openRouterModel || preset?.defaultModel; + if (!model && !parsedArgs.yes && !preset) { model = await InteractivePrompt.input('Default model (ANTHROPIC_MODEL)', { default: 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 sonnetModel = openRouterTierMapping?.sonnet || model; let haikuModel = openRouterTierMapping?.haiku || model; const isCustomModel = model !== defaultModel; const hasOpenRouterTierMapping = openRouterTierMapping !== undefined; + const hasPreset = preset !== null; - if (!parsedArgs.yes && !hasOpenRouterTierMapping) { + if (!parsedArgs.yes && !hasOpenRouterTierMapping && !hasPreset) { let wantCustomMapping = isCustomModel; if (!isCustomModel) { @@ -401,16 +427,31 @@ async function showHelp(): Promise { console.log(` ${color('remove ', 'command')} Remove an API profile`); console.log(''); console.log(subheader('Options')); + console.log( + ` ${color('--preset ', 'command')} Use provider preset (openrouter, glm, glmt, kimi)` + ); console.log(` ${color('--base-url ', 'command')} API base URL (create)`); console.log(` ${color('--api-key ', 'command')} API key (create)`); console.log(` ${color('--model ', 'command')} Default model (create)`); console.log(` ${color('--force', 'command')} Overwrite existing (create)`); console.log(` ${color('--yes, -y', 'command')} Skip confirmation prompts`); 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(` ${dim('# Interactive wizard')}`); console.log(` ${color('ccs api create', 'command')}`); 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(` ${color('ccs api create myapi', 'command')}`); console.log('');