Fix AI streaming response parsing
This commit is contained in:
96
src/index.ts
96
src/index.ts
@@ -486,6 +486,7 @@ async function reviewWithAi(
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
model: env.AI_MODEL || "gpt-4.1-mini",
|
model: env.AI_MODEL || "gpt-4.1-mini",
|
||||||
temperature: 0,
|
temperature: 0,
|
||||||
|
stream: false,
|
||||||
messages: [
|
messages: [
|
||||||
{
|
{
|
||||||
role: "system",
|
role: "system",
|
||||||
@@ -520,16 +521,8 @@ async function reviewWithAi(
|
|||||||
throw new HttpError(502, `ai_request_failed:${response.status}:${details.slice(0, 500)}`);
|
throw new HttpError(502, `ai_request_failed:${response.status}:${details.slice(0, 500)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload = (await response.json()) as {
|
const responseText = await response.text();
|
||||||
choices?: Array<{ message?: { content?: string | Array<{ type: string; text?: string }> } }>;
|
const text = extractAiCompletionText(responseText);
|
||||||
};
|
|
||||||
const rawContent = payload.choices?.[0]?.message?.content;
|
|
||||||
const text = Array.isArray(rawContent)
|
|
||||||
? rawContent
|
|
||||||
.map((item) => item.text ?? "")
|
|
||||||
.join("\n")
|
|
||||||
.trim()
|
|
||||||
: rawContent?.trim();
|
|
||||||
|
|
||||||
if (!text) {
|
if (!text) {
|
||||||
throw new HttpError(502, "ai_empty_response");
|
throw new HttpError(502, "ai_empty_response");
|
||||||
@@ -552,7 +545,10 @@ async function safelyReviewWithAi(
|
|||||||
): Promise<AiReview | null> {
|
): Promise<AiReview | null> {
|
||||||
try {
|
try {
|
||||||
return await reviewWithAi(env, input);
|
return await reviewWithAi(env, input);
|
||||||
} catch {
|
} catch (error) {
|
||||||
|
console.warn("AI review unavailable", {
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
});
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -801,6 +797,11 @@ async function approveJoinRequest(env: Env, chatId: number, userId: number): Pro
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (error instanceof HttpError && error.message.includes("HIDE_REQUESTER_MISSING")) {
|
||||||
|
console.log("join request missing when approving", { chatId, userId });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1151,18 +1152,83 @@ function escapeHtml(value: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function parseJsonFromText(value: string): unknown {
|
function parseJsonFromText(value: string): unknown {
|
||||||
|
const cleaned = value
|
||||||
|
.trim()
|
||||||
|
.replace(/^```(?:json)?\s*/i, "")
|
||||||
|
.replace(/\s*```$/i, "")
|
||||||
|
.trim();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return JSON.parse(value);
|
return JSON.parse(cleaned);
|
||||||
} catch {
|
} catch {
|
||||||
const start = value.indexOf("{");
|
const start = cleaned.indexOf("{");
|
||||||
const end = value.lastIndexOf("}");
|
const end = cleaned.lastIndexOf("}");
|
||||||
if (start === -1 || end === -1 || end <= start) {
|
if (start === -1 || end === -1 || end <= start) {
|
||||||
throw new HttpError(502, "ai_invalid_json");
|
throw new HttpError(502, "ai_invalid_json");
|
||||||
}
|
}
|
||||||
return JSON.parse(value.slice(start, end + 1));
|
return JSON.parse(cleaned.slice(start, end + 1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractAiCompletionText(responseText: string): string | undefined {
|
||||||
|
const trimmed = responseText.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trimmed.startsWith("data:")) {
|
||||||
|
let output = "";
|
||||||
|
for (const line of trimmed.split(/\r?\n/)) {
|
||||||
|
const current = line.trim();
|
||||||
|
if (!current.startsWith("data:")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = current.slice(5).trim();
|
||||||
|
if (!data || data === "[DONE]") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const chunk = JSON.parse(data) as {
|
||||||
|
choices?: Array<{ delta?: { content?: string }; message?: { content?: unknown } }>;
|
||||||
|
};
|
||||||
|
output += chunk.choices?.[0]?.delta?.content ?? extractAiText(chunk.choices?.[0]?.message?.content) ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return output.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = JSON.parse(trimmed) as {
|
||||||
|
choices?: Array<{ message?: { content?: unknown }; delta?: { content?: string } }>;
|
||||||
|
};
|
||||||
|
return extractAiText(payload.choices?.[0]?.message?.content) ?? payload.choices?.[0]?.delta?.content?.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractAiText(value: unknown): string | undefined {
|
||||||
|
if (typeof value === "string") {
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value
|
||||||
|
.map((item) => {
|
||||||
|
if (typeof item === "string") return item;
|
||||||
|
if (item && typeof item === "object" && "text" in item && typeof item.text === "string") return item.text;
|
||||||
|
if (item && typeof item === "object" && "content" in item && typeof item.content === "string") return item.content;
|
||||||
|
return "";
|
||||||
|
})
|
||||||
|
.join("\n")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value && typeof value === "object") {
|
||||||
|
if ("text" in value && typeof value.text === "string") return value.text.trim();
|
||||||
|
if ("content" in value && typeof value.content === "string") return value.content.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
function json(payload: unknown, status = 200): Response {
|
function json(payload: unknown, status = 200): Response {
|
||||||
return new Response(JSON.stringify(payload), {
|
return new Response(JSON.stringify(payload), {
|
||||||
status,
|
status,
|
||||||
|
|||||||
Reference in New Issue
Block a user