mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-10-07 02:07:47 +08:00
The Ollama SDK appends /chat to the base URL, and Ollama serves chat at /api/chat. The model list already added the missing /api, so with "http://localhost:11434" (the address our docs showed) models were listed but every chat request went to /chat and got a 404. An OpenAI-style ".../v1" address went to /v1/chat. ollamaApiUrl() turns the server address, ".../v1" and ".../api" into ".../api". Chat, the model list and the settings dialog's request URL hint all use it. The docs now show http://localhost:11434/api, which also works on released versions.
656 lines
19 KiB
TypeScript
656 lines
19 KiB
TypeScript
// Types for multi-provider model configuration
|
|
|
|
export type ProviderName =
|
|
| "openai"
|
|
| "anthropic"
|
|
| "google"
|
|
| "vertexai"
|
|
| "azure"
|
|
| "bedrock"
|
|
| "ollama"
|
|
| "openrouter"
|
|
| "aihubmix"
|
|
| "deepseek"
|
|
| "siliconflow"
|
|
| "sglang"
|
|
| "gateway"
|
|
| "edgeone"
|
|
| "doubao"
|
|
| "modelscope"
|
|
| "glm"
|
|
| "qwen"
|
|
| "qiniu"
|
|
| "kimi"
|
|
| "minimax"
|
|
| "novita"
|
|
| "mimo"
|
|
| "atlascloud"
|
|
|
|
// Individual model configuration
|
|
export interface ModelConfig {
|
|
id: string // UUID for this model
|
|
modelId: string // e.g., "gpt-4o", "claude-sonnet-4-5"
|
|
validated?: boolean // Has this model been validated
|
|
validationError?: string // Error message if validation failed
|
|
validationWarning?: string // Passed, but e.g. did not call a tool
|
|
responseTime?: number // Milliseconds the last test took
|
|
}
|
|
|
|
// Provider configuration
|
|
export interface ProviderConfig {
|
|
id: string // UUID for this provider config
|
|
provider: ProviderName
|
|
name?: string // Custom display name (e.g., "OpenAI Production")
|
|
apiKey: string
|
|
baseUrl?: string
|
|
// AWS Bedrock specific fields
|
|
awsAccessKeyId?: string
|
|
awsSecretAccessKey?: string
|
|
awsRegion?: string
|
|
awsSessionToken?: string // Optional, for temporary credentials
|
|
// Vertex AI specific fields
|
|
vertexApiKey?: string // Express Mode API key
|
|
|
|
models: ModelConfig[]
|
|
validated?: boolean // Has API key been validated
|
|
}
|
|
|
|
// The complete multi-model configuration
|
|
export interface MultiModelConfig {
|
|
version: 1
|
|
providers: ProviderConfig[]
|
|
selectedModelId?: string // Currently selected model's UUID
|
|
showUnvalidatedModels?: boolean // Show models that haven't been validated
|
|
}
|
|
|
|
// Flattened model for dropdown display
|
|
export interface FlattenedModel {
|
|
id: string // Model config UUID or synthetic server ID (e.g., "server:provider:modelId")
|
|
modelId: string // Actual model ID
|
|
provider: ProviderName
|
|
providerLabel: string // Provider display name
|
|
apiKey: string
|
|
baseUrl?: string
|
|
// AWS Bedrock specific fields
|
|
awsAccessKeyId?: string
|
|
awsSecretAccessKey?: string
|
|
awsRegion?: string
|
|
awsSessionToken?: string
|
|
// Vertex AI specific fields
|
|
vertexApiKey?: string // Express Mode API key
|
|
|
|
validated?: boolean // Has this model been validated
|
|
// Source of this model config: user-defined (client) or server-defined
|
|
source?: "user" | "server"
|
|
// Whether this model is the server default (matches AI_MODEL env var)
|
|
isDefault?: boolean
|
|
// Custom env var name(s) for server models
|
|
// Can be a single string or array of strings for load balancing
|
|
apiKeyEnv?: string | string[]
|
|
baseUrlEnv?: string
|
|
}
|
|
|
|
// Providers whose server credentials live in fixed env vars
|
|
// (AWS_ACCESS_KEY_ID, GOOGLE_VERTEX_API_KEY, OLLAMA_API_KEY) with no
|
|
// apiKeyEnv redirection support — their credentials are global
|
|
export const FIXED_CRED_PROVIDERS: ProviderName[] = [
|
|
"bedrock",
|
|
"vertexai",
|
|
"ollama",
|
|
]
|
|
|
|
// Map provider names to models.dev logo names
|
|
export const PROVIDER_LOGO_MAP: Record<string, string> = {
|
|
openai: "openai",
|
|
anthropic: "anthropic",
|
|
google: "google",
|
|
azure: "azure",
|
|
bedrock: "amazon-bedrock",
|
|
openrouter: "openrouter",
|
|
aihubmix: "aihubmix",
|
|
deepseek: "deepseek",
|
|
siliconflow: "siliconflow",
|
|
sglang: "openai", // SGLang is OpenAI-compatible
|
|
gateway: "vercel",
|
|
edgeone: "tencent-cloud",
|
|
vertexai: "google",
|
|
doubao: "bytedance",
|
|
modelscope: "modelscope",
|
|
minimax: "minimax",
|
|
novita: "novita",
|
|
mimo: "xiaomi",
|
|
atlascloud: "openai",
|
|
}
|
|
|
|
/** How a provider lists its models (see lib/provider-models.ts) */
|
|
export type ModelListStyle =
|
|
| "openai"
|
|
| "anthropic"
|
|
| "google"
|
|
| "ollama"
|
|
| "openrouter"
|
|
| "aihubmix"
|
|
| "gateway"
|
|
|
|
// Provider metadata. apiKeyUrl is the page where users create a key.
|
|
// modelList is missing where a key alone cannot list the models (Bedrock,
|
|
// Vertex, Azure) or the list is not reliable (Doubao, MiniMax).
|
|
export const PROVIDER_INFO: Record<
|
|
ProviderName,
|
|
{
|
|
label: string
|
|
defaultBaseUrl?: string
|
|
apiKeyUrl?: string
|
|
modelList?: ModelListStyle
|
|
}
|
|
> = {
|
|
openai: {
|
|
label: "OpenAI",
|
|
defaultBaseUrl: "https://api.openai.com/v1",
|
|
apiKeyUrl: "https://platform.openai.com/api-keys",
|
|
modelList: "openai",
|
|
},
|
|
anthropic: {
|
|
label: "Anthropic",
|
|
defaultBaseUrl: "https://api.anthropic.com/v1",
|
|
apiKeyUrl: "https://platform.claude.com/settings/keys",
|
|
modelList: "anthropic",
|
|
},
|
|
google: {
|
|
label: "Google",
|
|
defaultBaseUrl: "https://generativelanguage.googleapis.com/v1beta",
|
|
apiKeyUrl: "https://aistudio.google.com/apikey",
|
|
modelList: "google",
|
|
},
|
|
vertexai: { label: "Google Vertex AI" },
|
|
azure: {
|
|
label: "Azure OpenAI",
|
|
defaultBaseUrl: "https://your-resource.openai.azure.com/openai",
|
|
},
|
|
bedrock: { label: "Amazon Bedrock" },
|
|
ollama: {
|
|
label: "Ollama",
|
|
defaultBaseUrl: "https://ollama.com/api",
|
|
apiKeyUrl: "https://ollama.com/settings/keys",
|
|
modelList: "ollama",
|
|
},
|
|
openrouter: {
|
|
label: "OpenRouter",
|
|
defaultBaseUrl: "https://openrouter.ai/api/v1",
|
|
apiKeyUrl: "https://openrouter.ai/keys",
|
|
modelList: "openrouter",
|
|
},
|
|
aihubmix: {
|
|
label: "AIHubMix",
|
|
defaultBaseUrl: "https://aihubmix.com/v1",
|
|
apiKeyUrl: "https://aihubmix.com/token",
|
|
modelList: "aihubmix",
|
|
},
|
|
deepseek: {
|
|
label: "DeepSeek",
|
|
defaultBaseUrl: "https://api.deepseek.com/v1",
|
|
apiKeyUrl: "https://platform.deepseek.com/api_keys",
|
|
modelList: "openai",
|
|
},
|
|
siliconflow: {
|
|
label: "SiliconFlow",
|
|
defaultBaseUrl: "https://api.siliconflow.cn/v1",
|
|
apiKeyUrl: "https://cloud.siliconflow.cn/account/ak",
|
|
modelList: "openai",
|
|
},
|
|
sglang: {
|
|
label: "SGLang",
|
|
defaultBaseUrl: "http://127.0.0.1:8000/v1",
|
|
modelList: "openai",
|
|
},
|
|
gateway: {
|
|
label: "AI Gateway",
|
|
defaultBaseUrl: "https://ai-gateway.vercel.sh/v1/ai",
|
|
apiKeyUrl: "https://vercel.com/ai-gateway",
|
|
modelList: "gateway",
|
|
},
|
|
edgeone: { label: "EdgeOne Pages" },
|
|
doubao: {
|
|
label: "Doubao (ByteDance)",
|
|
defaultBaseUrl: "https://ark.cn-beijing.volces.com/api/v3",
|
|
apiKeyUrl:
|
|
"https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey",
|
|
},
|
|
modelscope: {
|
|
label: "ModelScope",
|
|
defaultBaseUrl: "https://api-inference.modelscope.cn/v1",
|
|
apiKeyUrl: "https://modelscope.cn/my/myaccesstoken",
|
|
modelList: "openai",
|
|
},
|
|
glm: {
|
|
label: "GLM (Zhipu)",
|
|
defaultBaseUrl: "https://open.bigmodel.cn/api/paas/v4",
|
|
apiKeyUrl: "https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys",
|
|
modelList: "openai",
|
|
},
|
|
qwen: {
|
|
label: "Qwen (Alibaba)",
|
|
defaultBaseUrl: "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
|
apiKeyUrl: "https://bailian.console.aliyun.com/?tab=model#/api-key",
|
|
modelList: "openai",
|
|
},
|
|
qiniu: {
|
|
label: "Qiniu",
|
|
defaultBaseUrl: "https://api.qnaigc.com/v1",
|
|
apiKeyUrl: "https://www.qiniu.com/ai/models",
|
|
modelList: "openai",
|
|
},
|
|
kimi: {
|
|
label: "Kimi (Moonshot)",
|
|
defaultBaseUrl: "https://api.moonshot.cn/v1",
|
|
apiKeyUrl: "https://platform.moonshot.cn/console/api-keys",
|
|
modelList: "openai",
|
|
},
|
|
minimax: {
|
|
label: "MiniMax",
|
|
defaultBaseUrl: "https://api.minimaxi.com/anthropic",
|
|
apiKeyUrl:
|
|
"https://platform.minimaxi.com/user-center/basic-information/interface-key",
|
|
},
|
|
novita: {
|
|
label: "Novita AI",
|
|
defaultBaseUrl: "https://api.novita.ai/openai",
|
|
apiKeyUrl: "https://novita.ai/dashboard/key",
|
|
modelList: "openai",
|
|
},
|
|
mimo: {
|
|
label: "MiMo (Xiaomi)",
|
|
defaultBaseUrl: "https://api.xiaomimimo.com/v1",
|
|
apiKeyUrl: "https://platform.xiaomimimo.com/#/console/api-keys",
|
|
modelList: "openai",
|
|
},
|
|
atlascloud: {
|
|
label: "Atlas Cloud",
|
|
defaultBaseUrl: "https://api.atlascloud.ai/v1",
|
|
apiKeyUrl: "https://www.atlascloud.ai/console/api-keys",
|
|
modelList: "openai",
|
|
},
|
|
}
|
|
|
|
// Suggested models per provider for quick add
|
|
export const SUGGESTED_MODELS: Partial<Record<ProviderName, string[]>> = {
|
|
openai: [
|
|
"gpt-6.1-sol",
|
|
"gpt-6-sol",
|
|
"gpt-6-luna",
|
|
"gpt-6-astra",
|
|
"gpt-5.5-pro",
|
|
"gpt-5.5",
|
|
"gpt-5.4-pro",
|
|
"gpt-5.4",
|
|
"gpt-5.4-mini",
|
|
"gpt-5.4-nano",
|
|
"gpt-5-codex-mini",
|
|
"gpt-4.1",
|
|
"gpt-4.1-mini",
|
|
"gpt-4o",
|
|
"gpt-4o-mini",
|
|
],
|
|
anthropic: [
|
|
// Claude 5 series (latest)
|
|
"claude-opus-5-5",
|
|
"claude-sonnet-5-5",
|
|
"claude-fable-5-1",
|
|
"claude-opus-5",
|
|
"claude-sonnet-5",
|
|
// Claude 4.8 / 4.7 / 4.6 series (dateless pinned IDs)
|
|
"claude-opus-4-8",
|
|
"claude-sonnet-4-6",
|
|
"claude-haiku-4-5",
|
|
"claude-opus-4-7",
|
|
"claude-opus-4-6",
|
|
// Claude 4.5 series
|
|
"claude-sonnet-4-5-20250929",
|
|
"claude-opus-4-5-20251101",
|
|
// Claude 3.7 series
|
|
"claude-3-7-sonnet-20250219",
|
|
// Claude 3.5 series
|
|
"claude-3-5-sonnet-20241022",
|
|
"claude-3-5-haiku-20241022",
|
|
],
|
|
google: [
|
|
// Gemini 3 series
|
|
"gemini-3.1-pro",
|
|
"gemini-3.5-flash",
|
|
"gemini-3-flash",
|
|
"gemini-3.1-flash-lite",
|
|
// Gemini 2.5 series
|
|
"gemini-2.5-pro",
|
|
"gemini-2.5-flash",
|
|
"gemini-2.5-flash-lite",
|
|
],
|
|
vertexai: [
|
|
// Gemini 3 series
|
|
"gemini-3.1-pro-preview",
|
|
"gemini-3.5-flash",
|
|
"gemini-3-flash-preview",
|
|
"gemini-3.1-flash-lite",
|
|
// Gemini 2.5 series
|
|
"gemini-2.5-pro",
|
|
"gemini-2.5-flash",
|
|
"gemini-2.5-flash-lite",
|
|
],
|
|
azure: [
|
|
"gpt-5.5",
|
|
"gpt-5.4",
|
|
"gpt-5.1",
|
|
"gpt-5",
|
|
"gpt-5-mini",
|
|
"gpt-4.1",
|
|
"gpt-4o",
|
|
"gpt-4o-mini",
|
|
"o3",
|
|
"o4-mini",
|
|
],
|
|
// Newer models only answer through an inference profile id (the region
|
|
// prefix). Each id here was called once on 2026-10-04.
|
|
bedrock: [
|
|
// Anthropic Claude ("global." works from any region)
|
|
"global.anthropic.claude-opus-5-5",
|
|
"global.anthropic.claude-sonnet-5-5",
|
|
"global.anthropic.claude-fable-5-1",
|
|
"global.anthropic.claude-opus-5",
|
|
"global.anthropic.claude-sonnet-5",
|
|
"global.anthropic.claude-opus-4-8",
|
|
"global.anthropic.claude-opus-4-7",
|
|
"global.anthropic.claude-sonnet-4-6",
|
|
"global.anthropic.claude-opus-4-6-v1",
|
|
"global.anthropic.claude-opus-4-5-20251101-v1:0",
|
|
"global.anthropic.claude-sonnet-4-5-20250929-v1:0",
|
|
"global.anthropic.claude-haiku-4-5-20251001-v1:0",
|
|
"global.anthropic.claude-sonnet-4-20250514-v1:0",
|
|
// Amazon Nova
|
|
"us.amazon.nova-2-lite-v1:0",
|
|
"amazon.nova-pro-v1:0",
|
|
"amazon.nova-lite-v1:0",
|
|
"amazon.nova-micro-v1:0",
|
|
// Meta Llama
|
|
"us.meta.llama4-maverick-17b-instruct-v1:0",
|
|
"us.meta.llama4-scout-17b-instruct-v1:0",
|
|
"us.meta.llama3-3-70b-instruct-v1:0",
|
|
// Mistral
|
|
"mistral.mistral-large-3-675b-instruct",
|
|
"us.mistral.pixtral-large-2502-v1:0",
|
|
],
|
|
openrouter: [
|
|
// Anthropic
|
|
"anthropic/claude-opus-5.5",
|
|
"anthropic/claude-sonnet-5.5",
|
|
"anthropic/claude-fable-5.1",
|
|
"anthropic/claude-opus-4.8",
|
|
"anthropic/claude-sonnet-4.6",
|
|
"anthropic/claude-haiku-4.5",
|
|
// OpenAI
|
|
"openai/gpt-6.1-sol",
|
|
"openai/gpt-6-luna",
|
|
"openai/gpt-5.5",
|
|
"openai/gpt-5.4",
|
|
"openai/gpt-5.4-mini",
|
|
"openai/gpt-4o-mini",
|
|
// Google
|
|
"google/gemini-3.1-pro-preview",
|
|
"google/gemini-3.5-flash",
|
|
"google/gemini-2.5-flash-lite",
|
|
// xAI
|
|
"x-ai/grok-4.3",
|
|
// Meta Llama
|
|
"meta-llama/llama-4-maverick",
|
|
"meta-llama/llama-4-scout",
|
|
"meta-llama/llama-3.3-70b-instruct",
|
|
// DeepSeek
|
|
"deepseek/deepseek-v4-pro",
|
|
"deepseek/deepseek-v3.2",
|
|
// Qwen
|
|
"qwen/qwen3.7-max",
|
|
"qwen/qwen3-coder",
|
|
// MiniMax
|
|
"minimax/minimax-m3",
|
|
],
|
|
aihubmix: [
|
|
// Fallback list. The settings UI loads the live model list from AIHubMix when available.
|
|
// Anthropic Claude
|
|
"claude-fable-5",
|
|
"claude-opus-4-8",
|
|
"claude-sonnet-4-6",
|
|
// OpenAI
|
|
"gpt-5.5",
|
|
"gpt-5.5-pro",
|
|
"gpt-5.4",
|
|
// Google Gemini
|
|
"gemini-3.5-flash",
|
|
"gemini-3.1-pro-preview",
|
|
"gemini-3-flash-preview",
|
|
// DeepSeek
|
|
"deepseek-v4-pro",
|
|
"deepseek-v4-flash",
|
|
// Qwen
|
|
"qwen3.7-max",
|
|
"qwen3-coder-next",
|
|
// Z.ai
|
|
"glm-5.1",
|
|
// Moonshot AI
|
|
"kimi-k2.6",
|
|
// MiniMax
|
|
"minimax-m3",
|
|
// xAI
|
|
"grok-4.3",
|
|
// Baidu
|
|
"ernie-5.1",
|
|
// Mistral
|
|
"mistral-large-3",
|
|
// Meta
|
|
"llama-4-maverick",
|
|
],
|
|
deepseek: [
|
|
"deepseek-v4-pro",
|
|
"deepseek-v4-flash",
|
|
"deepseek-chat",
|
|
"deepseek-reasoner",
|
|
],
|
|
siliconflow: [
|
|
// DeepSeek
|
|
"deepseek-ai/DeepSeek-V4-Pro",
|
|
"deepseek-ai/DeepSeek-V4-Flash",
|
|
"deepseek-ai/DeepSeek-V3.2",
|
|
// MiniMax
|
|
"MiniMaxAI/MiniMax-M3",
|
|
// Moonshot
|
|
"moonshotai/Kimi-K2.6",
|
|
// Z.ai
|
|
"zai-org/GLM-5",
|
|
// Qwen
|
|
"Qwen/Qwen3.6-35B-A3B",
|
|
"Qwen/Qwen3-Coder-480B-A35B-Instruct",
|
|
"Qwen/Qwen3-30B-A3B-Instruct-2507",
|
|
"Qwen/Qwen3-VL-32B-Instruct",
|
|
// OpenAI open-weights
|
|
"openai/gpt-oss-120b",
|
|
],
|
|
sglang: [
|
|
// SGLang is OpenAI-compatible, models depend on deployment
|
|
"default",
|
|
],
|
|
gateway: [
|
|
"anthropic/claude-opus-5.5",
|
|
"anthropic/claude-sonnet-5.5",
|
|
"openai/gpt-6.1-sol",
|
|
"openai/gpt-6-luna",
|
|
"openai/gpt-5.5",
|
|
"anthropic/claude-opus-4.7",
|
|
"google/gemini-3.1-pro-preview",
|
|
"xai/grok-4.3",
|
|
"anthropic/claude-sonnet-4.6",
|
|
"anthropic/claude-haiku-4.5",
|
|
"openai/gpt-5.4-mini",
|
|
],
|
|
edgeone: ["@tx/deepseek-ai/deepseek-v32"],
|
|
doubao: [
|
|
// ByteDance Doubao models (Volcengine Ark IDs use dash form)
|
|
"doubao-seed-2-0-pro-260215",
|
|
"doubao-seed-2-0-lite-260428",
|
|
"doubao-seed-2-0-mini-260428",
|
|
"doubao-seed-1-8-251228",
|
|
"doubao-seed-1-6-251015",
|
|
"doubao-seed-1-6-flash-250828",
|
|
"doubao-seed-1-6-vision-250815",
|
|
"doubao-1-5-pro-32k-250115",
|
|
"doubao-1-5-lite-32k-250115",
|
|
],
|
|
modelscope: [
|
|
// DeepSeek
|
|
"deepseek-ai/DeepSeek-V4-Pro",
|
|
"deepseek-ai/DeepSeek-V3.2",
|
|
"deepseek-ai/DeepSeek-R1-0528",
|
|
"deepseek-ai/DeepSeek-R1",
|
|
// Qwen
|
|
"Qwen/Qwen3-235B-A22B-Instruct-2507",
|
|
"Qwen/Qwen3-VL-235B-A22B-Instruct",
|
|
"Qwen/Qwen3-Coder-30B-A3B-Instruct",
|
|
"Qwen/Qwen3-32B",
|
|
"Qwen/Qwen2.5-72B-Instruct",
|
|
],
|
|
minimax: [
|
|
// MiniMax models (Anthropic-compatible API)
|
|
"MiniMax-M3",
|
|
"MiniMax-M2.7",
|
|
"MiniMax-M2.7-highspeed",
|
|
"MiniMax-M2.5",
|
|
],
|
|
novita: [
|
|
// Novita AI models (OpenAI-compatible API)
|
|
"minimax/minimax-m3",
|
|
"deepseek/deepseek-v4-pro",
|
|
"zai-org/glm-5.1",
|
|
"moonshotai/kimi-k2.6",
|
|
"deepseek/deepseek-v4-flash",
|
|
],
|
|
mimo: ["mimo-v2.5-pro", "mimo-v2.5"],
|
|
atlascloud: ["qwen/qwen3.5-flash", "deepseek-ai/deepseek-v4-pro"],
|
|
}
|
|
|
|
// Helper to generate UUID
|
|
export function generateId(): string {
|
|
return `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`
|
|
}
|
|
|
|
// Create empty config
|
|
export function createEmptyConfig(): MultiModelConfig {
|
|
return {
|
|
version: 1,
|
|
providers: [],
|
|
selectedModelId: undefined,
|
|
}
|
|
}
|
|
|
|
// Create new provider config
|
|
export function createProviderConfig(provider: ProviderName): ProviderConfig {
|
|
return {
|
|
id: generateId(),
|
|
provider,
|
|
apiKey: "",
|
|
baseUrl: PROVIDER_INFO[provider].defaultBaseUrl,
|
|
models: [],
|
|
validated: false,
|
|
}
|
|
}
|
|
|
|
// Create new model config
|
|
export function createModelConfig(modelId: string): ModelConfig {
|
|
return {
|
|
id: generateId(),
|
|
modelId,
|
|
}
|
|
}
|
|
|
|
// Get all models as flattened list for dropdown (user-defined only)
|
|
export function flattenModels(config: MultiModelConfig): FlattenedModel[] {
|
|
const models: FlattenedModel[] = []
|
|
|
|
for (const provider of config.providers) {
|
|
// Use custom name if provided, otherwise use default provider label
|
|
const providerLabel =
|
|
provider.name || PROVIDER_INFO[provider.provider].label
|
|
|
|
for (const model of provider.models) {
|
|
models.push({
|
|
id: model.id,
|
|
modelId: model.modelId,
|
|
provider: provider.provider,
|
|
providerLabel,
|
|
apiKey: provider.apiKey,
|
|
baseUrl: provider.baseUrl,
|
|
// AWS Bedrock fields
|
|
awsAccessKeyId: provider.awsAccessKeyId,
|
|
awsSecretAccessKey: provider.awsSecretAccessKey,
|
|
awsRegion: provider.awsRegion,
|
|
awsSessionToken: provider.awsSessionToken,
|
|
// Vertex AI fields
|
|
vertexApiKey: provider.vertexApiKey,
|
|
|
|
validated: model.validated,
|
|
source: "user",
|
|
isDefault: false,
|
|
})
|
|
}
|
|
}
|
|
|
|
return models
|
|
}
|
|
|
|
// Find model by ID
|
|
export function findModelById(
|
|
config: MultiModelConfig,
|
|
modelId: string,
|
|
): FlattenedModel | undefined {
|
|
return flattenModels(config).find((m) => m.id === modelId)
|
|
}
|
|
|
|
/**
|
|
* A base URL the way the SDKs expect it: no spaces, no trailing slash, and
|
|
* no endpoint path users often paste along (".../v1/chat/completions"),
|
|
* which the SDK would append a second time.
|
|
*/
|
|
export function normalizeBaseUrl(url: string): string {
|
|
return url
|
|
.trim()
|
|
.replace(/\/+$/, "")
|
|
.replace(/\/(?:chat\/completions|completions|messages|responses)$/, "")
|
|
}
|
|
|
|
/**
|
|
* Ollama's native API root, which the SDK appends /chat to and the model
|
|
* list /tags. Users often enter the server address ("http://localhost:11434")
|
|
* or its OpenAI-compatible one (".../v1"); both get /api.
|
|
*/
|
|
export function ollamaApiUrl(baseUrl: string): string {
|
|
return `${normalizeBaseUrl(baseUrl).replace(/\/(?:api|v1)$/, "")}/api`
|
|
}
|
|
|
|
/** Where a chat request goes for a base URL, or null when the SDK decides */
|
|
export function chatRequestUrl(
|
|
provider: ProviderName,
|
|
baseUrl: string,
|
|
): string | null {
|
|
const url = normalizeBaseUrl(baseUrl)
|
|
if (!url) return null
|
|
if (provider === "anthropic") return `${url}/messages`
|
|
if (provider === "ollama") return `${ollamaApiUrl(url)}/chat`
|
|
// These SDKs build their own paths (or, for MiniMax, pick the protocol
|
|
// from the URL)
|
|
const ownPaths: ProviderName[] = [
|
|
"google",
|
|
"vertexai",
|
|
"azure",
|
|
"bedrock",
|
|
"gateway",
|
|
"minimax",
|
|
"edgeone",
|
|
]
|
|
return ownPaths.includes(provider) ? null : `${url}/chat/completions`
|
|
}
|