import {Mistral} from "@mistralai/mistralai"; import {GoogleGenAI} from "@google/genai"; import {Ollama} from "ollama"; import {OpenAI} from "openai"; import {Environment} from "../common/environment"; import {AiModelCapabilities} from "../model/ai-model-capabilities"; import {AiProvider} from "../model/ai-provider"; export type AiCapabilityName = keyof AiModelCapabilities; export type AiRuntimePurpose = AiCapabilityName | "chat"; export type AiRuntimeTarget = { provider: AiProvider; purpose: AiRuntimePurpose; model: string; baseUrl?: string; apiKey?: string; }; export type GeminiApiMode = "google" | "openai"; const GEMINI_OPENAI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/openai/"; const PURPOSE_SUFFIXES: Record = { chat: ["CHAT"], vision: ["VISION", "IMAGE"], ocr: ["OCR", "VISION", "IMAGE"], thinking: ["THINKING", "THINK"], extendedThinking: ["EXTENDED_THINKING", "THINKING", "THINK"], tools: ["TOOLS", "CHAT"], toolRank: ["TOOL_RANK", "TOOL_RANKER"], audio: ["AUDIO"], documents: ["DOCUMENTS", "RAG", "EMBEDDING"], outputImages: ["OUTPUT_IMAGES", "IMAGE"], speechToText: ["SPEECH_TO_TEXT", "TRANSCRIPTION", "STT", "AUDIO"], textToSpeech: ["TEXT_TO_SPEECH", "TTS"], }; function providerPrefix(provider: AiProvider): string { return provider.toString(); } function env(name: string): string | undefined { return Environment.getOptionalConfigValue(name); } function firstEnv(names: string[]): string | undefined { for (const name of names) { const value = env(name); if (value) return value; } return undefined; } function endpointEnvNames(provider: AiProvider, purpose: AiRuntimePurpose): string[] { const prefix = providerPrefix(provider); return PURPOSE_SUFFIXES[purpose].flatMap(suffix => [ `${prefix}_${suffix}_BASE_URL`, `${prefix}_${suffix}_ENDPOINT`, `${prefix}_${suffix}_ADDRESS`, ]); } function apiKeyEnvNames(provider: AiProvider, purpose: AiRuntimePurpose): string[] { const prefix = providerPrefix(provider); return PURPOSE_SUFFIXES[purpose].map(suffix => `${prefix}_${suffix}_API_KEY`); } function modelEnvNames(provider: AiProvider, purpose: AiRuntimePurpose): string[] { const prefix = providerPrefix(provider); return PURPOSE_SUFFIXES[purpose].map(suffix => `${prefix}_${suffix}_MODEL`); } export function getProviderBaseUrl(provider: AiProvider): string | undefined { switch (provider) { case AiProvider.OLLAMA: return env("OLLAMA_ADDRESS"); case AiProvider.GEMINI: return env("GEMINI_BASE_URL") ?? env("GEMINI_ENDPOINT") ?? (Environment.GEMINI_API_MODE === "openai" ? GEMINI_OPENAI_BASE_URL : undefined); case AiProvider.MISTRAL: return env("MISTRAL_BASE_URL") ?? env("MISTRAL_ENDPOINT"); case AiProvider.OPENAI: return env("OPENAI_BASE_URL") ?? env("OPENAI_ENDPOINT"); } } export function getProviderApiKey(provider: AiProvider): string | undefined { switch (provider) { case AiProvider.OLLAMA: return Environment.OLLAMA_API_KEY; case AiProvider.GEMINI: return Environment.GEMINI_API_KEY; case AiProvider.MISTRAL: return Environment.MISTRAL_API_KEY; case AiProvider.OPENAI: return Environment.OPENAI_API_KEY; } } export function getDefaultModelForPurpose(provider: AiProvider, purpose: AiRuntimePurpose): string { switch (provider) { case AiProvider.OLLAMA: switch (purpose) { case "vision": case "ocr": case "outputImages": return Environment.OLLAMA_IMAGE_MODEL; case "thinking": case "extendedThinking": return Environment.OLLAMA_THINK_MODEL; case "audio": case "speechToText": return Environment.OLLAMA_AUDIO_MODEL; case "documents": return Environment.OLLAMA_EMBEDDING_MODEL; default: return Environment.OLLAMA_CHAT_MODEL; } case AiProvider.GEMINI: switch (purpose) { case "vision": case "ocr": case "outputImages": return Environment.GEMINI_IMAGE_MODEL; case "speechToText": return Environment.GEMINI_TRANSCRIPTION_MODEL; case "textToSpeech": return Environment.GEMINI_TTS_MODEL; default: return Environment.GEMINI_MODEL; } case AiProvider.MISTRAL: switch (purpose) { case "speechToText": return Environment.MISTRAL_TRANSCRIPTION_MODEL; case "textToSpeech": return Environment.MISTRAL_TTS_MODEL || Environment.MISTRAL_MODEL; default: return Environment.MISTRAL_MODEL; } case AiProvider.OPENAI: switch (purpose) { case "outputImages": return Environment.OPENAI_IMAGE_MODEL; case "speechToText": return Environment.OPENAI_TRANSCRIPTION_MODEL; case "textToSpeech": return Environment.OPENAI_TTS_MODEL; default: return Environment.OPENAI_MODEL; } } } export function resolveAiRuntimeTarget( provider: AiProvider, purpose: AiRuntimePurpose, modelOverride?: string, ): AiRuntimeTarget { const model = modelOverride ?? firstEnv(modelEnvNames(provider, purpose)) ?? getDefaultModelForPurpose(provider, purpose); const baseUrl = firstEnv(endpointEnvNames(provider, purpose)) ?? getProviderBaseUrl(provider); const apiKey = firstEnv(apiKeyEnvNames(provider, purpose)) ?? getProviderApiKey(provider); return {provider, purpose, model, baseUrl, apiKey}; } export function sameRuntimeEndpoint(left: AiRuntimeTarget, right: AiRuntimeTarget): boolean { return left.provider === right.provider && (left.baseUrl ?? "") === (right.baseUrl ?? "") && (left.apiKey ?? "") === (right.apiKey ?? ""); } export function createOpenAiClient(target: AiRuntimeTarget): OpenAI { return new OpenAI({ apiKey: target.apiKey, baseURL: target.baseUrl, }); } export function getGeminiApiMode(target?: AiRuntimeTarget): GeminiApiMode { if (Environment.GEMINI_API_MODE === "openai") return "openai"; if (Environment.GEMINI_API_MODE === "google") return "google"; if ((target?.baseUrl ?? "").includes("/openai")) return "openai"; return "google"; } export function createGeminiOpenAiClient(target: AiRuntimeTarget): OpenAI { return createOpenAiClient({ ...target, baseUrl: target.baseUrl ?? GEMINI_OPENAI_BASE_URL, }); } export function createGoogleGenAiClient(target: AiRuntimeTarget): GoogleGenAI { return new GoogleGenAI({ apiKey: target.apiKey, }); } export function createMistralClient(target: AiRuntimeTarget): Mistral { return new Mistral({ apiKey: target.apiKey, serverURL: target.baseUrl, }); } export function createOllamaClient(target: AiRuntimeTarget): Ollama { return new Ollama({ host: target.baseUrl, headers: target.apiKey ? {"Authorization": `Bearer ${target.apiKey}`} : undefined, }); }