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:
co-authored by
Claude Opus 4.8
parent
ee989f72bf
commit
84e7d89a75
+102
-103
@@ -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,
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user