Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,9 +115,16 @@ Youtube: [https://www.youtube.com/@Tech_Shrimp](https://www.youtube.com/@Tech_Sh
### Gemini 代理

可以使用 Gemini 的原生 API 格式进行代理请求。

模型列表也会直接代理,可供客户端自动刷新:
```bash
curl --location 'https://<YOUR_DEPLOYED_DOMAIN>/v1beta/models' \
--header 'x-goog-api-key: <YOUR_GEMINI_API_KEY_1>,<YOUR_GEMINI_API_KEY_2>'
```

**Curl 示例:**
```bash
curl -X POST --location 'https://<YOUR_DEPLOYED_DOMAIN>/v1beta/models/gemini-2.5-pro:generateContent' \
curl --location 'https://<YOUR_DEPLOYED_DOMAIN>/v1beta/models/gemini-2.5-pro:generateContent' \
--header 'Content-Type: application/json' \
--header 'x-goog-api-key: <YOUR_GEMINI_API_KEY_1>,<YOUR_GEMINI_API_KEY_2>' \
--data '{
Expand All @@ -135,7 +142,7 @@ curl -X POST --location 'https://<YOUR_DEPLOYED_DOMAIN>/v1beta/models/gemini-2.5
```
**Curl 示例:(流式)**
```bash
curl -X POST --location 'https://<YOUR_DEPLOYED_DOMAIN>/v1beta/models/gemini-2.5-pro:generateContent?alt=sse' \
curl --location 'https://<YOUR_DEPLOYED_DOMAIN>/v1beta/models/gemini-2.5-pro:generateContent?alt=sse' \
--header 'Content-Type: application/json' \
--header 'x-goog-api-key: <YOUR_GEMINI_API_KEY_1>,<YOUR_GEMINI_API_KEY_2>' \
--data '{
Expand All @@ -160,7 +167,7 @@ curl -X POST --location 'https://<YOUR_DEPLOYED_DOMAIN>/v1beta/models/gemini-2.5

**Curl 示例:**
```bash
curl -X POST --location 'https://<YOUR_DEPLOYED_DOMAIN>/verify' \
curl --location 'https://<YOUR_DEPLOYED_DOMAIN>/verify' \
--header 'x-goog-api-key: <YOUR_GEMINI_API_KEY_1>,<YOUR_GEMINI_API_KEY_2>'
```

Expand All @@ -170,7 +177,7 @@ curl -X POST --location 'https://<YOUR_DEPLOYED_DOMAIN>/verify' \

**Curl 示例:**
```bash
curl -X POST --location 'https://<YOUR_DEPLOYED_DOMAIN>/chat/completions' \
curl --location 'https://<YOUR_DEPLOYED_DOMAIN>/chat/completions' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer <YOUR_GEMINI_API_KEY>' \
--data '{
Expand Down
18 changes: 15 additions & 3 deletions src/handle_request.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ 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', {
Expand All @@ -18,12 +17,25 @@ export async function handleRequest(request) {
return handleVerification(request);
}

const isModelsRequest = pathname.endsWith("/models");
const usesGeminiAuth = request.headers.has("x-goog-api-key") || url.searchParams.has("key");
const isGeminiModelsRequest =
pathname === "/v1beta/models" ||
(pathname === "/v1/models" && usesGeminiAuth);

// 处理OpenAI格式请求
if (url.pathname.endsWith("/chat/completions") || url.pathname.endsWith("/completions") || url.pathname.endsWith("/embeddings") || url.pathname.endsWith("/models")) {
if (pathname.endsWith("/chat/completions") || pathname.endsWith("/completions") || pathname.endsWith("/embeddings") || (isModelsRequest && !isGeminiModelsRequest)) {
return openai.fetch(request);
}

const targetUrl = `https://generativelanguage.googleapis.com${pathname}${search}`;
if (url.searchParams.has("key")) {
const apiKeys = url.searchParams.get("key").split(',').map(k => k.trim()).filter(k => k);
if (apiKeys.length > 0) {
url.searchParams.set("key", apiKeys[Math.floor(Math.random() * apiKeys.length)]);
}
}

const targetUrl = `https://generativelanguage.googleapis.com${pathname}${url.search}`;

try {
const headers = new Headers();
Expand Down
8 changes: 4 additions & 4 deletions src/openai.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,10 @@ const makeHeaders = (apiKey, more) => ({
...more
});

async function handleModels (apiKey) {
const response = await fetch(`${BASE_URL}/${API_VERSION}/models`, {
headers: makeHeaders(apiKey),
});
async function handleModels (apiKey) {
const response = await fetch(`${BASE_URL}/${API_VERSION}/models?pageSize=1000`, {
headers: makeHeaders(apiKey),
});
let { body } = response;
if (response.ok) {
const { models } = JSON.parse(await response.text());
Expand Down
81 changes: 81 additions & 0 deletions test/handle_request.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import assert from "node:assert/strict";
import test from "node:test";

import { handleRequest } from "../src/handle_request.js";

test("proxies Gemini v1beta model listing with x-goog-api-key", async () => {
const originalFetch = globalThis.fetch;
let upstreamRequest;

globalThis.fetch = async (url, init) => {
upstreamRequest = { url: String(url), init };
return Response.json({ models: [{ name: "models/gemini-test" }] });
};

try {
const response = await handleRequest(new Request("https://proxy.example/v1beta/models", {
headers: { "x-goog-api-key": "test-key" },
}));

assert.equal(response.status, 200);
assert.equal(upstreamRequest.url, "https://generativelanguage.googleapis.com/v1beta/models");
assert.equal(upstreamRequest.init.headers.get("x-goog-api-key"), "test-key");
assert.deepEqual(await response.json(), {
models: [{ name: "models/gemini-test" }],
});
} finally {
globalThis.fetch = originalFetch;
}
});

test("keeps OpenAI v1 model listing compatible", async () => {
const originalFetch = globalThis.fetch;
let upstreamRequest;

globalThis.fetch = async (url, init) => {
upstreamRequest = { url: String(url), init };
return Response.json({
models: [{ name: "models/gemini-test" }],
});
};

try {
const response = await handleRequest(new Request("https://proxy.example/v1/models", {
headers: { Authorization: "Bearer test-key" },
}));

assert.equal(upstreamRequest.url, "https://generativelanguage.googleapis.com/v1beta/models?pageSize=1000");
assert.equal(upstreamRequest.init.headers["x-goog-api-key"], "test-key");
assert.deepEqual(await response.json(), {
object: "list",
data: [{
id: "gemini-test",
object: "model",
created: 0,
owned_by: "",
}],
});
} finally {
globalThis.fetch = originalFetch;
}
});

test("selects one key from Gemini query authentication", async () => {
const originalFetch = globalThis.fetch;
const originalRandom = Math.random;
let upstreamUrl;

Math.random = () => 0.75;
globalThis.fetch = async (url) => {
upstreamUrl = String(url);
return Response.json({ models: [] });
};

try {
await handleRequest(new Request("https://proxy.example/v1/models?key=first-key,second-key"));
assert.equal(upstreamUrl, "https://generativelanguage.googleapis.com/v1/models?key=second-key");
} finally {
Math.random = originalRandom;
globalThis.fetch = originalFetch;
}
});