UNPKG

@earendil-works/pi-ai

Version:

Unified LLM API with automatic model discovery and provider configuration

138 lines • 4.33 kB
import { parse as partialParse } from "partial-json"; const VALID_JSON_ESCAPES = new Set(['"', "\\", "/", "b", "f", "n", "r", "t"]); function isControlCharacter(char) { const codePoint = char.codePointAt(0); return codePoint !== undefined && codePoint >= 0x00 && codePoint <= 0x1f; } function escapeControlCharacter(char) { switch (char) { case "\b": return "\\b"; case "\f": return "\\f"; case "\n": return "\\n"; case "\r": return "\\r"; case "\t": return "\\t"; default: return `\\u${char.codePointAt(0)?.toString(16).padStart(4, "0") ?? "0000"}`; } } /** * Repairs malformed JSON string literals by: * - escaping raw control characters inside strings * - doubling backslashes before invalid escape characters */ export function repairJson(json) { let repaired = ""; let inString = false; for (let index = 0; index < json.length; index++) { const char = json[index]; if (!inString) { repaired += char; if (char === '"') { inString = true; } continue; } if (char === '"') { repaired += char; inString = false; continue; } if (char === "\\") { const nextChar = json[index + 1]; if (nextChar === undefined) { repaired += "\\\\"; continue; } if (nextChar === "u") { const unicodeDigits = json.slice(index + 2, index + 6); if (/^[0-9a-fA-F]{4}$/.test(unicodeDigits)) { repaired += `\\u${unicodeDigits}`; index += 5; continue; } } if (VALID_JSON_ESCAPES.has(nextChar)) { repaired += `\\${nextChar}`; index += 1; continue; } repaired += "\\\\"; continue; } repaired += isControlCharacter(char) ? escapeControlCharacter(char) : char; } return repaired; } export function parseJsonWithRepair(json) { try { return JSON.parse(json); } catch (error) { const repairedJson = repairJson(json); if (repairedJson !== json) { return JSON.parse(repairedJson); } throw error; } } /** * Parses the model's complete arguments text of a finished tool call. * * Unlike {@link parseStreamingJson}, this never keeps a partially parsed prefix: once the call is * finished, text that is not a valid JSON object means the model sent malformed arguments. In that * case `arguments` is `{}` and `argumentsError` describes the problem, including the text the * model sent, so tool validation can report it back to the model. */ export function parseToolCallArguments(json) { if (!json?.trim()) return { arguments: {} }; try { const parsed = parseJsonWithRepair(json); if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) { throw new Error("Tool arguments must be a JSON object"); } return { arguments: parsed }; } catch (error) { return { arguments: {}, argumentsError: `${error instanceof Error ? error.message : String(error)}\n\nReceived arguments:\n${json}`, }; } } /** * 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(partialJson) { if (!partialJson || partialJson.trim() === "") { return {}; } try { return parseJsonWithRepair(partialJson); } catch { try { const result = partialParse(partialJson); return (result ?? {}); } catch { try { const result = partialParse(repairJson(partialJson)); return (result ?? {}); } catch { return {}; } } } } //# sourceMappingURL=json-parse.js.map