mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-11 12:09:03 +00:00
344 lines
10 KiB
TypeScript
344 lines
10 KiB
TypeScript
/**
|
|
* Shared provider preset catalog for CLI + Dashboard.
|
|
*
|
|
* Keep this file runtime-agnostic (no Node/browser APIs) so both
|
|
* backend and UI can import the same source of truth.
|
|
*/
|
|
|
|
export type PresetCategory = 'recommended' | 'alternative';
|
|
|
|
export const PROVIDER_PRESET_IDS = [
|
|
'anthropic',
|
|
'openrouter',
|
|
'alibaba-coding-plan',
|
|
'ollama',
|
|
'llamacpp',
|
|
'glm',
|
|
'glmt',
|
|
'km',
|
|
'foundry',
|
|
'mm',
|
|
'deepseek',
|
|
'qwen',
|
|
'ollama-cloud',
|
|
'novita',
|
|
] as const;
|
|
|
|
export type ProviderPresetId = (typeof PROVIDER_PRESET_IDS)[number];
|
|
|
|
export interface ProviderPresetDefinition {
|
|
id: ProviderPresetId;
|
|
name: string;
|
|
description: string;
|
|
baseUrl: string;
|
|
defaultProfileName: string;
|
|
defaultModel: string;
|
|
apiKeyPlaceholder: string;
|
|
apiKeyHint: string;
|
|
category: PresetCategory;
|
|
requiresApiKey: boolean;
|
|
/** Additional env vars for thinking mode, etc. */
|
|
extraEnv?: Record<string, string>;
|
|
/** Enable always thinking mode. */
|
|
alwaysThinkingEnabled?: boolean;
|
|
/** UI metadata */
|
|
badge?: string;
|
|
featured?: boolean;
|
|
icon?: string;
|
|
}
|
|
|
|
export const OPENROUTER_BASE_URL = 'https://openrouter.ai/api';
|
|
|
|
/**
|
|
* Legacy aliases mapped to canonical preset IDs.
|
|
* Keep this minimal and explicit to avoid hidden implicit behavior.
|
|
*/
|
|
export const PROVIDER_PRESET_ALIASES: Readonly<Record<string, ProviderPresetId>> = Object.freeze({
|
|
kimi: 'km',
|
|
alibaba: 'alibaba-coding-plan',
|
|
acp: 'alibaba-coding-plan',
|
|
});
|
|
|
|
const RAW_PROVIDER_PRESET_DEFINITIONS: readonly ProviderPresetDefinition[] = [
|
|
{
|
|
id: 'anthropic',
|
|
name: 'Anthropic (Direct API)',
|
|
description: 'Use your own Anthropic API key (sk-ant-...)',
|
|
baseUrl: '',
|
|
defaultProfileName: 'anthropic',
|
|
defaultModel: 'claude-sonnet-4-5-20250929',
|
|
apiKeyPlaceholder: 'sk-ant-api03-...',
|
|
apiKeyHint: 'Get key at console.anthropic.com/settings/keys',
|
|
category: 'recommended',
|
|
requiresApiKey: true,
|
|
badge: 'Direct',
|
|
featured: true,
|
|
},
|
|
{
|
|
id: 'openrouter',
|
|
name: 'OpenRouter',
|
|
description: '349+ models from OpenAI, Anthropic, Google, Meta',
|
|
baseUrl: OPENROUTER_BASE_URL,
|
|
defaultProfileName: 'openrouter',
|
|
defaultModel: 'anthropic/claude-opus-4.5',
|
|
apiKeyPlaceholder: 'sk-or-...',
|
|
apiKeyHint: 'Get your API key at openrouter.ai/keys',
|
|
category: 'recommended',
|
|
requiresApiKey: true,
|
|
badge: '349+ models',
|
|
featured: true,
|
|
icon: '/icons/openrouter.svg',
|
|
},
|
|
{
|
|
id: 'alibaba-coding-plan',
|
|
name: 'Alibaba Coding Plan',
|
|
description: 'Alibaba Cloud Coding Plan via Anthropic-compatible endpoint',
|
|
baseUrl: 'https://coding-intl.dashscope.aliyuncs.com/apps/anthropic',
|
|
defaultProfileName: 'albb',
|
|
defaultModel: 'qwen3-coder-plus',
|
|
apiKeyPlaceholder: 'sk-sp-...',
|
|
apiKeyHint: 'Get your Coding Plan key from Alibaba Cloud Model Studio',
|
|
category: 'recommended',
|
|
requiresApiKey: true,
|
|
badge: 'Coding Plan',
|
|
featured: true,
|
|
icon: '/assets/providers/alibabacloud-color.svg',
|
|
},
|
|
{
|
|
id: 'ollama',
|
|
name: 'Ollama (Local)',
|
|
description: 'Local open-source models via Ollama (32K+ context)',
|
|
baseUrl: 'http://localhost:11434',
|
|
defaultProfileName: 'ollama',
|
|
defaultModel: 'qwen3-coder',
|
|
apiKeyPlaceholder: 'ollama',
|
|
apiKeyHint: 'Install Ollama from ollama.com - no API key needed for local',
|
|
category: 'recommended',
|
|
requiresApiKey: false,
|
|
badge: 'Local',
|
|
featured: true,
|
|
icon: '/icons/ollama.svg',
|
|
},
|
|
{
|
|
id: 'llamacpp',
|
|
name: 'llama.cpp (Local)',
|
|
description: 'Local inference via llama.cpp (LLaMA models)',
|
|
baseUrl: 'http://127.0.0.1:8080',
|
|
defaultProfileName: 'llamacpp',
|
|
defaultModel: 'llama3-8b',
|
|
apiKeyPlaceholder: 'llamacpp',
|
|
apiKeyHint: 'Run llama.cpp server: ./server --host 0.0.0.0 --port 8080 -m model.gguf',
|
|
category: 'recommended',
|
|
requiresApiKey: false,
|
|
badge: 'Local',
|
|
featured: true,
|
|
},
|
|
{
|
|
id: 'glm',
|
|
name: 'GLM',
|
|
description: 'Claude via Z.AI',
|
|
baseUrl: 'https://api.z.ai/api/anthropic',
|
|
defaultProfileName: 'glm',
|
|
defaultModel: 'glm-5',
|
|
apiKeyPlaceholder: 'ghp_...',
|
|
apiKeyHint: 'Get your API key from Z.AI',
|
|
category: 'alternative',
|
|
requiresApiKey: true,
|
|
badge: 'Z.AI',
|
|
icon: '/icons/zai.svg',
|
|
},
|
|
{
|
|
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-5',
|
|
apiKeyPlaceholder: 'ghp_...',
|
|
apiKeyHint: 'Same API key as GLM',
|
|
category: 'alternative',
|
|
requiresApiKey: true,
|
|
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,
|
|
badge: 'Thinking',
|
|
icon: '/icons/zai.svg',
|
|
},
|
|
{
|
|
id: 'km',
|
|
name: 'Kimi',
|
|
description: 'Moonshot AI - Fast reasoning model',
|
|
baseUrl: 'https://api.kimi.com/coding/',
|
|
defaultProfileName: 'km',
|
|
defaultModel: 'kimi-k2-thinking-turbo',
|
|
apiKeyPlaceholder: 'sk-...',
|
|
apiKeyHint: 'Get your API key from Moonshot AI',
|
|
category: 'alternative',
|
|
requiresApiKey: true,
|
|
alwaysThinkingEnabled: true,
|
|
badge: 'Reasoning',
|
|
icon: '/icons/kimi.svg',
|
|
},
|
|
{
|
|
id: 'foundry',
|
|
name: 'Azure Foundry',
|
|
description: 'Claude via Microsoft Azure AI Foundry',
|
|
baseUrl: 'https://<your-resource>.services.ai.azure.com/api/anthropic',
|
|
defaultProfileName: 'foundry',
|
|
defaultModel: 'claude-sonnet-4-5',
|
|
apiKeyPlaceholder: 'YOUR_AZURE_API_KEY',
|
|
apiKeyHint: 'Create resource at ai.azure.com, get API key from Keys tab',
|
|
category: 'alternative',
|
|
requiresApiKey: true,
|
|
badge: 'Azure',
|
|
icon: '/icons/azure.svg',
|
|
},
|
|
{
|
|
id: 'mm',
|
|
name: 'Minimax',
|
|
description: 'M2.1/M2.1-lightning/M2 - multilang coding (1M context)',
|
|
baseUrl: 'https://api.minimax.io/anthropic',
|
|
defaultProfileName: 'mm',
|
|
defaultModel: 'MiniMax-M2.1',
|
|
apiKeyPlaceholder: 'YOUR_MINIMAX_API_KEY_HERE',
|
|
apiKeyHint: 'Get your API key at platform.minimax.io',
|
|
category: 'alternative',
|
|
requiresApiKey: true,
|
|
badge: '1M context',
|
|
icon: '/icons/minimax.svg',
|
|
},
|
|
{
|
|
id: 'deepseek',
|
|
name: 'DeepSeek',
|
|
description: 'V3.2 and R1 reasoning model (128K context)',
|
|
baseUrl: 'https://api.deepseek.com/anthropic',
|
|
defaultProfileName: 'deepseek',
|
|
defaultModel: 'deepseek-chat',
|
|
apiKeyPlaceholder: 'sk-...',
|
|
apiKeyHint: 'Get your API key at platform.deepseek.com',
|
|
category: 'alternative',
|
|
requiresApiKey: true,
|
|
badge: 'Reasoning',
|
|
icon: '/icons/deepseek.svg',
|
|
},
|
|
{
|
|
id: 'qwen',
|
|
name: 'Qwen',
|
|
description: 'Alibaba Cloud - Qwen3 models (256K-1M context, thinking support)',
|
|
baseUrl: 'https://dashscope-intl.aliyuncs.com/apps/anthropic',
|
|
defaultProfileName: 'qwen-api',
|
|
defaultModel: 'qwen3-coder-plus',
|
|
apiKeyPlaceholder: 'sk-...',
|
|
apiKeyHint: 'Get your API key from Alibaba Cloud Model Studio',
|
|
category: 'alternative',
|
|
requiresApiKey: true,
|
|
badge: 'Alibaba',
|
|
icon: '/assets/providers/qwen-color.svg',
|
|
},
|
|
{
|
|
id: 'ollama-cloud',
|
|
name: 'Ollama Cloud',
|
|
description: 'Ollama cloud models via direct API (glm-5:cloud, minimax-m2.1:cloud)',
|
|
baseUrl: 'https://ollama.com',
|
|
defaultProfileName: 'ollama-cloud',
|
|
defaultModel: 'glm-5:cloud',
|
|
apiKeyPlaceholder: 'YOUR_OLLAMA_CLOUD_API_KEY',
|
|
apiKeyHint: 'Get your API key at ollama.com',
|
|
category: 'alternative',
|
|
requiresApiKey: true,
|
|
badge: 'Cloud',
|
|
icon: '/icons/ollama.svg',
|
|
},
|
|
{
|
|
id: 'novita',
|
|
name: 'Novita AI',
|
|
description: 'Anthropic-compatible API for Claude Code and CCS profiles',
|
|
baseUrl: 'https://api.novita.ai/anthropic',
|
|
defaultProfileName: 'novita',
|
|
defaultModel: 'deepseek/deepseek-v3.2',
|
|
apiKeyPlaceholder: 'YOUR_NOVITA_API_KEY',
|
|
apiKeyHint: 'Get your API key at novita.ai',
|
|
category: 'alternative',
|
|
requiresApiKey: true,
|
|
badge: 'Anthropic-compatible',
|
|
icon: '/icons/novita.svg',
|
|
},
|
|
];
|
|
|
|
function clonePresetDefinition(preset: ProviderPresetDefinition): ProviderPresetDefinition {
|
|
return {
|
|
...preset,
|
|
extraEnv: preset.extraEnv ? { ...preset.extraEnv } : undefined,
|
|
};
|
|
}
|
|
|
|
function freezePresetDefinition(preset: ProviderPresetDefinition): ProviderPresetDefinition {
|
|
const cloned = clonePresetDefinition(preset);
|
|
if (cloned.extraEnv) {
|
|
Object.freeze(cloned.extraEnv);
|
|
}
|
|
return Object.freeze(cloned);
|
|
}
|
|
|
|
function assertProviderPresetCatalogIntegrity(
|
|
definitions: readonly ProviderPresetDefinition[],
|
|
aliases: Readonly<Record<string, ProviderPresetId>>
|
|
): void {
|
|
const presetIdSet = new Set<string>();
|
|
for (const definition of definitions) {
|
|
const normalizedId = definition.id.trim().toLowerCase();
|
|
if (definition.id !== normalizedId) {
|
|
throw new Error(`Preset ID must be normalized: "${definition.id}"`);
|
|
}
|
|
if (presetIdSet.has(definition.id)) {
|
|
throw new Error(`Duplicate preset ID detected: "${definition.id}"`);
|
|
}
|
|
presetIdSet.add(definition.id);
|
|
}
|
|
|
|
const normalizedAliasSet = new Set<string>();
|
|
for (const [alias, target] of Object.entries(aliases)) {
|
|
const normalizedAlias = alias.trim().toLowerCase();
|
|
if (!normalizedAlias) {
|
|
throw new Error('Preset alias keys cannot be empty');
|
|
}
|
|
if (alias !== normalizedAlias) {
|
|
throw new Error(`Preset alias must be normalized: "${alias}"`);
|
|
}
|
|
if (normalizedAliasSet.has(normalizedAlias)) {
|
|
throw new Error(`Duplicate normalized preset alias detected: "${alias}"`);
|
|
}
|
|
normalizedAliasSet.add(normalizedAlias);
|
|
|
|
if (!presetIdSet.has(target)) {
|
|
throw new Error(`Preset alias "${alias}" points to unknown target "${target}"`);
|
|
}
|
|
if (presetIdSet.has(normalizedAlias)) {
|
|
throw new Error(
|
|
`Preset alias "${alias}" collides with canonical preset ID "${normalizedAlias}"`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
assertProviderPresetCatalogIntegrity(RAW_PROVIDER_PRESET_DEFINITIONS, PROVIDER_PRESET_ALIASES);
|
|
|
|
export const PROVIDER_PRESET_DEFINITIONS: readonly ProviderPresetDefinition[] = Object.freeze(
|
|
RAW_PROVIDER_PRESET_DEFINITIONS.map(freezePresetDefinition)
|
|
);
|
|
|
|
export function createProviderPresetDefinitions(): ProviderPresetDefinition[] {
|
|
return PROVIDER_PRESET_DEFINITIONS.map(clonePresetDefinition);
|
|
}
|
|
|
|
export function normalizeProviderPresetId(id: string): string {
|
|
const normalized = id.trim().toLowerCase();
|
|
return PROVIDER_PRESET_ALIASES[normalized] || normalized;
|
|
}
|