222 lines
5.5 KiB
TypeScript
222 lines
5.5 KiB
TypeScript
// Pure JSONL stream translation (HuggingChat NDJSON -> OpenAI SSE). Verbatim from huggingchat.ts.
|
|
|
|
export function sseChunk(data: unknown): string {
|
|
return `data: ${JSON.stringify(data)}\n\n`;
|
|
}
|
|
|
|
export function parseJsonlLine(line: string): {
|
|
token?: string;
|
|
done?: boolean;
|
|
error?: string;
|
|
text?: string;
|
|
} {
|
|
try {
|
|
const event = JSON.parse(line);
|
|
|
|
if (event.type === "stream" && typeof event.token === "string") {
|
|
const token = event.token.replace(/\0/g, "");
|
|
if (token) return { token };
|
|
}
|
|
|
|
if (event.type === "finalAnswer" && typeof event.text === "string") {
|
|
return { text: event.text, done: true };
|
|
}
|
|
|
|
if (event.type === "status") {
|
|
if (event.status === "error") {
|
|
return { error: event.message || "HuggingChat generation error" };
|
|
}
|
|
if (event.status === "finished") {
|
|
return { done: true };
|
|
}
|
|
}
|
|
} catch {
|
|
// Skip non-JSON lines
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
export async function* streamJsonlToOpenAi(
|
|
body: ReadableStream<Uint8Array>,
|
|
model: string,
|
|
id: string,
|
|
created: number,
|
|
signal?: AbortSignal | null
|
|
): AsyncGenerator<string> {
|
|
const reader = body.getReader();
|
|
const decoder = new TextDecoder();
|
|
let buffer = "";
|
|
let emittedRole = false;
|
|
let fullText = "";
|
|
let finished = false;
|
|
|
|
try {
|
|
while (true) {
|
|
if (signal?.aborted) break;
|
|
|
|
const { value, done } = await reader.read();
|
|
if (done) break;
|
|
|
|
buffer += decoder.decode(value, { stream: true });
|
|
|
|
const lines = buffer.split("\n");
|
|
buffer = lines.pop() || "";
|
|
|
|
for (const line of lines) {
|
|
const trimmed = line.trim();
|
|
if (!trimmed) continue;
|
|
|
|
const parsed = parseJsonlLine(trimmed);
|
|
|
|
if (parsed.error) {
|
|
yield sseChunk({
|
|
id,
|
|
object: "chat.completion.chunk",
|
|
created,
|
|
model,
|
|
choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
|
|
});
|
|
yield "data: [DONE]\n\n";
|
|
finished = true;
|
|
return;
|
|
}
|
|
|
|
if (parsed.token) {
|
|
if (!emittedRole) {
|
|
emittedRole = true;
|
|
yield sseChunk({
|
|
id,
|
|
object: "chat.completion.chunk",
|
|
created,
|
|
model,
|
|
choices: [{ index: 0, delta: { role: "assistant" }, finish_reason: null }],
|
|
});
|
|
}
|
|
|
|
fullText += parsed.token;
|
|
yield sseChunk({
|
|
id,
|
|
object: "chat.completion.chunk",
|
|
created,
|
|
model,
|
|
choices: [{ index: 0, delta: { content: parsed.token }, finish_reason: null }],
|
|
});
|
|
}
|
|
|
|
if (parsed.text) {
|
|
const remaining = parsed.text.slice(fullText.length);
|
|
if (remaining) {
|
|
if (!emittedRole) {
|
|
emittedRole = true;
|
|
yield sseChunk({
|
|
id,
|
|
object: "chat.completion.chunk",
|
|
created,
|
|
model,
|
|
choices: [{ index: 0, delta: { role: "assistant" }, finish_reason: null }],
|
|
});
|
|
}
|
|
yield sseChunk({
|
|
id,
|
|
object: "chat.completion.chunk",
|
|
created,
|
|
model,
|
|
choices: [{ index: 0, delta: { content: remaining }, finish_reason: null }],
|
|
});
|
|
}
|
|
finished = true;
|
|
break;
|
|
}
|
|
|
|
if (parsed.done) {
|
|
finished = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (finished) break;
|
|
}
|
|
|
|
if (!finished && buffer.trim()) {
|
|
const parsed = parseJsonlLine(buffer.trim());
|
|
if (parsed.token && !signal?.aborted) {
|
|
if (!emittedRole) {
|
|
emittedRole = true;
|
|
yield sseChunk({
|
|
id,
|
|
object: "chat.completion.chunk",
|
|
created,
|
|
model,
|
|
choices: [{ index: 0, delta: { role: "assistant" }, finish_reason: null }],
|
|
});
|
|
}
|
|
yield sseChunk({
|
|
id,
|
|
object: "chat.completion.chunk",
|
|
created,
|
|
model,
|
|
choices: [{ index: 0, delta: { content: parsed.token }, finish_reason: null }],
|
|
});
|
|
}
|
|
}
|
|
} finally {
|
|
reader.releaseLock();
|
|
}
|
|
|
|
if (!signal?.aborted) {
|
|
yield sseChunk({
|
|
id,
|
|
object: "chat.completion.chunk",
|
|
created,
|
|
model,
|
|
choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
|
|
});
|
|
yield "data: [DONE]\n\n";
|
|
}
|
|
}
|
|
|
|
export async function readJsonlResponse(
|
|
body: ReadableStream<Uint8Array>,
|
|
signal?: AbortSignal | null
|
|
): Promise<string> {
|
|
const reader = body.getReader();
|
|
const decoder = new TextDecoder();
|
|
let buffer = "";
|
|
let fullText = "";
|
|
|
|
try {
|
|
while (true) {
|
|
if (signal?.aborted) break;
|
|
|
|
const { value, done } = await reader.read();
|
|
if (done) break;
|
|
|
|
buffer += decoder.decode(value, { stream: true });
|
|
|
|
const lines = buffer.split("\n");
|
|
buffer = lines.pop() || "";
|
|
|
|
for (const line of lines) {
|
|
const trimmed = line.trim();
|
|
if (!trimmed) continue;
|
|
|
|
const parsed = parseJsonlLine(trimmed);
|
|
if (parsed.token) fullText += parsed.token;
|
|
if (parsed.text) return parsed.text;
|
|
if (parsed.error) throw new Error(parsed.error);
|
|
}
|
|
}
|
|
|
|
if (buffer.trim()) {
|
|
const parsed = parseJsonlLine(buffer.trim());
|
|
if (parsed.text) return parsed.text;
|
|
if (parsed.token) fullText += parsed.token;
|
|
}
|
|
} finally {
|
|
reader.releaseLock();
|
|
}
|
|
|
|
return fullText;
|
|
}
|