import { ENV } from "./env"; export type ResolvedAiTrace = { provider: string; resolvedModel: string; transport: string; origin: string; }; type ReferenceDecision = { application?: { slug?: string }; decision?: { selected?: { engine?: { slug?: string } } | null } | null; }; function configuration() { const url = ENV.aiReferentielUrl.trim().replace(/\/$/, ""); const token = ENV.aiReferentielToken.trim(); const applicationSlug = ENV.aiReferentielAppSlug.trim(); if (!url || !token || !applicationSlug) { throw new Error("Référentiel IA non configuré pour cette application."); } const parsed = new URL(url); const allowedHosts = new Set([ "referentiel-ia.recette.santinova-soft.org", "referentiel-ia.santinova-soft.org", ]); if (parsed.protocol !== "https:" || !allowedHosts.has(parsed.hostname)) { throw new Error("URL du Référentiel IA non autorisée."); } return { url, token, applicationSlug }; } /** Résout à chaque appel le moteur choisi par le Référentiel, sans cache durable. */ async function resolveSelectedEngine( currentConfiguration: { url: string; token: string; applicationSlug: string }, fetcher: typeof fetch, ): Promise { const response = await fetcher(`${currentConfiguration.url}/api/integration/v1/recommendations`, { method: "POST", headers: { "content-type": "application/json", authorization: `Bearer ${currentConfiguration.token}` }, body: JSON.stringify({ useCase: "texte", optimizeCost: false, needsFreeTier: false, includeConnection: false }), signal: AbortSignal.timeout(15_000), }); if (!response.ok) { throw new Error(`Référentiel IA : décision indisponible (HTTP ${response.status}).`); } const decision = await response.json() as ReferenceDecision; if (decision.application?.slug !== currentConfiguration.applicationSlug) { throw new Error("Référentiel IA : la décision ne correspond pas à l’application attendue."); } const engineSlug = decision.decision?.selected?.engine?.slug?.trim(); if (!engineSlug) { throw new Error("Référentiel IA : aucun moteur n’est sélectionné."); } return engineSlug; } /** * Le Référentiel décide seul du moteur. La Veille transmet ensuite son slug exact * à la passerelle centrale et ne connaît jamais de clé fournisseur. */ export async function invokeSelectedReference(payload: Record, fetcher: typeof fetch = fetch): Promise { const currentConfiguration = configuration(); const engineSlug = await resolveSelectedEngine(currentConfiguration, fetcher); const response = await fetcher(`${currentConfiguration.url}/api/integration/v1/invoke`, { method: "POST", headers: { "content-type": "application/json", authorization: `Bearer ${currentConfiguration.token}` }, body: JSON.stringify({ engineSlug, payload }), signal: AbortSignal.timeout(120_000), }); if (!response.ok) { throw new Error(`Référentiel IA : invocation refusée (HTTP ${response.status}).`); } const trace: ResolvedAiTrace = { provider: response.headers.get("x-santinova-ia-provider") || "unknown", resolvedModel: response.headers.get("x-santinova-ia-model") || "unknown", transport: response.headers.get("x-santinova-ia-transport") || "unknown", origin: response.headers.get("x-santinova-ia-origin") || "unknown", }; console.info(`[IA_TRACE] provider=${trace.provider} model=${trace.resolvedModel} transport=${trace.transport} origin=${trace.origin}`); const body = await response.json() as unknown; if (body && typeof body === "object" && "error" in body) { throw new Error("Référentiel IA : le moteur a renvoyé une erreur métier."); } return body as T; }