feat: centraliser le moteur IA via le Référentiel
All checks were successful
Deploy Production / deploy (push) Has been skipped
Deploy Recette / deploy (push) Successful in 1m32s
Validation applicative / TypeScript, tests et build (push) Successful in 2m42s

This commit is contained in:
manus-admin
2026-09-14 08:35:32 +00:00
parent 290e3709e2
commit 9db76af02c
7 changed files with 259 additions and 19 deletions

167
server/ai-reference.ts Normal file
View File

@@ -0,0 +1,167 @@
import { ENV } from "./_core/env";
export type AiEngineStatus = {
state: "available" | "unconfigured" | "unreachable";
engine?: {
name: string;
provider: string;
slug: string;
model?: string;
accessMode?: "gateway" | "recommendation_only";
configured: boolean;
origin: "proposal" | "manual" | "local";
rationale?: string;
};
proposal?: { name: string; provider: string; model?: string; origin: "proposal"; rationale?: string };
usage?: UsageSummary;
checkedAt: string;
message: string;
};
type ReferenceConfiguration = {
url: string;
token: string;
};
type RecommendationResponse = {
decision?: {
proposal?: DecisionEngine;
selected?: DecisionEngine;
};
recommendations?: DecisionEngine[];
usage?: UsageSummary;
};
type UsageMetric = { used: number; limit: number | null; percent: number | null; status: "not_configured" | "normal" | "alert" | "limit_reached" | "unpriced"; shouldBlock?: boolean };
type UsageAlert = { metric: "budget" | "requests" | "tokens"; label: string; status: "alert" | "limit_reached"; percent: number; message: string };
type UsageSummary = { canInvoke: boolean; blockReason?: string | null; budget: UsageMetric & { currency: string; estimated?: boolean }; requests: UsageMetric; tokens: UsageMetric; alerts: UsageAlert[] };
type DecisionEngine = {
origin?: "proposal" | "manual" | "local";
rationale?: string;
engine?: { name?: string; provider?: string; slug?: string };
connection?: { model?: string; accessMode?: "gateway" | "recommendation_only"; configured?: boolean };
};
const now = () => new Date().toISOString();
export async function getAiEngineStatus(
configuration: ReferenceConfiguration = {
url: ENV.aiReferentielUrl,
token: ENV.aiReferentielToken,
},
fetcher: typeof fetch = fetch,
): Promise<AiEngineStatus> {
if (!configuration.url || !configuration.token) {
return {
state: "unconfigured",
checkedAt: now(),
message: "Le Référentiel IA n’est pas encore configuré pour cette application.",
};
}
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 6_000);
try {
const response = await fetcher(`${configuration.url.replace(/\/$/, "")}/api/integration/v1/recommendations`, {
method: "POST",
headers: {
Authorization: `Bearer ${configuration.token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
useCase: "texte",
optimizeCost: true,
needsFreeTier: true,
includeConnection: true,
}),
signal: controller.signal,
});
if (!response.ok) {
return {
state: "unreachable",
checkedAt: now(),
message: `Le Référentiel IA est indisponible (HTTP ${response.status}).`,
};
}
const payload = (await response.json()) as RecommendationResponse;
const selected = payload.decision?.selected ?? payload.recommendations?.[0];
if (!selected?.engine?.name || !selected.engine.provider || !selected.engine.slug) {
return {
state: "unreachable",
checkedAt: now(),
message: "Le Référentiel IA n’a retourné aucun moteur autorisé.",
};
}
const proposal = payload.decision?.proposal;
const origin = selected.origin === "manual" || selected.origin === "local" ? selected.origin : "proposal";
return {
state: "available",
checkedAt: now(),
message: selected.connection?.configured
? origin === "local"
? "Moteur IA local sélectionné par le Référentiel IA."
: origin === "manual"
? "Moteur choisi par un administrateur pour cette application."
: "Proposition du Référentiel IA appliquée à cette application."
: "Moteur décidé par le Référentiel IA, en attente de configuration fournisseur.",
engine: {
name: selected.engine.name,
provider: selected.engine.provider,
slug: selected.engine.slug,
model: selected.connection?.model,
accessMode: selected.connection?.accessMode,
configured: Boolean(selected.connection?.configured),
origin,
rationale: selected.rationale,
},
proposal: proposal?.engine?.name && proposal.engine.provider ? {
name: proposal.engine.name,
provider: proposal.engine.provider,
model: proposal.connection?.model,
origin: "proposal",
rationale: proposal.rationale,
} : undefined,
usage: payload.usage,
};
} catch {
return {
state: "unreachable",
checkedAt: now(),
message: "La vérification auprès du Référentiel IA a échoué.",
};
} finally {
clearTimeout(timeout);
}
}
/**
* Métadonnées strictement descriptives de la décision active. Cette fonction
* ne retourne jamais le jeton applicatif ni une URL d'intégration privée.
*/
export async function getActiveAiConfiguration(
configuration: ReferenceConfiguration = {
url: ENV.aiReferentielUrl,
token: ENV.aiReferentielToken,
},
fetcher: typeof fetch = fetch,
) {
const status = await getAiEngineStatus(configuration, fetcher);
if (status.state !== "available" || !status.engine?.configured || !status.engine.model) {
return { active: false, engines: [] };
}
return {
active: true,
engines: [{
provider: status.engine.provider,
model: status.engine.model,
role: "Moteur sélectionné par le Référentiel IA",
billing: status.engine.origin === "local" ? "free" : "paid",
source: "referential",
}],
};
}