Spaces:
Sleeping
Sleeping
| import { ENV } from "./env"; | |
| 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?: "audio/mpeg" | "audio/wav" | "application/pdf" | "audio/mp4" | "video/mp4" ; | |
| }; | |
| }; | |
| 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 }; | |
| const ensureArray = ( | |
| value: MessageContent | MessageContent[] | |
| ): MessageContent[] => (Array.isArray(value) ? value : [value]); | |
| const normalizeContentPart = ( | |
| part: MessageContent | |
| ): TextContent | ImageContent | FileContent => { | |
| if (typeof part === "string") { | |
| return { type: "text", text: part }; | |
| } | |
| if (part.type === "text") { | |
| return part; | |
| } | |
| if (part.type === "image_url") { | |
| return part; | |
| } | |
| if (part.type === "file_url") { | |
| return part; | |
| } | |
| throw new Error("Unsupported message content part"); | |
| }; | |
| const normalizeMessage = (message: Message) => { | |
| const { role, name, tool_call_id } = message; | |
| if (role === "tool" || role === "function") { | |
| const content = ensureArray(message.content) | |
| .map(part => (typeof part === "string" ? part : JSON.stringify(part))) | |
| .join("\n"); | |
| return { | |
| role, | |
| name, | |
| tool_call_id, | |
| content, | |
| }; | |
| } | |
| const contentParts = ensureArray(message.content).map(normalizeContentPart); | |
| // If there's only text content, collapse to a single string for compatibility | |
| if (contentParts.length === 1 && contentParts[0].type === "text") { | |
| return { | |
| role, | |
| name, | |
| content: contentParts[0].text, | |
| }; | |
| } | |
| return { | |
| role, | |
| name, | |
| content: contentParts, | |
| }; | |
| }; | |
| const normalizeToolChoice = ( | |
| toolChoice: ToolChoice | undefined, | |
| tools: Tool[] | undefined | |
| ): "none" | "auto" | ToolChoiceExplicit | undefined => { | |
| if (!toolChoice) return undefined; | |
| if (toolChoice === "none" || toolChoice === "auto") { | |
| return toolChoice; | |
| } | |
| if (toolChoice === "required") { | |
| if (!tools || tools.length === 0) { | |
| throw new Error( | |
| "tool_choice 'required' was provided but no tools were configured" | |
| ); | |
| } | |
| if (tools.length > 1) { | |
| throw new Error( | |
| "tool_choice 'required' needs a single tool or specify the tool name explicitly" | |
| ); | |
| } | |
| return { | |
| type: "function", | |
| function: { name: tools[0].function.name }, | |
| }; | |
| } | |
| if ("name" in toolChoice) { | |
| return { | |
| type: "function", | |
| function: { name: toolChoice.name }, | |
| }; | |
| } | |
| return toolChoice; | |
| }; | |
| const resolveApiUrl = () => | |
| ENV.forgeApiUrl && ENV.forgeApiUrl.trim().length > 0 | |
| ? `${ENV.forgeApiUrl.replace(/\/$/, "")}/v1/chat/completions` | |
| : "https://forge.manus.im/v1/chat/completions"; | |
| const assertApiKey = () => { | |
| if (!ENV.forgeApiKey) { | |
| throw new Error("OPENAI_API_KEY is not configured"); | |
| } | |
| }; | |
| const normalizeResponseFormat = ({ | |
| responseFormat, | |
| response_format, | |
| outputSchema, | |
| output_schema, | |
| }: { | |
| responseFormat?: ResponseFormat; | |
| response_format?: ResponseFormat; | |
| outputSchema?: OutputSchema; | |
| output_schema?: OutputSchema; | |
| }): | |
| | { type: "json_schema"; json_schema: JsonSchema } | |
| | { type: "text" } | |
| | { type: "json_object" } | |
| | undefined => { | |
| const explicitFormat = responseFormat || response_format; | |
| if (explicitFormat) { | |
| if ( | |
| explicitFormat.type === "json_schema" && | |
| !explicitFormat.json_schema?.schema | |
| ) { | |
| throw new Error( | |
| "responseFormat json_schema requires a defined schema object" | |
| ); | |
| } | |
| return explicitFormat; | |
| } | |
| const schema = outputSchema || output_schema; | |
| if (!schema) return undefined; | |
| if (!schema.name || !schema.schema) { | |
| throw new Error("outputSchema requires both name and schema"); | |
| } | |
| return { | |
| type: "json_schema", | |
| json_schema: { | |
| name: schema.name, | |
| schema: schema.schema, | |
| ...(typeof schema.strict === "boolean" ? { strict: schema.strict } : {}), | |
| }, | |
| }; | |
| }; | |
| export async function invokeLLM(params: InvokeParams): Promise<InvokeResult> { | |
| assertApiKey(); | |
| const { | |
| messages, | |
| tools, | |
| toolChoice, | |
| tool_choice, | |
| outputSchema, | |
| output_schema, | |
| responseFormat, | |
| response_format, | |
| } = params; | |
| const payload: Record<string, unknown> = { | |
| model: "gemini-2.5-flash", | |
| messages: messages.map(normalizeMessage), | |
| }; | |
| if (tools && tools.length > 0) { | |
| payload.tools = tools; | |
| } | |
| const normalizedToolChoice = normalizeToolChoice( | |
| toolChoice || tool_choice, | |
| tools | |
| ); | |
| if (normalizedToolChoice) { | |
| payload.tool_choice = normalizedToolChoice; | |
| } | |
| payload.max_tokens = 32768 | |
| payload.thinking = { | |
| "budget_tokens": 128 | |
| } | |
| const normalizedResponseFormat = normalizeResponseFormat({ | |
| responseFormat, | |
| response_format, | |
| outputSchema, | |
| output_schema, | |
| }); | |
| if (normalizedResponseFormat) { | |
| payload.response_format = normalizedResponseFormat; | |
| } | |
| const response = await fetch(resolveApiUrl(), { | |
| method: "POST", | |
| headers: { | |
| "content-type": "application/json", | |
| authorization: `Bearer ${ENV.forgeApiKey}`, | |
| }, | |
| body: JSON.stringify(payload), | |
| }); | |
| if (!response.ok) { | |
| const errorText = await response.text(); | |
| throw new Error( | |
| `LLM invoke failed: ${response.status} ${response.statusText} – ${errorText}` | |
| ); | |
| } | |
| return (await response.json()) as InvokeResult; | |
| } | |