73 lines
2.7 KiB
TypeScript
73 lines
2.7 KiB
TypeScript
import type { LLMProvider } from "@maia/core";
|
|
import { buildChatBody, parseSse } from "./openai";
|
|
import { authHeaders, joinUrl, type MaiaSettings } from "./settings";
|
|
|
|
export * from "./settings";
|
|
export { buildChatBody, parseSse } from "./openai";
|
|
|
|
async function errorText(response: Response): Promise<string> {
|
|
try {
|
|
return (await response.text()).slice(0, 500);
|
|
} catch {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Provider unique : toutes les requêtes des agents partent au format OpenAI vers le endpoint OpenAI de LiteLLM
|
|
* (LiteLLM_URL + OpenAI_EndPoint), quel que soit le modèle choisi.
|
|
*/
|
|
export function createLiteLLMProvider(model: string, settings: MaiaSettings): LLMProvider {
|
|
const url = joinUrl(settings.LiteLLM_URL, settings.OpenAI_EndPoint);
|
|
return {
|
|
id: "litellm",
|
|
model,
|
|
capabilities: { tools: true },
|
|
async *chat(req, signal) {
|
|
let response: Response;
|
|
try {
|
|
response = await fetch(url, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json", Accept: "text/event-stream", ...authHeaders(settings) },
|
|
body: JSON.stringify(buildChatBody(model, req)),
|
|
signal,
|
|
});
|
|
} catch (err) {
|
|
if (signal?.aborted) throw err;
|
|
throw new Error(`LiteLLM injoignable (${url}) : ${(err as Error).message}`);
|
|
}
|
|
if (!response.ok) {
|
|
throw new Error(`Échec de l'appel au modèle ${model} (HTTP ${response.status} ${response.statusText}). ${await errorText(response)}`);
|
|
}
|
|
if (!response.body) throw new Error("Réponse LiteLLM sans contenu.");
|
|
yield* parseSse(response.body as unknown as AsyncIterable<Uint8Array>);
|
|
},
|
|
};
|
|
}
|
|
|
|
export interface ModelChoice {
|
|
value: string; // "litellm:<modèle>"
|
|
label: string;
|
|
hint?: string;
|
|
}
|
|
|
|
/** Modèles disponibles sur le serveur LiteLLM (LiteLLM_URL + LiteLLM_Models). */
|
|
export async function listLiteLLMModels(settings: MaiaSettings, signal?: AbortSignal): Promise<ModelChoice[]> {
|
|
const url = joinUrl(settings.LiteLLM_URL, settings.LiteLLM_Models);
|
|
let response: Response;
|
|
try {
|
|
response = await fetch(url, { headers: { Accept: "application/json", ...authHeaders(settings) }, signal });
|
|
} catch (err) {
|
|
throw new Error(`LiteLLM injoignable (${url}) : ${(err as Error).message}`);
|
|
}
|
|
if (!response.ok) {
|
|
throw new Error(`Liste des modèles refusée (HTTP ${response.status} ${response.statusText}) : ${url}. ${await errorText(response)}`);
|
|
}
|
|
const json = (await response.json()) as { data?: { id?: string; model_name?: string }[] };
|
|
const names = new Set<string>();
|
|
for (const m of json.data ?? []) {
|
|
const name = m.id ?? m.model_name;
|
|
if (name) names.add(name);
|
|
}
|
|
return [...names].sort().map((name) => ({ value: `litellm:${name}`, label: name, hint: "LiteLLM" }));
|
|
}
|