Closed zhu327 closed 2 months ago
import { serve } from "https://deno.land/std@0.181.0/http/server.ts"; import { Buffer } from "node:buffer"; async function handler(request) { if (request.method === "OPTIONS") { return handleOPTIONS(); } const url = new URL(request.url); if (!url.pathname.endsWith("/v1/chat/completions") || request.method !== "POST") { return new Response("404 Not Found", { status: 404 }); } const auth = request.headers.get("Authorization"); let apiKey = auth && auth.split(" ")[1]; if (!apiKey) { return new Response("Bad credentials", { status: 401 }); } let json; try { json = await request.json(); if (!Array.isArray(json.messages)) { throw SyntaxError(".messages array required"); } } catch (err) { console.error(err.toString()); return new Response(err, { status: 400 }); } return handleRequest(json, apiKey); }; var handleOPTIONS = async () => { return new Response(null, { headers: { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "*", "Access-Control-Allow-Headers": "*" } }); }; var BASE_URL = "https://generativelanguage.googleapis.com"; var API_VERSION = "v1beta"; var API_CLIENT = "genai-js/0.5.0"; async function handleRequest(req, apiKey) { const MODEL = "gemini-1.5-pro-latest"; const TASK = req.stream ? "streamGenerateContent" : "generateContent"; let url = `${BASE_URL}/${API_VERSION}/models/${MODEL}:${TASK}`; if (req.stream) { url += "?alt=sse"; } let response; try { response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", "x-goog-api-key": apiKey, "x-goog-api-client": API_CLIENT }, body: JSON.stringify(await transformRequest(req)) // try }); } catch (err) { console.error(err); return new Response(err, { status: 400, headers: { "Access-Control-Allow-Origin": "*" } }); } let body; const headers = new Headers(response.headers); headers.set("Access-Control-Allow-Origin", "*"); if (response.ok) { let id = generateChatcmplId(); if (req.stream) { body = response.body.pipeThrough(new TextDecoderStream()).pipeThrough(new TransformStream({ transform: parseStream, flush: parseStreamFlush, buffer: "" })).pipeThrough(new TransformStream({ transform: toOpenAiStream, flush: toOpenAiStreamFlush, MODEL, id, last: [] })).pipeThrough(new TextEncoderStream()); } else { body = await response.text(); try { body = await processResponse(JSON.parse(body).candidates, MODEL, id); } catch (err) { console.error(err); response = { status: 500 }; headers.set("Content-Type", "text/plain"); } } } else { body = await response.text(); try { const { code, status, message } = JSON.parse(body).error; body = `Error: [${code} ${status}] ${message}`; } catch (err) {} headers.set("Content-Type", "text/plain"); } return new Response(body, { status: response.status, statusText: response.statusText, headers }); } var harmCategory = [ "HARM_CATEGORY_HATE_SPEECH", "HARM_CATEGORY_SEXUALLY_EXPLICIT", "HARM_CATEGORY_DANGEROUS_CONTENT", "HARM_CATEGORY_HARASSMENT" ]; var safetySettings = harmCategory.map((category) => ({ category, threshold: "BLOCK_NONE" })); var fieldsMap = { stop: "stopSequences", // n: "candidateCount", // { "error": { "code": 400, "message": "Only one candidate can be specified", "status": "INVALID_ARGUMENT" } } max_tokens: "maxOutputTokens", temperature: "temperature", top_p: "topP" //..."topK" }; var transformConfig = (req) => { let cfg = {}; for (let key in req) { const matchedKey = fieldsMap[key]; if (matchedKey) { cfg[matchedKey] = req[key]; } } if (req.response_format?.type === "json_object") { cfg.response_mime_type = "application/json"; } return cfg; }; var parseImg = async (url) => { let mimeType, data; if (url.startsWith("http://") || url.startsWith("https://")) { try { const response = await fetch(url); mimeType = response.headers.get("content-type"); data = Buffer.from(await response.arrayBuffer()).toString("base64"); } catch (err) { throw Error("Error fetching image: " + err.toString()); } } else { const match = url.match(/^data:(?<mimeType>.*?)(;base64)?,(?<data>.*)$/); if (!match) { throw Error("Invalid image data: " + url); } ({ mimeType, data } = match.groups); } return { inlineData: { mimeType, data } }; }; var transformMsg = async ({ role, content }) => { const parts = []; if (!Array.isArray(content)) { parts.push({ text: content }); return { role, parts }; } for (const item of content) { switch (item.type) { case "text": parts.push({ text: item.text }); break; case "image_url": parts.push(await parseImg(item.image_url.url)); break; default: throw TypeError(`Unknown "content" item type: "${item.type}"`); } } return { role, parts }; }; var transformMessages = async (messages) => { const contents = []; let system_instruction; for (const item of messages) { if (item.role === "system") { delete item.role; system_instruction = await transformMsg(item); } else { item.role = item.role === "assistant" ? "model" : "user"; contents.push(await transformMsg(item)); } } if (system_instruction && contents.length === 0) { contents.push({ role: "user", parts: { text: "" } }); } return { system_instruction, contents }; }; var transformRequest = async (req) => ({ ...await transformMessages(req.messages), safetySettings, generationConfig: transformConfig(req) }); var generateChatcmplId = () => { const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; let result = "chatcmpl-"; for (let i = 0; i <= 29; i++) { result += characters.charAt(Math.floor(Math.random() * characters.length)); } return result; }; var reasonsMap = { //https://ai.google.dev/api/rest/v1/GenerateContentResponse#finishreason //"FINISH_REASON_UNSPECIFIED": // Default value. This value is unused. "STOP": "stop", "MAX_TOKENS": "length", "SAFETY": "content_filter", "RECITATION": "content_filter" //"OTHER": "OTHER", // :"function_call", }; var transformCandidates = (key, cand) => ({ index: cand.index, [key]: { role: "assistant", content: cand.content?.parts[0].text }, logprobs: null, finish_reason: reasonsMap[cand.finishReason] || cand.finishReason }); var transformCandidatesMessage = transformCandidates.bind(null, "message"); var transformCandidatesDelta = transformCandidates.bind(null, "delta"); var processResponse = async (candidates, model, id) => { return JSON.stringify({ id, object: "chat.completion", created: Math.floor(Date.now() / 1e3), model, // system_fingerprint: "fp_69829325d0", choices: candidates.map(transformCandidatesMessage) }); }; var responseLineRE = /^data: (.*)(?:\n\n|\r\r|\r\n\r\n)/; async function parseStream(chunk, controller) { chunk = await chunk; if (!chunk) { return; } this.buffer += chunk; do { const match = this.buffer.match(responseLineRE); if (!match) { break; } controller.enqueue(match[1]); this.buffer = this.buffer.substring(match[0].length); } while (true); } async function parseStreamFlush(controller) { if (this.buffer) { console.error("Invalid data:", this.buffer); controller.enqueue(this.buffer); } } function transformResponseStream(cand, stop, first) { const item = transformCandidatesDelta(cand); if (stop) { item.delta = {}; } else { item.finish_reason = null; } if (first) { item.delta.content = ""; } else { delete item.delta.role; } const data = { id: this.id, object: "chat.completion.chunk", created: Math.floor(Date.now() / 1e3), model: this.MODEL, // system_fingerprint: "fp_69829325d0", choices: [item] }; return "data: " + JSON.stringify(data) + delimiter; } var delimiter = "\n\n"; async function toOpenAiStream(chunk, controller) { const transform = transformResponseStream.bind(this); const line = await chunk; if (!line) { return; } let candidates; try { candidates = JSON.parse(line).candidates; } catch (err) { console.error(line); console.error(err); const length = this.last.length || 1; candidates = Array.from({ length }, (_, index) => ({ finishReason: "error", content: { parts: [{ text: err }] }, index })); } for (const cand of candidates) { if (!this.last[cand.index]) { controller.enqueue(transform(cand, false, "first")); } this.last[cand.index] = cand; if (cand.content) { controller.enqueue(transform(cand)); } } } async function toOpenAiStreamFlush(controller) { const transform = transformResponseStream.bind(this); if (this.last.length > 0) { for (const cand of this.last) { controller.enqueue(transform(cand, "stop")); } controller.enqueue("data: [DONE]" + delimiter); } } serve(handler);
Just made a few code changes and adapted from Cloudflare Workers to Deno Deploy.