import type { BtpAiConfig } from "./config"; import { safeReadText } from "./http"; export const DEFAULT_RESOURCE_GROUP = "default"; /** Famille de modèle : détermine l'API à appeler sur le déploiement. */ export type ModelType = "claude" | "openai" | "other"; export interface Deployment { id: string; name: string; version: string | null; deploymentUrl: string; type: ModelType; } /** * Hypothèse (la fonction n'était pas dans les samples) : * - anthropic--* / claude → Claude (AWS Bedrock, API Converse) * - gpt*, o1/o3/o4… → OpenAI (Azure OpenAI, chat/completions) * - le reste (gemini, embeddings, mistral…) → non pris en charge pour l'instant */ export function detectModelType(name: string): ModelType { const n = name.toLowerCase(); if (n.startsWith("anthropic--") || n.includes("claude")) return "claude"; if (/^(gpt|chatgpt|o\d)/.test(n)) return "openai"; return "other"; } /** * Liste les modèles/déploiements disponibles. * * GET {AI_API_URL}/v2/lm/deployments * Headers: Authorization: Bearer {token}, AI-Resource-Group */ export async function listDeployments(config: BtpAiConfig, token: string): Promise { const url = `${config.serviceurls.AI_API_URL.replace(/\/+$/, "")}/v2/lm/deployments`; const response = await fetch(url, { method: "GET", headers: { Authorization: `Bearer ${token}`, "AI-Resource-Group": config.resourceGroup ?? DEFAULT_RESOURCE_GROUP, }, }); if (!response.ok) { const body = await safeReadText(response); throw new Error( `Échec de récupération de la liste des modèles (HTTP ${response.status} ${response.statusText}). ${body}`, ); } const data = (await response.json()) as { resources?: RawDeployment[] }; const resources = Array.isArray(data.resources) ? data.resources : []; const deployments = resources .filter((r) => r.deploymentUrl) .map((r): Deployment => { const model = extractModelInfo(r); const name = model?.name ?? r.configurationName ?? r.id ?? "(nom inconnu)"; return { id: r.id ?? r.deploymentUrl!, name, version: model?.version ?? null, deploymentUrl: r.deploymentUrl!, type: detectModelType(name), }; }); if (deployments.length === 0) { throw new Error( "Aucun modèle déployé (avec deploymentUrl) n'a été trouvé pour ce service/resource group.", ); } return deployments; } /** * Choisit le déploiement : nom/id exact, sinon nom partiel. Vide ou "auto" → premier * déploiement de chat pris en charge. */ export function findDeployment(deployments: Deployment[], wanted: string): Deployment { const w = wanted.trim().toLowerCase(); const usable = deployments.filter((d) => d.type !== "other"); if (!w || w === "auto") { const first = usable[0]; if (!first) { throw new Error("Aucun déploiement de chat pris en charge (Claude ou GPT) n'a été trouvé."); } return first; } const found = usable.find((d) => d.name.toLowerCase() === w || d.id.toLowerCase() === w) ?? usable.find((d) => d.name.toLowerCase().includes(w)); if (!found) { throw new Error( `Modèle "${wanted}" introuvable. Disponibles : ${usable.map((d) => d.name).join(", ") || "(aucun)"}`, ); } return found; } interface RawModel { name?: string; version?: string | null; } interface RawDeployment { id?: string; deploymentUrl?: string; configurationName?: string; details?: { resources?: { backend_details?: { model?: RawModel }; backendDetails?: { model?: RawModel } } }; backend_details?: { model?: RawModel }; backendDetails?: { model?: RawModel }; } /** Chemin confirmé "details.resources.backend_details.model", avec variantes de repli. */ function extractModelInfo(r: RawDeployment): { name: string; version: string | null } | null { const candidates = [ r.details?.resources?.backend_details?.model, r.details?.resources?.backendDetails?.model, r.backend_details?.model, r.backendDetails?.model, ]; const found = candidates.find((c) => c?.name); return found?.name ? { name: found.name, version: found.version ?? null } : null; }