Skip to content

GeminiAI Compatibility with OpenAI

GeminiAI is a developer-friendly large language model known for its elegant interface, powerful features, and generous daily free quota that easily covers everyday usage.

However, it has some inconveniences, such as requiring a VPN at all times and API incompatibility with the OpenAI SDK.

To address these issues and achieve OpenAI compatibility, I wrote a JavaScript code snippet, deployed it to Cloudflare, and linked it to my own domain. This enables using Gemini in China without a VPN and ensures compatibility with OpenAI. You can easily replace the API address and secret key (SK) in any OpenAI-using tool.

Creating a Worker on Cloudflare

If you don't have a Cloudflare account, register for one (it's free). Registration address: https://dash.cloudflare.com/ After logging in, remember to link your own domain, otherwise, you won't be able to achieve proxy-free access.

After logging in, find Compute (Workers) in the left sidebar and click it, then click the Create button.

image.png

image.png

Click Create Worker on the page that appears.

image.png

Then click Deploy in the lower right corner. This completes the creation of the Worker.

image.png

Editing the Code

The code below is the key to achieving OpenAI compatibility. Copy it and replace the default generated code in the Worker.

On the page after deployment is complete, click Edit code.

image.png

Delete all the code on the left, then copy and paste the code below, and finally click Deploy in the upper right corner.

image.png

Copy the following code

javascript
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");
      const apiKey = auth?.split(" ")[1];
      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);
	  if(!pathname.endsWith("/chat/completions")){
		  return new Response("hello")
	  }
        assert(request.method === "POST");
        return handleCompletions(await request.json(), apiKey).catch(errHandler);
    } 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
});

const DEFAULT_MODEL = "gemini-2.0-flash-exp";
async function handleCompletions (req, apiKey) {
  let model = DEFAULT_MODEL;
  if(req.model.startsWith("gemini-")) {
      model = req.model;
  }
  const TASK = "generateContent";
  let url = `${BASE_URL}/${API_VERSION}/models/${model}:${TASK}`;

  const response = await fetch(url, {
    method: "POST",
    headers: makeHeaders(apiKey, { "Content-Type": "application/json" }),
    body: JSON.stringify(await transformRequest(req)), // try
  });

  let body = response.body;
  if (response.ok) {
    let id = generateChatcmplId();
      body = await response.text();
      body = processCompletionsResponse(JSON.parse(body), model, id);
  }
  return new Response(body, fixCors(response));
}

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 = {
  stop: "stopSequences",
  n: "candidateCount", 
  max_tokens: "maxOutputTokens",
  max_completion_tokens: "maxOutputTokens",
  temperature: "temperature",
  top_p: "topP",
  top_k: "topK", 
  frequency_penalty: "frequencyPenalty",
  presence_penalty: "presencePenalty",
};
const transformConfig = (req) => {
  let cfg = {};

  for (let key in req) {
    const matchedKey = fieldsMap[key];
    if (matchedKey) {
      cfg[matchedKey] = req[key];
    }
  }
  cfg.responseMimeType = "text/plain";
  return cfg;
};


const 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 "input_audio":
        parts.push({
          inlineData: {
            mimeType: "audio/" + item.input_audio.format,
            data: item.input_audio.data,
          }
        });
        break;
      default:
        throw new TypeError(`Unknown "content" item type: "${item.type}"`);
    }
  }
  if (content.every(item => item.type === "image_url")) {
    parts.push({ text: "" });	
  }
  return { role, parts };
};

const transformMessages = async (messages) => {
  if (!messages) { return; }
  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: "model", parts: { text: " " } });
  }
  return { system_instruction, contents };
};

const transformRequest = async (req) => ({
  ...await transformMessages(req.messages),
  safetySettings,
  generationConfig: transformConfig(req),
});

const generateChatcmplId = () => {
  const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
  const randomChar = () => characters[Math.floor(Math.random() * characters.length)];
  return "chatcmpl-" + Array.from({ length: 29 }, randomChar).join("");
};

const reasonsMap = { 
  "STOP": "stop",
  "MAX_TOKENS": "length",
  "SAFETY": "content_filter",
  "RECITATION": "content_filter"
};
const SEP = "\n\n|>";
const transformCandidates = (key, cand) => ({
  index: cand.index || 0,
  [key]: {
    role: "assistant",
    content: cand.content?.parts.map(p => p.text).join(SEP) },
  logprobs: null,
  finish_reason: reasonsMap[cand.finishReason] || 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 processCompletionsResponse = (data, model, id) => {
  return JSON.stringify({
    id,
    choices: data.candidates.map(transformCandidatesMessage),
    created: Math.floor(Date.now()/1000),
    model,
    object: "chat.completion",
    usage: transformUsage(data.usageMetadata),
  });
};

Binding a Domain

After deployment, you'll have a Cloudflare-provided second-level subdomain, but this domain cannot be accessed normally in China. Therefore, you need to bind your own domain to achieve proxy-free access.

After deployment, click Back on the left.

image.png

Then find Settings -- Domains & Routes and click Add.

image.png

image.png

As shown below, add the domain you've already hosted on Cloudflare.

image.png

Once done, you can use this domain to access Gemini.

Accessing Gemini with OpenAI SDK

python
from openai import OpenAI, APIConnectionError
model = OpenAI(api_key='Your Gemini API Key', base_url='https://yourcustomdomain.com')
response = model.chat.completions.create(
        model='gemini-2.0-flash-exp',
        messages=[
            {
                'role': 'user',
                'content': 'Who are you?'},
        ]
    )
    
print(response.choices[0])

Returns:

Choice(finish_reason='stop', index=0, logprobs=None, message=ChatCompletionMessage(content='I am a large language model, trained by Google.\n', refusal=None, role='assistant', audio=None, function_call=None, tool_calls=None))

Using in Other OpenAI-Compatible Tools

Find the location where you configure OpenAI information in the tool, change the API address to the custom domain you added in Cloudflare, change the SK to your Gemini API Key, and fill in gemini-2.0-flash-exp for the model.

image.png

image.png

Accessing Directly Using Requests

If you don't use the OpenAI SDK, you can also access it directly using the requests library.

python
import requests

payload={
    "model":"gemini-1.5-flash",
    "messages":[{
        "role":"user",
        "content":[{"type":"text","text":"Who are you?"}]
    }]
}

res=requests.post('https://xxxx.com/chat/completions',headers={"Authorization":"Bearer Your Gemini API Key","Content-Type":"application:/json"},json=payload)

print(res.json())

Output:

image.png

  1. The source code is modified from the project PublicAffairs/openai-gemini
  2. GeminiAI Documentation