Skip to content

Commit daf7069

Browse files
committed
fix: count malformed surrogate pairs as separate U+FFFD replacements in the repair cap
The 4-byte branch of utf8BytesExceed skipped the next code unit whenever it existed, without checking it is a low surrogate — so a run of high surrogates was counted at 2 bytes per unit while TextEncoder emits 3. The pair path now requires the next unit in 0xDC00..0xDFFF; anything else counts 3 bytes without skipping. Regression: 200k high-surrogate pairs (400k code units, 1.2 MB on the wire) decline repair instead of being admitted at 800k counted bytes.
1 parent d8b707e commit daf7069

2 files changed

Lines changed: 17 additions & 1 deletion

File tree

src/adapters/anthropic.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,11 @@ function utf8BytesExceed(input: string, max: number): boolean {
299299
const code = input.charCodeAt(i);
300300
if (code < 0x80) bytes += 1;
301301
else if (code < 0x800) bytes += 2;
302-
else if (code >= 0xd800 && code <= 0xdbff && i + 1 < input.length) {
302+
else if (code >= 0xd800 && code <= 0xdbff && i + 1 < input.length
303+
&& input.charCodeAt(i + 1) >= 0xdc00 && input.charCodeAt(i + 1) <= 0xdfff) {
304+
// A complete surrogate pair is one 4-byte scalar. Anything else — a high surrogate
305+
// followed by another high surrogate or a non-surrogate — encodes as two separate
306+
// U+FFFD replacements, so the next unit must NOT be skipped.
303307
bytes += 4;
304308
i++;
305309
} else bytes += 3; // lone surrogates encode as U+FFFD (3 bytes)

tests/anthropic-eof-tolerance.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,4 +190,16 @@ describe("AgentRouter Anthropic EOF tolerance (#658)", () => {
190190
const events = await createAnthropicAdapter(tolerant).parseResponse(new Response(payload));
191191
expect(events).toContainEqual({ type: "tool_call_delta", arguments: "{}" });
192192
});
193+
194+
test("malformed surrogate pairs count as separate U+FFFD replacements against the cap", async () => {
195+
// 200k high-surrogate pairs: 400k code units, but every lone surrogate encodes as its
196+
// own 3-byte U+FFFD — 1.2 MB over the wire, which a pair-skipping count would admit.
197+
const oversized = `${"\ud800\ud800".repeat(200_000)}{"ok":true}`;
198+
const payload = JSON.stringify({
199+
content: [{ type: "tool_use", id: "toolu_1", name: "get_weather", input: oversized }],
200+
});
201+
202+
const events = await createAnthropicAdapter(tolerant).parseResponse(new Response(payload));
203+
expect(events).toContainEqual({ type: "tool_call_delta", arguments: "{}" });
204+
});
193205
});

0 commit comments

Comments
 (0)