import "server-only"; import { createOllama } from "ollama-ai-provider-v2"; import { openai } from "@ai-sdk/openai"; import { google } from "@ai-sdk/google"; import { anthropic } from "@ai-sdk/anthropic"; import { xai } from "@ai-sdk/xai"; import { LanguageModelV2, openrouter } from "@openrouter/ai-sdk-provider"; import { createGroq } from "@ai-sdk/groq"; import { LanguageModel } from "ai"; import { createOpenAICompatibleModels, openaiCompatibleModelsSafeParse, } from "./create-openai-compatiable"; import openaiCompatibleConfig from "../../../openai-compatible.config"; import { ChatModel } from "app-types/chat"; import { DEFAULT_FILE_PART_MIME_TYPES, OPENAI_FILE_MIME_TYPES, GEMINI_FILE_MIME_TYPES, ANTHROPIC_FILE_MIME_TYPES, XAI_FILE_MIME_TYPES, } from "./file-support"; const ollama = createOllama({ baseURL: process.env.OLLAMA_BASE_URL || "http://localhost:11434/api", }); const groq = createGroq({ baseURL: process.env.GROQ_BASE_URL || "https://api.groq.com/openai/v1", apiKey: process.env.GROQ_API_KEY, }); const staticModels = { openai: { "gpt-4.1": openai("gpt-4.1"), "gpt-4.1-mini": openai("gpt-4.1-mini"), "o4-mini": openai("o4-mini"), o3: openai("o3"), "gpt-5.1-chat": openai("gpt-5.1-chat-latest"), "gpt-5.1": openai("gpt-5.1"), "gpt-5.1-codex": openai("gpt-5.1-codex"), "gpt-5.1-codex-mini": openai("gpt-5.1-codex-mini"), }, google: { "gemini-2.5-flash-lite": google("gemini-2.5-flash-lite"), "gemini-2.5-flash": google("gemini-2.5-flash"), "gemini-3-pro": google("gemini-3-pro-preview"), "gemini-2.5-pro": google("gemini-2.5-pro"), }, anthropic: { "sonnet-4.5": anthropic("claude-sonnet-4-5"), "haiku-4.5": anthropic("claude-haiku-4-5"), "opus-4.5": anthropic("claude-opus-4-5"), }, xai: { "grok-4-1-fast": xai("grok-4-1-fast-non-reasoning"), "grok-4-1": xai("grok-4-1"), "grok-3-mini": xai("grok-3-mini"), }, ollama: { "gemma3:1b": ollama("gemma3:1b"), "gemma3:4b": ollama("gemma3:4b"), "gemma3:12b": ollama("gemma3:12b"), }, groq: { "kimi-k2-instruct": groq("moonshotai/kimi-k2-instruct"), "llama-4-scout-17b": groq("meta-llama/llama-4-scout-17b-16e-instruct"), "gpt-oss-20b": groq("openai/gpt-oss-20b"), "gpt-oss-120b": groq("openai/gpt-oss-120b"), "qwen3-32b": groq("qwen/qwen3-32b"), }, openRouter: { "gpt-oss-20b:free": openrouter("openai/gpt-oss-20b:free"), "qwen3-8b:free": openrouter("qwen/qwen3-8b:free"), "qwen3-14b:free": openrouter("qwen/qwen3-14b:free"), "qwen3-coder:free": openrouter("qwen/qwen3-coder:free"), "deepseek-r1:free": openrouter("deepseek/deepseek-r1-0528:free"), "deepseek-v3:free": openrouter("deepseek/deepseek-chat-v3-0324:free"), "gemini-2.0-flash-exp:free": openrouter("google/gemini-2.0-flash-exp:free"), }, }; const staticUnsupportedModels = new Set([ staticModels.openai["o4-mini"], staticModels.ollama["gemma3:1b"], staticModels.ollama["gemma3:4b"], staticModels.ollama["gemma3:12b"], staticModels.openRouter["gpt-oss-20b:free"], staticModels.openRouter["qwen3-8b:free"], staticModels.openRouter["qwen3-14b:free"], staticModels.openRouter["deepseek-r1:free"], staticModels.openRouter["gemini-2.0-flash-exp:free"], ]); const staticSupportImageInputModels = { ...staticModels.google, ...staticModels.xai, ...staticModels.openai, ...staticModels.anthropic, }; const staticFilePartSupportByModel = new Map< LanguageModel, readonly string[] >(); const registerFileSupport = ( model: LanguageModel | undefined, mimeTypes: readonly string[] = DEFAULT_FILE_PART_MIME_TYPES, ) => { if (!model) return; staticFilePartSupportByModel.set(model, Array.from(mimeTypes)); }; registerFileSupport(staticModels.openai["gpt-4.1"], OPENAI_FILE_MIME_TYPES); registerFileSupport( staticModels.openai["gpt-4.1-mini"], OPENAI_FILE_MIME_TYPES, ); registerFileSupport(staticModels.openai["gpt-5"], OPENAI_FILE_MIME_TYPES); registerFileSupport(staticModels.openai["gpt-5-mini"], OPENAI_FILE_MIME_TYPES); registerFileSupport(staticModels.openai["gpt-5-nano"], OPENAI_FILE_MIME_TYPES); registerFileSupport( staticModels.google["gemini-2.5-flash-lite"], GEMINI_FILE_MIME_TYPES, ); registerFileSupport( staticModels.google["gemini-2.5-flash"], GEMINI_FILE_MIME_TYPES, ); registerFileSupport( staticModels.google["gemini-2.5-pro"], GEMINI_FILE_MIME_TYPES, ); registerFileSupport( staticModels.anthropic["sonnet-4.5"], ANTHROPIC_FILE_MIME_TYPES, ); registerFileSupport( staticModels.anthropic["opus-4.1"], ANTHROPIC_FILE_MIME_TYPES, ); registerFileSupport(staticModels.xai["grok-4-fast"], XAI_FILE_MIME_TYPES); registerFileSupport(staticModels.xai["grok-4"], XAI_FILE_MIME_TYPES); registerFileSupport(staticModels.xai["grok-3"], XAI_FILE_MIME_TYPES); registerFileSupport(staticModels.xai["grok-3-mini"], XAI_FILE_MIME_TYPES); registerFileSupport( staticModels.openRouter["gemini-2.0-flash-exp:free"], GEMINI_FILE_MIME_TYPES, ); const openaiCompatibleProviders = openaiCompatibleModelsSafeParse( process.env.OPENAI_COMPATIBLE_DATA || openaiCompatibleConfig, ); const { providers: openaiCompatibleModels, unsupportedModels: openaiCompatibleUnsupportedModels, } = createOpenAICompatibleModels(openaiCompatibleProviders); const allModels = { ...openaiCompatibleModels, ...staticModels }; const allUnsupportedModels = new Set([ ...openaiCompatibleUnsupportedModels, ...staticUnsupportedModels, ]); export const isToolCallUnsupportedModel = (model: LanguageModel) => { return allUnsupportedModels.has(model); }; const isImageInputUnsupportedModel = (model: LanguageModelV2) => { return !Object.values(staticSupportImageInputModels).includes(model); }; export const getFilePartSupportedMimeTypes = (model: LanguageModel) => { return staticFilePartSupportByModel.get(model) ?? []; }; const fallbackModel = staticModels.openai["gpt-4.1"]; export const customModelProvider = { modelsInfo: Object.entries(allModels).map(([provider, models]) => ({ provider, models: Object.entries(models).map(([name, model]) => ({ name, isToolCallUnsupported: isToolCallUnsupportedModel(model), isImageInputUnsupported: isImageInputUnsupportedModel(model), supportedFileMimeTypes: [...getFilePartSupportedMimeTypes(model)], })), hasAPIKey: checkProviderAPIKey(provider as keyof typeof staticModels), })), getModel: (model?: ChatModel): LanguageModel => { if (!model) return fallbackModel; return allModels[model.provider]?.[model.model] || fallbackModel; }, getDynamicModel: (model: ChatModel | undefined, userPreferences: any): LanguageModel => { if (!model) return fallbackModel; // If the user is trying to use an NVIDIA NIM model, let's inject their personal API key if they have one. if (model.provider === "nvidia_nim" && userPreferences?.apiKeys?.["nvidia_nim"]) { const { createOpenAICompatible } = require("@ai-sdk/openai-compatible"); const customProvider = createOpenAICompatible({ name: "nvidia_nim", apiKey: userPreferences.apiKeys["nvidia_nim"], baseURL: "https://integrate.api.nvidia.com/v1", }); return customProvider(model.model); } return allModels[model.provider]?.[model.model] || fallbackModel; }, }; function checkProviderAPIKey(provider: keyof typeof staticModels) { let key: string | undefined; switch (provider) { case "openai": key = process.env.OPENAI_API_KEY; break; case "google": key = process.env.GOOGLE_GENERATIVE_AI_API_KEY; break; case "anthropic": key = process.env.ANTHROPIC_API_KEY; break; case "xai": key = process.env.XAI_API_KEY; break; case "groq": key = process.env.GROQ_API_KEY; break; case "openRouter": key = process.env.OPENROUTER_API_KEY; break; default: return true; // assume the provider has an API key } return !!key && key != "****"; }