ai-chatbot/lib/ai/zcode-agent/model-gateway-protocol.ts

63 lines
5.1 KiB
TypeScript

import { AgentModelProtocolError } from "../runtime/agent-model-failover";
type RecordValue = Record<string, any>;
export function toResponsesRequest(body: RecordValue): RecordValue {
const input: RecordValue[] = [];
for (const message of body.messages || []) {
if (message.role === "tool") {
input.push({ type: "function_call_output", call_id: message.tool_call_id, output: typeof message.content === "string" ? message.content : JSON.stringify(message.content) });
continue;
}
if (message.content) input.push({ role: message.role, content: typeof message.content === "string" ? message.content : message.content.map((part: RecordValue) => {
if (part.type === "text") return { type: message.role === "assistant" ? "output_text" : "input_text", text: part.text };
if (part.type === "image_url") return { type: "input_image", image_url: part.image_url.url, detail: part.image_url.detail || "auto" };
throw new Error("Unsupported Agent input content");
}) });
for (const call of message.tool_calls || []) input.push({ type: "function_call", call_id: call.id, name: call.function.name, arguments: call.function.arguments });
}
const request: RecordValue = { input, store: false };
if (body.tools?.length) request.tools = body.tools.map((tool: RecordValue) => ({ type: "function", ...tool.function }));
if (body.tool_choice) request.tool_choice = typeof body.tool_choice === "string" ? body.tool_choice : { type: "function", name: body.tool_choice.function.name };
if (body.max_completion_tokens || body.max_tokens) request.max_output_tokens = body.max_completion_tokens || body.max_tokens;
for (const key of ["temperature", "top_p", "parallel_tool_calls"]) if (body[key] !== undefined) request[key] = body[key];
return request;
}
export function responsesCompletion(payload: RecordValue): RecordValue {
if (/content.?policy|content.?filter|safety|moderation/.test(JSON.stringify(payload.error || {}).toLowerCase()) ||
payload.output?.some((item: RecordValue) => item.content?.some((part: RecordValue) => part.type === "refusal"))) throw new Error("content policy rejection");
if (payload.error || payload.status !== "completed" || !Array.isArray(payload.output)) throw new AgentModelProtocolError();
const calls = payload.output.filter((item: RecordValue) => item.type === "function_call")
.map((item: RecordValue) => ({ id: item.call_id, type: "function", function: { name: item.name, arguments: item.arguments } }));
const text = payload.output.filter((item: RecordValue) => item.type === "message")
.flatMap((item: RecordValue) => item.content || []).filter((part: RecordValue) => part.type === "output_text")
.map((part: RecordValue) => part.text).join("");
return { id: payload.id, model: payload.model, choices: [{ index: 0, finish_reason: calls.length ? "tool_calls" : "stop", message: { role: "assistant", content: text, ...(calls.length ? { tool_calls: calls } : {}) } }],
usage: { prompt_tokens: payload.usage?.input_tokens || 0, completion_tokens: payload.usage?.output_tokens || 0, total_tokens: payload.usage?.total_tokens || 0 } };
}
export function completionResponse(payload: RecordValue, request: RecordValue) {
const choice = payload?.choices?.[0];
const message = choice?.message;
if (choice?.finish_reason === "content_filter" || message?.refusal) throw new Error("content policy rejection");
if (payload?.error || !message || !["stop", "tool_calls", "function_call"].includes(choice.finish_reason)) throw new AgentModelProtocolError();
const calls = message.tool_calls || [];
if (!Array.isArray(calls) || (!message.content && !calls.length)) throw new AgentModelProtocolError();
const allowed = new Set((request.tools || []).map((tool: RecordValue) => tool.function?.name));
for (const call of calls) {
if (!call.id || !allowed.has(call.function?.name) || typeof call.function?.arguments !== "string") throw new AgentModelProtocolError();
try { const args = JSON.parse(call.function.arguments); if (!args || typeof args !== "object" || Array.isArray(args)) throw new Error(); }
catch { throw new AgentModelProtocolError(); }
}
if (!request.stream) return Response.json(payload);
const base = { id: payload.id || `chatcmpl-${crypto.randomUUID()}`, model: payload.model || request.model, object: "chat.completion.chunk", created: payload.created || Math.floor(Date.now() / 1000) };
// Buffer the provider reply before handing tool fragments to the native core.
// A broken/truncated upstream answer can then fail over without replaying a tool.
const delta = { role: "assistant", content: message.content || null,
...(calls.length ? { tool_calls: calls.map((call: RecordValue, index: number) => ({ ...call, index })) } : {}) };
const chunk = (value: unknown) => `data: ${JSON.stringify(value)}\n\n`;
return new Response(chunk({ ...base, choices: [{ index: 0, delta, finish_reason: null }] }) +
chunk({ ...base, choices: [{ index: 0, delta: {}, finish_reason: choice.finish_reason }], usage: payload.usage }) + "data: [DONE]\n\n",
{ headers: { "content-type": "text/event-stream", "cache-control": "no-cache" } });
}