@@ -19,6 +19,7 @@ import { BaseAdapter } from './base';
1919
2020export class OpenAiAdapter extends BaseAdapter {
2121 private buffer = '' ;
22+ private dataBuffer = '' ; // Buffer for incomplete JSON data within a single data: line
2223
2324 constructor ( config ?: ServiceAdapterConfig ) {
2425 super (
@@ -182,6 +183,40 @@ export class OpenAiAdapter extends BaseAdapter {
182183 return null ;
183184 }
184185
186+ private isLikelyIncompleteJSON ( str : string ) : boolean {
187+ // Simple heuristic: if parsing fails, check if we have unmatched brackets
188+ // This avoids complex parsing and handles most streaming JSON cases
189+ try {
190+ JSON . parse ( str ) ;
191+ return false ; // Valid JSON, not incomplete
192+ } catch {
193+ // Check for common incomplete JSON patterns
194+ // Remove all escaped characters and strings to simplify bracket counting
195+ const simplified = str
196+ . replace ( / \\ ./ g, '' ) // Remove escaped characters
197+ . replace ( / " [ ^ " ] * " / g, '""' ) ; // Replace string contents with empty strings
198+
199+ // Count unmatched brackets
200+ const openBraces = ( simplified . match ( / { / g) || [ ] ) . length ;
201+ const closeBraces = ( simplified . match ( / } / g) || [ ] ) . length ;
202+ const openBrackets = ( simplified . match ( / \[ / g) || [ ] ) . length ;
203+ const closeBrackets = ( simplified . match ( / ] / g) || [ ] ) . length ;
204+
205+ // Also check if string ends with incomplete patterns
206+ const endsWithIncomplete =
207+ / [ , : ] \s * $ / . test ( str ) || // Ends with comma or colon
208+ / " \s * $ / . test ( str ) || // Ends with quote
209+ / \\ $ / . test ( str ) ; // Ends with escape
210+
211+ // Likely incomplete if brackets don't match or has incomplete ending
212+ return (
213+ openBraces !== closeBraces ||
214+ openBrackets !== closeBrackets ||
215+ endsWithIncomplete
216+ ) ;
217+ }
218+ }
219+
185220 public handleStream (
186221 streamData : { text : string } ,
187222 query : TextTranslateQuery ,
@@ -245,8 +280,12 @@ export class OpenAiAdapter extends BaseAdapter {
245280
246281 // Only process response.output_text.delta events
247282 if ( eventType === 'response.output_text.delta' && eventData ) {
283+ // Combine with any buffered data from previous incomplete JSON
284+ const dataToProcess = this . dataBuffer + eventData ;
285+ this . dataBuffer = '' ;
286+
248287 try {
249- const dataObj = JSON . parse ( eventData ) ;
288+ const dataObj = JSON . parse ( dataToProcess ) ;
250289 if ( dataObj . delta ) {
251290 targetText += dataObj . delta ;
252291 query . onStream ( {
@@ -258,14 +297,25 @@ export class OpenAiAdapter extends BaseAdapter {
258297 } ) ;
259298 }
260299 } catch ( error ) {
261- // Log error for debugging but continue processing
262- if ( error instanceof Error ) {
263- console . error (
264- 'Failed to parse SSE data:' ,
265- error . message ,
266- 'Data:' ,
267- eventData ,
268- ) ;
300+ // Check if this might be incomplete JSON
301+ if (
302+ error instanceof SyntaxError &&
303+ this . isLikelyIncompleteJSON ( dataToProcess )
304+ ) {
305+ // Buffer the incomplete JSON for next iteration
306+ this . dataBuffer = dataToProcess ;
307+ } else {
308+ // This is a real parsing error, log it but continue
309+ if ( error instanceof Error ) {
310+ console . error (
311+ 'Failed to parse SSE data:' ,
312+ error . message ,
313+ 'Data:' ,
314+ dataToProcess ,
315+ ) ;
316+ }
317+ // Clear the buffer on real errors
318+ this . dataBuffer = '' ;
269319 }
270320 }
271321 }
0 commit comments