Skip to content

Commit 9840327

Browse files
committed
fix: handle incomplete JSON in SSE streaming responses
Buffer partial JSON data when network packets split `data:` lines mid-JSON, preventing "Unexpected identifier" parse errors during streaming translations
1 parent dc0c2da commit 9840327

File tree

4 files changed

+84
-12
lines changed

4 files changed

+84
-12
lines changed

.github/workflows/lint.yaml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
name: Lint
2+
3+
on:
4+
push:
5+
pull_request:
6+
7+
jobs:
8+
lint:
9+
runs-on: ubuntu-latest
10+
11+
steps:
12+
- uses: actions/checkout@v4
13+
14+
- uses: oven-sh/setup-bun@v2
15+
with:
16+
bun-version: latest
17+
18+
- run: bun install
19+
20+
- name: Run Biome checks
21+
run: bun run lint

.github/workflows/release.yaml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,6 @@ jobs:
5050

5151
- run: bun install
5252

53-
- name: Run Biome checks
54-
run: bun run lint
55-
5653
- name: Package plugin and update appcast
5754
env:
5855
VERSION_NUMBER: ${{ env.VERSION_NUMBER }}

biome.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
"clientKind": "git",
66
"useIgnoreFile": true
77
},
8+
"files": {
9+
"ignoreUnknown": false,
10+
"includes": ["**", "!appcast.json"]
11+
},
812
"formatter": {
913
"enabled": true,
1014
"indentStyle": "space"

src/adapter/openai.ts

Lines changed: 59 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { BaseAdapter } from './base';
1919

2020
export 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

Comments
 (0)