Skip to content

Commit 40a21a4

Browse files
authored
fix(json): sanitize lone surrogates before serialization (#40)
Normalize string values to well-formed Unicode before JSON serialization so DeepSeek does not reject requests containing lone surrogate code units. Also apply the same safe serialization to nested tool-call arguments and tool-result payloads.
1 parent e283bc0 commit 40a21a4

3 files changed

Lines changed: 37 additions & 3 deletions

File tree

src/client.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { CancellationToken } from 'vscode';
2+
import { safeStringify } from './json';
23
import { logger } from './logger';
34
import type {
45
DeepSeekRequest,
@@ -47,7 +48,7 @@ export class DeepSeekClient {
4748
'Content-Type': 'application/json',
4849
Authorization: `Bearer ${this.apiKey}`,
4950
},
50-
body: JSON.stringify(requestBody),
51+
body: safeStringify(requestBody),
5152
signal: controller.signal,
5253
});
5354

src/json.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
const REPLACEMENT_CHARACTER = '\uFFFD';
2+
const LONE_SURROGATE_PATTERN = /([\uD800-\uDBFF][\uDC00-\uDFFF])|[\uD800-\uDFFF]/g;
3+
4+
type WellFormedString = string & {
5+
toWellFormed?: () => string;
6+
};
7+
8+
export function safeStringify(value: unknown): string {
9+
const json = JSON.stringify(value, (_key, entryValue: unknown) => {
10+
if (typeof entryValue === 'string') {
11+
return toWellFormedString(entryValue);
12+
}
13+
return entryValue;
14+
});
15+
16+
if (json === undefined) {
17+
throw new TypeError('Value cannot be serialized as JSON');
18+
}
19+
20+
return json;
21+
}
22+
23+
export function toWellFormedString(value: string): string {
24+
const toWellFormed = (value as WellFormedString).toWellFormed;
25+
if (typeof toWellFormed === 'function') {
26+
return toWellFormed.call(value);
27+
}
28+
29+
return value.replace(LONE_SURROGATE_PATTERN, (_match, pair: string | undefined) =>
30+
pair ? pair : REPLACEMENT_CHARACTER,
31+
);
32+
}

src/provider/convert.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import vscode from 'vscode';
2+
import { safeStringify } from '../json';
23
import type { DeepSeekMessage, DeepSeekTool, DeepSeekToolCall } from '../types';
34
import { createPostToolReasoningKey, createToolReasoningKey, type ReasoningEntry } from './cache';
45

@@ -31,7 +32,7 @@ export function convertMessages(
3132
type: 'function',
3233
function: {
3334
name: part.name,
34-
arguments: JSON.stringify(part.input),
35+
arguments: safeStringify(part.input),
3536
},
3637
});
3738
} else if (part instanceof vscode.LanguageModelToolResultPart) {
@@ -43,7 +44,7 @@ export function convertMessages(
4344
}
4445
toolResults.push({
4546
callId: part.callId,
46-
content: toolContent || JSON.stringify(part.content),
47+
content: toolContent || safeStringify(part.content),
4748
});
4849
}
4950
}

0 commit comments

Comments
 (0)