Spaces:
Running
Running
| 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<ResponsesPayload>() | |
| 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 = <T>(value: unknown): value is AsyncIterable<T> => | |
| Boolean(value) | |
| && typeof (value as AsyncIterable<T>)[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<string> = [] | |
| 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) | |
| } | |
| } | |