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>
116 lines
3.7 KiB
TypeScript
116 lines
3.7 KiB
TypeScript
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;
|
||
}
|