- Removed the unused `antigravity-user-agent.ts` file from the AI providers. - Updated `parseStreamingJson` to trim only leading whitespace, try `JSON.parse` first, then partial parsing on repaired input before returning an empty object on failure. - Removed a streaming JSON test that depended on preserving trailing spaces in incomplete string fragments.
149 lines
3.4 KiB
TypeScript
149 lines
3.4 KiB
TypeScript
import { parse as partialParse } from "partial-json";
|
|
|
|
const QUOTE = 0x22;
|
|
const BACKSLASH = 0x5c;
|
|
const U = 0x75;
|
|
|
|
// Valid chars after `\`: " \ / b f n r t u
|
|
const VALID_ESCAPE_CHAR = new Uint8Array(128);
|
|
for (const ch of '"\\/bfnrtu') VALID_ESCAPE_CHAR[ch.charCodeAt(0)] = 1;
|
|
|
|
const CONTROL_ESCAPES: readonly string[] = (() => {
|
|
const e: string[] = [];
|
|
e[0x08] = "\\b";
|
|
e[0x09] = "\\t";
|
|
e[0x0a] = "\\n";
|
|
e[0x0c] = "\\f";
|
|
e[0x0d] = "\\r";
|
|
for (let cp = 0; cp <= 0x1f; cp++) {
|
|
e[cp] ??= `\\u${cp.toString(16).padStart(4, "0")}`;
|
|
}
|
|
return e;
|
|
})();
|
|
|
|
function isHexDigit(cp: number): boolean {
|
|
return (cp >= 0x30 && cp <= 0x39) || ((cp | 0x20) >= 0x61 && (cp | 0x20) <= 0x66);
|
|
}
|
|
|
|
export function repairJson(json: string): string {
|
|
const len = json.length;
|
|
const parts: string[] = [];
|
|
let lastEmit = 0;
|
|
let inString = false;
|
|
let i = 0;
|
|
|
|
while (i < len) {
|
|
if (!inString) {
|
|
// Fast scan: skip to next quote.
|
|
while (i < len && json.charCodeAt(i) !== QUOTE) i++;
|
|
if (i >= len) break;
|
|
inString = true;
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
// Fast scan inside string: advance past chars that need no handling.
|
|
while (i < len) {
|
|
const cp = json.charCodeAt(i);
|
|
if (cp < 0x20 || cp === QUOTE || cp === BACKSLASH) break;
|
|
i++;
|
|
}
|
|
if (i >= len) break;
|
|
|
|
const cp = json.charCodeAt(i);
|
|
|
|
if (cp === QUOTE) {
|
|
inString = false;
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
if (cp === BACKSLASH) {
|
|
// Need at least one char after the backslash; treat EOI as invalid escape.
|
|
if (i + 1 >= len) {
|
|
parts.push(json.slice(lastEmit, i), "\\\\");
|
|
lastEmit = i + 1;
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
const nextCp = json.charCodeAt(i + 1);
|
|
|
|
if (nextCp === U) {
|
|
// Need full \uXXXX, all four digits, all hex.
|
|
if (
|
|
i + 5 < len &&
|
|
isHexDigit(json.charCodeAt(i + 2)) &&
|
|
isHexDigit(json.charCodeAt(i + 3)) &&
|
|
isHexDigit(json.charCodeAt(i + 4)) &&
|
|
isHexDigit(json.charCodeAt(i + 5))
|
|
) {
|
|
i += 6;
|
|
continue;
|
|
}
|
|
// Truncated or non-hex \u — escape the backslash, re-process the rest.
|
|
parts.push(json.slice(lastEmit, i), "\\\\");
|
|
lastEmit = i + 1;
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
if (nextCp < 128 && VALID_ESCAPE_CHAR[nextCp] === 1) {
|
|
i += 2;
|
|
continue;
|
|
}
|
|
|
|
parts.push(json.slice(lastEmit, i), "\\\\");
|
|
lastEmit = i + 1;
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
// Control character (cp < 0x20).
|
|
parts.push(json.slice(lastEmit, i), CONTROL_ESCAPES[cp]);
|
|
lastEmit = i + 1;
|
|
i++;
|
|
}
|
|
|
|
if (!parts.length) return json;
|
|
if (lastEmit < len) parts.push(json.slice(lastEmit));
|
|
return parts.join("");
|
|
}
|
|
|
|
export function parseJsonWithRepair<T>(json: string): T {
|
|
try {
|
|
return JSON.parse(json) as T;
|
|
} catch (error) {
|
|
const repairedJson = repairJson(json);
|
|
if (repairedJson !== json) {
|
|
return JSON.parse(repairedJson) as T;
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Attempts to parse potentially incomplete JSON during streaming.
|
|
* Always returns a valid object, even if the JSON is incomplete.
|
|
*
|
|
* @param partialJson The partial JSON string from streaming
|
|
* @returns Parsed object or empty object if parsing fails
|
|
*/
|
|
export function parseStreamingJson<T = Record<string, unknown>>(partialJson: string | undefined): T {
|
|
partialJson = partialJson?.trimStart();
|
|
if (!partialJson) {
|
|
return {} as T;
|
|
}
|
|
try {
|
|
return JSON.parse(partialJson) as T;
|
|
} catch {
|
|
partialJson = repairJson(partialJson);
|
|
try {
|
|
return (partialParse(partialJson) ?? {}) as T;
|
|
} catch {
|
|
// If all parsing fails, return empty object
|
|
return {} as T;
|
|
}
|
|
}
|
|
}
|