feat(cloud): harden sync/auth flow, SSE fallback, and update changelog
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -225,6 +225,81 @@ function extractUsageFromResponse(responseBody, provider) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert OpenAI-style SSE chunks into a single non-streaming JSON response.
|
||||
* Used as a fallback when upstream returns text/event-stream for stream=false.
|
||||
*/
|
||||
function parseSSEToOpenAIResponse(rawSSE, fallbackModel) {
|
||||
const lines = String(rawSSE || "").split("\n");
|
||||
const chunks = [];
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed.startsWith("data:")) continue;
|
||||
const payload = trimmed.slice(5).trim();
|
||||
if (!payload || payload === "[DONE]") continue;
|
||||
try {
|
||||
chunks.push(JSON.parse(payload));
|
||||
} catch {
|
||||
// Ignore malformed SSE lines and continue best-effort parsing.
|
||||
}
|
||||
}
|
||||
|
||||
if (chunks.length === 0) return null;
|
||||
|
||||
const first = chunks[0];
|
||||
const contentParts = [];
|
||||
const reasoningParts = [];
|
||||
let finishReason = "stop";
|
||||
let usage = null;
|
||||
|
||||
for (const chunk of chunks) {
|
||||
const choice = chunk?.choices?.[0];
|
||||
const delta = choice?.delta || {};
|
||||
|
||||
if (typeof delta.content === "string" && delta.content.length > 0) {
|
||||
contentParts.push(delta.content);
|
||||
}
|
||||
if (typeof delta.reasoning_content === "string" && delta.reasoning_content.length > 0) {
|
||||
reasoningParts.push(delta.reasoning_content);
|
||||
}
|
||||
if (choice?.finish_reason) {
|
||||
finishReason = choice.finish_reason;
|
||||
}
|
||||
if (chunk?.usage && typeof chunk.usage === "object") {
|
||||
usage = chunk.usage;
|
||||
}
|
||||
}
|
||||
|
||||
const message = {
|
||||
role: "assistant",
|
||||
content: contentParts.join("")
|
||||
};
|
||||
if (reasoningParts.length > 0) {
|
||||
message.reasoning_content = reasoningParts.join("");
|
||||
}
|
||||
|
||||
const result = {
|
||||
id: first.id || `chatcmpl-${Date.now()}`,
|
||||
object: "chat.completion",
|
||||
created: first.created || Math.floor(Date.now() / 1000),
|
||||
model: first.model || fallbackModel || "unknown",
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
message,
|
||||
finish_reason: finishReason
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
if (usage) {
|
||||
result.usage = usage;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Core chat handler - shared between SSE and Worker
|
||||
* Returns { success, response, status, error } for caller to handle fallback
|
||||
@@ -406,7 +481,21 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred
|
||||
// Non-streaming response
|
||||
if (!stream) {
|
||||
trackPendingRequest(model, provider, connectionId, false);
|
||||
const responseBody = await providerResponse.json();
|
||||
const contentType = providerResponse.headers.get("content-type") || "";
|
||||
let responseBody;
|
||||
|
||||
if (contentType.includes("text/event-stream")) {
|
||||
// Upstream returned SSE even though stream=false; convert best-effort to JSON.
|
||||
const sseText = await providerResponse.text();
|
||||
const parsedFromSSE = parseSSEToOpenAIResponse(sseText, model);
|
||||
if (!parsedFromSSE) {
|
||||
appendRequestLog({ model, provider, connectionId, status: `FAILED ${HTTP_STATUS.BAD_GATEWAY}` }).catch(() => { });
|
||||
return createErrorResult(HTTP_STATUS.BAD_GATEWAY, "Invalid SSE response for non-streaming request");
|
||||
}
|
||||
responseBody = parsedFromSSE;
|
||||
} else {
|
||||
responseBody = await providerResponse.json();
|
||||
}
|
||||
|
||||
// Notify success - caller can clear error status if needed
|
||||
if (onRequestSuccess) {
|
||||
@@ -507,4 +596,4 @@ export function isTokenExpiringSoon(expiresAt, bufferMs = 5 * 60 * 1000) {
|
||||
if (!expiresAt) return false;
|
||||
const expiresAtMs = new Date(expiresAt).getTime();
|
||||
return expiresAtMs - Date.now() < bufferMs;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user