mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 02:11:28 +00:00
refactor(presets): centralize shared provider preset catalog
- add shared preset catalog used by CLI and Dashboard - enforce alias/id integrity and canonical normalization - replace duplicated preset definitions in service and UI layers - allow Vite dev fs access to repo root for shared catalog imports
This commit is contained in:
@@ -36,9 +36,12 @@ export { pickOpenRouterModel, type OpenRouterSelection } from './openrouter-pick
|
||||
// Provider presets for CLI
|
||||
export {
|
||||
PROVIDER_PRESETS,
|
||||
PRESET_ALIASES,
|
||||
OPENROUTER_BASE_URL,
|
||||
getPresetById,
|
||||
getPresetAliases,
|
||||
getPresetIds,
|
||||
isValidPresetId,
|
||||
type ProviderPreset,
|
||||
type PresetCategory,
|
||||
} from './provider-presets';
|
||||
|
||||
@@ -2,177 +2,33 @@
|
||||
* Provider Presets for CLI
|
||||
*
|
||||
* Pre-configured templates for common API providers.
|
||||
* Mirrors the UI presets in ui/src/lib/provider-presets.ts
|
||||
* Uses shared source-of-truth catalog in src/shared/provider-preset-catalog.ts.
|
||||
*/
|
||||
|
||||
import { resolveAliasToCanonical } from '../../utils/profile-compat';
|
||||
import {
|
||||
OPENROUTER_BASE_URL,
|
||||
PROVIDER_PRESET_ALIASES,
|
||||
createProviderPresetDefinitions,
|
||||
normalizeProviderPresetId,
|
||||
type PresetCategory,
|
||||
type ProviderPresetDefinition,
|
||||
} from '../../shared/provider-preset-catalog';
|
||||
|
||||
export type PresetCategory = 'recommended' | 'alternative';
|
||||
|
||||
export interface ProviderPreset {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
baseUrl: string;
|
||||
defaultProfileName: string;
|
||||
defaultModel: string;
|
||||
apiKeyPlaceholder: string;
|
||||
apiKeyHint: string;
|
||||
category: PresetCategory;
|
||||
/** Whether API key is required (default: true, set false for local providers) */
|
||||
requiresApiKey: boolean;
|
||||
/** 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';
|
||||
export { OPENROUTER_BASE_URL };
|
||||
export type { PresetCategory };
|
||||
export type ProviderPreset = ProviderPresetDefinition;
|
||||
|
||||
/**
|
||||
* Provider presets available via CLI and UI
|
||||
*
|
||||
* NOTE: Keep in sync with ui/src/lib/provider-presets.ts
|
||||
*/
|
||||
export const PROVIDER_PRESETS: ProviderPreset[] = [
|
||||
// Recommended
|
||||
{
|
||||
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,
|
||||
},
|
||||
{
|
||||
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,
|
||||
},
|
||||
// Alternative providers
|
||||
{
|
||||
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,
|
||||
},
|
||||
{
|
||||
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,
|
||||
},
|
||||
{
|
||||
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,
|
||||
},
|
||||
{
|
||||
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,
|
||||
},
|
||||
{
|
||||
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,
|
||||
},
|
||||
{
|
||||
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,
|
||||
},
|
||||
{
|
||||
id: 'qwen',
|
||||
name: 'Qwen',
|
||||
description: 'Alibaba Cloud - Qwen3 models (256K-1M context, thinking support)',
|
||||
baseUrl: 'https://dashscope-intl.aliyuncs.com/apps/anthropic',
|
||||
defaultProfileName: 'qwen',
|
||||
defaultModel: 'qwen3-coder-plus',
|
||||
apiKeyPlaceholder: 'sk-...',
|
||||
apiKeyHint: 'Get your API key from Alibaba Cloud Model Studio',
|
||||
category: 'alternative',
|
||||
requiresApiKey: true,
|
||||
},
|
||||
{
|
||||
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,
|
||||
},
|
||||
];
|
||||
export const PROVIDER_PRESETS: readonly ProviderPreset[] = Object.freeze(
|
||||
createProviderPresetDefinitions()
|
||||
);
|
||||
export const PRESET_ALIASES: Readonly<Record<string, string>> = PROVIDER_PRESET_ALIASES;
|
||||
|
||||
/** Get preset by ID */
|
||||
export function getPresetById(id: string): ProviderPreset | undefined {
|
||||
const normalized = id.trim().toLowerCase();
|
||||
const canonical = resolveAliasToCanonical(normalized).toLowerCase();
|
||||
const canonical = normalizeProviderPresetId(id);
|
||||
return PROVIDER_PRESETS.find((p) => p.id === canonical);
|
||||
}
|
||||
|
||||
@@ -181,6 +37,11 @@ export function getPresetIds(): string[] {
|
||||
return PROVIDER_PRESETS.map((p) => p.id);
|
||||
}
|
||||
|
||||
/** Get alias map (alias -> canonical preset ID). */
|
||||
export function getPresetAliases(): Readonly<Record<string, string>> {
|
||||
return PRESET_ALIASES;
|
||||
}
|
||||
|
||||
/** Check if preset ID is valid */
|
||||
export function isValidPresetId(id: string): boolean {
|
||||
return getPresetById(id) !== undefined;
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
/**
|
||||
* 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 = [
|
||||
'openrouter',
|
||||
'ollama',
|
||||
'glm',
|
||||
'glmt',
|
||||
'km',
|
||||
'foundry',
|
||||
'mm',
|
||||
'deepseek',
|
||||
'qwen',
|
||||
'ollama-cloud',
|
||||
] 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',
|
||||
});
|
||||
|
||||
const RAW_PROVIDER_PRESET_DEFINITIONS: readonly ProviderPresetDefinition[] = [
|
||||
{
|
||||
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: '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: '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',
|
||||
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',
|
||||
},
|
||||
];
|
||||
|
||||
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;
|
||||
}
|
||||
+52
-180
@@ -1,199 +1,71 @@
|
||||
/**
|
||||
* Provider Presets Configuration
|
||||
* Pre-configured templates for common API providers
|
||||
* Shared catalog from backend source-of-truth with UI-only presentation overrides.
|
||||
*/
|
||||
|
||||
export type PresetCategory = 'recommended' | 'alternative';
|
||||
import {
|
||||
OPENROUTER_BASE_URL,
|
||||
createProviderPresetDefinitions,
|
||||
normalizeProviderPresetId,
|
||||
type PresetCategory,
|
||||
type ProviderPresetDefinition,
|
||||
type ProviderPresetId,
|
||||
} from '../../../src/shared/provider-preset-catalog';
|
||||
|
||||
export interface ProviderPreset {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
baseUrl: string;
|
||||
defaultProfileName: string;
|
||||
badge?: string;
|
||||
featured?: boolean;
|
||||
icon?: string;
|
||||
defaultModel?: string;
|
||||
requiresApiKey: boolean;
|
||||
apiKeyPlaceholder: string;
|
||||
apiKeyHint?: string;
|
||||
category: PresetCategory;
|
||||
/** Additional env vars for thinking mode, etc. */
|
||||
extraEnv?: Record<string, string>;
|
||||
/** Enable always thinking mode */
|
||||
alwaysThinkingEnabled?: boolean;
|
||||
export { OPENROUTER_BASE_URL };
|
||||
export type { PresetCategory };
|
||||
|
||||
export type ProviderPreset = ProviderPresetDefinition;
|
||||
|
||||
/**
|
||||
* UI-only overrides for presentation details that differ from CLI semantics.
|
||||
* Keep this tiny; provider data itself belongs in shared catalog.
|
||||
*/
|
||||
type UiPresetOverride = Pick<ProviderPreset, 'apiKeyPlaceholder'>;
|
||||
|
||||
const UI_PRESET_OVERRIDES = Object.freeze({
|
||||
ollama: {
|
||||
apiKeyPlaceholder: '',
|
||||
},
|
||||
}) satisfies Readonly<Partial<Record<ProviderPresetId, UiPresetOverride>>>;
|
||||
|
||||
function withUiOverrides(preset: ProviderPresetDefinition): ProviderPreset {
|
||||
const overrides = UI_PRESET_OVERRIDES[preset.id];
|
||||
return overrides ? { ...preset, ...overrides } : { ...preset };
|
||||
}
|
||||
|
||||
export const OPENROUTER_BASE_URL = 'https://openrouter.ai/api';
|
||||
const BASE_PROVIDER_PRESETS = createProviderPresetDefinitions();
|
||||
|
||||
export const PROVIDER_PRESETS: ProviderPreset[] = [
|
||||
// Recommended - OpenRouter
|
||||
{
|
||||
id: 'openrouter',
|
||||
name: 'OpenRouter',
|
||||
description: '349+ models from OpenAI, Anthropic, Google, Meta',
|
||||
baseUrl: OPENROUTER_BASE_URL,
|
||||
defaultProfileName: 'openrouter',
|
||||
badge: '349+ models',
|
||||
featured: true,
|
||||
icon: '/icons/openrouter.svg',
|
||||
defaultModel: 'anthropic/claude-opus-4.5',
|
||||
requiresApiKey: true,
|
||||
apiKeyPlaceholder: 'sk-or-...',
|
||||
apiKeyHint: 'Get your API key at openrouter.ai/keys',
|
||||
category: 'recommended',
|
||||
},
|
||||
// Recommended - Ollama (Local)
|
||||
{
|
||||
id: 'ollama',
|
||||
name: 'Ollama (Local)',
|
||||
description: 'Local open-source models via Ollama (32K+ context)',
|
||||
baseUrl: 'http://localhost:11434',
|
||||
defaultProfileName: 'ollama',
|
||||
badge: 'Local',
|
||||
featured: true,
|
||||
icon: '/icons/ollama.svg',
|
||||
defaultModel: 'qwen3-coder',
|
||||
requiresApiKey: false,
|
||||
apiKeyPlaceholder: '',
|
||||
apiKeyHint: 'No API key required for local Ollama',
|
||||
category: 'recommended',
|
||||
},
|
||||
// Alternative providers - GLM/GLMT/Kimi
|
||||
{
|
||||
id: 'glm',
|
||||
name: 'GLM',
|
||||
description: 'Claude via Z.AI',
|
||||
baseUrl: 'https://api.z.ai/api/anthropic',
|
||||
defaultProfileName: 'glm',
|
||||
badge: 'Z.AI',
|
||||
icon: '/icons/zai.svg',
|
||||
defaultModel: 'glm-5',
|
||||
requiresApiKey: true,
|
||||
apiKeyPlaceholder: 'ghp_...',
|
||||
apiKeyHint: 'Get your API key from Z.AI',
|
||||
category: 'alternative',
|
||||
},
|
||||
{
|
||||
id: 'glmt',
|
||||
name: 'GLMT',
|
||||
description: 'GLM with Thinking mode support',
|
||||
baseUrl: 'https://api.z.ai/api/coding/paas/v4/chat/completions',
|
||||
defaultProfileName: 'glmt',
|
||||
badge: 'Thinking',
|
||||
icon: '/icons/zai.svg',
|
||||
defaultModel: 'glm-5',
|
||||
requiresApiKey: true,
|
||||
apiKeyPlaceholder: 'ghp_...',
|
||||
apiKeyHint: 'Same API key as GLM',
|
||||
category: 'alternative',
|
||||
},
|
||||
{
|
||||
id: 'km',
|
||||
name: 'Kimi',
|
||||
description: 'Moonshot AI - Fast reasoning model',
|
||||
baseUrl: 'https://api.kimi.com/coding/',
|
||||
defaultProfileName: 'km',
|
||||
badge: 'Reasoning',
|
||||
icon: '/icons/kimi.svg',
|
||||
defaultModel: 'kimi-k2-thinking-turbo',
|
||||
requiresApiKey: true,
|
||||
apiKeyPlaceholder: 'sk-...',
|
||||
apiKeyHint: 'Get your API key from Moonshot AI',
|
||||
category: 'alternative',
|
||||
alwaysThinkingEnabled: true,
|
||||
},
|
||||
{
|
||||
id: 'foundry',
|
||||
name: 'Azure Foundry',
|
||||
description: 'Claude via Microsoft Azure AI Foundry',
|
||||
baseUrl: 'https://<your-resource>.services.ai.azure.com/api/anthropic',
|
||||
defaultProfileName: 'foundry',
|
||||
badge: 'Azure',
|
||||
icon: '/icons/azure.svg',
|
||||
defaultModel: 'claude-sonnet-4-5',
|
||||
requiresApiKey: true,
|
||||
apiKeyPlaceholder: 'YOUR_AZURE_API_KEY',
|
||||
apiKeyHint: 'Create resource at ai.azure.com, get API key from Keys tab',
|
||||
category: 'alternative',
|
||||
},
|
||||
{
|
||||
id: 'mm',
|
||||
name: 'Minimax',
|
||||
description: 'M2.1/M2.1-lightning/M2 - multilang coding (1M context)',
|
||||
baseUrl: 'https://api.minimax.io/anthropic',
|
||||
defaultProfileName: 'mm',
|
||||
badge: '1M context',
|
||||
icon: '/icons/minimax.svg',
|
||||
defaultModel: 'MiniMax-M2.1',
|
||||
requiresApiKey: true,
|
||||
apiKeyPlaceholder: 'YOUR_MINIMAX_API_KEY_HERE',
|
||||
apiKeyHint: 'Get your API key at platform.minimax.io',
|
||||
category: 'alternative',
|
||||
},
|
||||
{
|
||||
id: 'deepseek',
|
||||
name: 'DeepSeek',
|
||||
description: 'V3.2 and R1 reasoning model (128K context)',
|
||||
baseUrl: 'https://api.deepseek.com/anthropic',
|
||||
defaultProfileName: 'deepseek',
|
||||
badge: 'Reasoning',
|
||||
icon: '/icons/deepseek.svg',
|
||||
defaultModel: 'deepseek-chat',
|
||||
requiresApiKey: true,
|
||||
apiKeyPlaceholder: 'sk-...',
|
||||
apiKeyHint: 'Get your API key at platform.deepseek.com',
|
||||
category: 'alternative',
|
||||
},
|
||||
{
|
||||
id: 'qwen',
|
||||
name: 'Qwen',
|
||||
description: 'Alibaba Cloud - Qwen3 models (256K-1M context, thinking support)',
|
||||
baseUrl: 'https://dashscope-intl.aliyuncs.com/apps/anthropic',
|
||||
defaultProfileName: 'qwen',
|
||||
badge: 'Alibaba',
|
||||
icon: '/assets/providers/qwen-color.svg',
|
||||
defaultModel: 'qwen3-coder-plus',
|
||||
requiresApiKey: true,
|
||||
apiKeyPlaceholder: 'sk-...',
|
||||
apiKeyHint: 'Get your API key from Alibaba Cloud Model Studio',
|
||||
category: 'alternative',
|
||||
},
|
||||
{
|
||||
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',
|
||||
badge: 'Cloud',
|
||||
icon: '/icons/ollama.svg',
|
||||
defaultModel: 'glm-5:cloud',
|
||||
requiresApiKey: true,
|
||||
apiKeyPlaceholder: 'YOUR_OLLAMA_CLOUD_API_KEY',
|
||||
apiKeyHint: 'Get your API key at ollama.com',
|
||||
category: 'alternative',
|
||||
},
|
||||
];
|
||||
|
||||
const LEGACY_PRESET_ALIASES: Readonly<Record<string, string>> = Object.freeze({
|
||||
kimi: 'km',
|
||||
});
|
||||
export const PROVIDER_PRESETS: readonly ProviderPreset[] = Object.freeze(
|
||||
BASE_PROVIDER_PRESETS.map(withUiOverrides)
|
||||
);
|
||||
|
||||
/** Get presets by category */
|
||||
export function getPresetsByCategory(category: PresetCategory): ProviderPreset[] {
|
||||
return PROVIDER_PRESETS.filter((p) => p.category === category);
|
||||
return PROVIDER_PRESETS.filter((preset) => preset.category === category);
|
||||
}
|
||||
|
||||
/** Get preset by ID */
|
||||
/** Get preset by ID (supports legacy aliases via shared alias map). */
|
||||
export function getPresetById(id: string): ProviderPreset | undefined {
|
||||
const normalized = id.trim().toLowerCase();
|
||||
const canonical = LEGACY_PRESET_ALIASES[normalized] || normalized;
|
||||
return PROVIDER_PRESETS.find((p) => p.id.toLowerCase() === canonical);
|
||||
const canonical = normalizeProviderPresetId(id);
|
||||
return PROVIDER_PRESETS.find((preset) => preset.id === canonical);
|
||||
}
|
||||
|
||||
/** Check if a URL matches a known preset */
|
||||
export function detectPresetFromUrl(baseUrl: string): ProviderPreset | undefined {
|
||||
const normalizedUrl = baseUrl.toLowerCase().trim();
|
||||
return PROVIDER_PRESETS.find((p) => normalizedUrl.includes(p.baseUrl.toLowerCase()));
|
||||
const normalizedInput = baseUrl.trim().toLowerCase().replace(/\/+$/, '');
|
||||
if (!normalizedInput) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return PROVIDER_PRESETS.find((preset) => {
|
||||
const normalizedPresetUrl = preset.baseUrl.trim().toLowerCase().replace(/\/+$/, '');
|
||||
if (!normalizedPresetUrl) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
normalizedInput === normalizedPresetUrl ||
|
||||
normalizedInput.startsWith(`${normalizedPresetUrl}/`)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,6 +3,9 @@ import react from '@vitejs/plugin-react';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import path from 'path';
|
||||
|
||||
const UI_ROOT = __dirname;
|
||||
const REPO_ROOT = path.resolve(__dirname, '..');
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
@@ -54,6 +57,9 @@ export default defineConfig({
|
||||
},
|
||||
server: {
|
||||
port: 5173,
|
||||
fs: {
|
||||
allow: [UI_ROOT, REPO_ROOT],
|
||||
},
|
||||
proxy: {
|
||||
'/api': 'http://localhost:3000',
|
||||
'/ws': {
|
||||
|
||||
Reference in New Issue
Block a user