mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 20:17:45 +00:00
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:
@@ -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';
|
||||
|
||||
@@ -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
@@ -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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
}
|
||||
}
|
||||
|
||||
// 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<void> {
|
||||
// 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<void> {
|
||||
console.log(` ${color('remove <name>', 'command')} Remove an API profile`);
|
||||
console.log('');
|
||||
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('--api-key <key>', 'command')} API key (create)`);
|
||||
console.log(` ${color('--model <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('');
|
||||
|
||||
Reference in New Issue
Block a user