ai: add unified runtime and provider adapters
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
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<AiRuntimePurpose, string[]> = {
|
||||
chat: ["CHAT"],
|
||||
vision: ["VISION", "IMAGE"],
|
||||
ocr: ["OCR", "VISION", "IMAGE"],
|
||||
thinking: ["THINKING", "THINK"],
|
||||
extendedThinking: ["EXTENDED_THINKING", "THINKING", "THINK"],
|
||||
tools: ["TOOLS", "CHAT"],
|
||||
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_ENDPOINT");
|
||||
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 "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?.endsWith(":11434") ? target.baseUrl : target.baseUrl + ":11434",
|
||||
headers: target.apiKey ? {"Authorization": `Bearer ${target.apiKey}`} : undefined,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user