Skip to content
Merged
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
3 changes: 3 additions & 0 deletions src/app/v1/[...route]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import "@/lib/polyfills/file";
import { Hono } from "hono";
import { handle } from "hono/vercel";
import { handleChatCompletions } from "@/app/v1/_lib/codex/chat-completions-handler";
import { registerCors } from "@/app/v1/_lib/cors";
import { handleProxyRequest } from "@/app/v1/_lib/proxy-handler";
import { logger } from "@/lib/logger";
import { sensitiveWordDetector } from "@/lib/sensitive-word-detector";
Expand All @@ -21,6 +22,8 @@ sensitiveWordDetector.reload().catch((err) => {

const app = new Hono().basePath("/v1");

registerCors(app);

// OpenAI Compatible API 路由
app.post("/chat/completions", handleChatCompletions);

Expand Down
94 changes: 94 additions & 0 deletions src/app/v1/_lib/cors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import type { Context, Hono } from "hono";

const DEFAULT_ALLOW_HEADERS =
"authorization,x-api-key,x-goog-api-key,content-type,anthropic-version,x-session-id,x-client-version";

const DEFAULT_CORS_HEADERS: Record<string, string> = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET,POST,PUT,PATCH,DELETE,OPTIONS",
"Access-Control-Allow-Headers": DEFAULT_ALLOW_HEADERS,
"Access-Control-Expose-Headers":
"x-request-id,x-ratelimit-limit,x-ratelimit-remaining,x-ratelimit-reset,retry-after",
"Access-Control-Max-Age": "86400", // 24 hours
};

/**
* 动态构建 CORS 响应头
*/
function buildCorsHeaders(options: { origin?: string | null; requestHeaders?: string | null }) {
const headers = new Headers(DEFAULT_CORS_HEADERS);

if (options.origin) {
headers.set("Access-Control-Allow-Origin", options.origin);
headers.append("Vary", "Origin");
}

if (options.requestHeaders) {
headers.set("Access-Control-Allow-Headers", options.requestHeaders);
headers.append("Vary", "Access-Control-Request-Headers");
}

if (headers.get("Access-Control-Allow-Origin") !== "*") {
headers.set("Access-Control-Allow-Credentials", "true");
}

return headers;
}

/**
* 为响应添加 CORS 头
*/
export function applyCors(
res: Response,
ctx: { origin?: string | null; requestHeaders?: string | null }
): Response {
const corsHeaders = buildCorsHeaders(ctx);
const headers = res.headers;

if (!headers || typeof headers.set !== "function") {
const safeStatus =
typeof res.status === "number" && res.status >= 200 && res.status <= 599 ? res.status : 200;
return new Response(res.body, { status: safeStatus, headers: corsHeaders });
}

corsHeaders.forEach((value, key) => {
if (key === "vary") {
headers.append(key, value);
} else {
headers.set(key, value);
}
});
return res;
}

/**
* 构建预检请求响应
*/
export function buildPreflightResponse(options: {
origin?: string | null;
requestHeaders?: string | null;
}): Response {
return new Response(null, { status: 204, headers: buildCorsHeaders(options) });
}

export const CORS_HEADERS = DEFAULT_CORS_HEADERS;

/**
* 注册 CORS 中间件
*/
export function registerCors(app: Hono): void {
app.use("*", async (c, next) => {
await next();
return applyCors(c.res, {
origin: c.req.header("origin"),
requestHeaders: c.req.header("access-control-request-headers"),
});
});

app.options("*", (c: Context) =>
buildPreflightResponse({
origin: c.req.header("origin"),
requestHeaders: c.req.header("access-control-request-headers"),
})
);
}
3 changes: 3 additions & 0 deletions src/app/v1beta/[...route]/route.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import "@/lib/polyfills/file";
import { Hono } from "hono";
import { handle } from "hono/vercel";
import { registerCors } from "@/app/v1/_lib/cors";
import { handleProxyRequest } from "@/app/v1/_lib/proxy-handler";

export const runtime = "nodejs";

// Gemini API 路由处理器(/v1beta/models/{model}:generateContent)
const app = new Hono().basePath("/v1beta");

registerCors(app);

// 所有 Gemini API 请求都通过 proxy handler 处理
// 格式检测会自动识别 Gemini 请求体中的 contents 字段
app.all("*", handleProxyRequest);
Expand Down