fix: hybrid default export to satisfy Vercel/Node and Bun/Workers contracts

- Export handler as default function for Vercel Serverless Function runtime.
- Attach fetch method to default export object for Bun and Cloudflare Workers compatibility.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MythEclipse
2026-06-24 20:59:00 +07:00
co-authored by Claude Opus 4.8
parent ee989f72bf
commit 84e7d89a75
+102 -103
View File
@@ -247,7 +247,7 @@ async function handleRelay(req: Request): Promise<Response> {
// ── Execute upstream fetch ────────────────────────────────────── // ── Execute upstream fetch ──────────────────────────────────────
let response: Response; let response: Response;
try { try {
response = await fetch(targetUrlString, fetchOptions); response = await fetch(targetUrlString, fetchOptions);
} catch (err) { } catch (err) {
const classified = classifyFetchError(err); const classified = classifyFetchError(err);
logRelayEvent({ logRelayEvent({
@@ -280,126 +280,125 @@ async function handleRelay(req: Request): Promise<Response> {
// ─── Exported Vercel Function Handler ─────────────────────────────────────────── // ─── Exported Vercel Function Handler ───────────────────────────────────────────
/** /**
* Vercel Bun runtime handler. * Hybrid handler for Vercel/Node web-api and Bun/Workers runtimes.
* *
* Vercel's Bun runtime expects a `default` export that is an object with * Exports both:
* a `fetch` method — NOT a bare default function. This matches the * 1. Default function: parsed by Vercel Node runtime.
* standard `Bun.serve()` handler shape. * 2. Default.fetch(): parsed by Cloudflare / Bun runtimes.
*
* Handles routing, middleware, and relay logic — same semantics as the
* standalone Bun.serve() server, minus WebSocket support.
*/ */
export default { async function fetchHandler(req: Request): Promise<Response> {
async fetch(req: Request): Promise<Response> { const url = new URL(req.url);
const url = new URL(req.url);
// Static routes — show index only when no relay target is requested // Static routes — show index only when no relay target is requested
if (url.pathname === "/health") return handleHealth(); if (url.pathname === "/health") return handleHealth();
if (url.pathname === "/docs" || url.pathname === "/test") { if (url.pathname === "/docs" || url.pathname === "/test") {
return new Response(getTestPageHtml(), { return new Response(getTestPageHtml(), {
headers: { "Content-Type": "text/html; charset=utf-8" }, headers: { "Content-Type": "text/html; charset=utf-8" },
}); });
} }
if ( if (
url.pathname === "/" && url.pathname === "/" &&
req.method === "GET" && req.method === "GET" &&
!req.headers.get("x-relay-target") !req.headers.get("x-relay-target")
) { ) {
return handleIndex(); return handleIndex();
} }
// WebSocket upgrade — not supported in Vercel Functions // WebSocket upgrade — not supported in Vercel Functions
if ( if (
req.method === "GET" && req.method === "GET" &&
req.headers.get("upgrade")?.toLowerCase() === "websocket" req.headers.get("upgrade")?.toLowerCase() === "websocket"
) { ) {
return new Response( return new Response(
JSON.stringify({ JSON.stringify({
error: true, error: true,
code: "UNSUPPORTED", code: "UNSUPPORTED",
message: "WebSocket relay is not supported on this deployment", message: "WebSocket relay is not supported on this deployment",
}), }),
{ {
status: 400, status: 400,
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
"Access-Control-Allow-Origin": "*", "Access-Control-Allow-Origin": "*",
},
}, },
); },
} );
}
// AI proxy routes — OpenAI-compatible // AI proxy routes — OpenAI-compatible
if (url.pathname === "/v1/chat/completions") { if (url.pathname === "/v1/chat/completions") {
if (req.method === "OPTIONS") return createCorsPreflightResponse(); if (req.method === "OPTIONS") return createCorsPreflightResponse();
if (req.method !== "POST") return new Response("Method Not Allowed", { status: 405 }); if (req.method !== "POST") return new Response("Method Not Allowed", { status: 405 });
const authErr = requireAuth(req); const authErr = requireAuth(req);
if (authErr) return authErr; if (authErr) return authErr;
try { try {
const body = await req.json(); const body = await req.json();
return await handleChatCompletion(body); return await handleChatCompletion(body);
} catch (err) { } catch (err) {
const isJsonError = err instanceof Error && (err.name === "SyntaxError" || err.message.includes("JSON")); const isJsonError = err instanceof Error && (err.name === "SyntaxError" || err.message.includes("JSON"));
if (isJsonError) { if (isJsonError) {
return new Response(
JSON.stringify({ error: { message: "Invalid JSON body", type: "invalid_request_error" } }),
{ status: 400, headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" } },
);
}
return new Response( return new Response(
JSON.stringify({ error: { message: err instanceof Error ? err.message : "Internal Server Error", type: "server_error" } }), JSON.stringify({ error: { message: "Invalid JSON body", type: "invalid_request_error" } }),
{ status: 500, headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" } }, { status: 400, headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" } },
); );
} }
return new Response(
JSON.stringify({ error: { message: err instanceof Error ? err.message : "Internal Server Error", type: "server_error" } }),
{ status: 500, headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" } },
);
} }
}
// AI proxy routes — Anthropic-compatible // AI proxy routes — Anthropic-compatible
if (url.pathname === "/v1/messages") { if (url.pathname === "/v1/messages") {
if (req.method === "OPTIONS") return createCorsPreflightResponse(); if (req.method === "OPTIONS") return createCorsPreflightResponse();
if (req.method !== "POST") return new Response("Method Not Allowed", { status: 405 }); if (req.method !== "POST") return new Response("Method Not Allowed", { status: 405 });
const authErr = requireAuth(req); const authErr = requireAuth(req);
if (authErr) return authErr; if (authErr) return authErr;
try { try {
const body = await req.json(); const body = await req.json();
return await handleAnthropicMessages(body); return await handleAnthropicMessages(body);
} catch (err) { } catch (err) {
const isJsonError = err instanceof Error && (err.name === "SyntaxError" || err.message.includes("JSON")); const isJsonError = err instanceof Error && (err.name === "SyntaxError" || err.message.includes("JSON"));
if (isJsonError) { if (isJsonError) {
return new Response(
JSON.stringify({
type: "error",
error: { message: "Invalid JSON body", type: "invalid_request_error" },
}),
{ status: 400, headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" } },
);
}
return new Response( return new Response(
JSON.stringify({ JSON.stringify({
type: "error", type: "error",
error: { message: err instanceof Error ? err.message : "Internal Server Error", type: "server_error" }, error: { message: "Invalid JSON body", type: "invalid_request_error" },
}), }),
{ status: 500, headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" } }, { status: 400, headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" } },
); );
} }
}
// Models list
if (url.pathname === "/v1/models" && req.method === "GET") {
const authErr = requireAuth(req);
if (authErr) return authErr;
const models = listModels().map((id) => ({
id,
object: "model",
created: Math.floor(Date.now() / 1000),
owned_by: "proxy",
}));
return new Response( return new Response(
JSON.stringify({ object: "list", data: models }), JSON.stringify({
{ status: 200, headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" } }, type: "error",
error: { message: err instanceof Error ? err.message : "Internal Server Error", type: "server_error" },
}),
{ status: 500, headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" } },
); );
} }
}
// Generic HTTP relay // Models list
return handleRelay(req); if (url.pathname === "/v1/models" && req.method === "GET") {
}, const authErr = requireAuth(req);
}; if (authErr) return authErr;
const models = listModels().map((id) => ({
id,
object: "model",
created: Math.floor(Date.now() / 1000),
owned_by: "proxy",
}));
return new Response(
JSON.stringify({ object: "list", data: models }),
{ status: 200, headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" } },
);
}
// Generic HTTP relay
return handleRelay(req);
}
export default Object.assign(fetchHandler, {
fetch: fetchHandler,
});