import type { Context } from "hono" import { streamSSE, type SSEMessage } from "hono/streaming" import { awaitApproval } from "~/lib/approval" import { createHandlerLogger, debugJson, debugJsonTail } from "~/lib/logger" import { checkRateLimit } from "~/lib/rate-limit" import { state } from "~/lib/state" import { createCopilotTokenUsageRecorder, normalizeOpenAIUsage, type UsageTokens, } from "~/lib/token-usage" import { generateRequestIdFromPayload, getUUID, isNullish } from "~/lib/utils" import { createChatCompletions, type ChatCompletionChunk, type ChatCompletionResponse, type ChatCompletionsPayload, } from "~/services/copilot/create-chat-completions" const logger = createHandlerLogger("chat-completions-handler") export async function handleCompletion(c: Context) { await checkRateLimit(state) let payload = await c.req.json() debugJsonTail(logger, "Request payload:", { value: payload, tailLength: 400 }) // Find the selected model const selectedModel = state.models?.data.find( (model) => model.id === payload.model, ) if (selectedModel?.id === "gpt-5.4") { return c.json( { error: { message: "Please use `/v1/responses` or `/v1/messages` API", type: "invalid_request_error", }, }, 400, ) } if (state.manualApprove) await awaitApproval() if (isNullish(payload.max_tokens)) { payload = { ...payload, max_tokens: selectedModel?.capabilities.limits.max_output_tokens, } debugJson(logger, "Set max_tokens to:", payload.max_tokens) } // not support subagent marker for now , set sessionId = getUUID(requestId) const requestId = generateRequestIdFromPayload(payload) logger.debug("Generated request ID:", requestId) const sessionId = getUUID(requestId) logger.debug("Extracted session ID:", sessionId) const recordUsage = createCopilotTokenUsageRecorder({ endpoint: "chat_completions", fallbackSessionId: sessionId, model: payload.model, }) const response = await createChatCompletions(payload, { requestId, sessionId, }) if (isNonStreaming(response)) { debugJson(logger, "Non-streaming response:", response) recordUsage(normalizeOpenAIUsage(response.usage)) return c.json(response) } logger.debug("Streaming response") return streamSSE(c, async (stream) => { let usage: UsageTokens = {} for await (const chunk of response) { debugJson(logger, "Streaming chunk:", chunk) const parsedChunk = parseChatCompletionChunk(chunk) if (parsedChunk?.usage) { usage = normalizeOpenAIUsage(parsedChunk.usage) } await stream.writeSSE(chunk as SSEMessage) } recordUsage(usage) }) } const isNonStreaming = ( response: Awaited>, ): response is ChatCompletionResponse => Object.hasOwn(response, "choices") const parseChatCompletionChunk = ( chunk: unknown, ): ChatCompletionChunk | null => { const data = (chunk as { data?: string }).data if (!data || data === "[DONE]") { return null } try { return JSON.parse(data) as ChatCompletionChunk } catch { return null } }