feat(cloud): harden sync/auth flow, SSE fallback, and update changelog

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Diego Souza
2026-02-08 16:45:31 +07:00
committed by decolua
parent 2e854bd4c9
commit 3d439839d9
10 changed files with 356 additions and 65 deletions

View File

@@ -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;
}
}