From 6b904421b742bcfd45f2e605c0320b0ae5fffda7 Mon Sep 17 00:00:00 2001 From: Icewind52 Date: Sun, 22 Feb 2026 03:30:49 -0800 Subject: [PATCH] feat: add /v1/responses endpoint for Codex CLI support - Add passthrough proxy to Copilot's /responses backend - Filter unsupported tools (e.g. web_search) before forwarding - Register /v1/responses route in server.ts - Enables models like gpt-5.3-codex that require the Responses API Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/routes/responses/handler.ts | 85 +++++++++++++++++++++++++++++++++ src/routes/responses/route.ts | 15 ++++++ src/server.ts | 4 ++ 3 files changed, 104 insertions(+) create mode 100644 src/routes/responses/handler.ts create mode 100644 src/routes/responses/route.ts diff --git a/src/routes/responses/handler.ts b/src/routes/responses/handler.ts new file mode 100644 index 000000000..bf944007b --- /dev/null +++ b/src/routes/responses/handler.ts @@ -0,0 +1,85 @@ +import type { Context } from "hono" + +import consola from "consola" +import { stream as honoStream } from "hono/streaming" + +import { copilotHeaders, copilotBaseUrl } from "~/lib/api-config" +import { awaitApproval } from "~/lib/approval" +import { HTTPError } from "~/lib/error" +import { checkRateLimit } from "~/lib/rate-limit" +import { state } from "~/lib/state" + +interface ResponsesTool { + type: string + [key: string]: unknown +} + +interface ResponsesPayload { + model: string + input: unknown + stream?: boolean + tools?: Array + [key: string]: unknown +} + +export async function handleResponse(c: Context) { + await checkRateLimit(state) + + if (!state.copilotToken) throw new Error("Copilot token not found") + + const payload = await c.req.json() + consola.debug("Responses API payload:", JSON.stringify(payload).slice(-400)) + + if (state.manualApprove) { + await awaitApproval() + } + + const headers: Record = { + ...copilotHeaders(state), + "X-Initiator": "user", + } + + // Strip tools not supported by Copilot backend (e.g. web_search) + if (Array.isArray(payload.tools)) { + payload.tools = payload.tools.filter((t) => t.type === "function") + if (payload.tools.length === 0) delete payload.tools + } + + const response = await fetch(`${copilotBaseUrl(state)}/responses`, { + method: "POST", + headers, + body: JSON.stringify(payload), + }) + + if (!response.ok) { + consola.error("Failed to create response", response) + throw new HTTPError("Failed to create response", response) + } + + // If streaming, pipe through the raw SSE stream + if (payload.stream) { + c.header("Content-Type", "text/event-stream") + c.header("Cache-Control", "no-cache") + c.header("Connection", "keep-alive") + + return honoStream(c, async (stream) => { + const body = response.body as ReadableStream | null + const reader = body?.getReader() + if (!reader) return + + try { + for (;;) { + const result = await reader.read() + if (result.done) break + await stream.write(result.value) + } + } finally { + reader.releaseLock() + } + }) + } + + // Non-streaming: return JSON directly + const result: unknown = await response.json() + return c.json(result as Record) +} diff --git a/src/routes/responses/route.ts b/src/routes/responses/route.ts new file mode 100644 index 000000000..e1f9d0b3c --- /dev/null +++ b/src/routes/responses/route.ts @@ -0,0 +1,15 @@ +import { Hono } from "hono" + +import { forwardError } from "~/lib/error" + +import { handleResponse } from "./handler" + +export const responsesRoutes = new Hono() + +responsesRoutes.post("/", async (c) => { + try { + return await handleResponse(c) + } catch (error) { + return await forwardError(c, error) + } +}) diff --git a/src/server.ts b/src/server.ts index 462a278f3..d1fad2ecc 100644 --- a/src/server.ts +++ b/src/server.ts @@ -6,6 +6,7 @@ import { completionRoutes } from "./routes/chat-completions/route" import { embeddingRoutes } from "./routes/embeddings/route" import { messageRoutes } from "./routes/messages/route" import { modelRoutes } from "./routes/models/route" +import { responsesRoutes } from "./routes/responses/route" import { tokenRoute } from "./routes/token/route" import { usageRoute } from "./routes/usage/route" @@ -29,3 +30,6 @@ server.route("/v1/embeddings", embeddingRoutes) // Anthropic compatible endpoints server.route("/v1/messages", messageRoutes) + +// OpenAI Responses API compatible endpoint +server.route("/v1/responses", responsesRoutes)