-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add OpenAI Responses API endpoint (/responses) #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<ResponsesPayload>() | ||
| 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, | ||
| }) | ||
| } | ||
|
Comment on lines
+48
to
+55
|
||
| }) | ||
| } | ||
|
|
||
| const isNonStreaming = ( | ||
| response: Awaited<ReturnType<typeof createResponses>>, | ||
| ): response is ResponseObject => Object.hasOwn(response, "output") | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) | ||
| } | ||
| }) |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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<string, string> = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ...copilotHeaders(state, enableVision), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "X-Initiator": isAgentCall ? "agent" : "user", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+8
to
+27
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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<string, string> = { | |
| ...copilotHeaders(state, enableVision), | |
| "X-Initiator": isAgentCall ? "agent" : "user", | |
| } | |
| export const responsesPayloadHasImageInput = (payload: ResponsesPayload) => | |
| Array.isArray(payload.input) | |
| && payload.input.some( | |
| (message) => | |
| Array.isArray(message.content) | |
| && message.content.some((part) => part.type === "input_image"), | |
| ) | |
| export const responsesPayloadIsAgentCall = (payload: ResponsesPayload) => | |
| Array.isArray(payload.input) | |
| && payload.input.some((message) => ["assistant", "tool"].includes(message.role)) | |
| export const createResponsesHeaders = (payload: ResponsesPayload) => { | |
| const enableVision = responsesPayloadHasImageInput(payload) | |
| const isAgentCall = responsesPayloadIsAgentCall(payload) | |
| return { | |
| ...copilotHeaders(state, enableVision), | |
| "X-Initiator": isAgentCall ? "agent" : "user", | |
| } | |
| } | |
| export const createResponses = async (payload: ResponsesPayload) => { | |
| if (!state.copilotToken) throw new Error("Copilot token not found") | |
| const headers = createResponsesHeaders(payload) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Verbose debug logging prints portions of the raw request payload, which can include user prompts, tool arguments, or image/file URLs. Consider redacting sensitive fields (e.g.,
input,instructions,metadata) or logging only high-level metadata (model, stream flag, token limits) to avoid accidental disclosure when--verboseis enabled.