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:
Tam Nhu Tran
2026-02-20 23:24:11 +07:00
parent e32192177b
commit 21d6754ec6
5 changed files with 363 additions and 341 deletions
+3
View File
@@ -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';
+22 -161
View File
@@ -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;
+280
View File
@@ -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
View File
@@ -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}/`)
);
});
}
+6
View File
@@ -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': {