Files
veille-reglementaire/server/_core/aiReferenceClient.ts
manus-admin ba34233189
All checks were successful
Deploy Production / deploy (push) Has been skipped
Deploy Recette / deploy (push) Successful in 1m4s
feat: finaliser l’intégration au Référentiel IA
2026-10-06 16:58:11 +02:00

87 lines
3.6 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<string> {
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<T>(payload: Record<string, unknown>, fetcher: typeof fetch = fetch): Promise<T> {
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;
}