From db1562e49231cf1c06c05ad823c6c6981c6329f2 Mon Sep 17 00:00:00 2001 From: Anshul Date: Wed, 18 Feb 2026 16:18:24 +0530 Subject: [PATCH] fix(perplexity): handle [DONE] SSE event and add stream error resilience The sonar-deep-research model sends a literal `data: [DONE]` SSE event which caused JSON.parse to throw a SyntaxError, terminating the stream before the answer content was delivered. Changes: - Guard against [DONE] before JSON.parse to avoid SyntaxError crash - Wrap transform in try/catch so malformed chunks don't kill the stream - Log raw chunk value alongside parse errors for easier debugging - Make `usage` and `choices` optional on stream chunk interface to handle intermediate research-phase chunks that omit these fields - Default finish_reason to null (instead of undefined) per OpenAI spec --- src/providers/perplexity-ai/chatComplete.ts | 73 +++++++++++++-------- 1 file changed, 45 insertions(+), 28 deletions(-) diff --git a/src/providers/perplexity-ai/chatComplete.ts b/src/providers/perplexity-ai/chatComplete.ts index 11889f7ce..376e3a4ca 100644 --- a/src/providers/perplexity-ai/chatComplete.ts +++ b/src/providers/perplexity-ai/chatComplete.ts @@ -129,12 +129,12 @@ export interface PerplexityAIChatCompletionStreamChunk { object: string; created: number; citations?: string[]; - usage: { + usage?: { prompt_tokens: number; completion_tokens: number; total_tokens: number; }; - choices: PerplexityAIChatChoice[]; + choices?: PerplexityAIChatChoice[]; } export const PerplexityAIChatCompleteResponseTransform: ( @@ -207,33 +207,50 @@ export const PerplexityAIChatCompleteStreamChunkTransform: ( chunk = chunk.replace(/^data: /, ''); chunk = chunk.trim(); - const parsedChunk: PerplexityAIChatCompletionStreamChunk = JSON.parse(chunk); - let returnChunk = - `data: ${JSON.stringify({ - id: parsedChunk.id, - object: parsedChunk.object, - created: Math.floor(Date.now() / 1000), - model: parsedChunk.model, - provider: PERPLEXITY_AI, - ...(!strictOpenAiCompliance && { - citations: parsedChunk.citations, - }), - choices: [ - { - delta: { - role: parsedChunk.choices[0]?.delta.role, - content: parsedChunk.choices[0]?.delta.content, + if (chunk === '[DONE]') { + return `data: [DONE]\n\n`; + } + + try { + const parsedChunk: PerplexityAIChatCompletionStreamChunk = + JSON.parse(chunk); + + const choice = parsedChunk.choices?.[0]; + + const returnChunk = + `data: ${JSON.stringify({ + id: parsedChunk.id, + object: parsedChunk.object, + created: Math.floor(Date.now() / 1000), + model: parsedChunk.model, + provider: PERPLEXITY_AI, + ...(!strictOpenAiCompliance && { + citations: parsedChunk.citations, + }), + choices: [ + { + delta: { + role: choice?.delta?.role, + content: choice?.delta?.content, + }, + index: 0, + finish_reason: choice?.finish_reason ?? null, }, - index: 0, - finish_reason: parsedChunk.choices[0]?.finish_reason, - }, - ], - ...(parsedChunk.usage && - parsedChunk.choices[0]?.finish_reason && { usage: parsedChunk.usage }), - })}` + '\n\n'; + ], + ...(parsedChunk.usage && + choice?.finish_reason && { usage: parsedChunk.usage }), + })}` + '\n\n'; - if (parsedChunk.choices[0]?.finish_reason) - return returnChunk + `data: [DONE]\n\n`; + if (choice?.finish_reason) return returnChunk + `data: [DONE]\n\n`; - return returnChunk; + return returnChunk; + } catch (error) { + console.error( + 'Error parsing Perplexity AI stream chunk:', + error, + 'Raw chunk:', + chunk + ); + return ''; + } };