使用Deno构建多种大模型AI API代理
3087 字
15 分钟
使用Deno构建多种大模型AI API代理
Image API Error

使用 Deno 搭建大模型 API 反向代理服务#
背景介绍#
随着各大 AI 公司相继推出自己的语言模型 API 服务,开发者在使用这些服务时经常会遇到网络访问的问题。本文将介绍如何使用 Deno 搭建一个反向代理服务器,用于代理访问各种 AI 模型的 API 接口。
为什么使用 Deno#
众所周知cloudflare workers可以作为一个非常好的反向代理服务,但是它的免费版有很多限制,比如每天的请求次数有限制,还有会暴露你的ip地址等。 Deno是一个安全的运行时环境,它的安全性和性能都非常好,而且它的部署也非常简单。
更新日志#
- 2025.1.10 更新: 避免出现 “Unable to submit request because it must have a text parameter”
- 2025.4.7 更新: 添加 mistral 代理;添加 gemini 的 gemma 系列模型;更换默认模型为 gemini-2.0-flash
- 2025.4.16 更新: 跟随上游更新,添加工具调用
- 2025.5.2 更新: 添加 seed 参数支持
- 2025.8.9 更新: 修改默认模型为 gemini-2.5-flash
代码实现#
1import { serve } from "https://deno.land/std/http/server.ts";2import { Buffer } from "node:buffer";3
4const apiMapping = {5 '/discord': 'https://discord.com/api',6 '/telegram': 'https://api.telegram.org',7 '/openai': 'https://api.openai.com',8 '/claude': 'https://api.anthropic.com',9 '/meta': 'https://www.meta.ai/api',10 '/groq': 'https://api.groq.com/openai',11 '/xai': 'https://api.x.ai',12 '/cohere': 'https://api.cohere.ai',13 '/huggingface': 'https://api-inference.huggingface.co',14 '/together': 'https://api.together.xyz',15 '/novita': 'https://api.novita.ai',16 '/portkey': 'https://api.portkey.ai',17 '/fireworks': 'https://api.fireworks.ai',18 '/openrouter': 'https://openrouter.ai/api',19 '/nvidia': 'https://integrate.api.nvidia.com',20 '/cerebras': 'https://api.cerebras.ai',21 '/sambanova': 'https://api.sambanova.ai',22 '/gemini': 'https://generativelanguage.googleapis.com',23 '/mistral': 'https://api.mistral.ai'24};25
26class HttpError extends Error {27 constructor(message, status) {28 super(message);29 this.name = this.constructor.name;30 this.status = status;31 }32}33
34const fixCors = ({ headers, status, statusText }) => {35 headers = new Headers(headers);36 headers.set("Access-Control-Allow-Origin", "*");37 return { headers, status, statusText };38};39
40const handleOPTIONS = async () => {41 return new Response(null, {42 headers: {43 "Access-Control-Allow-Origin": "*",44 "Access-Control-Allow-Methods": "*",45 "Access-Control-Allow-Headers": "*",46 }47 });48};49
50const BASE_URL = "https://generativelanguage.googleapis.com";51const API_VERSION = "v1beta";52const API_CLIENT = "genai-js/0.21.0";53
54const makeHeaders = (apiKey, more) => ({55 "x-goog-api-client": API_CLIENT,56 ...(apiKey && { "x-goog-api-key": apiKey }),57 ...more58});59
60async function handleModels(apiKey) {61 const response = await fetch(`${BASE_URL}/${API_VERSION}/models`, {62 headers: makeHeaders(apiKey),63 });64 let { body } = response;65 if (response.ok) {66 const { models } = JSON.parse(await response.text());67 body = JSON.stringify({68 object: "list",69 data: models.map(({ name }) => ({70 id: name.replace("models/", ""),71 object: "model",72 created: 0,73 owned_by: "",74 })),75 }, null, " ");76 }77 return new Response(body, fixCors(response));78}79
80const DEFAULT_EMBEDDINGS_MODEL = "text-embedding-004";81async function handleEmbeddings(req, apiKey) {82 if (typeof req.model !== "string") {83 throw new HttpError("model is not specified", 400);84 }85 let model;86 if (req.model.startsWith("models/")) {87 model = req.model;88 } else {89 if (!req.model.startsWith("gemini-")) {90 req.model = DEFAULT_EMBEDDINGS_MODEL;91 }92 model = "models/" + req.model;93 }94 if (!Array.isArray(req.input)) {95 req.input = [ req.input ];96 }97 const response = await fetch(`${BASE_URL}/${API_VERSION}/${model}:batchEmbedContents`, {98 method: "POST",99 headers: makeHeaders(apiKey, { "Content-Type": "application/json" }),100 body: JSON.stringify({101 "requests": req.input.map(text => ({102 model,103 content: { parts: { text } },104 outputDimensionality: req.dimensions,105 }))106 })107 });108 let { body } = response;109 if (response.ok) {110 const { embeddings } = JSON.parse(await response.text());111 body = JSON.stringify({112 object: "list",113 data: embeddings.map(({ values }, index) => ({114 object: "embedding",115 index,116 embedding: values,117 })),118 model: req.model,119 }, null, " ");120 }121 return new Response(body, fixCors(response));122}123
124const DEFAULT_MODEL = "gemini-2.5-flash";125async function handleCompletions(req, apiKey) {126 let model = DEFAULT_MODEL;127 switch (true) {128 case typeof req.model !== "string":129 break;130 case req.model.startsWith("models/"):131 model = req.model.substring(7);132 break;133 case req.model.startsWith("gemini-"):134 case req.model.startsWith("gemma-"):135 case req.model.startsWith("learnlm-"):136 model = req.model;137 }138 let body = await transformRequest(req);139 const extra = req.extra_body?.google140 if (extra) {141 if (extra.safety_settings) {142 body.safetySettings = extra.safety_settings;143 }144 if (extra.cached_content) {145 body.cachedContent = extra.cached_content;146 }147 if (extra.thinking_config) {148 body.generationConfig.thinkingConfig = extra.thinking_config;149 }150 }151 switch (true) {152 case model.endsWith(":search"):153 model = model.substring(0, model.length - 7);154 // eslint-disable-next-line no-fallthrough155 case req.model.endsWith("-search-preview"):156 body.tools = body.tools || [];157 body.tools.push({googleSearch: {}});158 }159 const TASK = req.stream ? "streamGenerateContent" : "generateContent";160 let url = `${BASE_URL}/${API_VERSION}/models/${model}:${TASK}`;161 if (req.stream) { url += "?alt=sse"; }162 const response = await fetch(url, {163 method: "POST",164 headers: makeHeaders(apiKey, { "Content-Type": "application/json" }),165 body: JSON.stringify(body),166 });167
168 body = response.body;169 if (response.ok) {170 let id = generateChatcmplId();171 if (req.stream) {172 body = response.body173 .pipeThrough(new TextDecoderStream())174 .pipeThrough(new TransformStream({175 transform: parseStream,176 flush: parseStreamFlush,177 buffer: "",178 }))179 .pipeThrough(new TransformStream({180 transform: toOpenAiStream,181 flush: toOpenAiStreamFlush,182 streamIncludeUsage: req.stream_options?.include_usage,183 model, id, last: [],184 }))185 .pipeThrough(new TextEncoderStream());186 } else {187 body = await response.text();188 try {189 body = JSON.parse(body);190 if (!body.candidates) {191 throw new Error("Invalid completion object");192 }193 } catch (err) {194 console.error("Error parsing response:", err);195 return new Response(body, fixCors(response)); // output as is196 }197 body = processCompletionsResponse(body, model, id);198 }199 }200 return new Response(body, fixCors(response));201}202
203const adjustProps = (schemaPart) => {204 if (typeof schemaPart !== "object" || schemaPart === null) {205 return;206 }207 if (Array.isArray(schemaPart)) {208 schemaPart.forEach(adjustProps);209 } else {210 if (schemaPart.type === "object" && schemaPart.properties && schemaPart.additionalProperties === false) {211 delete schemaPart.additionalProperties;212 }213 Object.values(schemaPart).forEach(adjustProps);214 }215 };216 const adjustSchema = (schema) => {217 const obj = schema[schema.type];218 delete obj.strict;219 return adjustProps(schema);220 };221
222const harmCategory = [223 "HARM_CATEGORY_HATE_SPEECH",224 "HARM_CATEGORY_SEXUALLY_EXPLICIT",225 "HARM_CATEGORY_DANGEROUS_CONTENT",226 "HARM_CATEGORY_HARASSMENT",227 "HARM_CATEGORY_CIVIC_INTEGRITY",228];229
230const safetySettings = harmCategory.map(category => ({231 category,232 threshold: "BLOCK_NONE",233}));234
235const fieldsMap = {236 frequency_penalty: "frequencyPenalty",237 max_completion_tokens: "maxOutputTokens",238 max_tokens: "maxOutputTokens",239 n: "candidateCount", // not for streaming240 presence_penalty: "presencePenalty",241 seed: "seed",242 stop: "stopSequences",243 temperature: "temperature",244 top_k: "topK", // non-standard245 top_p: "topP",246};247
248const thinkingBudgetMap = {249 low: 1024,250 medium: 8192,251 high: 24576,252};253
254const transformConfig = (req) => {255 let cfg = {};256 for (let key in req) {257 const matchedKey = fieldsMap[key];258 if (matchedKey) {259 cfg[matchedKey] = req[key];260 }261 }262 if (req.response_format) {263 switch (req.response_format.type) {264 case "json_schema":265 adjustSchema(req.response_format);266 cfg.responseSchema = req.response_format.json_schema?.schema;267 if (cfg.responseSchema && "enum" in cfg.responseSchema) {268 cfg.responseMimeType = "text/x.enum";269 break;270 }271 case "json_object":272 cfg.responseMimeType = "application/json";273 break;274 case "text":275 cfg.responseMimeType = "text/plain";276 break;277 default:278 throw new HttpError("Unsupported response_format.type", 400);279 }280 }281 if (req.reasoning_effort) {282 cfg.thinkingConfig = { thinkingBudget: thinkingBudgetMap[req.reasoning_effort] };283 }284 return cfg;285};286
287const parseImg = async (url) => {288 let mimeType, data;289 if (url.startsWith("http://") || url.startsWith("https://")) {290 try {291 const response = await fetch(url);292 if (!response.ok) {293 throw new Error(`${response.status} ${response.statusText} (${url})`);294 }295 mimeType = response.headers.get("content-type");296 data = Buffer.from(await response.arrayBuffer()).toString("base64");297 } catch (err) {298 throw new HttpError("Error fetching image: " + err.toString(), 400);299 }300 } else {301 const match = url.match(/^data:(?<mimeType>.*?)(;base64)?,(?<data>.*)$/);302 if (!match) {303 throw new HttpError("Invalid image data: " + url, 400);304 }305 ({ mimeType, data } = match.groups);306 }307 return {308 inlineData: {309 mimeType,310 data,311 },312 };313};314
315const transformMsg = async ({ content, tool_calls, tool_call_id }, fnames) => {316 const parts = [];317 if (tool_call_id !== undefined) {318 let response;319 try {320 response = JSON.parse(content);321 } catch (err) {322 console.error("Error parsing function response content:", err);323 throw new HttpError("Invalid function response: " + content, 400);324 }325 if (typeof response !== "object" || response === null || Array.isArray(response)) {326 response = { result: response };327 }328 parts.push({329 functionResponse: {330 id: tool_call_id.startsWith("{") ? null : tool_call_id,331 name: fnames[tool_call_id],332 response,333 }334 });335 return parts;336 }337 if (tool_calls) {338 for (const tcall of tool_calls) {339 if (tcall.type !== "function") {340 throw new HttpError(`Unsupported tool_call type: "${tcall.type}"`, 400);341 }342 const { function: { arguments: argstr, name }, id } = tcall;343 let args;344 try {345 args = JSON.parse(argstr);346 } catch (err) {347 console.error("Error parsing function arguments:", err);348 throw new HttpError("Invalid function arguments: " + argstr, 400);349 }350 parts.push({351 functionCall: {352 id: id.startsWith("{") ? null : id,353 name,354 args,355 }356 });357 fnames[id] = name;358 }359 return parts;360 }361 if (!Array.isArray(content)) {362 parts.push({ text: content });363 return parts;364 }365 for (const item of content) {366 switch (item.type) {367 case "text":368 parts.push({ text: item.text });369 break;370 case "image_url":371 parts.push(await parseImg(item.image_url.url));372 break;373 case "input_audio":374 parts.push({375 inlineData: {376 mimeType: "audio/" + item.input_audio.format,377 data: item.input_audio.data,378 }379 });380 break;381 default:382 throw new HttpError(`Unknown "content" item type: "${item.type}"`, 400);383 }384 }385 if (content.every(item => item.type === "image_url")) {386 parts.push({ text: "" }); // to avoid "Unable to submit request because it must have a text parameter"387 }388 return parts;389};390
391const transformMessages = async (messages) => {392 if (!messages) { return; }393 const contents = [];394 let system_instruction;395 const fnames = {}; // cache function names by tool_call_id between messages396 for (const item of messages) {397 if (item.role === "system") {398 delete item.role;399 system_instruction = { parts: await transformMsg(item) };400 } else {401 if (item.role === "assistant") {402 item.role = "model";403 } else if (item.role === "tool") {404 const prev = contents[contents.length - 1];405 if (prev?.role === "function") {406 prev.parts.push(...await transformMsg(item, fnames));407 continue;408 }409 item.role = "function"; // ignored410 } else if (item.role !== "user") {411 throw HttpError(`Unknown message role: "${item.role}"`, 400);412 }413 contents.push({414 role: item.role,415 parts: await transformMsg(item, fnames)416 });417 }418 }419 if (system_instruction) {420 if (!contents[0]?.parts.some(part => part.text)) {421 contents.unshift({ role: "user", parts: { text: " " } });422 }423 }424 return { system_instruction, contents };425};426
427const transformTools = (req) => {428 let tools, tool_config;429 if (req.tools) {430 const funcs = req.tools.filter(tool => tool.type === "function");431 funcs.forEach(adjustSchema);432 tools = [{ function_declarations: funcs.map(schema => schema.function) }];433 }434 if (req.tool_choice) {435 const allowed_function_names = req.tool_choice?.type === "function" ? [ req.tool_choice?.function?.name ] : undefined;436 if (allowed_function_names || typeof req.tool_choice === "string") {437 tool_config = {438 function_calling_config: {439 mode: allowed_function_names ? "ANY" : req.tool_choice.toUpperCase(),440 allowed_function_names441 }442 };443 }444 }445 return { tools, tool_config };446};447
448const transformRequest = async (req) => ({449 ...await transformMessages(req.messages),450 safetySettings,451 generationConfig: transformConfig(req),452 ...transformTools(req),453});454
455const generateChatcmplId = () => {456 const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";457 const randomChar = () => characters[Math.floor(Math.random() * characters.length)];458 return "chatcmpl-" + Array.from({ length: 29 }, randomChar).join("");459};460
461const reasonsMap = {462 "STOP": "stop",463 "MAX_TOKENS": "length",464 "SAFETY": "content_filter",465 "RECITATION": "content_filter",466};467const SEP = "\n\n|>";468const transformCandidates = (key, cand) => {469 const message = { role: "assistant", content: [] };470 for (const part of cand.content?.parts ?? []) {471 if (part.functionCall) {472 const fc = part.functionCall;473 message.tool_calls = message.tool_calls ?? [];474 message.tool_calls.push({475 id: fc.id ?? `{${fc.name}}`,476 type: "function",477 function: {478 name: fc.name,479 arguments: JSON.stringify(fc.args),480 }481 });482 } else {483 message.content.push(part.text);484 }485 }486 message.content = message.content.join(SEP) || null;487 return {488 index: cand.index || 0, // 0-index is absent in new -002 models response489 [key]: message,490 logprobs: null,491 finish_reason: message.tool_calls ? "tool_calls" : reasonsMap[cand.finishReason] || cand.finishReason,492 //original_finish_reason: cand.finishReason,493 };494};495
496const transformCandidatesMessage = transformCandidates.bind(null, "message");497const transformCandidatesDelta = transformCandidates.bind(null, "delta");498
499const transformUsage = (data) => ({500 completion_tokens: data.candidatesTokenCount,501 prompt_tokens: data.promptTokenCount,502 total_tokens: data.totalTokenCount503});504
505const checkPromptBlock = (choices, promptFeedback, key) => {506 if (choices.length) { return; }507 if (promptFeedback?.blockReason) {508 console.log("Prompt block reason:", promptFeedback.blockReason);509 if (promptFeedback.blockReason === "SAFETY") {510 promptFeedback.safetyRatings511 .filter(r => r.blocked)512 .forEach(r => console.log(r));513 }514 choices.push({515 index: 0,516 [key]: null,517 finish_reason: "content_filter",518 //original_finish_reason: data.promptFeedback.blockReason,519 });520 }521 return true;522};523
524const processCompletionsResponse = (data, model, id) => {525 const obj = {526 id,527 choices: data.candidates.map(transformCandidatesMessage),528 created: Math.floor(Date.now() / 1000),529 model: data.modelVersion ?? model,530 object: "chat.completion",531 usage: data.usageMetadata && transformUsage(data.usageMetadata),532 };533 if (obj.choices.length === 0 ) {534 checkPromptBlock(obj.choices, data.promptFeedback, "message");535 }536 return JSON.stringify(obj);537};538
539const responseLineRE = /^data: (.*)(?:\n\n|\r\r|\r\n\r\n)/;540function parseStream (chunk, controller) {541 this.buffer += chunk;542 do {543 const match = this.buffer.match(responseLineRE);544 if (!match) { break; }545 controller.enqueue(match[1]);546 this.buffer = this.buffer.substring(match[0].length);547 } while (true);548}549
550function parseStreamFlush (controller) {551 if (this.buffer) {552 console.error("Invalid data:", this.buffer);553 controller.enqueue(this.buffer);554 }555}556
557
558const delimiter = "\n\n";559const sseline = (obj) => {560 obj.created = Math.floor(Date.now() / 1000);561 return "data: " + JSON.stringify(obj) + delimiter;562};563
564function toOpenAiStream (line, controller) {565 let data;566 try {567 data = JSON.parse(line);568 if (!data.candidates) {569 throw new Error("Invalid completion chunk object");570 }571 } catch (err) {572 console.error("Error parsing response:", err);573 controller.enqueue(line); // output as is574 return;575 }576
577 const obj = {578 id: this.id,579 choices: data.candidates.map(transformCandidatesDelta),580 //created: Math.floor(Date.now()/1000),581 model: data.modelVersion ?? this.model,582 //system_fingerprint: "fp_69829325d0",583 object: "chat.completion.chunk",584 usage: data.usageMetadata && this.streamIncludeUsage ? null : undefined,585 };586 if (checkPromptBlock(obj.choices, data.promptFeedback, "delta")) {587 controller.enqueue(sseline(obj));588 return;589 }590 console.assert(data.candidates.length === 1, "Unexpected candidates count: %d", data.candidates.length);591 const cand = obj.choices[0];592 cand.index = cand.index || 0;593 const finish_reason = cand.finish_reason;594 cand.finish_reason = null;595 if (!this.last[cand.index]) { // first596 controller.enqueue(sseline({597 ...obj,598 choices: [{ ...cand, tool_calls: undefined, delta: { role: "assistant", content: "" } }],599 }));600 }601 delete cand.delta.role;602 if ("content" in cand.delta) { // prevent empty data (e.g. when MAX_TOKENS)603 controller.enqueue(sseline(obj));604 }605 cand.finish_reason = finish_reason;606 if (data.usageMetadata && this.streamIncludeUsage) {607 obj.usage = transformUsage(data.usageMetadata);608 }609 cand.delta = {};610 this.last[cand.index] = obj;611}612
613function toOpenAiStreamFlush (controller) {614 if (this.last.length > 0) {615 for (const obj of this.last) {616 controller.enqueue(sseline(obj));617 }618 controller.enqueue("data: [DONE]" + delimiter);619 }620}621
622const errHandler = (err) => {623 console.error(err);624 return new Response(err.message, fixCors({ status: err.status ?? 500 }));625};626
627function extractPrefixAndRest(pathname, prefixes) {628 for (const prefix of prefixes) {629 if (pathname.startsWith(prefix)) {630 return [prefix, pathname.slice(prefix.length)];631 }632 }633 return [null, null];634}635
636serve(async (request) => {637 const url = new URL(request.url);638 const pathname = url.pathname;639
640 if (pathname === '/' || pathname === '/index.html') {641 return new Response('Service is running!', {642 status: 200,643 headers: { 'Content-Type': 'text/html' }644 });645 }646
647 if (pathname === '/robots.txt') {648 return new Response('User-agent: *\nDisallow: /', {649 status: 200,650 headers: { 'Content-Type': 'text/plain' }651 });652 }653
654 // 特殊处理 /gemini 路径655 if (pathname.startsWith('/gemini')) {656 if (request.method === "OPTIONS") {657 return handleOPTIONS();658 }659
660 try {661 const auth = request.headers.get("Authorization");662 const apiKey = auth?.split(" ")[1];663
664 if (pathname.endsWith("/chat/completions")) {665 return handleCompletions(await request.json(), apiKey)666 .catch(errHandler);667 } else if (pathname.endsWith("/embeddings")) {668 return handleEmbeddings(await request.json(), apiKey)669 .catch(errHandler);670 } else if (pathname.endsWith("/models")) {671 return handleModels(apiKey)672 .catch(errHandler);673 } else {674 throw new HttpError("404 Not Found", 404);675 }676 } catch (err) {677 return errHandler(err);678 }679 }680
681 // 处理其他 API 路径682 const [prefix, rest] = extractPrefixAndRest(pathname, Object.keys(apiMapping));683 if (!prefix) {684 return new Response('Not Found', { status: 404 });685 }686
687 const targetUrl = `${apiMapping[prefix]}${rest}`;688
689 try {690 const headers = new Headers();691 const allowedHeaders = ['accept', 'content-type', 'authorization'];692 for (const [key, value] of request.headers.entries()) {693 if (allowedHeaders.includes(key.toLowerCase())) {694 headers.set(key, value);695 }696 }697
698 const response = await fetch(targetUrl, {699 method: request.method,700 headers: headers,701 body: request.body702 });703
704 const responseHeaders = new Headers(response.headers);705 responseHeaders.set('X-Content-Type-Options', 'nosniff');706 responseHeaders.set('X-Frame-Options', 'DENY');707 responseHeaders.set('Referrer-Policy', 'no-referrer');708
709 return new Response(response.body, {710 status: response.status,711 headers: responseHeaders712 });713
714 } catch (error) {715 console.error('Failed to fetch:', error);716 return new Response('Internal Server Error', { status: 500 });717 }718});食用方法#
- 复制上面的代码。
- 打开 Deno Playground。
- 用github账号登录。

image - 点击蓝色的
New Playground按钮。
image - 粘贴代码到左边的编辑器中。
- 点击上方的
Save & Deploy按钮。 - 当右边的终端显示
Service is running!时,你的服务就已经部署成功了。 - 在
Settings中可以设置你的域名。
image - 可以参考下面的表格,将BASE_URL改成对应的代理地址。
例如:
BASE_URL = "https://api.openai.com";改成BASE_URL = "https://你的地址/openai";请求就会变成https://你的地址/openai/v1/completions。
Important
Gemini已经转成了Openai格式,所以Gemini的请求地址也要改成Openai格式的。不能直接使用Gemini的请求格式。 如果只想单纯的代理Gemini的请求,可以部署下面的代码。
1import { serve } from "https://deno.land/std/http/server.ts";2
3const apiMapping = {4 '/discord': 'https://discord.com/api',5 '/telegram': 'https://api.telegram.org',6 '/openai': 'https://api.openai.com',7 '/claude': 'https://api.anthropic.com',8 '/meta': 'https://www.meta.ai/api',9 '/groq': 'https://api.groq.com/openai',10 '/xai': 'https://api.x.ai',11 '/cohere': 'https://api.cohere.ai',12 '/huggingface': 'https://api-inference.huggingface.co',13 '/together': 'https://api.together.xyz',14 '/novita': 'https://api.novita.ai',15 '/portkey': 'https://api.portkey.ai',16 '/fireworks': 'https://api.fireworks.ai',17 '/openrouter': 'https://openrouter.ai/api',18 '/nvidia': 'https://integrate.api.nvidia.com',19 '/cerebras': 'https://api.cerebras.ai',20 '/sambanova': 'https://api.sambanova.ai',21 '/gemini': 'https://generativelanguage.googleapis.com',22 '/mistral': 'https://api.mistral.ai'23};24
25const CORS_HEADERS = {26 "access-control-allow-origin": "*",27 "access-control-allow-methods": "*",28 "access-control-allow-headers": "*",29};30
31serve(async (request) => {32 // 处理 CORS 预检请求33 if (request.method === "OPTIONS") {34 return new Response(null, {35 headers: CORS_HEADERS,36 });37 }38
39 const url = new URL(request.url);40 const pathname = url.pathname;41
42 if (pathname === '/' || pathname === '/index.html') {43 return new Response('Service is running!', {44 status: 200,45 headers: {46 'Content-Type': 'text/html',47 ...CORS_HEADERS48 }49 });50 }51
52 if (pathname === '/robots.txt') {53 return new Response('User-agent: *\nDisallow: /', {54 status: 200,55 headers: {56 'Content-Type': 'text/plain',57 ...CORS_HEADERS58 }59 });60 }61
62 const [prefix, rest] = extractPrefixAndRest(pathname, Object.keys(apiMapping));63 if (!prefix) {64 return new Response('Not Found', { status: 404 });65 }66
67 try {68 // 特殊处理 Gemini API 请求69 if (prefix === '/gemini') {70 const targetUrl = new URL(`${apiMapping[prefix]}${rest}`);71
72 // 转发所有查询参数73 url.searchParams.forEach((value, key) => {74 targetUrl.searchParams.append(key, value);75 });76
77 const headers = new Headers();78 const geminiHeaders = [79 'accept',80 'content-type',81 'authorization',82 'x-goog-api-client',83 'x-goog-api-key'84 ];85
86 for (const [key, value] of request.headers.entries()) {87 if (geminiHeaders.includes(key.toLowerCase())) {88 headers.set(key, value);89 }90 }91
92 const response = await fetch(targetUrl, {93 method: request.method,94 headers: headers,95 body: request.body96 });97
98 const responseHeaders = new Headers({99 ...CORS_HEADERS,100 ...Object.fromEntries(response.headers),101 'X-Content-Type-Options': 'nosniff',102 'X-Frame-Options': 'DENY',103 'Referrer-Policy': 'no-referrer'104 });105
106 return new Response(response.body, {107 status: response.status,108 headers: responseHeaders109 });110 }111
112 // 处理其他 API 请求113 const targetUrl = `${apiMapping[prefix]}${rest}`;114 const headers = new Headers();115 const allowedHeaders = ['accept', 'content-type', 'authorization'];116
117 for (const [key, value] of request.headers.entries()) {118 if (allowedHeaders.includes(key.toLowerCase())) {119 headers.set(key, value);120 }121 }122
123 const response = await fetch(targetUrl, {124 method: request.method,125 headers: headers,126 body: request.body127 });128
129 const responseHeaders = new Headers(response.headers);130 responseHeaders.set('X-Content-Type-Options', 'nosniff');131 responseHeaders.set('X-Frame-Options', 'DENY');132 responseHeaders.set('Referrer-Policy', 'no-referrer');133
134 // 添加 CORS 头135 Object.entries(CORS_HEADERS).forEach(([key, value]) => {136 responseHeaders.set(key, value);137 });138
139 return new Response(response.body, {140 status: response.status,141 headers: responseHeaders142 });143
144 } catch (error) {145 console.error('Failed to fetch:', error);146 return new Response('Internal Server Error', { status: 500 });147 }148});149
150function extractPrefixAndRest(pathname: string, prefixes: string[]): [string | null, string | null] {151 for (const prefix of prefixes) {152 if (pathname.startsWith(prefix)) {153 return [prefix, pathname.slice(prefix.length)];154 }155 }156 return [null, null];157}代理地址#
| 代理地址 | 源地址 |
|---|---|
https://你的地址/anthropic | https://api.anthropic.com |
https://你的地址/cerebras | https://api.cerebras.ai |
https://你的地址/cohere | https://api.cohere.ai |
https://你的地址/discord | https://discord.com/api |
https://你的地址/fireworks | https://api.fireworks.ai |
https://你的地址/gemini | https://generativelanguage.googleapis.com |
https://你的地址/groq | https://api.groq.com/openai |
https://你的地址/huggingface | https://api-inference.huggingface.co |
https://你的地址/meta | https://www.meta.ai/api |
https://你的地址/novita | https://api.novita.ai |
https://你的地址/nvidia | https://integrate.api.nvidia.com |
https://你的地址/openai | https://api.openai.com |
https://你的地址/openrouter | https://openrouter.ai/api |
https://你的地址/portkey | https://api.portkey.ai |
https://你的地址/telegram | https://api.telegram.org |
https://你的地址/together | https://api.together.xyz |
https://你的地址/xai | https://api.x.ai |
https://你的地址/sambanova | https://api.sambanova.ai |
https://你的地址/mistral | https://api.mistral.ai |
参考项目#
PublicAffairs
/
openai-gemini
✦ Gemini ➜ OpenAI API proxy. Serverless!
—
—
MIT
JavaScript文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!
使用Deno构建多种大模型AI API代理
https://blog.wakaplus.com/posts/deno-api-proxy/最后更新于 2025-04-16
部分内容可能已过时
相关文章智能推荐
1
利用deno代理cloudflare ai gateway
Guides使用deno反代ai gateway,解决地区限制
2
Gemini的OpenAI API兼容性
Guidesgemini添加了对openai api的兼容性支持,原生支持openai api格式的调用
3
Llama-3 8B-Instruct自我托管的成本(转载)
Guides对llama-3 8B-Instruct自我托管的成本进行分析
4
Bitwarden 密码去重
Guides使用Python脚本给Bitwarden 密码去重
5
Windsurf 使用指南
GuidesWindsurf使用小技巧
随机文章随机推荐
暂无随机文章


