From 38d7a83fe9ca5c0bde02f0c00276e9c3dba26495 Mon Sep 17 00:00:00 2001 From: MythEclipse Date: Wed, 10 Jun 2026 23:45:38 +0700 Subject: [PATCH] feat: add AI proxy routes to Workers and Vercel entry points - Add /v1/chat/completions, /v1/messages, /v1/models routes to src/worker.ts (Cloudflare Workers) and api/relay.ts (Vercel) - Import handleChatCompletion, handleAnthropicMessages handlers - Using the same backend routing as the standalone server Co-Authored-By: Claude Fable 5 --- api/relay.ts | 46 ++++++++++++++++++++++++++++++++++++++++++++++ src/worker.ts | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) diff --git a/api/relay.ts b/api/relay.ts index 1a47f58..21ebca6 100644 --- a/api/relay.ts +++ b/api/relay.ts @@ -27,6 +27,8 @@ import { import { checkBodySize } from "../src/middleware/body-limiter"; import { createRateLimiter } from "../src/middleware/rate-limiter"; import { logRelayEvent } from "../src/middleware/logger"; +import { handleChatCompletion, listModels } from "../src/lib/ai-proxy"; +import { handleAnthropicMessages, listAnthropicModels } from "../src/lib/anthropic-proxy"; // ─── Configuration ────────────────────────────────────────────────────────────── @@ -394,6 +396,50 @@ export default { ); } + // AI proxy routes — OpenAI-compatible + if (url.pathname === "/v1/chat/completions") { + if (req.method === "OPTIONS") return createCorsPreflightResponse(); + if (req.method !== "POST") return new Response("Method Not Allowed", { status: 405 }); + try { + const body = await req.json(); + return handleChatCompletion(body); + } catch { + 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": "*" } }, + ); + } + } + + // AI proxy routes — Anthropic-compatible + if (url.pathname === "/v1/messages") { + if (req.method === "OPTIONS") return createCorsPreflightResponse(); + if (req.method !== "POST") return new Response("Method Not Allowed", { status: 405 }); + try { + const body = await req.json(); + return handleAnthropicMessages(body); + } catch { + 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": "*" } }, + ); + } + } + + // Models list + if (url.pathname === "/v1/models" && req.method === "GET") { + const models = [...listModels(), ...listAnthropicModels()].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); }, diff --git a/src/worker.ts b/src/worker.ts index e32c9aa..e347cca 100644 --- a/src/worker.ts +++ b/src/worker.ts @@ -26,6 +26,8 @@ import { import { checkBodySize } from "./middleware/body-limiter"; import { createRateLimiter } from "./middleware/rate-limiter"; import { logRelayEvent } from "./middleware/logger"; +import { handleChatCompletion, listModels } from "./lib/ai-proxy"; +import { handleAnthropicMessages, listAnthropicModels } from "./lib/anthropic-proxy"; // ─── Types ─────────────────────────────────────────────────────────────────────── @@ -390,6 +392,50 @@ export default { ); } + // AI proxy routes — OpenAI-compatible + if (url.pathname === "/v1/chat/completions") { + if (req.method === "OPTIONS") return createCorsPreflightResponse(); + if (req.method !== "POST") return new Response("Method Not Allowed", { status: 405 }); + try { + const body = await req.json(); + return handleChatCompletion(body); + } catch { + 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": "*" } }, + ); + } + } + + // AI proxy routes — Anthropic-compatible + if (url.pathname === "/v1/messages") { + if (req.method === "OPTIONS") return createCorsPreflightResponse(); + if (req.method !== "POST") return new Response("Method Not Allowed", { status: 405 }); + try { + const body = await req.json(); + return handleAnthropicMessages(body); + } catch { + 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": "*" } }, + ); + } + } + + // Models list + if (url.pathname === "/v1/models" && req.method === "GET") { + const models = [...listModels(), ...listAnthropicModels()].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, env); },