diff --git a/packages/console/app/src/routes/zen/go/v1/chat/completions.ts b/packages/console/app/src/routes/zen/go/v1/chat/completions.ts index 71fb8f2e6dbf..62d2c364b7dc 100644 --- a/packages/console/app/src/routes/zen/go/v1/chat/completions.ts +++ b/packages/console/app/src/routes/zen/go/v1/chat/completions.ts @@ -1,7 +1,12 @@ import type { APIEvent } from "@solidjs/start/server" import { handler } from "~/routes/zen/util/handler" +import { corsHeaders } from "~/routes/zen/util/cors" import { parseOpenAiVariant } from "~/routes/zen/util/variant" +export function OPTIONS(_input: APIEvent) { + return new Response(null, { status: 200, headers: corsHeaders }) +} + export function POST(input: APIEvent) { return handler(input, { format: "oa-compat", diff --git a/packages/console/app/src/routes/zen/go/v1/messages.ts b/packages/console/app/src/routes/zen/go/v1/messages.ts index d356b0bf53fd..8d3c566eb758 100644 --- a/packages/console/app/src/routes/zen/go/v1/messages.ts +++ b/packages/console/app/src/routes/zen/go/v1/messages.ts @@ -1,7 +1,12 @@ import type { APIEvent } from "@solidjs/start/server" import { handler } from "~/routes/zen/util/handler" +import { corsHeaders } from "~/routes/zen/util/cors" import { parseAnthropicVariant } from "~/routes/zen/util/variant" +export function OPTIONS(_input: APIEvent) { + return new Response(null, { status: 200, headers: corsHeaders }) +} + export function POST(input: APIEvent) { return handler(input, { format: "anthropic", diff --git a/packages/console/app/src/routes/zen/go/v1/responses.ts b/packages/console/app/src/routes/zen/go/v1/responses.ts index 54f5b6bfebcf..ac3ff4ed51e6 100644 --- a/packages/console/app/src/routes/zen/go/v1/responses.ts +++ b/packages/console/app/src/routes/zen/go/v1/responses.ts @@ -1,7 +1,12 @@ import type { APIEvent } from "@solidjs/start/server" import { handler } from "~/routes/zen/util/handler" +import { corsHeaders } from "~/routes/zen/util/cors" import { parseOpenAiVariant } from "~/routes/zen/util/variant" +export function OPTIONS(_input: APIEvent) { + return new Response(null, { status: 200, headers: corsHeaders }) +} + export function POST(input: APIEvent) { return handler(input, { format: "openai", diff --git a/packages/console/app/src/routes/zen/util/cors.ts b/packages/console/app/src/routes/zen/util/cors.ts new file mode 100644 index 000000000000..0ac237309d68 --- /dev/null +++ b/packages/console/app/src/routes/zen/util/cors.ts @@ -0,0 +1,5 @@ +export const corsHeaders = { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, POST, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type, Authorization", +} diff --git a/packages/console/app/src/routes/zen/util/handler.ts b/packages/console/app/src/routes/zen/util/handler.ts index c25cfddc9a38..c526cbedf9f7 100644 --- a/packages/console/app/src/routes/zen/util/handler.ts +++ b/packages/console/app/src/routes/zen/util/handler.ts @@ -15,6 +15,7 @@ import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js" import { ModelTable } from "@opencode-ai/console-core/schema/model.sql.js" import { ProviderTable } from "@opencode-ai/console-core/schema/provider.sql.js" import { logger } from "./logger" +import { corsHeaders } from "./cors" import { AuthError, CreditsError, @@ -304,6 +305,9 @@ export async function handler( resHeaders.set(k, v) } } + for (const [k, v] of Object.entries(corsHeaders)) { + resHeaders.set(k, v) + } logger.debug("STATUS: " + res.status + " " + res.statusText) // Handle non-streaming response @@ -450,7 +454,7 @@ export async function handler( // metric and 500 and return a quiet client-closed response. if (input.request.signal.aborted || error?.name === "AbortError") { logger.debug("REQUEST ABORTED BY CALLER") - return new Response(null, { status: 499 }) + return new Response(null, { status: 499, headers: corsHeaders }) } logger.metric({ @@ -472,7 +476,7 @@ export async function handler( type: "error", error: { type: error.constructor.name, message: error.message }, }), - { status: 403 }, + { status: 403, headers: corsHeaders }, ) // Note: both top level "type" and "error.type" fields are used by the @ai-sdk/anthropic client to render the error message. @@ -488,7 +492,7 @@ export async function handler( type: "error", error: { type: error.constructor.name, message: error.message }, }), - { status: 401 }, + { status: 401, headers: corsHeaders }, ) if ( @@ -498,6 +502,9 @@ export async function handler( error instanceof BlackUsageLimitError ) { const headers = new Headers() + for (const [k, v] of Object.entries(corsHeaders)) { + headers.set(k, v) + } if (error.retryAfter) { headers.set("retry-after", String(error.retryAfter)) } @@ -528,7 +535,7 @@ export async function handler( message: "Internal server error", }, }), - { status: 500 }, + { status: 500, headers: corsHeaders }, ) } diff --git a/packages/console/app/src/routes/zen/util/modelsHandler.ts b/packages/console/app/src/routes/zen/util/modelsHandler.ts index 102e4151b9fc..5efd9001d8fa 100644 --- a/packages/console/app/src/routes/zen/util/modelsHandler.ts +++ b/packages/console/app/src/routes/zen/util/modelsHandler.ts @@ -1,11 +1,9 @@ +import { corsHeaders } from "./cors" + export async function buildOptionsResponse() { return new Response(null, { status: 200, - headers: { - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Methods": "GET, POST, OPTIONS", - "Access-Control-Allow-Headers": "Content-Type, Authorization", - }, + headers: corsHeaders, }) } @@ -25,6 +23,7 @@ export async function buildModelsResponse(models: string[]) { { headers: { "Content-Type": "application/json", + ...corsHeaders, }, }, ) diff --git a/packages/console/app/src/routes/zen/v1/chat/completions.ts b/packages/console/app/src/routes/zen/v1/chat/completions.ts index 745e0c2182d4..282b7dc85e2c 100644 --- a/packages/console/app/src/routes/zen/v1/chat/completions.ts +++ b/packages/console/app/src/routes/zen/v1/chat/completions.ts @@ -1,7 +1,12 @@ import type { APIEvent } from "@solidjs/start/server" import { handler } from "~/routes/zen/util/handler" +import { corsHeaders } from "~/routes/zen/util/cors" import { parseOpenAiVariant } from "~/routes/zen/util/variant" +export function OPTIONS(_input: APIEvent) { + return new Response(null, { status: 200, headers: corsHeaders }) +} + export function POST(input: APIEvent) { return handler(input, { format: "oa-compat", diff --git a/packages/console/app/src/routes/zen/v1/messages.ts b/packages/console/app/src/routes/zen/v1/messages.ts index 876a16029ec1..6935caa4508b 100644 --- a/packages/console/app/src/routes/zen/v1/messages.ts +++ b/packages/console/app/src/routes/zen/v1/messages.ts @@ -1,7 +1,12 @@ import type { APIEvent } from "@solidjs/start/server" import { handler } from "~/routes/zen/util/handler" +import { corsHeaders } from "~/routes/zen/util/cors" import { parseAnthropicVariant } from "~/routes/zen/util/variant" +export function OPTIONS(_input: APIEvent) { + return new Response(null, { status: 200, headers: corsHeaders }) +} + export function POST(input: APIEvent) { return handler(input, { format: "anthropic", diff --git a/packages/console/app/src/routes/zen/v1/models/[model].ts b/packages/console/app/src/routes/zen/v1/models/[model].ts index 372f66676114..6e6527382412 100644 --- a/packages/console/app/src/routes/zen/v1/models/[model].ts +++ b/packages/console/app/src/routes/zen/v1/models/[model].ts @@ -1,7 +1,12 @@ import type { APIEvent } from "@solidjs/start/server" import { handler } from "~/routes/zen/util/handler" +import { corsHeaders } from "~/routes/zen/util/cors" import { parseGoogleVariant } from "~/routes/zen/util/variant" +export function OPTIONS(_input: APIEvent) { + return new Response(null, { status: 200, headers: corsHeaders }) +} + export function POST(input: APIEvent) { return handler(input, { format: "google", diff --git a/packages/console/app/src/routes/zen/v1/responses.ts b/packages/console/app/src/routes/zen/v1/responses.ts index b82735817f9c..455da5e9504e 100644 --- a/packages/console/app/src/routes/zen/v1/responses.ts +++ b/packages/console/app/src/routes/zen/v1/responses.ts @@ -1,7 +1,12 @@ import type { APIEvent } from "@solidjs/start/server" import { handler } from "~/routes/zen/util/handler" +import { corsHeaders } from "~/routes/zen/util/cors" import { parseOpenAiVariant } from "~/routes/zen/util/variant" +export function OPTIONS(_input: APIEvent) { + return new Response(null, { status: 200, headers: corsHeaders }) +} + export function POST(input: APIEvent) { return handler(input, { format: "openai", diff --git a/packages/console/app/test/cors.test.ts b/packages/console/app/test/cors.test.ts new file mode 100644 index 000000000000..d4c89862b486 --- /dev/null +++ b/packages/console/app/test/cors.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, test } from "bun:test" +import { corsHeaders } from "../src/routes/zen/util/cors" +import { buildModelsResponse, buildOptionsResponse } from "../src/routes/zen/util/modelsHandler" + +describe("corsHeaders", () => { + test("allows all origins", () => { + expect(corsHeaders["Access-Control-Allow-Origin"]).toBe("*") + }) + + test("allows GET, POST, and OPTIONS", () => { + expect(corsHeaders["Access-Control-Allow-Methods"]).toBe("GET, POST, OPTIONS") + }) + + test("allows content-type and authorization headers", () => { + expect(corsHeaders["Access-Control-Allow-Headers"]).toBe("Content-Type, Authorization") + }) +}) + +describe("buildOptionsResponse", () => { + test("returns 200 with CORS headers", async () => { + const response = await buildOptionsResponse() + expect(response.status).toBe(200) + expect(response.headers.get("Access-Control-Allow-Origin")).toBe("*") + expect(response.headers.get("Access-Control-Allow-Methods")).toBe("GET, POST, OPTIONS") + expect(response.headers.get("Access-Control-Allow-Headers")).toBe("Content-Type, Authorization") + }) +}) + +describe("buildModelsResponse", () => { + test("returns models with CORS headers", async () => { + const response = await buildModelsResponse(["model-1", "model-2"]) + expect(response.status).toBe(200) + expect(response.headers.get("Access-Control-Allow-Origin")).toBe("*") + expect(response.headers.get("Content-Type")).toBe("application/json") + + const body = await response.json() + expect(body.data.map((model: { id: string }) => model.id)).toEqual(["model-1", "model-2"]) + }) +})