import type { Context } from "hono" import { streamSSE } from "hono/streaming" import { awaitApproval } from "~/lib/approval" import { isResponsesApiWebSearchEnabled as isConfiguredResponsesApiWebSearchEnabled } from "~/lib/config" import { createHandlerLogger, debugJson, debugJsonTail } from "~/lib/logger" import { checkRateLimit as checkConfiguredRateLimit } from "~/lib/rate-limit" import { state } from "~/lib/state" import { createCopilotTokenUsageRecorder, normalizeResponsesUsage, type UsageTokens, } from "~/lib/token-usage" import { generateRequestIdFromPayload, getUUID } from "~/lib/utils" import { createResponses as createCopilotResponses, type ResponsesPayload, type ResponsesResult, type ResponseStreamEvent, } from "~/services/copilot/create-responses" import { createStreamIdTracker, fixStreamIds } from "./stream-id-sync" import { applyResponsesApiContextManagement, compactInputByLatestCompaction, getResponsesTransportForModel, getResponsesRequestOptions, } from "./utils" const logger = createHandlerLogger("responses-handler") export const responsesHandlerDependencies = { checkRateLimit: checkConfiguredRateLimit, createResponses: createCopilotResponses, isResponsesApiWebSearchEnabled: isConfiguredResponsesApiWebSearchEnabled, } export const handleResponses = async (c: Context) => { await responsesHandlerDependencies.checkRateLimit(state) const payload = await c.req.json() debugJson(logger, "Responses request payload:", payload) // not support subagent marker for now , set sessionId = getUUID(requestId) const requestId = generateRequestIdFromPayload({ messages: payload.input }) logger.debug("Generated request ID:", requestId) const sessionId = getUUID(requestId) logger.debug("Extracted session ID:", sessionId) const recordUsage = createCopilotTokenUsageRecorder({ endpoint: "responses", fallbackSessionId: sessionId, model: payload.model, }) removeUnsupportedTools(payload) if (!responsesHandlerDependencies.isResponsesApiWebSearchEnabled()) { removeWebSearchTool(payload) } compactInputByLatestCompaction(payload) const selectedModel = state.models?.data.find( (model) => model.id === payload.model, ) const responsesTransport = getResponsesTransportForModel(selectedModel) if (!responsesTransport) { return c.json( { error: { message: "This model does not support the responses endpoint. Please choose a different model.", type: "invalid_request_error", }, }, 400, ) } applyResponsesApiContextManagement( payload, selectedModel?.capabilities.limits.max_prompt_tokens, ) debugJson(logger, "Translated Responses payload:", payload) const { vision, initiator } = getResponsesRequestOptions(payload) if (state.manualApprove) { await awaitApproval() } const response = await responsesHandlerDependencies.createResponses(payload, { vision, initiator, requestId, sessionId: sessionId, transport: responsesTransport, }) if (isStreamingRequested(payload) && isAsyncIterable(response)) { logger.debug("Forwarding native Responses stream") return streamSSE(c, async (stream) => { const idTracker = createStreamIdTracker() let usage: UsageTokens = {} for await (const chunk of response) { debugJson(logger, "Responses stream chunk:", chunk) const parsedEvent = parseResponsesStreamEvent(chunk) if ( parsedEvent?.type === "response.completed" || parsedEvent?.type === "response.failed" || parsedEvent?.type === "response.incomplete" ) { usage = normalizeResponsesUsage(parsedEvent.response.usage) } const processedData = fixStreamIds( (chunk as { data?: string }).data ?? "", (chunk as { event?: string }).event, idTracker, ) await stream.writeSSE({ id: (chunk as { id?: string }).id, event: (chunk as { event?: string }).event, data: processedData, }) } recordUsage(usage) }) } debugJsonTail(logger, "Forwarding native Responses result:", { value: response, tailLength: 400, }) recordUsage(normalizeResponsesUsage((response as ResponsesResult).usage)) return c.json(response as ResponsesResult) } const isAsyncIterable = (value: unknown): value is AsyncIterable => Boolean(value) && typeof (value as AsyncIterable)[Symbol.asyncIterator] === "function" const isStreamingRequested = (payload: ResponsesPayload): boolean => Boolean(payload.stream) const parseResponsesStreamEvent = ( chunk: unknown, ): ResponseStreamEvent | null => { const data = (chunk as { data?: string }).data if (!data || data === "[DONE]") { return null } try { return JSON.parse(data) as ResponseStreamEvent } catch { return null } } const removeWebSearchTool = (payload: ResponsesPayload): void => { if (!Array.isArray(payload.tools) || payload.tools.length === 0) return payload.tools = payload.tools.filter((t) => { return t.type !== "web_search" }) } const COPILOT_UNSUPPORTED_TOOL_TYPES = new Set(["image_generation"]) export const removeUnsupportedTools = (payload: ResponsesPayload): void => { if (!Array.isArray(payload.tools) || payload.tools.length === 0) return const dropped: Array = [] payload.tools = payload.tools.filter((t) => { const type = t.type as string if (COPILOT_UNSUPPORTED_TOOL_TYPES.has(type)) { dropped.push(type) return false } return true }) if (dropped.length > 0) { logger.debug("Removed unsupported tools:", dropped) } }