source repo import
This commit is contained in:
9
src/deno_index.ts
Normal file
9
src/deno_index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { handleRequest } from "./handle_request.js";
|
||||
|
||||
async function denoHandleRequest(req: Request): Promise<Response> {
|
||||
const url = new URL(req.url);
|
||||
console.log('Request URL:', req.url);
|
||||
return handleRequest(req);
|
||||
};
|
||||
|
||||
Deno.serve({ port: 80 },denoHandleRequest);
|
||||
81
src/handle_request.js
Normal file
81
src/handle_request.js
Normal file
@@ -0,0 +1,81 @@
|
||||
import { handleVerification } from './verify_keys.js';
|
||||
import openai from './openai.mjs';
|
||||
|
||||
export async function handleRequest(request) {
|
||||
|
||||
const url = new URL(request.url);
|
||||
const pathname = url.pathname;
|
||||
const search = url.search;
|
||||
|
||||
if (pathname === '/' || pathname === '/index.html') {
|
||||
return new Response('Proxy is Running! More Details: https://github.com/tech-shrimp/gemini-balance-lite', {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'text/html' }
|
||||
});
|
||||
}
|
||||
|
||||
if (pathname === '/verify' && request.method === 'POST') {
|
||||
return handleVerification(request);
|
||||
}
|
||||
|
||||
// 处理OpenAI格式请求
|
||||
if (url.pathname.endsWith("/chat/completions") || url.pathname.endsWith("/completions") || url.pathname.endsWith("/embeddings") || url.pathname.endsWith("/models")) {
|
||||
return openai.fetch(request);
|
||||
}
|
||||
|
||||
const targetUrl = `https://generativelanguage.googleapis.com${pathname}${search}`;
|
||||
|
||||
try {
|
||||
const headers = new Headers();
|
||||
for (const [key, value] of request.headers.entries()) {
|
||||
if (key.trim().toLowerCase() === 'x-goog-api-key') {
|
||||
const apiKeys = value.split(',').map(k => k.trim()).filter(k => k);
|
||||
if (apiKeys.length > 0) {
|
||||
const selectedKey = apiKeys[Math.floor(Math.random() * apiKeys.length)];
|
||||
console.log(`Gemini Selected API Key: ${selectedKey}`);
|
||||
headers.set('x-goog-api-key', selectedKey);
|
||||
}
|
||||
} else {
|
||||
if (key.trim().toLowerCase()==='content-type')
|
||||
{
|
||||
headers.set(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Request Sending to Gemini')
|
||||
console.log('targetUrl:'+targetUrl)
|
||||
console.log(headers)
|
||||
|
||||
const response = await fetch(targetUrl, {
|
||||
method: request.method,
|
||||
headers: headers,
|
||||
body: request.body
|
||||
});
|
||||
|
||||
console.log("Call Gemini Success")
|
||||
|
||||
const responseHeaders = new Headers(response.headers);
|
||||
|
||||
console.log('Header from Gemini:')
|
||||
console.log(responseHeaders)
|
||||
|
||||
responseHeaders.delete('transfer-encoding');
|
||||
responseHeaders.delete('connection');
|
||||
responseHeaders.delete('keep-alive');
|
||||
responseHeaders.delete('content-encoding');
|
||||
responseHeaders.set('Referrer-Policy', 'no-referrer');
|
||||
|
||||
return new Response(response.body, {
|
||||
status: response.status,
|
||||
headers: responseHeaders
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch:', error);
|
||||
return new Response('Internal Server Error\n' + error?.stack, {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'text/plain' }
|
||||
});
|
||||
}
|
||||
};
|
||||
9
src/index.js
Normal file
9
src/index.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import { handleRequest } from "./handle_request.js";
|
||||
|
||||
export default {
|
||||
async fetch (req, env, context) {
|
||||
const url = new URL(req.url);
|
||||
console.log('Request URL:', req.url);
|
||||
return handleRequest(req);
|
||||
}
|
||||
}
|
||||
681
src/openai.mjs
Normal file
681
src/openai.mjs
Normal file
@@ -0,0 +1,681 @@
|
||||
//Author: PublicAffairs
|
||||
//Project: https://github.com/PublicAffairs/openai-gemini
|
||||
//MIT License : https://github.com/PublicAffairs/openai-gemini/blob/main/LICENSE
|
||||
|
||||
|
||||
import { Buffer } from "node:buffer";
|
||||
|
||||
export default {
|
||||
async fetch (request) {
|
||||
if (request.method === "OPTIONS") {
|
||||
return handleOPTIONS();
|
||||
}
|
||||
const errHandler = (err) => {
|
||||
console.error(err);
|
||||
return new Response(err.message, fixCors({ status: err.status ?? 500 }));
|
||||
};
|
||||
try {
|
||||
const auth = request.headers.get("Authorization");
|
||||
let apiKey = auth?.split(" ")[1];
|
||||
if (apiKey && apiKey.includes(',')) {
|
||||
const apiKeys = apiKey.split(',').map(k => k.trim()).filter(k => k);
|
||||
apiKey = apiKeys[Math.floor(Math.random() * apiKeys.length)];
|
||||
console.log(`OpenAI Selected API Key: ${apiKey}`);
|
||||
}
|
||||
const assert = (success) => {
|
||||
if (!success) {
|
||||
throw new HttpError("The specified HTTP method is not allowed for the requested resource", 400);
|
||||
}
|
||||
};
|
||||
const { pathname } = new URL(request.url);
|
||||
switch (true) {
|
||||
case pathname.endsWith("/chat/completions"):
|
||||
assert(request.method === "POST");
|
||||
return handleCompletions(await request.json(), apiKey)
|
||||
.catch(errHandler);
|
||||
case pathname.endsWith("/embeddings"):
|
||||
assert(request.method === "POST");
|
||||
return handleEmbeddings(await request.json(), apiKey)
|
||||
.catch(errHandler);
|
||||
case pathname.endsWith("/models"):
|
||||
assert(request.method === "GET");
|
||||
return handleModels(apiKey)
|
||||
.catch(errHandler);
|
||||
default:
|
||||
throw new HttpError("404 Not Found", 404);
|
||||
}
|
||||
} catch (err) {
|
||||
return errHandler(err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class HttpError extends Error {
|
||||
constructor(message, status) {
|
||||
super(message);
|
||||
this.name = this.constructor.name;
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
|
||||
const fixCors = ({ headers, status, statusText }) => {
|
||||
headers = new Headers(headers);
|
||||
headers.set("Access-Control-Allow-Origin", "*");
|
||||
return { headers, status, statusText };
|
||||
};
|
||||
|
||||
const handleOPTIONS = async () => {
|
||||
return new Response(null, {
|
||||
headers: {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "*",
|
||||
"Access-Control-Allow-Headers": "*",
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const BASE_URL = "https://generativelanguage.googleapis.com";
|
||||
const API_VERSION = "v1beta";
|
||||
|
||||
// https://github.com/google-gemini/generative-ai-js/blob/cf223ff4a1ee5a2d944c53cddb8976136382bee6/src/requests/request.ts#L71
|
||||
const API_CLIENT = "genai-js/0.21.0"; // npm view @google/generative-ai version
|
||||
const makeHeaders = (apiKey, more) => ({
|
||||
"x-goog-api-client": API_CLIENT,
|
||||
...(apiKey && { "x-goog-api-key": apiKey }),
|
||||
...more
|
||||
});
|
||||
|
||||
async function handleModels (apiKey) {
|
||||
const response = await fetch(`${BASE_URL}/${API_VERSION}/models`, {
|
||||
headers: makeHeaders(apiKey),
|
||||
});
|
||||
let { body } = response;
|
||||
if (response.ok) {
|
||||
const { models } = JSON.parse(await response.text());
|
||||
body = JSON.stringify({
|
||||
object: "list",
|
||||
data: models.map(({ name }) => ({
|
||||
id: name.replace("models/", ""),
|
||||
object: "model",
|
||||
created: 0,
|
||||
owned_by: "",
|
||||
})),
|
||||
}, null, " ");
|
||||
}
|
||||
return new Response(body, fixCors(response));
|
||||
}
|
||||
|
||||
const DEFAULT_EMBEDDINGS_MODEL = "text-embedding-004";
|
||||
async function handleEmbeddings (req, apiKey) {
|
||||
if (typeof req.model !== "string") {
|
||||
throw new HttpError("model is not specified", 400);
|
||||
}
|
||||
let model;
|
||||
if (req.model.startsWith("models/")) {
|
||||
model = req.model;
|
||||
} else {
|
||||
if (!req.model.startsWith("gemini-")) {
|
||||
req.model = DEFAULT_EMBEDDINGS_MODEL;
|
||||
}
|
||||
model = "models/" + req.model;
|
||||
}
|
||||
if (!Array.isArray(req.input)) {
|
||||
req.input = [ req.input ];
|
||||
}
|
||||
const response = await fetch(`${BASE_URL}/${API_VERSION}/${model}:batchEmbedContents`, {
|
||||
method: "POST",
|
||||
headers: makeHeaders(apiKey, { "Content-Type": "application/json" }),
|
||||
body: JSON.stringify({
|
||||
"requests": req.input.map(text => ({
|
||||
model,
|
||||
content: { parts: { text } },
|
||||
outputDimensionality: req.dimensions,
|
||||
}))
|
||||
})
|
||||
});
|
||||
let { body } = response;
|
||||
if (response.ok) {
|
||||
const { embeddings } = JSON.parse(await response.text());
|
||||
body = JSON.stringify({
|
||||
object: "list",
|
||||
data: embeddings.map(({ values }, index) => ({
|
||||
object: "embedding",
|
||||
index,
|
||||
embedding: values,
|
||||
})),
|
||||
model: req.model,
|
||||
}, null, " ");
|
||||
}
|
||||
return new Response(body, fixCors(response));
|
||||
}
|
||||
|
||||
const DEFAULT_MODEL = "gemini-2.5-flash";
|
||||
async function handleCompletions (req, apiKey) {
|
||||
let model = DEFAULT_MODEL;
|
||||
switch (true) {
|
||||
case typeof req.model !== "string":
|
||||
break;
|
||||
case req.model.startsWith("models/"):
|
||||
model = req.model.substring(7);
|
||||
break;
|
||||
case req.model.startsWith("gemini-"):
|
||||
case req.model.startsWith("gemma-"):
|
||||
case req.model.startsWith("learnlm-"):
|
||||
model = req.model;
|
||||
}
|
||||
let body = await transformRequest(req);
|
||||
const extra = req.extra_body?.google
|
||||
if (extra) {
|
||||
if (extra.safety_settings) {
|
||||
body.safetySettings = extra.safety_settings;
|
||||
}
|
||||
if (extra.cached_content) {
|
||||
body.cachedContent = extra.cached_content;
|
||||
}
|
||||
if (extra.thinking_config) {
|
||||
body.generationConfig.thinkingConfig = extra.thinking_config;
|
||||
}
|
||||
}
|
||||
switch (true) {
|
||||
case model.endsWith(":search"):
|
||||
model = model.substring(0, model.length - 7);
|
||||
// eslint-disable-next-line no-fallthrough
|
||||
case req.model.endsWith("-search-preview"):
|
||||
case req.tools?.some(tool => tool.function?.name === 'googleSearch'):
|
||||
body.tools = body.tools || [];
|
||||
body.tools.push({googleSearch: {}});
|
||||
}
|
||||
console.log(body.tools)
|
||||
const TASK = req.stream ? "streamGenerateContent" : "generateContent";
|
||||
let url = `${BASE_URL}/${API_VERSION}/models/${model}:${TASK}`;
|
||||
if (req.stream) { url += "?alt=sse"; }
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: makeHeaders(apiKey, { "Content-Type": "application/json" }),
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
body = response.body;
|
||||
if (response.ok) {
|
||||
let id = "chatcmpl-" + generateId(); //"chatcmpl-8pMMaqXMK68B3nyDBrapTDrhkHBQK";
|
||||
const shared = {};
|
||||
if (req.stream) {
|
||||
body = response.body
|
||||
.pipeThrough(new TextDecoderStream())
|
||||
.pipeThrough(new TransformStream({
|
||||
transform: parseStream,
|
||||
flush: parseStreamFlush,
|
||||
buffer: "",
|
||||
shared,
|
||||
}))
|
||||
.pipeThrough(new TransformStream({
|
||||
transform: toOpenAiStream,
|
||||
flush: toOpenAiStreamFlush,
|
||||
streamIncludeUsage: req.stream_options?.include_usage,
|
||||
model, id, last: [],
|
||||
shared,
|
||||
}))
|
||||
.pipeThrough(new TextEncoderStream());
|
||||
} else {
|
||||
body = await response.text();
|
||||
try {
|
||||
body = JSON.parse(body);
|
||||
if (!body.candidates) {
|
||||
throw new Error("Invalid completion object");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error parsing response:", err);
|
||||
return new Response(body, fixCors(response)); // output as is
|
||||
}
|
||||
body = processCompletionsResponse(body, model, id);
|
||||
}
|
||||
}
|
||||
return new Response(body, fixCors(response));
|
||||
}
|
||||
|
||||
const adjustProps = (schemaPart) => {
|
||||
if (typeof schemaPart !== "object" || schemaPart === null) {
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(schemaPart)) {
|
||||
schemaPart.forEach(adjustProps);
|
||||
} else {
|
||||
if (schemaPart.type === "object" && schemaPart.properties && schemaPart.additionalProperties === false) {
|
||||
delete schemaPart.additionalProperties;
|
||||
}
|
||||
Object.values(schemaPart).forEach(adjustProps);
|
||||
}
|
||||
};
|
||||
const adjustSchema = (schema) => {
|
||||
const obj = schema[schema.type];
|
||||
delete obj.strict;
|
||||
return adjustProps(schema);
|
||||
};
|
||||
|
||||
const harmCategory = [
|
||||
"HARM_CATEGORY_HATE_SPEECH",
|
||||
"HARM_CATEGORY_SEXUALLY_EXPLICIT",
|
||||
"HARM_CATEGORY_DANGEROUS_CONTENT",
|
||||
"HARM_CATEGORY_HARASSMENT",
|
||||
"HARM_CATEGORY_CIVIC_INTEGRITY",
|
||||
];
|
||||
const safetySettings = harmCategory.map(category => ({
|
||||
category,
|
||||
threshold: "BLOCK_NONE",
|
||||
}));
|
||||
const fieldsMap = {
|
||||
frequency_penalty: "frequencyPenalty",
|
||||
max_completion_tokens: "maxOutputTokens",
|
||||
max_tokens: "maxOutputTokens",
|
||||
n: "candidateCount", // not for streaming
|
||||
presence_penalty: "presencePenalty",
|
||||
seed: "seed",
|
||||
stop: "stopSequences",
|
||||
temperature: "temperature",
|
||||
top_k: "topK", // non-standard
|
||||
top_p: "topP",
|
||||
};
|
||||
const thinkingBudgetMap = {
|
||||
low: 1024,
|
||||
medium: 8192,
|
||||
high: 24576,
|
||||
};
|
||||
const transformConfig = (req) => {
|
||||
let cfg = {};
|
||||
//if (typeof req.stop === "string") { req.stop = [req.stop]; } // no need
|
||||
for (let key in req) {
|
||||
const matchedKey = fieldsMap[key];
|
||||
if (matchedKey) {
|
||||
cfg[matchedKey] = req[key];
|
||||
}
|
||||
}
|
||||
if (req.response_format) {
|
||||
switch (req.response_format.type) {
|
||||
case "json_schema":
|
||||
adjustSchema(req.response_format);
|
||||
cfg.responseSchema = req.response_format.json_schema?.schema;
|
||||
if (cfg.responseSchema && "enum" in cfg.responseSchema) {
|
||||
cfg.responseMimeType = "text/x.enum";
|
||||
break;
|
||||
}
|
||||
// eslint-disable-next-line no-fallthrough
|
||||
case "json_object":
|
||||
cfg.responseMimeType = "application/json";
|
||||
break;
|
||||
case "text":
|
||||
cfg.responseMimeType = "text/plain";
|
||||
break;
|
||||
default:
|
||||
throw new HttpError("Unsupported response_format.type", 400);
|
||||
}
|
||||
}
|
||||
if (req.reasoning_effort) {
|
||||
cfg.thinkingConfig = { thinkingBudget: thinkingBudgetMap[req.reasoning_effort] };
|
||||
}
|
||||
return cfg;
|
||||
};
|
||||
|
||||
const parseImg = async (url) => {
|
||||
let mimeType, data;
|
||||
if (url.startsWith("http://") || url.startsWith("https://")) {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`${response.status} ${response.statusText} (${url})`);
|
||||
}
|
||||
mimeType = response.headers.get("content-type");
|
||||
data = Buffer.from(await response.arrayBuffer()).toString("base64");
|
||||
} catch (err) {
|
||||
throw new Error("Error fetching image: " + err.toString());
|
||||
}
|
||||
} else {
|
||||
const match = url.match(/^data:(?<mimeType>.*?)(;base64)?,(?<data>.*)$/);
|
||||
if (!match) {
|
||||
throw new HttpError("Invalid image data: " + url, 400);
|
||||
}
|
||||
({ mimeType, data } = match.groups);
|
||||
}
|
||||
return {
|
||||
inlineData: {
|
||||
mimeType,
|
||||
data,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const transformFnResponse = ({ content, tool_call_id }, parts) => {
|
||||
if (!parts.calls) {
|
||||
throw new HttpError("No function calls found in the previous message", 400);
|
||||
}
|
||||
let response;
|
||||
try {
|
||||
response = JSON.parse(content);
|
||||
} catch (err) {
|
||||
console.error("Error parsing function response content:", err);
|
||||
throw new HttpError("Invalid function response: " + content, 400);
|
||||
}
|
||||
if (typeof response !== "object" || response === null || Array.isArray(response)) {
|
||||
response = { result: response };
|
||||
}
|
||||
if (!tool_call_id) {
|
||||
throw new HttpError("tool_call_id not specified", 400);
|
||||
}
|
||||
const { i, name } = parts.calls[tool_call_id] ?? {};
|
||||
if (!name) {
|
||||
throw new HttpError("Unknown tool_call_id: " + tool_call_id, 400);
|
||||
}
|
||||
if (parts[i]) {
|
||||
throw new HttpError("Duplicated tool_call_id: " + tool_call_id, 400);
|
||||
}
|
||||
parts[i] = {
|
||||
functionResponse: {
|
||||
id: tool_call_id.startsWith("call_") ? null : tool_call_id,
|
||||
name,
|
||||
response,
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const transformFnCalls = ({ tool_calls }) => {
|
||||
const calls = {};
|
||||
const parts = tool_calls.map(({ function: { arguments: argstr, name }, id, type }, i) => {
|
||||
if (type !== "function") {
|
||||
throw new HttpError(`Unsupported tool_call type: "${type}"`, 400);
|
||||
}
|
||||
let args;
|
||||
try {
|
||||
args = JSON.parse(argstr);
|
||||
} catch (err) {
|
||||
console.error("Error parsing function arguments:", err);
|
||||
throw new HttpError("Invalid function arguments: " + argstr, 400);
|
||||
}
|
||||
calls[id] = {i, name};
|
||||
return {
|
||||
functionCall: {
|
||||
id: id.startsWith("call_") ? null : id,
|
||||
name,
|
||||
args,
|
||||
}
|
||||
};
|
||||
});
|
||||
parts.calls = calls;
|
||||
return parts;
|
||||
};
|
||||
|
||||
const transformMsg = async ({ content }) => {
|
||||
const parts = [];
|
||||
if (!Array.isArray(content)) {
|
||||
// system, user: string
|
||||
// assistant: string or null (Required unless tool_calls is specified.)
|
||||
parts.push({ text: content });
|
||||
return parts;
|
||||
}
|
||||
// user:
|
||||
// An array of content parts with a defined type.
|
||||
// Supported options differ based on the model being used to generate the response.
|
||||
// Can contain text, image, or audio inputs.
|
||||
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;
|
||||
case "input_audio":
|
||||
parts.push({
|
||||
inlineData: {
|
||||
mimeType: "audio/" + item.input_audio.format,
|
||||
data: item.input_audio.data,
|
||||
}
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw new HttpError(`Unknown "content" item type: "${item.type}"`, 400);
|
||||
}
|
||||
}
|
||||
if (content.every(item => item.type === "image_url")) {
|
||||
parts.push({ text: "" }); // to avoid "Unable to submit request because it must have a text parameter"
|
||||
}
|
||||
return parts;
|
||||
};
|
||||
|
||||
const transformMessages = async (messages) => {
|
||||
if (!messages) { return; }
|
||||
const contents = [];
|
||||
let system_instruction;
|
||||
for (const item of messages) {
|
||||
switch (item.role) {
|
||||
case "system":
|
||||
system_instruction = { parts: await transformMsg(item) };
|
||||
continue;
|
||||
case "tool":
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
let { role, parts } = contents[contents.length - 1] ?? {};
|
||||
if (role !== "function") {
|
||||
const calls = parts?.calls;
|
||||
parts = []; parts.calls = calls;
|
||||
contents.push({
|
||||
role: "function", // ignored
|
||||
parts
|
||||
});
|
||||
}
|
||||
transformFnResponse(item, parts);
|
||||
continue;
|
||||
case "assistant":
|
||||
item.role = "model";
|
||||
break;
|
||||
case "user":
|
||||
break;
|
||||
default:
|
||||
throw new HttpError(`Unknown message role: "${item.role}"`, 400);
|
||||
}
|
||||
contents.push({
|
||||
role: item.role,
|
||||
parts: item.tool_calls ? transformFnCalls(item) : await transformMsg(item)
|
||||
});
|
||||
}
|
||||
if (system_instruction) {
|
||||
if (!contents[0]?.parts.some(part => part.text)) {
|
||||
contents.unshift({ role: "user", parts: { text: " " } });
|
||||
}
|
||||
}
|
||||
//console.info(JSON.stringify(contents, 2));
|
||||
return { system_instruction, contents };
|
||||
};
|
||||
|
||||
const transformTools = (req) => {
|
||||
let tools, tool_config;
|
||||
if (req.tools) {
|
||||
const funcs = req.tools.filter(tool => tool.type === "function" && tool.function?.name !== 'googleSearch');
|
||||
if (funcs.length > 0) {
|
||||
funcs.forEach(adjustSchema);
|
||||
tools = [{ function_declarations: funcs.map(schema => schema.function) }];
|
||||
}
|
||||
}
|
||||
if (req.tool_choice) {
|
||||
const allowed_function_names = req.tool_choice?.type === "function" ? [ req.tool_choice?.function?.name ] : undefined;
|
||||
if (allowed_function_names || typeof req.tool_choice === "string") {
|
||||
tool_config = {
|
||||
function_calling_config: {
|
||||
mode: allowed_function_names ? "ANY" : req.tool_choice.toUpperCase(),
|
||||
allowed_function_names
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
return { tools, tool_config };
|
||||
};
|
||||
|
||||
const transformRequest = async (req) => ({
|
||||
...await transformMessages(req.messages),
|
||||
safetySettings,
|
||||
generationConfig: transformConfig(req),
|
||||
...transformTools(req),
|
||||
});
|
||||
|
||||
const generateId = () => {
|
||||
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
const randomChar = () => characters[Math.floor(Math.random() * characters.length)];
|
||||
return Array.from({ length: 29 }, randomChar).join("");
|
||||
};
|
||||
|
||||
const 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",
|
||||
};
|
||||
const SEP = "\n\n|>";
|
||||
const transformCandidates = (key, cand) => {
|
||||
const message = { role: "assistant", content: [] };
|
||||
for (const part of cand.content?.parts ?? []) {
|
||||
if (part.functionCall) {
|
||||
const fc = part.functionCall;
|
||||
message.tool_calls = message.tool_calls ?? [];
|
||||
message.tool_calls.push({
|
||||
id: fc.id ?? "call_" + generateId(),
|
||||
type: "function",
|
||||
function: {
|
||||
name: fc.name,
|
||||
arguments: JSON.stringify(fc.args),
|
||||
}
|
||||
});
|
||||
} else {
|
||||
message.content.push(part.text);
|
||||
}
|
||||
}
|
||||
message.content = message.content.join(SEP) || null;
|
||||
return {
|
||||
index: cand.index || 0, // 0-index is absent in new -002 models response
|
||||
[key]: message,
|
||||
logprobs: null,
|
||||
finish_reason: message.tool_calls ? "tool_calls" : reasonsMap[cand.finishReason] || cand.finishReason,
|
||||
//original_finish_reason: cand.finishReason,
|
||||
};
|
||||
};
|
||||
const transformCandidatesMessage = transformCandidates.bind(null, "message");
|
||||
const transformCandidatesDelta = transformCandidates.bind(null, "delta");
|
||||
|
||||
const transformUsage = (data) => ({
|
||||
completion_tokens: data.candidatesTokenCount,
|
||||
prompt_tokens: data.promptTokenCount,
|
||||
total_tokens: data.totalTokenCount
|
||||
});
|
||||
|
||||
const checkPromptBlock = (choices, promptFeedback, key) => {
|
||||
if (choices.length) { return; }
|
||||
if (promptFeedback?.blockReason) {
|
||||
console.log("Prompt block reason:", promptFeedback.blockReason);
|
||||
if (promptFeedback.blockReason === "SAFETY") {
|
||||
promptFeedback.safetyRatings
|
||||
.filter(r => r.blocked)
|
||||
.forEach(r => console.log(r));
|
||||
}
|
||||
choices.push({
|
||||
index: 0,
|
||||
[key]: null,
|
||||
finish_reason: "content_filter",
|
||||
//original_finish_reason: data.promptFeedback.blockReason,
|
||||
});
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const processCompletionsResponse = (data, model, id) => {
|
||||
const obj = {
|
||||
id,
|
||||
choices: data.candidates.map(transformCandidatesMessage),
|
||||
created: Math.floor(Date.now()/1000),
|
||||
model: data.modelVersion ?? model,
|
||||
//system_fingerprint: "fp_69829325d0",
|
||||
object: "chat.completion",
|
||||
usage: data.usageMetadata && transformUsage(data.usageMetadata),
|
||||
};
|
||||
if (obj.choices.length === 0 ) {
|
||||
checkPromptBlock(obj.choices, data.promptFeedback, "message");
|
||||
}
|
||||
return JSON.stringify(obj);
|
||||
};
|
||||
|
||||
const responseLineRE = /^data: (.*)(?:\n\n|\r\r|\r\n\r\n)/;
|
||||
function parseStream (chunk, controller) {
|
||||
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); // eslint-disable-line no-constant-condition
|
||||
}
|
||||
function parseStreamFlush (controller) {
|
||||
if (this.buffer) {
|
||||
console.error("Invalid data:", this.buffer);
|
||||
controller.enqueue(this.buffer);
|
||||
this.shared.is_buffers_rest = true;
|
||||
}
|
||||
}
|
||||
|
||||
const delimiter = "\n\n";
|
||||
const sseline = (obj) => {
|
||||
obj.created = Math.floor(Date.now()/1000);
|
||||
return "data: " + JSON.stringify(obj) + delimiter;
|
||||
};
|
||||
function toOpenAiStream (line, controller) {
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(line);
|
||||
if (!data.candidates) {
|
||||
throw new Error("Invalid completion chunk object");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error parsing response:", err);
|
||||
if (!this.shared.is_buffers_rest) { line =+ delimiter; }
|
||||
controller.enqueue(line); // output as is
|
||||
return;
|
||||
}
|
||||
const obj = {
|
||||
id: this.id,
|
||||
choices: data.candidates.map(transformCandidatesDelta),
|
||||
//created: Math.floor(Date.now()/1000),
|
||||
model: data.modelVersion ?? this.model,
|
||||
//system_fingerprint: "fp_69829325d0",
|
||||
object: "chat.completion.chunk",
|
||||
usage: data.usageMetadata && this.streamIncludeUsage ? null : undefined,
|
||||
};
|
||||
if (checkPromptBlock(obj.choices, data.promptFeedback, "delta")) {
|
||||
controller.enqueue(sseline(obj));
|
||||
return;
|
||||
}
|
||||
console.assert(data.candidates.length === 1, "Unexpected candidates count: %d", data.candidates.length);
|
||||
const cand = obj.choices[0];
|
||||
cand.index = cand.index || 0; // absent in new -002 models response
|
||||
const finish_reason = cand.finish_reason;
|
||||
cand.finish_reason = null;
|
||||
if (!this.last[cand.index]) { // first
|
||||
controller.enqueue(sseline({
|
||||
...obj,
|
||||
choices: [{ ...cand, tool_calls: undefined, delta: { role: "assistant", content: "" } }],
|
||||
}));
|
||||
}
|
||||
delete cand.delta.role;
|
||||
if ("content" in cand.delta) { // prevent empty data (e.g. when MAX_TOKENS)
|
||||
controller.enqueue(sseline(obj));
|
||||
}
|
||||
cand.finish_reason = finish_reason;
|
||||
if (data.usageMetadata && this.streamIncludeUsage) {
|
||||
obj.usage = transformUsage(data.usageMetadata);
|
||||
}
|
||||
cand.delta = {};
|
||||
this.last[cand.index] = obj;
|
||||
}
|
||||
function toOpenAiStreamFlush (controller) {
|
||||
if (this.last.length > 0) {
|
||||
for (const obj of this.last) {
|
||||
controller.enqueue(sseline(obj));
|
||||
}
|
||||
controller.enqueue("data: [DONE]" + delimiter);
|
||||
}
|
||||
}
|
||||
65
src/verify_keys.js
Normal file
65
src/verify_keys.js
Normal file
@@ -0,0 +1,65 @@
|
||||
async function verifyKey(key, controller) {
|
||||
const url = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent';
|
||||
const body = {
|
||||
"contents": [{
|
||||
"role": "user",
|
||||
"parts": [{
|
||||
"text": "Hello"
|
||||
}]
|
||||
}]
|
||||
};
|
||||
let result;
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-goog-api-key': key,
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (response.ok) {
|
||||
await response.text(); // Consume body to release connection
|
||||
result = { key: `${key.slice(0, 7)}......${key.slice(-7)}`, status: 'GOOD' };
|
||||
} else {
|
||||
const errorData = await response.json().catch(() => ({ error: { message: 'Unknown error' } }));
|
||||
result = { key: `${key.slice(0, 7)}......${key.slice(-7)}`, status: 'BAD', error: errorData.error.message };
|
||||
}
|
||||
} catch (e) {
|
||||
result = { key: `${key.slice(0, 7)}......${key.slice(-7)}`, status: 'ERROR', error: e.message };
|
||||
}
|
||||
controller.enqueue(new TextEncoder().encode('data: ' + JSON.stringify(result) + '\n\n'));
|
||||
}
|
||||
|
||||
export async function handleVerification(request) {
|
||||
try {
|
||||
const authHeader = request.headers.get('x-goog-api-key');
|
||||
if (!authHeader) {
|
||||
return new Response(JSON.stringify({ error: 'Missing x-goog-api-key header.' }), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
const keys = authHeader.split(',').map(k => k.trim()).filter(Boolean);
|
||||
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
const verificationPromises = keys.map(key => verifyKey(key, controller));
|
||||
await Promise.all(verificationPromises);
|
||||
controller.close();
|
||||
}
|
||||
});
|
||||
|
||||
return new Response(stream, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive',
|
||||
}
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
return new Response(JSON.stringify({ error: 'An unexpected error occurred: ' + e.message }), { status: 500, headers: { 'Content-Type': 'application/json' } });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user