From ffd19ffe4910cea24fe005e389283ec50daeef32 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Apr 2026 22:20:26 +0000 Subject: [PATCH 1/7] feat: add OpenAI Responses API endpoint (/responses and /v1/responses) --- bun.lock | 1 + src/routes/responses/handler.ts | 61 +++++++++ src/routes/responses/route.ts | 15 ++ src/server.ts | 3 + src/services/copilot/create-responses.ts | 166 +++++++++++++++++++++++ 5 files changed, 246 insertions(+) create mode 100644 src/routes/responses/handler.ts create mode 100644 src/routes/responses/route.ts create mode 100644 src/services/copilot/create-responses.ts diff --git a/bun.lock b/bun.lock index 20e895e7f..9ece87578 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "copilot-api", diff --git a/src/routes/responses/handler.ts b/src/routes/responses/handler.ts new file mode 100644 index 000000000..0261897a7 --- /dev/null +++ b/src/routes/responses/handler.ts @@ -0,0 +1,61 @@ +import type { Context } from "hono" + +import consola from "consola" +import { streamSSE } from "hono/streaming" + +import { awaitApproval } from "~/lib/approval" +import { checkRateLimit } from "~/lib/rate-limit" +import { state } from "~/lib/state" +import { isNullish } from "~/lib/utils" +import { + createResponses, + type ResponseObject, + type ResponsesPayload, +} from "~/services/copilot/create-responses" + +export async function handleResponse(c: Context) { + await checkRateLimit(state) + + let payload = await c.req.json() + consola.debug("Request payload:", JSON.stringify(payload).slice(-400)) + + const selectedModel = state.models?.data.find( + (model) => model.id === payload.model, + ) + + if (isNullish(payload.max_output_tokens)) { + payload = { + ...payload, + max_output_tokens: selectedModel?.capabilities.limits.max_output_tokens, + } + consola.debug( + "Set max_output_tokens to:", + JSON.stringify(payload.max_output_tokens), + ) + } + + if (state.manualApprove) await awaitApproval() + + const response = await createResponses(payload) + + if (isNonStreaming(response)) { + consola.debug("Non-streaming response:", JSON.stringify(response)) + return c.json(response) + } + + consola.debug("Streaming response") + return streamSSE(c, async (stream) => { + for await (const chunk of response) { + consola.debug("Streaming chunk:", JSON.stringify(chunk)) + if (!chunk.data) continue + await stream.writeSSE({ + event: chunk.event, + data: chunk.data, + }) + } + }) +} + +const isNonStreaming = ( + response: Awaited>, +): response is ResponseObject => Object.hasOwn(response, "output") 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..4c968195e 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" @@ -19,6 +20,7 @@ server.get("/", (c) => c.text("Server running")) server.route("/chat/completions", completionRoutes) server.route("/models", modelRoutes) server.route("/embeddings", embeddingRoutes) +server.route("/responses", responsesRoutes) server.route("/usage", usageRoute) server.route("/token", tokenRoute) @@ -26,6 +28,7 @@ server.route("/token", tokenRoute) server.route("/v1/chat/completions", completionRoutes) server.route("/v1/models", modelRoutes) server.route("/v1/embeddings", embeddingRoutes) +server.route("/v1/responses", responsesRoutes) // Anthropic compatible endpoints server.route("/v1/messages", messageRoutes) diff --git a/src/services/copilot/create-responses.ts b/src/services/copilot/create-responses.ts new file mode 100644 index 000000000..d19681743 --- /dev/null +++ b/src/services/copilot/create-responses.ts @@ -0,0 +1,166 @@ +import consola from "consola" +import { events } from "fetch-event-stream" + +import { copilotHeaders, copilotBaseUrl } from "~/lib/api-config" +import { HTTPError } from "~/lib/error" +import { state } from "~/lib/state" + +export const createResponses = async (payload: ResponsesPayload) => { + if (!state.copilotToken) throw new Error("Copilot token not found") + + const enableVision = + Array.isArray(payload.input) + && payload.input.some( + (x) => + Array.isArray(x.content) + && x.content.some((part) => part.type === "input_image"), + ) + + const isAgentCall = + Array.isArray(payload.input) + && payload.input.some((msg) => ["assistant", "tool"].includes(msg.role)) + + const headers: Record = { + ...copilotHeaders(state, enableVision), + "X-Initiator": isAgentCall ? "agent" : "user", + } + + 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 (payload.stream) { + return events(response) + } + + return (await response.json()) as ResponseObject +} + +// Payload types + +export interface ResponsesPayload { + model: string + input: string | Array + stream?: boolean | null + temperature?: number | null + top_p?: number | null + max_output_tokens?: number | null + tools?: Array | null + tool_choice?: + | "auto" + | "none" + | "required" + | { type: "function"; name: string } + | null + previous_response_id?: string | null + instructions?: string | null + reasoning?: { effort: "low" | "medium" | "high" } | null + metadata?: Record | null + user?: string | null +} + +export interface InputMessage { + role: "user" | "assistant" | "system" | "developer" | "tool" + content: string | Array + name?: string + tool_call_id?: string +} + +export type InputContentPart = InputTextPart | InputImagePart | InputFilePart + +export interface InputTextPart { + type: "input_text" + text: string +} + +export interface InputImagePart { + type: "input_image" + image_url?: { url: string; detail?: "low" | "high" | "auto" } + file_id?: string +} + +export interface InputFilePart { + type: "input_file" + file_id?: string + file_url?: string + filename?: string +} + +export interface ResponseTool { + type: "function" + name: string + description?: string + parameters?: Record + strict?: boolean | null +} + +// Response types (non-streaming) + +export interface ResponseObject { + id: string + object: "response" + created_at: number + model: string + output: Array + status: "completed" | "incomplete" | "failed" | "cancelled" + usage?: ResponseUsage + instructions?: string | null + error?: ResponseError | null + metadata?: Record | null +} + +export type OutputItem = MessageOutputItem | FunctionCallOutputItem + +export interface MessageOutputItem { + type: "message" + id: string + role: "assistant" + content: Array + status: "completed" | "incomplete" +} + +export type OutputContentPart = OutputTextPart | RefusalPart + +export interface OutputTextPart { + type: "output_text" + text: string + annotations?: Array +} + +export interface RefusalPart { + type: "refusal" + refusal: string +} + +export interface FunctionCallOutputItem { + type: "function_call" + id: string + call_id: string + name: string + arguments: string + status: "completed" +} + +export interface ResponseUsage { + input_tokens: number + output_tokens: number + total_tokens: number + input_tokens_details?: { + cached_tokens: number + } + output_tokens_details?: { + reasoning_tokens: number + } +} + +export interface ResponseError { + code: string + message: string +} From fe293d27d50b442d6d10f991f448978ea1ddfce3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Apr 2026 01:42:04 +0000 Subject: [PATCH 2/7] feat: add better error logging for debugging - In forwardError: log HTTP status+URL for HTTPError, log stack trace for unexpected errors - In service files: log status code, status text, and URL on fetch failures - Add consola logging to create-embeddings.ts and get-copilot-token.ts (previously silent) - Add debug request logging to embeddings route; fix typo (paylod -> payload) Agent-Logs-Url: https://github.com/SlushEE0/copilot-api/sessions/7dc859ce-2373-4a74-bfca-df6bf8a5587a Co-authored-by: SlushEE0 <101753528+SlushEE0@users.noreply.github.com> --- src/lib/error.ts | 11 +++++++---- src/routes/embeddings/route.ts | 6 ++++-- src/services/copilot/create-chat-completions.ts | 4 +++- src/services/copilot/create-embeddings.ts | 9 ++++++++- src/services/copilot/create-responses.ts | 4 +++- src/services/github/get-copilot-token.ts | 9 ++++++++- 6 files changed, 33 insertions(+), 10 deletions(-) diff --git a/src/lib/error.ts b/src/lib/error.ts index c39c22596..11aad9f8e 100644 --- a/src/lib/error.ts +++ b/src/lib/error.ts @@ -13,8 +13,6 @@ export class HTTPError extends Error { } export async function forwardError(c: Context, error: unknown) { - consola.error("Error occurred:", error) - if (error instanceof HTTPError) { const errorText = await error.response.text() let errorJson: unknown @@ -23,7 +21,10 @@ export async function forwardError(c: Context, error: unknown) { } catch { errorJson = errorText } - consola.error("HTTP error:", errorJson) + consola.error( + `HTTP ${error.response.status} error from ${error.response.url}:`, + errorJson, + ) return c.json( { error: { @@ -35,10 +36,12 @@ export async function forwardError(c: Context, error: unknown) { ) } + const err = error instanceof Error ? error : new Error(String(error)) + consola.error(`Unexpected error: ${err.message}`, err.stack ?? err) return c.json( { error: { - message: (error as Error).message, + message: err.message, type: "error", }, }, diff --git a/src/routes/embeddings/route.ts b/src/routes/embeddings/route.ts index 4c4fc7b8a..96d42301f 100644 --- a/src/routes/embeddings/route.ts +++ b/src/routes/embeddings/route.ts @@ -1,4 +1,5 @@ import { Hono } from "hono" +import consola from "consola" import { forwardError } from "~/lib/error" import { @@ -10,8 +11,9 @@ export const embeddingRoutes = new Hono() embeddingRoutes.post("/", async (c) => { try { - const paylod = await c.req.json() - const response = await createEmbeddings(paylod) + const payload = await c.req.json() + consola.debug("Embeddings request payload:", JSON.stringify(payload)) + const response = await createEmbeddings(payload) return c.json(response) } catch (error) { diff --git a/src/services/copilot/create-chat-completions.ts b/src/services/copilot/create-chat-completions.ts index 8534151da..a763595ba 100644 --- a/src/services/copilot/create-chat-completions.ts +++ b/src/services/copilot/create-chat-completions.ts @@ -35,7 +35,9 @@ export const createChatCompletions = async ( }) if (!response.ok) { - consola.error("Failed to create chat completions", response) + consola.error( + `Failed to create chat completions: HTTP ${response.status} ${response.statusText} from ${response.url}`, + ) throw new HTTPError("Failed to create chat completions", response) } diff --git a/src/services/copilot/create-embeddings.ts b/src/services/copilot/create-embeddings.ts index f2ad5c233..3d6573bf2 100644 --- a/src/services/copilot/create-embeddings.ts +++ b/src/services/copilot/create-embeddings.ts @@ -1,3 +1,5 @@ +import consola from "consola" + import { copilotHeaders, copilotBaseUrl } from "~/lib/api-config" import { HTTPError } from "~/lib/error" import { state } from "~/lib/state" @@ -11,7 +13,12 @@ export const createEmbeddings = async (payload: EmbeddingRequest) => { body: JSON.stringify(payload), }) - if (!response.ok) throw new HTTPError("Failed to create embeddings", response) + if (!response.ok) { + consola.error( + `Failed to create embeddings: HTTP ${response.status} ${response.statusText} from ${response.url}`, + ) + throw new HTTPError("Failed to create embeddings", response) + } return (await response.json()) as EmbeddingResponse } diff --git a/src/services/copilot/create-responses.ts b/src/services/copilot/create-responses.ts index d19681743..a216ad34e 100644 --- a/src/services/copilot/create-responses.ts +++ b/src/services/copilot/create-responses.ts @@ -32,7 +32,9 @@ export const createResponses = async (payload: ResponsesPayload) => { }) if (!response.ok) { - consola.error("Failed to create response", response) + consola.error( + `Failed to create response: HTTP ${response.status} ${response.statusText} from ${response.url}`, + ) throw new HTTPError("Failed to create response", response) } diff --git a/src/services/github/get-copilot-token.ts b/src/services/github/get-copilot-token.ts index 98744bab1..307ba34ea 100644 --- a/src/services/github/get-copilot-token.ts +++ b/src/services/github/get-copilot-token.ts @@ -1,3 +1,5 @@ +import consola from "consola" + import { GITHUB_API_BASE_URL, githubHeaders } from "~/lib/api-config" import { HTTPError } from "~/lib/error" import { state } from "~/lib/state" @@ -10,7 +12,12 @@ export const getCopilotToken = async () => { }, ) - if (!response.ok) throw new HTTPError("Failed to get Copilot token", response) + if (!response.ok) { + consola.error( + `Failed to get Copilot token: HTTP ${response.status} ${response.statusText} from ${response.url}`, + ) + throw new HTTPError("Failed to get Copilot token", response) + } return (await response.json()) as GetCopilotTokenResponse } From de2cc2ff1f0bf3a5f0a4ff6fa6d90eba07ea0d8d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Apr 2026 01:43:06 +0000 Subject: [PATCH 3/7] refine: use clearer error prefix in forwardError Agent-Logs-Url: https://github.com/SlushEE0/copilot-api/sessions/7dc859ce-2373-4a74-bfca-df6bf8a5587a Co-authored-by: SlushEE0 <101753528+SlushEE0@users.noreply.github.com> --- src/lib/error.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/error.ts b/src/lib/error.ts index 11aad9f8e..8fb86df1d 100644 --- a/src/lib/error.ts +++ b/src/lib/error.ts @@ -37,7 +37,7 @@ export async function forwardError(c: Context, error: unknown) { } const err = error instanceof Error ? error : new Error(String(error)) - consola.error(`Unexpected error: ${err.message}`, err.stack ?? err) + consola.error(`Unhandled error in request: ${err.message}`, err.stack ?? err) return c.json( { error: { From 030d81c92609f7757b3785e94671ab39ddd06342 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Apr 2026 02:38:15 +0000 Subject: [PATCH 4/7] fix: replace Hono logger with consola-based middleware to fix Docker logging Agent-Logs-Url: https://github.com/SlushEE0/copilot-api/sessions/f73d813e-8b48-4d60-9b0d-ecb92412f572 Co-authored-by: SlushEE0 <101753528+SlushEE0@users.noreply.github.com> --- src/server.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/server.ts b/src/server.ts index 4c968195e..7cf618e52 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,6 +1,6 @@ +import consola from "consola" import { Hono } from "hono" import { cors } from "hono/cors" -import { logger } from "hono/logger" import { completionRoutes } from "./routes/chat-completions/route" import { embeddingRoutes } from "./routes/embeddings/route" @@ -12,7 +12,17 @@ import { usageRoute } from "./routes/usage/route" export const server = new Hono() -server.use(logger()) +server.use(async (c, next) => { + const isHealthCheck = c.req.method === "GET" && c.req.path === "/" + const log = isHealthCheck ? consola.debug.bind(consola) : consola.info.bind(consola) + + log(`<-- ${c.req.method} ${c.req.path}`) + const start = Date.now() + + await next() + + log(`--> ${c.req.method} ${c.req.path} ${c.res.status} ${Date.now() - start}ms`) +}) server.use(cors()) server.get("/", (c) => c.text("Server running")) From b579206f7f574bf53563380ddd4c55dd1cf320a0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Apr 2026 02:39:20 +0000 Subject: [PATCH 5/7] refactor: clean up custom logger middleware per code review Agent-Logs-Url: https://github.com/SlushEE0/copilot-api/sessions/f73d813e-8b48-4d60-9b0d-ecb92412f572 Co-authored-by: SlushEE0 <101753528+SlushEE0@users.noreply.github.com> --- src/server.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/server.ts b/src/server.ts index 7cf618e52..b83ce8509 100644 --- a/src/server.ts +++ b/src/server.ts @@ -13,15 +13,18 @@ import { usageRoute } from "./routes/usage/route" export const server = new Hono() server.use(async (c, next) => { - const isHealthCheck = c.req.method === "GET" && c.req.path === "/" - const log = isHealthCheck ? consola.debug.bind(consola) : consola.info.bind(consola) + const { method } = c.req + const { path } = c.req - log(`<-- ${c.req.method} ${c.req.path}`) + const isHealthCheck = method === "GET" && path === "/" + const log = isHealthCheck ? consola.debug : consola.info + + log(`<-- ${method} ${path}`) const start = Date.now() await next() - log(`--> ${c.req.method} ${c.req.path} ${c.res.status} ${Date.now() - start}ms`) + log(`--> ${method} ${path} ${c.res.status} ${Date.now() - start}ms`) }) server.use(cors()) From 8b06094bc43a3630dbd6a4d14ddb2cf4849638f4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Apr 2026 02:49:16 +0000 Subject: [PATCH 6/7] fix: configure consola to write through console.log for Docker/Bun compatibility Agent-Logs-Url: https://github.com/SlushEE0/copilot-api/sessions/a39b06f5-f170-403b-81cd-e2cdd4f6d95d Co-authored-by: SlushEE0 <101753528+SlushEE0@users.noreply.github.com> --- src/start.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/start.ts b/src/start.ts index 14abbbdff..5286a1db4 100644 --- a/src/start.ts +++ b/src/start.ts @@ -3,6 +3,7 @@ import { defineCommand } from "citty" import clipboard from "clipboardy" import consola from "consola" +import type { LogObject } from "consola" import { serve, type ServerHandler } from "srvx" import invariant from "tiny-invariant" @@ -28,6 +29,24 @@ interface RunServerOptions { } export async function runServer(options: RunServerOptions): Promise { + // Consola's default reporter uses process.stdout.write which Bun buffers in + // non-TTY (Docker) environments. Route through console.log/warn/error which + // Bun always flushes synchronously regardless of TTY. + consola.setReporters([ + { + log(logObj: LogObject) { + const args = logObj.args as unknown[] + if (logObj.level <= 0) { + console.error(...args) + } else if (logObj.level === 1) { + console.warn(...args) + } else { + console.log(...args) + } + }, + }, + ]) + if (options.proxyEnv) { initProxyFromEnv() } From a65cea52efd9b2810adbfdb21feb61b097681228 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Apr 2026 02:50:43 +0000 Subject: [PATCH 7/7] refactor: apply code review cleanups to logging changes Agent-Logs-Url: https://github.com/SlushEE0/copilot-api/sessions/a39b06f5-f170-403b-81cd-e2cdd4f6d95d Co-authored-by: SlushEE0 <101753528+SlushEE0@users.noreply.github.com> --- src/server.ts | 3 +-- src/start.ts | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/server.ts b/src/server.ts index b83ce8509..5911a4e4d 100644 --- a/src/server.ts +++ b/src/server.ts @@ -13,8 +13,7 @@ import { usageRoute } from "./routes/usage/route" export const server = new Hono() server.use(async (c, next) => { - const { method } = c.req - const { path } = c.req + const { method, path } = c.req const isHealthCheck = method === "GET" && path === "/" const log = isHealthCheck ? consola.debug : consola.info diff --git a/src/start.ts b/src/start.ts index 5286a1db4..829bf1ab6 100644 --- a/src/start.ts +++ b/src/start.ts @@ -35,7 +35,7 @@ export async function runServer(options: RunServerOptions): Promise { consola.setReporters([ { log(logObj: LogObject) { - const args = logObj.args as unknown[] + const args = logObj.args if (logObj.level <= 0) { console.error(...args) } else if (logObj.level === 1) {