better-chatbot / src /lib /ai /models.ts
Bot
Inject openai compatible config as fallback
a0f7b65
Raw
History Blame Contribute Delete
8.02 kB
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 != "****";
}