import type { InvokeParams, InvokeResult } from "./_core/llm"; import { ENV } from "./_core/env"; export type ReferentielIaConnection = { baseUrl: string; 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; const RECOMMENDATIONS_PATH = "/api/integration/v1/recommendations"; const INVOCATION_PATH = "/api/integration/v1/invoke"; /** * Le jeton est exclusivement fourni au serveur via l'environnement. Il n'est * jamais exposé à l'interface ni stocké dans les paramètres de l'utilisateur. */ export function getReferentielIaConnection(): ReferentielIaConnection | null { const baseUrl = ENV.referentielIaUrl.trim().replace(/\/$/, ""); const token = ENV.referentielIaToken.trim(); if (!baseUrl || !token) return null; try { const url = new URL(baseUrl); if (url.protocol !== "https:") return null; return { baseUrl: url.toString().replace(/\/$/, ""), token }; } catch { return null; } } function requireConnection(connection?: ReferentielIaConnection | null): ReferentielIaConnection { if (!connection?.baseUrl.trim() || !connection.token.trim()) { throw new Error("Le Référentiel IA n'est pas configuré pour cette application."); } return connection; } async function readJson(response: Response): Promise { const raw = await response.text(); try { return JSON.parse(raw) as unknown; } catch { return raw; } } function toGatewayPayload(params: InvokeParams): Record { const payload: Record = { messages: params.messages, max_tokens: params.maxTokens ?? params.max_tokens ?? 32768, stream: false, }; if (params.tools?.length) payload.tools = params.tools; if (params.toolChoice ?? params.tool_choice) payload.tool_choice = params.toolChoice ?? params.tool_choice; if (params.outputSchema ?? params.output_schema) payload.response_format = { type: "json_schema", json_schema: params.outputSchema ?? params.output_schema, }; if (params.responseFormat ?? params.response_format) payload.response_format = params.responseFormat ?? params.response_format; return payload; } function gatewayError(status: number): Error { if (status === 401 || status === 403) return new Error("Le Référentiel IA refuse l'accès de cette application."); if (status === 422) return new Error("Le moteur choisi dans le Référentiel IA n'est pas opérationnel."); if (status === 429) return new Error("La limite d'usage définie dans le Référentiel IA est atteinte."); 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 { 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, connection = getReferentielIaConnection(), fetcher: Fetcher = fetch, ): Promise { 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({ engineSlug, useCase: "texte", payload: toGatewayPayload(params), }), signal: AbortSignal.timeout(120_000), }); if (!response.ok) throw gatewayError(response.status); return (await readJson(response)) as InvokeResult; } export type ReferentielIaConnectionTest = { applicationName: string | null; engineName: string | null; model: string | null; }; /** Vérifie l'autorisation et le moteur choisi, sans envoyer de facture au modèle. */ export async function testReferentialIaConnection( connection = getReferentielIaConnection(), fetcher: Fetcher = fetch, ): Promise { const config = requireConnection(connection); 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, model: typeof data.decision?.selected?.connection?.model === "string" ? data.decision.selected.connection.model : null, }; }