Spaces:
Runtime error
Runtime error
import { ServerRequest } from "https://deno.land/[email protected]/http/server.ts"; | |
interface OpenAIChoice { | |
index: number; | |
message?: { | |
role: string; | |
content: string; | |
}; | |
delta?: { | |
content: string; | |
}; | |
finish_reason: string | null; | |
} | |
interface OpenAIUsage { | |
prompt_tokens: number; | |
completion_tokens: number; | |
total_tokens: number; | |
} | |
interface OpenAIResponse { | |
id: string; | |
object: string; | |
created: number; | |
model: string; | |
choices: OpenAIChoice[]; | |
usage?: OpenAIUsage; | |
} | |
export class ResponseBuilder { | |
private static log( | |
level: "info" | "warn" | "error", | |
message: string, | |
data?: any | |
) { | |
const timestamp = new Date().toISOString(); | |
const logData = data ? ` | Data: ${JSON.stringify(data)}` : ""; | |
console[level](`[${timestamp}] [ResponseBuilder] ${message}${logData}`); | |
} | |
// 构建非流式响应 | |
static buildNonStreamResponse( | |
modelName: string, | |
fullContent: string, | |
finishReason: string = "stop" | |
): Response { | |
this.log("info", "Building non-stream response", { | |
modelName, | |
contentLength: fullContent.length, | |
}); | |
const response = { | |
id: "Chat-Nekohy", | |
object: "chat.completion", | |
created: Math.floor(Date.now() / 1000), | |
model: modelName, | |
choices: [ | |
{ | |
index: 0, | |
message: { role: "assistant", content: fullContent }, | |
finish_reason: finishReason, | |
}, | |
], | |
}; | |
return new Response(JSON.stringify(response), { | |
status: 200, | |
headers: { "Content-Type": "application/json" }, | |
}); | |
} | |
// 构建SSE数据块 | |
static buildSSEChunk( | |
model: string, | |
content: string, | |
isFinish: boolean = false | |
): string { | |
const response = { | |
id: "chatcmpl-Nekohy", | |
object: "chat.completion.chunk", | |
created: Math.floor(Date.now() / 1000), | |
model, | |
choices: [ | |
{ | |
index: 0, | |
delta: isFinish ? {} : { content }, | |
finish_reason: isFinish ? "stop" : null, | |
}, | |
], | |
}; | |
const chunk = `data: ${JSON.stringify(response)}\n\n`; | |
return isFinish ? chunk + "data: [DONE]\n\n" : chunk; | |
} | |
// 主要响应构建方法 | |
static buildResponse( | |
readableStream: ReadableStream, | |
stream: boolean = false, | |
modelName: string = "default" | |
): Response { | |
this.log("info", "Building response", { stream, modelName }); | |
const transformedStream = new ReadableStream({ | |
start: async (controller) => { | |
const reader = readableStream.getReader(); | |
const decoder = new TextDecoder(); | |
let fullContent = ""; | |
let chunkCount = 0; | |
try { | |
while (true) { | |
const { done, value } = await reader.read(); | |
if (done) break; | |
const chunk = decoder.decode(value, { stream: true }); | |
const content = this.extractContent(chunk); | |
if (content) { | |
chunkCount++; | |
fullContent += content; | |
if (stream) { | |
const sseChunk = this.buildSSEChunk(modelName, content); | |
controller.enqueue(new TextEncoder().encode(sseChunk)); | |
} | |
} | |
} | |
this.log("info", "Stream processing completed", { | |
chunkCount, | |
totalLength: fullContent.length, | |
stream, | |
}); | |
if (stream) { | |
// 发送结束标记 | |
const finishChunk = this.buildSSEChunk(modelName, "", true); | |
controller.enqueue(new TextEncoder().encode(finishChunk)); | |
} else { | |
// 发送完整响应 | |
const response = this.buildNonStreamResponse( | |
modelName, | |
fullContent | |
); | |
const responseText = await response.text(); | |
controller.enqueue(new TextEncoder().encode(responseText)); | |
} | |
controller.close(); | |
} catch (error) { | |
this.log("error", "Stream processing failed", { | |
error: error.message, | |
}); | |
controller.error(error); | |
} finally { | |
reader.releaseLock(); | |
} | |
}, | |
}); | |
const headers = stream | |
? { | |
"Content-Type": "text/event-stream", | |
Connection: "keep-alive", | |
"Cache-Control": "no-cache", | |
} | |
: { "Content-Type": "application/json" }; | |
return new Response(transformedStream, { status: 200, headers }); | |
} | |
// 提取SSE数据中的内容 | |
private static extractContent(chunk: string): string { | |
let content = ""; | |
const lines = chunk.split("\n"); | |
for (const line of lines) { | |
const trimmedLine = line.trim(); | |
if (trimmedLine.startsWith("data: ") && !trimmedLine.includes("[DONE]")) { | |
try { | |
const jsonStr = trimmedLine.slice(6).trim(); | |
if (jsonStr) { | |
const data = JSON.parse(jsonStr); | |
if (data.message && typeof data.message === "string") { | |
content += data.message; | |
} | |
} | |
} catch (e) { | |
this.log("warn", "Failed to parse SSE data", { | |
line: trimmedLine, | |
error: e.message, | |
}); | |
} | |
} | |
} | |
return content; | |
} | |
// 通用JSON响应 | |
static jsonResponse(data: unknown, status = 200): Response { | |
this.log("info", "Creating JSON response", { status }); | |
return new Response(JSON.stringify(data), { | |
status, | |
headers: { "Content-Type": "application/json" }, | |
}); | |
} | |
} | |
export function streamResponse(body: ReadableStream): Response { | |
return new Response(body, { | |
status: 200, | |
headers: { | |
"Content-Type": "text/event-stream", | |
Connection: "keep-alive", | |
"Cache-Control": "no-cache", | |
}, | |
}); | |
} | |
export function unauthorizedResponse(message: string, status = 401): Response { | |
return new Response(JSON.stringify({ error: message }), { | |
status, | |
headers: { | |
"Content-Type": "application/json", | |
"WWW-Authenticate": "Bearer", | |
}, | |
}); | |
} | |
export function errorResponse(message: string, status = 500): Response { | |
return ResponseBuilder.jsonResponse({ error: message }, status); | |
} | |