fix: transmettre le moteur Référentiel IA

This commit is contained in:
Manus
2026-10-09 07:30:44 +00:00
parent 3b0be885e2
commit e402233120
2 changed files with 68 additions and 22 deletions

View File

@@ -48,9 +48,10 @@ describe("Référentiel IA", () => {
}
});
it("transmet une requête OpenAI-compatible à la passerelle sélectionnée", async () => {
it("transmet le slug du moteur décidé avant la requête OpenAI-compatible", async () => {
let receivedUrl = "";
let receivedInit: RequestInit | undefined;
let callCount = 0;
const result = await invokeReferentialIa(
{
messages: [{ role: "user", content: "Extrais la facture" }],
@@ -58,8 +59,22 @@ describe("Référentiel IA", () => {
},
connection,
async (url, init) => {
callCount += 1;
receivedUrl = String(url);
receivedInit = init;
if (callCount === 1) {
expect(receivedUrl).toBe("https://referentiel-ia.recette.santinova-soft.org/api/integration/v1/recommendations");
expect(JSON.parse(String(receivedInit?.body))).toEqual({
useCase: "texte",
optimizeCost: true,
needsFreeTier: false,
includeConnection: false,
});
return jsonResponse({
application: { name: "Démat. Facturation DSI" },
decision: { selected: { engine: { name: "SANTINO Expert", slug: "santino-expert" } } },
});
}
return jsonResponse({
id: "ref-1",
created: 1,
@@ -72,12 +87,15 @@ describe("Référentiel IA", () => {
expect(receivedUrl).toBe("https://referentiel-ia.recette.santinova-soft.org/api/integration/v1/invoke");
expect(receivedInit?.headers).toMatchObject({ authorization: "Bearer test-token" });
expect(JSON.parse(String(receivedInit?.body))).toMatchObject({
engineSlug: "santino-expert",
useCase: "texte",
payload: {
messages: [{ role: "user", content: "Extrais la facture" }],
max_tokens: 1200,
stream: false,
},
});
expect(callCount).toBe(2);
expect(result.choices[0]?.message.content).toBe("{}");
});

View File

@@ -6,6 +6,16 @@ export type ReferentielIaConnection = {
token: string;
};
type ReferentielIaRecommendation = {
application?: { name?: unknown };
decision?: {
selected?: {
engine?: { name?: unknown; slug?: unknown };
connection?: { model?: unknown };
} | null;
} | null;
};
type Fetcher = (input: string | URL, init?: RequestInit) => Promise<Response>;
const RECOMMENDATIONS_PATH = "/api/integration/v1/recommendations";
@@ -68,6 +78,37 @@ function gatewayError(status: number): Error {
return new Error(`Le Référentiel IA est indisponible (HTTP ${status}).`);
}
/** Demande au Référentiel le moteur actuellement décidé pour cette application. */
async function getReferentialIaRecommendation(
connection: ReferentielIaConnection,
fetcher: Fetcher,
): Promise<ReferentielIaRecommendation> {
const response = await fetcher(`${connection.baseUrl}${RECOMMENDATIONS_PATH}`, {
method: "POST",
headers: {
"content-type": "application/json",
authorization: `Bearer ${connection.token}`,
},
body: JSON.stringify({
useCase: "texte",
optimizeCost: true,
needsFreeTier: false,
includeConnection: false,
}),
signal: AbortSignal.timeout(15_000),
});
if (!response.ok) throw gatewayError(response.status);
return (await readJson(response)) as ReferentielIaRecommendation;
}
function selectedEngineSlug(recommendation: ReferentielIaRecommendation): string {
const slug = recommendation.decision?.selected?.engine?.slug;
if (typeof slug !== "string" || !slug.trim()) {
throw new Error("Le Référentiel IA ne fournit aucun moteur sélectionné pour cette application.");
}
return slug.trim();
}
/** Exécute l'appel sur le moteur décidé et tracé par le Référentiel IA. */
export async function invokeReferentialIa(
params: InvokeParams,
@@ -75,13 +116,19 @@ export async function invokeReferentialIa(
fetcher: Fetcher = fetch,
): Promise<InvokeResult> {
const config = requireConnection(connection);
const recommendation = await getReferentialIaRecommendation(config, fetcher);
const engineSlug = selectedEngineSlug(recommendation);
const response = await fetcher(`${config.baseUrl}${INVOCATION_PATH}`, {
method: "POST",
headers: {
"content-type": "application/json",
authorization: `Bearer ${config.token}`,
},
body: JSON.stringify({ payload: toGatewayPayload(params) }),
body: JSON.stringify({
engineSlug,
useCase: "texte",
payload: toGatewayPayload(params),
}),
signal: AbortSignal.timeout(120_000),
});
if (!response.ok) throw gatewayError(response.status);
@@ -100,26 +147,7 @@ export async function testReferentialIaConnection(
fetcher: Fetcher = fetch,
): Promise<ReferentielIaConnectionTest> {
const config = requireConnection(connection);
const response = await fetcher(`${config.baseUrl}${RECOMMENDATIONS_PATH}`, {
method: "POST",
headers: {
"content-type": "application/json",
authorization: `Bearer ${config.token}`,
},
body: JSON.stringify({
useCase: "texte",
optimizeCost: true,
needsFreeTier: false,
includeConnection: false,
}),
signal: AbortSignal.timeout(15_000),
});
if (!response.ok) throw gatewayError(response.status);
const data = (await readJson(response)) as {
application?: { name?: unknown };
decision?: { selected?: { engine?: { name?: unknown }; connection?: { model?: unknown } } | null } | null;
};
const data = await getReferentialIaRecommendation(config, fetcher);
return {
applicationName: typeof data.application?.name === "string" ? data.application.name : null,
engineName: typeof data.decision?.selected?.engine?.name === "string" ? data.decision.selected.engine.name : null,