157 lines
5.6 KiB
TypeScript
157 lines
5.6 KiB
TypeScript
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<Response>;
|
|
|
|
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<unknown> {
|
|
const raw = await response.text();
|
|
try {
|
|
return JSON.parse(raw) as unknown;
|
|
} catch {
|
|
return raw;
|
|
}
|
|
}
|
|
|
|
function toGatewayPayload(params: InvokeParams): Record<string, unknown> {
|
|
const payload: Record<string, unknown> = {
|
|
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<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,
|
|
connection = getReferentielIaConnection(),
|
|
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({
|
|
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<ReferentielIaConnectionTest> {
|
|
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,
|
|
};
|
|
}
|