retrotoon-studio/server/_core/llm.ts
Ubuntu c1606ad4c9 feat: migration complète Manus -> auto-hébergé (MinIO + Gemini)
Infrastructure:
- MinIO déployé en local pour le stockage S3 (docker-compose)
- Storage proxy réécrit: sert les fichiers depuis MinIO en streaming
  (plus de 307 redirect vers CDN externe)
- Legacy /manus-storage/ redirige vers /storage/

LLM & Image Generation:
- LLM: Gemini uniquement (suppression du fallback Forge)
- Image generation: Gemini Imagen direct (suppression Forge GenerateImage)
- llmConfig simplifié, un seul provider

Nettoyage Manus:
- Modules Forge stubbés (dataApi, heartbeat, map, notification, voiceTranscription)
- ENV simplifié (suppression forgeApiUrl, forgeApiKey)
- Analytics Manus supprimées du HTML
- systemRouter simplifié

Migration données:
- 750 fichiers migrés de Forge S3 vers MinIO (69.8 MB)
- URLs DB mises à jour: /manus-storage/ -> /storage/
- Script de migration inclus (scripts/migrate-to-minio.mjs)

Performance:
- Frame load: 500ms -> 62ms (8x plus rapide)
- Plus aucune dépendance réseau transatlantique pour le stockage

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-21 04:27:48 +00:00

116 lines
3.7 KiB
TypeScript
Raw 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.

export type Role = "system" | "user" | "assistant" | "tool" | "function";
export type TextContent = { type: "text"; text: string };
export type ImageContent = { type: "image_url"; image_url: { url: string; detail?: "auto" | "low" | "high" } };
export type FileContent = { type: "file_url"; file_url: { url: string; mime_type?: string } };
export type MessageContent = string | TextContent | ImageContent | FileContent;
export type Message = {
role: Role;
content: MessageContent | MessageContent[];
name?: string;
tool_call_id?: string;
};
export type Tool = {
type: "function";
function: { name: string; description?: string; parameters?: Record<string, unknown> };
};
export type ToolChoicePrimitive = "none" | "auto" | "required";
export type ToolChoiceByName = { name: string };
export type ToolChoiceExplicit = { type: "function"; function: { name: string } };
export type ToolChoice = ToolChoicePrimitive | ToolChoiceByName | ToolChoiceExplicit;
export type InvokeParams = {
messages: Message[];
tools?: Tool[];
toolChoice?: ToolChoice;
tool_choice?: ToolChoice;
maxTokens?: number;
max_tokens?: number;
outputSchema?: OutputSchema;
output_schema?: OutputSchema;
responseFormat?: ResponseFormat;
response_format?: ResponseFormat;
};
export type ToolCall = {
id: string;
type: "function";
function: { name: string; arguments: string };
};
export type InvokeResult = {
id: string;
created: number;
model: string;
choices: Array<{
index: number;
message: {
role: Role;
content: string | Array<TextContent | ImageContent | FileContent>;
tool_calls?: ToolCall[];
};
finish_reason: string | null;
}>;
usage?: { prompt_tokens: number; completion_tokens: number; total_tokens: number };
};
export type JsonSchema = { name: string; schema: Record<string, unknown>; strict?: boolean };
export type OutputSchema = JsonSchema;
export type ResponseFormat =
| { type: "text" }
| { type: "json_object" }
| { type: "json_schema"; json_schema: JsonSchema };
export async function invokeLLM(params: InvokeParams): Promise<InvokeResult> {
const apiKey = process.env.GEMINI_API_KEY;
if (!apiKey) {
throw new Error("GEMINI_API_KEY is not configured");
}
const url = "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions";
const messages = params.messages.map(msg => {
if (typeof msg.content === "string") {
return { role: msg.role, content: msg.content };
}
const parts = Array.isArray(msg.content) ? msg.content : [msg.content];
const mapped = parts.map(p => {
if (typeof p === "string") return { type: "text" as const, text: p };
if (p.type === "text") return p;
if (p.type === "image_url") return p;
return { type: "text" as const, text: "" };
}).filter(p => p.type !== "text" || ("text" in p && p.text));
if (mapped.length === 1 && mapped[0].type === "text") {
return { role: msg.role, content: (mapped[0] as any).text };
}
return { role: msg.role, content: mapped };
});
const payload: Record<string, unknown> = {
model: "gemini-2.5-flash",
messages,
max_tokens: params.maxTokens || params.max_tokens || 8192,
};
const rf = params.responseFormat || params.response_format;
if (rf) payload.response_format = rf;
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${apiKey}`,
},
body: JSON.stringify(payload),
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`LLM invoke failed: ${response.status} ${errorText.slice(0, 300)}`);
}
return (await response.json()) as InvokeResult;
}