MagicCode/packages/providers/src/litellm/index.ts

74 lines
2.7 KiB
TypeScript
Raw Normal View History

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" }));
}