@bastani/pi-ai 0.9.43 → 0.9.44-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/dist/api/anthropic-messages.d.ts.map +1 -1
- package/dist/api/anthropic-messages.js +45 -58
- package/dist/api/anthropic-messages.js.map +1 -1
- package/dist/api/azure-openai-responses.d.ts.map +1 -1
- package/dist/api/azure-openai-responses.js +0 -6
- package/dist/api/azure-openai-responses.js.map +1 -1
- package/dist/api/bedrock-converse-stream.d.ts.map +1 -1
- package/dist/api/bedrock-converse-stream.js +71 -78
- package/dist/api/bedrock-converse-stream.js.map +1 -1
- package/dist/api/constrained-sampling.d.ts +11 -1
- package/dist/api/constrained-sampling.d.ts.map +1 -1
- package/dist/api/constrained-sampling.js +33 -18
- package/dist/api/constrained-sampling.js.map +1 -1
- package/dist/api/google-generative-ai.d.ts.map +1 -1
- package/dist/api/google-generative-ai.js +0 -6
- package/dist/api/google-generative-ai.js.map +1 -1
- package/dist/api/google-shared.d.ts +2 -1
- package/dist/api/google-shared.d.ts.map +1 -1
- package/dist/api/google-shared.js +57 -2
- package/dist/api/google-shared.js.map +1 -1
- package/dist/api/google-vertex.d.ts.map +1 -1
- package/dist/api/google-vertex.js +0 -6
- package/dist/api/google-vertex.js.map +1 -1
- package/dist/api/mistral-conversations.d.ts.map +1 -1
- package/dist/api/mistral-conversations.js +17 -44
- package/dist/api/mistral-conversations.js.map +1 -1
- package/dist/api/openai-codex-responses.d.ts.map +1 -1
- package/dist/api/openai-codex-responses.js +0 -5
- package/dist/api/openai-codex-responses.js.map +1 -1
- package/dist/api/openai-completions.d.ts.map +1 -1
- package/dist/api/openai-completions.js +34 -53
- package/dist/api/openai-completions.js.map +1 -1
- package/dist/api/openai-responses-shared.d.ts.map +1 -1
- package/dist/api/openai-responses-shared.js +58 -57
- package/dist/api/openai-responses-shared.js.map +1 -1
- package/dist/api/openai-responses.d.ts.map +1 -1
- package/dist/api/openai-responses.js +0 -6
- package/dist/api/openai-responses.js.map +1 -1
- package/dist/api/pi-messages.d.ts +2 -0
- package/dist/api/pi-messages.d.ts.map +1 -1
- package/dist/api/pi-messages.js +15 -9
- package/dist/api/pi-messages.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/providers/data/.manifest.json +1 -1
- package/dist/providers/data/kimi-coding.json +1 -1
- package/dist/providers/data/moonshotai-cn.json +1 -1
- package/dist/providers/data/moonshotai.json +1 -1
- package/dist/providers/data/opencode-go.json +1 -1
- package/dist/providers/data/openrouter.json +1 -1
- package/dist/providers/data/vercel-ai-gateway.json +1 -1
- package/dist/providers/faux.d.ts.map +1 -1
- package/dist/providers/faux.js +7 -2
- package/dist/providers/faux.js.map +1 -1
- package/dist/providers/radius-config.d.ts.map +1 -1
- package/dist/providers/radius-config.js +2 -1
- package/dist/providers/radius-config.js.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/error-body.d.ts.map +1 -1
- package/dist/utils/error-body.js +2 -1
- package/dist/utils/error-body.js.map +1 -1
- package/dist/utils/streaming-json.d.ts +84 -0
- package/dist/utils/streaming-json.d.ts.map +1 -0
- package/dist/utils/streaming-json.js +513 -0
- package/dist/utils/streaming-json.js.map +1 -0
- package/dist/utils/text.d.ts +7 -0
- package/dist/utils/text.d.ts.map +1 -1
- package/dist/utils/text.js +9 -0
- package/dist/utils/text.js.map +1 -1
- package/dist/utils/version.d.ts +1 -1
- package/dist/utils/version.d.ts.map +1 -1
- package/dist/utils/version.js +1 -1
- package/dist/utils/version.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"error-body.js","sourceRoot":"","sources":["../../src/utils/error-body.ts"],"names":[],"mappings":"AAAA,wDAAwD;AACxD,EAAE;AACF,8EAA8E;AAC9E,gFAAgF;AAChF,gFAAgF;AAChF,6EAA6E;AAC7E,6EAA6E;AAC7E,yCAAyC;AACzC,EAAE;AACF,uEAAuE;AACvE,6EAA6E;AAC7E,+EAA+E;AAC/E,+EAA+E;AAC/E,0EAA0E;AAE1E,MAAM,CAAC,MAAM,6BAA6B,GAAG,IAAI,CAAC;AAsBlD,MAAM,UAAU,sBAAsB,CAAC,KAAc;IACpD,IAAI,CAAC,CAAC,KAAK,YAAY,KAAK,CAAC,EAAE,CAAC;QAC/B,OAAO,EAAE,OAAO,EAAE,iBAAiB,CAAC,KAAK,CAAC,EAAE,kBAAkB,EAAE,KAAK,EAAE,CAAC;IACzE,CAAC;IAED,MAAM,QAAQ,GAAG,KAAsB,CAAC;IACxC,MAAM,MAAM,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAC;IACvC,MAAM,IAAI,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAC;IACnC,MAAM,kBAAkB,GAAG,IAAI,KAAK,SAAS,IAAI,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IAE9E,OAAO;QACN,MAAM;QACN,IAAI;QACJ,OAAO,EAAE,KAAK,CAAC,OAAO;QACtB,kBAAkB;KACgB,CAAC;AACrC,CAAC;AAED;;;;GAIG;AACH,SAAS,aAAa,CAAC,KAAoB;IAC1C,IAAI,OAAO,KAAK,CAAC,UAAU,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC,UAAU,CAAC;IAClE,IAAI,OAAO,KAAK,CAAC,MAAM,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC,MAAM,CAAC;IAC1D,IAAI,OAAO,KAAK,CAAC,SAAS,EAAE,cAAc,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC,SAAS,CAAC,cAAc,CAAC;IAC/F,IAAI,OAAO,KAAK,CAAC,SAAS,EAAE,UAAU,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC,SAAS,CAAC,UAAU,CAAC;IACvF,OAAO,SAAS,CAAC;AAClB,CAAC;AAED;;;;;;GAMG;AACH,SAAS,WAAW,CAAC,KAAoB;IACxC,MAAM,QAAQ,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC;IACrC,IAAI,QAAQ,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAC7C,MAAM,OAAO,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC;IAChC,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IAC3C,OAAO,iBAAiB,CAAC,OAAO,EAAE,6BAA6B,CAAC,CAAC;AAClE,CAAC;AAED,SAAS,YAAY,CAAC,KAAoB;IACzC,IAAI,OAAO,KAAK,CAAC,IAAI,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC,IAAI,CAAC;IACtD,IAAI,qBAAqB,CAAC,KAAK,CAAC,KAAK,CAAC;QAAE,OAAO,iBAAiB,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAC9E,MAAM,YAAY,GAAG,KAAK,CAAC,SAAS,EAAE,IAAI,CAAC;IAC3C,IAAI,OAAO,YAAY,KAAK,QAAQ;QAAE,OAAO,YAAY,CAAC;IAC1D,IAAI,oBAAoB,CAAC,YAAY,CAAC;QAAE,OAAO,SAAS,CAAC;IACzD,IAAI,qBAAqB,CAAC,YAAY,CAAC;QAAE,OAAO,iBAAiB,CAAC,YAAY,CAAC,CAAC;IAChF,OAAO,SAAS,CAAC;AAClB,CAAC;AAED,SAAS,oBAAoB,CAAC,KAAc;IAC3C,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,MAAM,IAAI,KAAK,IAAI,OAAO,KAAK,CAAC,IAAI,KAAK,UAAU,CAAC;AAC3G,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,SAAS,qBAAqB,CAAC,KAAc;IAC5C,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAC9D,MAAM,KAAK,GAAG,MAAM,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC;IAC3C,IAAI,KAAK,KAAK,MAAM,CAAC,SAAS,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAC/D,OAAO,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;AACtC,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,mBAAmB,CAAC,IAA6B,EAAE,MAAe;IACjF,IAAI,IAAI,CAAC,kBAAkB,IAAI,IAAI,CAAC,MAAM,KAAK,SAAS,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QACrF,OAAO,MAAM,KAAK,SAAS,IAAI,IAAI,CAAC,MAAM,KAAK,SAAS;YACvD,CAAC,CAAC,GAAG,MAAM,KAAK,IAAI,CAAC,MAAM,MAAM,IAAI,CAAC,OAAO,EAAE;YAC/C,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC;IACjB,CAAC;IACD,OAAO,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,GAAG,MAAM,KAAK,IAAI,CAAC,MAAM,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,MAAM,KAAK,IAAI,CAAC,IAAI,EAAE,CAAC;AAC3G,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,IAAY,EAAE,QAAgB;IAC/D,IAAI,IAAI,CAAC,MAAM,IAAI,QAAQ;QAAE,OAAO,IAAI,CAAC;IACzC,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,kBAAkB,IAAI,CAAC,MAAM,GAAG,QAAQ,SAAS,CAAC;AACpF,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,KAAc;IAC/C,IAAI,CAAC;QACJ,MAAM,UAAU,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;QACzC,OAAO,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC;IAC9D,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;IACtB,CAAC;AACF,CAAC","sourcesContent":["// Shared normalization for provider HTTP error objects.\n//\n// Endpoints behind a proxy / gateway may return a non-2xx response whose body\n// the provider SDK cannot fold into `error.message`. The SDK error object still\n// carries the HTTP status and the raw/parsed body, but under SDK-specific field\n// names. Provider catch blocks that read only `error.message` therefore drop\n// the body and surface opaque messages like `\"403 status code (no body)\"` or\n// collapse to `\"Unknown: UnknownError\"`.\n//\n// `normalizeProviderError` probes the known SDK field shapes (Mistral,\n// `openai`, `@google/genai`, AWS Bedrock) and returns a struct each provider\n// composes into its display string. The `messageCarriesBody` flag captures the\n// Anthropic / `@google/genai` happy path where the SDK already folded the body\n// into the message, so providers can preserve it without double-printing.\n\nexport const MAX_PROVIDER_ERROR_BODY_CHARS = 4000;\n\nexport interface NormalizedProviderError {\n\t/** HTTP status code, when one could be extracted from the SDK error object. */\n\tstatus?: number;\n\t/** Raw HTTP body reason, already trimmed and truncated to the cap. */\n\tbody?: string;\n\t/** `error.message`, or `safeJsonStringify(error)` for a non-`Error` throw. */\n\tmessage: string;\n\t/** True when `message` already contains the body (no separate body to add). */\n\tmessageCarriesBody: boolean;\n}\n\ntype SdkErrorShape = Error & {\n\tstatusCode?: unknown;\n\tstatus?: unknown;\n\tbody?: unknown;\n\terror?: unknown;\n\t$metadata?: { httpStatusCode?: unknown };\n\t$response?: { statusCode?: unknown; body?: unknown };\n};\n\nexport function normalizeProviderError(error: unknown): NormalizedProviderError {\n\tif (!(error instanceof Error)) {\n\t\treturn { message: safeJsonStringify(error), messageCarriesBody: false };\n\t}\n\n\tconst sdkError = error as SdkErrorShape;\n\tconst status = extractStatus(sdkError);\n\tconst body = extractBody(sdkError);\n\tconst messageCarriesBody = body === undefined || error.message.includes(body);\n\n\treturn {\n\t\tstatus,\n\t\tbody,\n\t\tmessage: error.message,\n\t\tmessageCarriesBody,\n\t} satisfies NormalizedProviderError;\n}\n\n/**\n * Probe the HTTP status, first numeric hit wins, in SDK-field order:\n * `statusCode` (Mistral) → `status` (`openai`, `@google/genai`) →\n * `$metadata.httpStatusCode` (Bedrock) → `$response.statusCode` (Bedrock).\n */\nfunction extractStatus(error: SdkErrorShape): number | undefined {\n\tif (typeof error.statusCode === \"number\") return error.statusCode;\n\tif (typeof error.status === \"number\") return error.status;\n\tif (typeof error.$metadata?.httpStatusCode === \"number\") return error.$metadata.httpStatusCode;\n\tif (typeof error.$response?.statusCode === \"number\") return error.$response.statusCode;\n\treturn undefined;\n}\n\n/**\n * Probe the raw body reason, first usable hit wins, in SDK-field order:\n * `body` string (Mistral) → `error` parsed JSON body object (`openai` SDK's\n * `this.error`) → `$response.body` (Bedrock). Empty objects and unread response\n * streams are treated as no body so they do not surface as `\"{}\"` or serialized\n * stream internals. The chosen body is truncated to the cap.\n */\nfunction extractBody(error: SdkErrorShape): string | undefined {\n\tconst bodyText = pickBodyText(error);\n\tif (bodyText === undefined) return undefined;\n\tconst trimmed = bodyText.trim();\n\tif (trimmed.length === 0) return undefined;\n\treturn truncateErrorText(trimmed, MAX_PROVIDER_ERROR_BODY_CHARS);\n}\n\nfunction pickBodyText(error: SdkErrorShape): string | undefined {\n\tif (typeof error.body === \"string\") return error.body;\n\tif (isPlainNonEmptyObject(error.error)) return safeJsonStringify(error.error);\n\tconst responseBody = error.$response?.body;\n\tif (typeof responseBody === \"string\") return responseBody;\n\tif (isReadableStreamLike(responseBody)) return undefined;\n\tif (isPlainNonEmptyObject(responseBody)) return safeJsonStringify(responseBody);\n\treturn undefined;\n}\n\nfunction isReadableStreamLike(value: unknown): boolean {\n\treturn typeof value === \"object\" && value !== null && \"pipe\" in value && typeof value.pipe === \"function\";\n}\n\n/**\n * Only a PLAIN object counts as an HTTP body. SDK error fields can hold class\n * instances instead of parsed bodies — AWS SDK v3's `$response.body` is an\n * HTTP stream/response wrapper object, and stringifying one produced garbage\n * like `{\"_events\":...}` as the \"body\", which then REPLACED `error.message`\n * in the composed display string. `error.message` is where the SDK puts the\n * real deserialized exception text (\"Input is too long...\", schema validation\n * details, ...), so the one useful string was discarded for noise. A class\n * instance yields no body, `messageCarriesBody` stays true, and the real\n * message survives. Complements the `pipe` sniffing above: web\n * ReadableStreams (pipeTo/pipeThrough, no `pipe`) and non-stream SDK wrapper\n * classes fail the prototype check, while parsed JSON bodies (plain objects\n * by construction) still pass.\n */\nfunction isPlainNonEmptyObject(value: unknown): boolean {\n\tif (typeof value !== \"object\" || value === null) return false;\n\tconst proto = Object.getPrototypeOf(value);\n\tif (proto !== Object.prototype && proto !== null) return false;\n\treturn Object.keys(value).length > 0;\n}\n\n/**\n * Compose a display string from a normalized error. When the message already\n * carries the body (Anthropic / `@google/genai` happy path) or no body/status\n * was extracted, the message is returned unchanged. Otherwise the status and\n * body are surfaced, with an optional provider prefix.\n *\n * - no prefix: `\"<status>: <body>\"`\n * - prefix: `\"<prefix> (<status>): <body>\"`\n */\nexport function formatProviderError(norm: NormalizedProviderError, prefix?: string): string {\n\tif (norm.messageCarriesBody || norm.status === undefined || norm.body === undefined) {\n\t\treturn prefix !== undefined && norm.status !== undefined\n\t\t\t? `${prefix} (${norm.status}): ${norm.message}`\n\t\t\t: norm.message;\n\t}\n\treturn prefix !== undefined ? `${prefix} (${norm.status}): ${norm.body}` : `${norm.status}: ${norm.body}`;\n}\n\nexport function truncateErrorText(text: string, maxChars: number): string {\n\tif (text.length <= maxChars) return text;\n\treturn `${text.slice(0, maxChars)}... [truncated ${text.length - maxChars} chars]`;\n}\n\nexport function safeJsonStringify(value: unknown): string {\n\ttry {\n\t\tconst serialized = JSON.stringify(value);\n\t\treturn serialized === undefined ? String(value) : serialized;\n\t} catch {\n\t\treturn String(value);\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"error-body.js","sourceRoot":"","sources":["../../src/utils/error-body.ts"],"names":[],"mappings":"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","sourcesContent":["// Shared normalization for provider HTTP error objects.\n//\n// Endpoints behind a proxy / gateway may return a non-2xx response whose body\n// the provider SDK cannot fold into `error.message`. The SDK error object still\n// carries the HTTP status and the raw/parsed body, but under SDK-specific field\n// names. Provider catch blocks that read only `error.message` therefore drop\n// the body and surface opaque messages like `\"403 status code (no body)\"` or\n// collapse to `\"Unknown: UnknownError\"`.\n//\n// `normalizeProviderError` probes the known SDK field shapes (Mistral,\n// `openai`, `@google/genai`, AWS Bedrock) and returns a struct each provider\n// composes into its display string. The `messageCarriesBody` flag captures the\n// Anthropic / `@google/genai` happy path where the SDK already folded the body\n// into the message, so providers can preserve it without double-printing.\n\nimport { ownedSlice } from \"./text.ts\";\n\nexport const MAX_PROVIDER_ERROR_BODY_CHARS = 4000;\n\nexport interface NormalizedProviderError {\n\t/** HTTP status code, when one could be extracted from the SDK error object. */\n\tstatus?: number;\n\t/** Raw HTTP body reason, already trimmed and truncated to the cap. */\n\tbody?: string;\n\t/** `error.message`, or `safeJsonStringify(error)` for a non-`Error` throw. */\n\tmessage: string;\n\t/** True when `message` already contains the body (no separate body to add). */\n\tmessageCarriesBody: boolean;\n}\n\ntype SdkErrorShape = Error & {\n\tstatusCode?: unknown;\n\tstatus?: unknown;\n\tbody?: unknown;\n\terror?: unknown;\n\t$metadata?: { httpStatusCode?: unknown };\n\t$response?: { statusCode?: unknown; body?: unknown };\n};\n\nexport function normalizeProviderError(error: unknown): NormalizedProviderError {\n\tif (!(error instanceof Error)) {\n\t\treturn { message: safeJsonStringify(error), messageCarriesBody: false };\n\t}\n\n\tconst sdkError = error as SdkErrorShape;\n\tconst status = extractStatus(sdkError);\n\tconst body = extractBody(sdkError);\n\tconst messageCarriesBody = body === undefined || error.message.includes(body);\n\n\treturn {\n\t\tstatus,\n\t\tbody,\n\t\tmessage: error.message,\n\t\tmessageCarriesBody,\n\t} satisfies NormalizedProviderError;\n}\n\n/**\n * Probe the HTTP status, first numeric hit wins, in SDK-field order:\n * `statusCode` (Mistral) → `status` (`openai`, `@google/genai`) →\n * `$metadata.httpStatusCode` (Bedrock) → `$response.statusCode` (Bedrock).\n */\nfunction extractStatus(error: SdkErrorShape): number | undefined {\n\tif (typeof error.statusCode === \"number\") return error.statusCode;\n\tif (typeof error.status === \"number\") return error.status;\n\tif (typeof error.$metadata?.httpStatusCode === \"number\") return error.$metadata.httpStatusCode;\n\tif (typeof error.$response?.statusCode === \"number\") return error.$response.statusCode;\n\treturn undefined;\n}\n\n/**\n * Probe the raw body reason, first usable hit wins, in SDK-field order:\n * `body` string (Mistral) → `error` parsed JSON body object (`openai` SDK's\n * `this.error`) → `$response.body` (Bedrock). Empty objects and unread response\n * streams are treated as no body so they do not surface as `\"{}\"` or serialized\n * stream internals. The chosen body is truncated to the cap.\n */\nfunction extractBody(error: SdkErrorShape): string | undefined {\n\tconst bodyText = pickBodyText(error);\n\tif (bodyText === undefined) return undefined;\n\tconst trimmed = bodyText.trim();\n\tif (trimmed.length === 0) return undefined;\n\treturn truncateErrorText(trimmed, MAX_PROVIDER_ERROR_BODY_CHARS);\n}\n\nfunction pickBodyText(error: SdkErrorShape): string | undefined {\n\tif (typeof error.body === \"string\") return error.body;\n\tif (isPlainNonEmptyObject(error.error)) return safeJsonStringify(error.error);\n\tconst responseBody = error.$response?.body;\n\tif (typeof responseBody === \"string\") return responseBody;\n\tif (isReadableStreamLike(responseBody)) return undefined;\n\tif (isPlainNonEmptyObject(responseBody)) return safeJsonStringify(responseBody);\n\treturn undefined;\n}\n\nfunction isReadableStreamLike(value: unknown): boolean {\n\treturn typeof value === \"object\" && value !== null && \"pipe\" in value && typeof value.pipe === \"function\";\n}\n\n/**\n * Only a PLAIN object counts as an HTTP body. SDK error fields can hold class\n * instances instead of parsed bodies — AWS SDK v3's `$response.body` is an\n * HTTP stream/response wrapper object, and stringifying one produced garbage\n * like `{\"_events\":...}` as the \"body\", which then REPLACED `error.message`\n * in the composed display string. `error.message` is where the SDK puts the\n * real deserialized exception text (\"Input is too long...\", schema validation\n * details, ...), so the one useful string was discarded for noise. A class\n * instance yields no body, `messageCarriesBody` stays true, and the real\n * message survives. Complements the `pipe` sniffing above: web\n * ReadableStreams (pipeTo/pipeThrough, no `pipe`) and non-stream SDK wrapper\n * classes fail the prototype check, while parsed JSON bodies (plain objects\n * by construction) still pass.\n */\nfunction isPlainNonEmptyObject(value: unknown): boolean {\n\tif (typeof value !== \"object\" || value === null) return false;\n\tconst proto = Object.getPrototypeOf(value);\n\tif (proto !== Object.prototype && proto !== null) return false;\n\treturn Object.keys(value).length > 0;\n}\n\n/**\n * Compose a display string from a normalized error. When the message already\n * carries the body (Anthropic / `@google/genai` happy path) or no body/status\n * was extracted, the message is returned unchanged. Otherwise the status and\n * body are surfaced, with an optional provider prefix.\n *\n * - no prefix: `\"<status>: <body>\"`\n * - prefix: `\"<prefix> (<status>): <body>\"`\n */\nexport function formatProviderError(norm: NormalizedProviderError, prefix?: string): string {\n\tif (norm.messageCarriesBody || norm.status === undefined || norm.body === undefined) {\n\t\treturn prefix !== undefined && norm.status !== undefined\n\t\t\t? `${prefix} (${norm.status}): ${norm.message}`\n\t\t\t: norm.message;\n\t}\n\treturn prefix !== undefined ? `${prefix} (${norm.status}): ${norm.body}` : `${norm.status}: ${norm.body}`;\n}\n\nexport function truncateErrorText(text: string, maxChars: number): string {\n\tif (text.length <= maxChars) return text;\n\treturn `${ownedSlice(text, 0, maxChars)}... [truncated ${text.length - maxChars} chars]`;\n}\n\nexport function safeJsonStringify(value: unknown): string {\n\ttry {\n\t\tconst serialized = JSON.stringify(value);\n\t\treturn serialized === undefined ? String(value) : serialized;\n\t} catch {\n\t\treturn String(value);\n\t}\n}\n"]}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Incremental, tolerant parsing of JSON text that arrives in pieces, such as streamed tool-call arguments.
|
|
3
|
+
*
|
|
4
|
+
* `StreamingJsonParser.append()` reads each piece once, so a stream costs time and memory linear in its length, and
|
|
5
|
+
* `snapshot()` returns the value of the text read so far. Snapshots are plain JSON values and never change once
|
|
6
|
+
* returned: the parser copies the containers that are still open (the "open path") into every new snapshot and shares
|
|
7
|
+
* completed containers and strings. The rules for incomplete text follow `parseStreamingJson()`:
|
|
8
|
+
*
|
|
9
|
+
* - An unterminated string shows the characters read so far; an unterminated escape is held back until complete.
|
|
10
|
+
* - An unterminated container shows the members and elements read so far.
|
|
11
|
+
* - A member whose key or value has not started is omitted.
|
|
12
|
+
* - An unterminated literal shows its value once it is a prefix of `true`, `false` or `null`; an unterminated number
|
|
13
|
+
* shows its value when the digits read so far form a number (an exponent without digits is ignored).
|
|
14
|
+
*
|
|
15
|
+
* Malformed text is read leniently, the way `repairJson()` and `parseStreamingJson()` treat it: raw control characters
|
|
16
|
+
* in strings are kept, an invalid escape such as `\d` stays literal text, missing commas and colons are tolerated,
|
|
17
|
+
* values in invalid positions and unknown words are dropped, and text after the root value is ignored. The parser never
|
|
18
|
+
* throws. Callers that need the exact value of the finished text parse it once, e.g. with `parseToolCallArguments()`.
|
|
19
|
+
*
|
|
20
|
+
* Differences from `parseStreamingJson()` on incomplete text: trailing whitespace inside an unterminated string is
|
|
21
|
+
* kept (it was trimmed), a string that contains a raw control character or an invalid escape stays visible while it
|
|
22
|
+
* streams (it was dropped until the document was complete), and `NaN` and `Infinity` words are dropped.
|
|
23
|
+
*
|
|
24
|
+
* `append()` also returns the changes the piece made, for a writer that keeps its own copy of the value and applies
|
|
25
|
+
* them instead of re-assigning snapshots: `set` when a key, an array item or a finished value appears (a new string is
|
|
26
|
+
* set once with its first text, a container empty), `append` when the piece extended an open string. Unterminated
|
|
27
|
+
* numbers and literals are not reported until they end. A container nested more than 32 levels deep is reported as one
|
|
28
|
+
* `set` of its whole value when it closes, so paths stay short.
|
|
29
|
+
*
|
|
30
|
+
* Cost: copying the open path costs its member and element count. Each `append()` earns copy credit proportional to its
|
|
31
|
+
* length plus a constant; `snapshot()` spends it and returns the previous snapshot while the open path costs more than
|
|
32
|
+
* the credit. So a document that keeps a very large container open (thousands of elements) shows a preview that lags
|
|
33
|
+
* a little behind the text, and no input makes the total work grow faster than the text.
|
|
34
|
+
*/
|
|
35
|
+
import type { JsonValue, ToolCall } from "../types.ts";
|
|
36
|
+
/** A path from the root value: member keys and array indices. */
|
|
37
|
+
export type JsonPath = readonly (string | number)[];
|
|
38
|
+
/** One change a piece of text made to the value; see the module comment. */
|
|
39
|
+
export type JsonChange = {
|
|
40
|
+
readonly kind: "set";
|
|
41
|
+
readonly path: JsonPath;
|
|
42
|
+
readonly value: JsonValue;
|
|
43
|
+
} | {
|
|
44
|
+
readonly kind: "append";
|
|
45
|
+
readonly path: JsonPath;
|
|
46
|
+
readonly text: string;
|
|
47
|
+
};
|
|
48
|
+
/** Incremental, tolerant JSON parser; see the module comment for its rules and costs. */
|
|
49
|
+
export declare class StreamingJsonParser {
|
|
50
|
+
#private;
|
|
51
|
+
/**
|
|
52
|
+
* Read the next piece of text; returns the changes it made, in order. Costs time linear in `text.length`.
|
|
53
|
+
*/
|
|
54
|
+
append(text: string): readonly JsonChange[];
|
|
55
|
+
/**
|
|
56
|
+
* The value of the text read so far: `undefined` before a value starts, otherwise a plain JSON value that never
|
|
57
|
+
* changes afterwards. Returns the previous snapshot while the text read since did not change the value, or while a
|
|
58
|
+
* fresh copy of the open path is not yet paid for (see the module comment).
|
|
59
|
+
*/
|
|
60
|
+
snapshot(): JsonValue | undefined;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The arguments of one streaming tool call: the raw text, for the one full parse when the call ends, and a
|
|
64
|
+
* `StreamingJsonParser` for the partial arguments in between. Providers keep one per call in their own stream state,
|
|
65
|
+
* never on the content block, so the raw text never reaches partial messages or transcripts.
|
|
66
|
+
*/
|
|
67
|
+
export declare class ToolCallArgumentsStream {
|
|
68
|
+
#private;
|
|
69
|
+
/** The raw argument text received so far. */
|
|
70
|
+
get text(): string;
|
|
71
|
+
/** Append a piece of argument text; returns the partial arguments, a new object whenever they changed. */
|
|
72
|
+
append(delta: string): ToolCall["arguments"];
|
|
73
|
+
/**
|
|
74
|
+
* Take the provider's complete argument text. When it continues the text so far, returns the missing piece as
|
|
75
|
+
* `delta`; otherwise starts over with `text`. Either way returns the partial arguments of `text`.
|
|
76
|
+
*/
|
|
77
|
+
replace(text: string): {
|
|
78
|
+
readonly delta: string | undefined;
|
|
79
|
+
readonly arguments: ToolCall["arguments"];
|
|
80
|
+
};
|
|
81
|
+
/** The final arguments: one full parse of the complete text, which reports malformed arguments. */
|
|
82
|
+
finish(): Pick<ToolCall, "arguments" | "argumentsError">;
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=streaming-json.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"streaming-json.d.ts","sourceRoot":"","sources":["../../src/utils/streaming-json.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAWvD,iEAAiE;AACjE,MAAM,MAAM,QAAQ,GAAG,SAAS,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC;AAEpD,4EAA4E;AAC5E,MAAM,MAAM,UAAU,GACnB;IAAE,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAA;CAAE,GAC5E;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAuD/E,yFAAyF;AACzF,qBAAa,mBAAmB;;IAe/B;;OAEG;IACH,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,UAAU,EAAE,CAY1C;IAED;;;;OAIG;IACH,QAAQ,IAAI,SAAS,GAAG,SAAS,CAShC;CAkSD;AAED;;;;GAIG;AACH,qBAAa,uBAAuB;;IAMnC,6CAA6C;IAC7C,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,0GAA0G;IAC1G,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,QAAQ,CAAC,WAAW,CAAC,CAI3C;IAED;;;OAGG;IACH,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;QAAC,QAAQ,CAAC,SAAS,EAAE,QAAQ,CAAC,WAAW,CAAC,CAAA;KAAE,CAQvG;IAED,mGAAmG;IACnG,MAAM,IAAI,IAAI,CAAC,QAAQ,EAAE,WAAW,GAAG,gBAAgB,CAAC,CAEvD;CAaD"}
|
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Incremental, tolerant parsing of JSON text that arrives in pieces, such as streamed tool-call arguments.
|
|
3
|
+
*
|
|
4
|
+
* `StreamingJsonParser.append()` reads each piece once, so a stream costs time and memory linear in its length, and
|
|
5
|
+
* `snapshot()` returns the value of the text read so far. Snapshots are plain JSON values and never change once
|
|
6
|
+
* returned: the parser copies the containers that are still open (the "open path") into every new snapshot and shares
|
|
7
|
+
* completed containers and strings. The rules for incomplete text follow `parseStreamingJson()`:
|
|
8
|
+
*
|
|
9
|
+
* - An unterminated string shows the characters read so far; an unterminated escape is held back until complete.
|
|
10
|
+
* - An unterminated container shows the members and elements read so far.
|
|
11
|
+
* - A member whose key or value has not started is omitted.
|
|
12
|
+
* - An unterminated literal shows its value once it is a prefix of `true`, `false` or `null`; an unterminated number
|
|
13
|
+
* shows its value when the digits read so far form a number (an exponent without digits is ignored).
|
|
14
|
+
*
|
|
15
|
+
* Malformed text is read leniently, the way `repairJson()` and `parseStreamingJson()` treat it: raw control characters
|
|
16
|
+
* in strings are kept, an invalid escape such as `\d` stays literal text, missing commas and colons are tolerated,
|
|
17
|
+
* values in invalid positions and unknown words are dropped, and text after the root value is ignored. The parser never
|
|
18
|
+
* throws. Callers that need the exact value of the finished text parse it once, e.g. with `parseToolCallArguments()`.
|
|
19
|
+
*
|
|
20
|
+
* Differences from `parseStreamingJson()` on incomplete text: trailing whitespace inside an unterminated string is
|
|
21
|
+
* kept (it was trimmed), a string that contains a raw control character or an invalid escape stays visible while it
|
|
22
|
+
* streams (it was dropped until the document was complete), and `NaN` and `Infinity` words are dropped.
|
|
23
|
+
*
|
|
24
|
+
* `append()` also returns the changes the piece made, for a writer that keeps its own copy of the value and applies
|
|
25
|
+
* them instead of re-assigning snapshots: `set` when a key, an array item or a finished value appears (a new string is
|
|
26
|
+
* set once with its first text, a container empty), `append` when the piece extended an open string. Unterminated
|
|
27
|
+
* numbers and literals are not reported until they end. A container nested more than 32 levels deep is reported as one
|
|
28
|
+
* `set` of its whole value when it closes, so paths stay short.
|
|
29
|
+
*
|
|
30
|
+
* Cost: copying the open path costs its member and element count. Each `append()` earns copy credit proportional to its
|
|
31
|
+
* length plus a constant; `snapshot()` spends it and returns the previous snapshot while the open path costs more than
|
|
32
|
+
* the credit. So a document that keeps a very large container open (thousands of elements) shows a preview that lags
|
|
33
|
+
* a little behind the text, and no input makes the total work grow faster than the text.
|
|
34
|
+
*/
|
|
35
|
+
import { parseToolCallArguments } from "./json-parse.js";
|
|
36
|
+
// Changes are reported at most this deep, so a path costs O(1) to build.
|
|
37
|
+
const MAX_REPORTED_DEPTH = 32;
|
|
38
|
+
const ROOT_SLOT = { kind: "root" };
|
|
39
|
+
// Copy credit per append and per appended character. The floor keeps small documents fresh at every snapshot.
|
|
40
|
+
const CREDIT_PER_APPEND = 256;
|
|
41
|
+
const CREDIT_PER_CHARACTER = 8;
|
|
42
|
+
// An unterminated number longer than this is not shown until it ends, so `snapshot()` stays cheap.
|
|
43
|
+
const MAX_PENDING_SCALAR_LENGTH = 64;
|
|
44
|
+
const NUMBER = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/;
|
|
45
|
+
const NUMBER_WITHOUT_EXPONENT_DIGITS = /^(-?(?:0|[1-9]\d*)(?:\.\d+)?)[eE][+-]?$/;
|
|
46
|
+
const LITERALS = [
|
|
47
|
+
["true", true],
|
|
48
|
+
["false", false],
|
|
49
|
+
["null", null],
|
|
50
|
+
];
|
|
51
|
+
const QUOTE = 0x22;
|
|
52
|
+
const BACKSLASH = 0x5c;
|
|
53
|
+
/** Incremental, tolerant JSON parser; see the module comment for its rules and costs. */
|
|
54
|
+
export class StreamingJsonParser {
|
|
55
|
+
#root = undefined;
|
|
56
|
+
#done = false;
|
|
57
|
+
#stack = [];
|
|
58
|
+
#string = undefined;
|
|
59
|
+
#scalar = "";
|
|
60
|
+
#scalarSlot = undefined;
|
|
61
|
+
/** Sum of `size + 1` over the open path: what a fresh snapshot copies. */
|
|
62
|
+
#pathCost = 0;
|
|
63
|
+
#credit = 0;
|
|
64
|
+
#dirty = false;
|
|
65
|
+
#snapshot = undefined;
|
|
66
|
+
/** Changes of the piece being read. */
|
|
67
|
+
#changes = [];
|
|
68
|
+
/**
|
|
69
|
+
* Read the next piece of text; returns the changes it made, in order. Costs time linear in `text.length`.
|
|
70
|
+
*/
|
|
71
|
+
append(text) {
|
|
72
|
+
this.#credit += CREDIT_PER_APPEND + CREDIT_PER_CHARACTER * text.length;
|
|
73
|
+
this.#changes = [];
|
|
74
|
+
if (this.#string !== undefined)
|
|
75
|
+
this.#string.change = undefined;
|
|
76
|
+
let index = 0;
|
|
77
|
+
while (index < text.length && !this.#done) {
|
|
78
|
+
const open = this.#string;
|
|
79
|
+
if (open !== undefined)
|
|
80
|
+
index = this.#readString(open, text, index);
|
|
81
|
+
else if (this.#scalar !== "")
|
|
82
|
+
index = this.#readScalar(text, index);
|
|
83
|
+
else
|
|
84
|
+
this.#structural(text[index++]);
|
|
85
|
+
}
|
|
86
|
+
return this.#changes;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The value of the text read so far: `undefined` before a value starts, otherwise a plain JSON value that never
|
|
90
|
+
* changes afterwards. Returns the previous snapshot while the text read since did not change the value, or while a
|
|
91
|
+
* fresh copy of the open path is not yet paid for (see the module comment).
|
|
92
|
+
*/
|
|
93
|
+
snapshot() {
|
|
94
|
+
if (!this.#dirty)
|
|
95
|
+
return this.#snapshot;
|
|
96
|
+
const pending = this.#pendingScalar();
|
|
97
|
+
const cost = this.#pathCost + (pending === undefined ? 0 : 1);
|
|
98
|
+
if (cost > this.#credit)
|
|
99
|
+
return this.#snapshot;
|
|
100
|
+
this.#credit -= cost;
|
|
101
|
+
this.#dirty = false;
|
|
102
|
+
this.#snapshot = this.#copyOpenPath(pending);
|
|
103
|
+
return this.#snapshot;
|
|
104
|
+
}
|
|
105
|
+
/** Read string characters from `start` until the string ends or the text does; returns the next index. */
|
|
106
|
+
#readString(open, text, start) {
|
|
107
|
+
let runStart = start;
|
|
108
|
+
for (let index = start; index < text.length; index++) {
|
|
109
|
+
const code = text.charCodeAt(index);
|
|
110
|
+
if (open.escape !== "") {
|
|
111
|
+
if (this.#continueEscape(open, text[index])) {
|
|
112
|
+
runStart = index + 1;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
// Not part of the escape: read it as a normal string character below.
|
|
116
|
+
runStart = index;
|
|
117
|
+
}
|
|
118
|
+
if (code === QUOTE) {
|
|
119
|
+
if (index > runStart)
|
|
120
|
+
this.#appendString(open, text.slice(runStart, index));
|
|
121
|
+
this.#endString(open);
|
|
122
|
+
return index + 1;
|
|
123
|
+
}
|
|
124
|
+
if (code === BACKSLASH) {
|
|
125
|
+
if (index > runStart)
|
|
126
|
+
this.#appendString(open, text.slice(runStart, index));
|
|
127
|
+
open.escape = "\\";
|
|
128
|
+
runStart = index + 1;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (text.length > runStart)
|
|
132
|
+
this.#appendString(open, text.slice(runStart));
|
|
133
|
+
return text.length;
|
|
134
|
+
}
|
|
135
|
+
/** Read number or literal characters from `start` up to the next delimiter; returns the delimiter's index. */
|
|
136
|
+
#readScalar(text, start) {
|
|
137
|
+
let end = start;
|
|
138
|
+
while (end < text.length && !isDelimiter(text[end]))
|
|
139
|
+
end++;
|
|
140
|
+
if (end > start) {
|
|
141
|
+
this.#scalar += text.slice(start, end);
|
|
142
|
+
if (this.#scalarSlot !== undefined)
|
|
143
|
+
this.#dirty = true;
|
|
144
|
+
}
|
|
145
|
+
if (end < text.length)
|
|
146
|
+
this.#endScalar();
|
|
147
|
+
return end;
|
|
148
|
+
}
|
|
149
|
+
/** Handle one character outside strings. */
|
|
150
|
+
#structural(char) {
|
|
151
|
+
switch (char) {
|
|
152
|
+
case " ":
|
|
153
|
+
case "\t":
|
|
154
|
+
case "\n":
|
|
155
|
+
case "\r":
|
|
156
|
+
return;
|
|
157
|
+
case '"':
|
|
158
|
+
this.#startString();
|
|
159
|
+
return;
|
|
160
|
+
case "{":
|
|
161
|
+
case "[":
|
|
162
|
+
this.#openContainer(char === "{" ? {} : []);
|
|
163
|
+
return;
|
|
164
|
+
case "}":
|
|
165
|
+
case "]":
|
|
166
|
+
this.#closeContainer();
|
|
167
|
+
return;
|
|
168
|
+
case ":": {
|
|
169
|
+
const frame = this.#stack.at(-1);
|
|
170
|
+
if (frame !== undefined && !Array.isArray(frame.container) && frame.phase === "colon")
|
|
171
|
+
frame.phase = "value";
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
case ",": {
|
|
175
|
+
const frame = this.#stack.at(-1);
|
|
176
|
+
if (frame !== undefined)
|
|
177
|
+
frame.phase = Array.isArray(frame.container) ? "value" : "key";
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
default:
|
|
181
|
+
this.#scalar = char;
|
|
182
|
+
this.#scalarSlot = this.#valueSlot();
|
|
183
|
+
if (this.#scalarSlot !== undefined)
|
|
184
|
+
this.#dirty = true;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/** Where a value starting now goes, or `undefined` when this position takes no value. */
|
|
188
|
+
#valueSlot() {
|
|
189
|
+
const frame = this.#stack.at(-1);
|
|
190
|
+
if (frame === undefined)
|
|
191
|
+
return this.#root === undefined ? ROOT_SLOT : undefined;
|
|
192
|
+
if (Array.isArray(frame.container))
|
|
193
|
+
return { kind: "element", index: frame.container.length };
|
|
194
|
+
// A missing colon is tolerated: `{"a" 1}` reads as `{"a": 1}`.
|
|
195
|
+
if ((frame.phase === "value" || frame.phase === "colon") && frame.key !== undefined) {
|
|
196
|
+
return { kind: "member", key: frame.key };
|
|
197
|
+
}
|
|
198
|
+
return undefined;
|
|
199
|
+
}
|
|
200
|
+
#startString() {
|
|
201
|
+
const frame = this.#stack.at(-1);
|
|
202
|
+
// A missing comma is tolerated: `{"a": 1 "b": 2}` reads the second member.
|
|
203
|
+
if (frame !== undefined &&
|
|
204
|
+
!Array.isArray(frame.container) &&
|
|
205
|
+
(frame.phase === "key" || frame.phase === "comma")) {
|
|
206
|
+
this.#string = { target: "key", slot: undefined, text: "", escape: "", path: undefined, change: undefined };
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const slot = this.#valueSlot();
|
|
210
|
+
const path = this.#reportedPath(slot);
|
|
211
|
+
this.#string = { target: "value", slot, text: "", escape: "", path, change: undefined };
|
|
212
|
+
this.#place(slot, "");
|
|
213
|
+
if (path !== undefined)
|
|
214
|
+
this.#string.change = this.#report({ kind: "set", path, value: "" });
|
|
215
|
+
}
|
|
216
|
+
/** The path of a value at `slot` in the innermost open container, when its changes are reported. */
|
|
217
|
+
#reportedPath(slot) {
|
|
218
|
+
if (slot === undefined)
|
|
219
|
+
return undefined;
|
|
220
|
+
if (slot.kind === "root")
|
|
221
|
+
return [];
|
|
222
|
+
const frame = this.#stack.at(-1);
|
|
223
|
+
if (!frame.reportsInside)
|
|
224
|
+
return undefined;
|
|
225
|
+
return [...frame.path, slot.kind === "member" ? slot.key : slot.index];
|
|
226
|
+
}
|
|
227
|
+
#report(change) {
|
|
228
|
+
this.#changes.push(change);
|
|
229
|
+
return change;
|
|
230
|
+
}
|
|
231
|
+
/** Continue an escape with `char`; returns false when `char` is not part of it and must be read normally. */
|
|
232
|
+
#continueEscape(open, char) {
|
|
233
|
+
if (open.escape === "\\") {
|
|
234
|
+
const decoded = SIMPLE_ESCAPES[char];
|
|
235
|
+
if (decoded !== undefined) {
|
|
236
|
+
open.escape = "";
|
|
237
|
+
this.#appendString(open, decoded);
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
if (char === "u") {
|
|
241
|
+
open.escape = "\\u";
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
// Invalid escape: keep the backslash as text and read the character normally, as repairJson() does.
|
|
245
|
+
open.escape = "";
|
|
246
|
+
this.#appendString(open, "\\");
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
if (isHexDigit(char)) {
|
|
250
|
+
open.escape += char;
|
|
251
|
+
if (open.escape.length < 6)
|
|
252
|
+
return true;
|
|
253
|
+
const decoded = String.fromCharCode(Number.parseInt(open.escape.slice(2), 16));
|
|
254
|
+
open.escape = "";
|
|
255
|
+
this.#appendString(open, decoded);
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
// `\u` without four hex digits: keep it as text and read the character normally, as repairJson() does.
|
|
259
|
+
const literal = open.escape;
|
|
260
|
+
open.escape = "";
|
|
261
|
+
this.#appendString(open, literal);
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
#appendString(open, text) {
|
|
265
|
+
open.text += text;
|
|
266
|
+
if (open.target !== "value")
|
|
267
|
+
return;
|
|
268
|
+
this.#place(open.slot, open.text);
|
|
269
|
+
if (open.path === undefined)
|
|
270
|
+
return;
|
|
271
|
+
// Extend this string's newest change while nothing came after it; otherwise report an append.
|
|
272
|
+
const change = open.change;
|
|
273
|
+
if (change !== undefined && this.#changes.at(-1) === change) {
|
|
274
|
+
if (change.kind === "set")
|
|
275
|
+
change.value = change.value + text;
|
|
276
|
+
else
|
|
277
|
+
change.text += text;
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
open.change = this.#report({ kind: "append", path: open.path, text });
|
|
281
|
+
}
|
|
282
|
+
#endString(open) {
|
|
283
|
+
this.#string = undefined;
|
|
284
|
+
const frame = this.#stack.at(-1);
|
|
285
|
+
if (open.target === "key") {
|
|
286
|
+
if (frame === undefined)
|
|
287
|
+
return;
|
|
288
|
+
frame.key = open.text;
|
|
289
|
+
frame.phase = "colon";
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
this.#valueEnded(frame);
|
|
293
|
+
}
|
|
294
|
+
#endScalar() {
|
|
295
|
+
const token = this.#scalar;
|
|
296
|
+
const slot = this.#scalarSlot;
|
|
297
|
+
this.#scalar = "";
|
|
298
|
+
this.#scalarSlot = undefined;
|
|
299
|
+
const value = scalarValue(token);
|
|
300
|
+
if (value !== undefined) {
|
|
301
|
+
this.#place(slot, value);
|
|
302
|
+
const path = this.#reportedPath(slot);
|
|
303
|
+
if (path !== undefined)
|
|
304
|
+
this.#report({ kind: "set", path, value });
|
|
305
|
+
}
|
|
306
|
+
this.#valueEnded(this.#stack.at(-1));
|
|
307
|
+
}
|
|
308
|
+
/** The value of the unterminated scalar at the end of the text, if it shows. */
|
|
309
|
+
#pendingScalar() {
|
|
310
|
+
const slot = this.#scalarSlot;
|
|
311
|
+
const token = this.#scalar;
|
|
312
|
+
if (slot === undefined || token === "" || token.length > MAX_PENDING_SCALAR_LENGTH)
|
|
313
|
+
return undefined;
|
|
314
|
+
const value = pendingScalarValue(token);
|
|
315
|
+
return value === undefined ? undefined : { slot, value };
|
|
316
|
+
}
|
|
317
|
+
#openContainer(container) {
|
|
318
|
+
const slot = this.#valueSlot();
|
|
319
|
+
const path = this.#reportedPath(slot);
|
|
320
|
+
this.#place(slot, container);
|
|
321
|
+
this.#stack.push({
|
|
322
|
+
container,
|
|
323
|
+
slot,
|
|
324
|
+
path,
|
|
325
|
+
reportsInside: path !== undefined && path.length < MAX_REPORTED_DEPTH,
|
|
326
|
+
size: 0,
|
|
327
|
+
key: undefined,
|
|
328
|
+
phase: Array.isArray(container) ? "value" : "key",
|
|
329
|
+
});
|
|
330
|
+
this.#pathCost += 1;
|
|
331
|
+
this.#dirty = true;
|
|
332
|
+
// A fresh container: the working one keeps changing, and a change never does.
|
|
333
|
+
if (path !== undefined)
|
|
334
|
+
this.#report({ kind: "set", path, value: Array.isArray(container) ? [] : {} });
|
|
335
|
+
}
|
|
336
|
+
#closeContainer() {
|
|
337
|
+
const frame = this.#stack.pop();
|
|
338
|
+
// A stray closer outside any container is ignored.
|
|
339
|
+
if (frame === undefined)
|
|
340
|
+
return;
|
|
341
|
+
this.#pathCost -= frame.size + 1;
|
|
342
|
+
this.#dirty = true;
|
|
343
|
+
// Too deep to report inside: the whole value, final now that it is closed.
|
|
344
|
+
if (frame.path !== undefined && !frame.reportsInside) {
|
|
345
|
+
this.#report({ kind: "set", path: frame.path, value: frame.container });
|
|
346
|
+
}
|
|
347
|
+
this.#valueEnded(this.#stack.at(-1));
|
|
348
|
+
}
|
|
349
|
+
/** A value in `frame`, or the root value when there is no frame, has ended. */
|
|
350
|
+
#valueEnded(frame) {
|
|
351
|
+
if (frame === undefined) {
|
|
352
|
+
// Text after the root value is ignored, but only once a root value exists.
|
|
353
|
+
if (this.#root !== undefined)
|
|
354
|
+
this.#done = true;
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
frame.phase = "comma";
|
|
358
|
+
}
|
|
359
|
+
/** Set the value at `slot` in the open path; nothing for a value in an invalid position. */
|
|
360
|
+
#place(slot, value) {
|
|
361
|
+
if (slot === undefined)
|
|
362
|
+
return;
|
|
363
|
+
this.#dirty = true;
|
|
364
|
+
if (slot.kind === "root") {
|
|
365
|
+
this.#root = value;
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
const frame = this.#stack.at(-1);
|
|
369
|
+
if (slot.kind === "element") {
|
|
370
|
+
const elements = frame.container;
|
|
371
|
+
if (slot.index === elements.length) {
|
|
372
|
+
frame.size++;
|
|
373
|
+
this.#pathCost++;
|
|
374
|
+
}
|
|
375
|
+
elements[slot.index] = value;
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
const members = frame.container;
|
|
379
|
+
if (!Object.hasOwn(members, slot.key)) {
|
|
380
|
+
frame.size++;
|
|
381
|
+
this.#pathCost++;
|
|
382
|
+
}
|
|
383
|
+
setMember(members, slot.key, value);
|
|
384
|
+
}
|
|
385
|
+
/** A fresh snapshot: copies of the open containers, innermost first, each holding the copy of its open child. */
|
|
386
|
+
#copyOpenPath(pending) {
|
|
387
|
+
if (this.#stack.length === 0)
|
|
388
|
+
return pending?.slot.kind === "root" ? pending.value : this.#root;
|
|
389
|
+
let child;
|
|
390
|
+
let childSlot;
|
|
391
|
+
for (let depth = this.#stack.length - 1; depth >= 0; depth--) {
|
|
392
|
+
const frame = this.#stack[depth];
|
|
393
|
+
const copy = Array.isArray(frame.container) ? frame.container.slice() : { ...frame.container };
|
|
394
|
+
if (depth === this.#stack.length - 1 && pending !== undefined)
|
|
395
|
+
setSlot(copy, pending.slot, pending.value);
|
|
396
|
+
if (child !== undefined && childSlot !== undefined)
|
|
397
|
+
setSlot(copy, childSlot, child);
|
|
398
|
+
child = copy;
|
|
399
|
+
childSlot = frame.slot;
|
|
400
|
+
}
|
|
401
|
+
// The outermost open container is the root unless it sits in an invalid position.
|
|
402
|
+
return childSlot?.kind === "root" ? child : this.#root;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* The arguments of one streaming tool call: the raw text, for the one full parse when the call ends, and a
|
|
407
|
+
* `StreamingJsonParser` for the partial arguments in between. Providers keep one per call in their own stream state,
|
|
408
|
+
* never on the content block, so the raw text never reaches partial messages or transcripts.
|
|
409
|
+
*/
|
|
410
|
+
export class ToolCallArgumentsStream {
|
|
411
|
+
#text = "";
|
|
412
|
+
#parser = new StreamingJsonParser();
|
|
413
|
+
#snapshot = undefined;
|
|
414
|
+
#arguments = {};
|
|
415
|
+
/** The raw argument text received so far. */
|
|
416
|
+
get text() {
|
|
417
|
+
return this.#text;
|
|
418
|
+
}
|
|
419
|
+
/** Append a piece of argument text; returns the partial arguments, a new object whenever they changed. */
|
|
420
|
+
append(delta) {
|
|
421
|
+
this.#text += delta;
|
|
422
|
+
this.#parser.append(delta);
|
|
423
|
+
return this.#partial();
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Take the provider's complete argument text. When it continues the text so far, returns the missing piece as
|
|
427
|
+
* `delta`; otherwise starts over with `text`. Either way returns the partial arguments of `text`.
|
|
428
|
+
*/
|
|
429
|
+
replace(text) {
|
|
430
|
+
if (text.startsWith(this.#text)) {
|
|
431
|
+
const delta = text.slice(this.#text.length);
|
|
432
|
+
return { delta, arguments: delta === "" ? this.#partial() : this.append(delta) };
|
|
433
|
+
}
|
|
434
|
+
this.#text = "";
|
|
435
|
+
this.#parser = new StreamingJsonParser();
|
|
436
|
+
return { delta: undefined, arguments: this.append(text) };
|
|
437
|
+
}
|
|
438
|
+
/** The final arguments: one full parse of the complete text, which reports malformed arguments. */
|
|
439
|
+
finish() {
|
|
440
|
+
return parseToolCallArguments(this.#text);
|
|
441
|
+
}
|
|
442
|
+
#partial() {
|
|
443
|
+
const snapshot = this.#parser.snapshot();
|
|
444
|
+
if (snapshot === this.#snapshot)
|
|
445
|
+
return this.#arguments;
|
|
446
|
+
this.#snapshot = snapshot;
|
|
447
|
+
// Tool arguments are an object; any other partial value shows as no arguments yet.
|
|
448
|
+
this.#arguments =
|
|
449
|
+
typeof snapshot === "object" && snapshot !== null && !Array.isArray(snapshot)
|
|
450
|
+
? snapshot
|
|
451
|
+
: {};
|
|
452
|
+
return this.#arguments;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
const SIMPLE_ESCAPES = {
|
|
456
|
+
'"': '"',
|
|
457
|
+
"\\": "\\",
|
|
458
|
+
"/": "/",
|
|
459
|
+
b: "\b",
|
|
460
|
+
f: "\f",
|
|
461
|
+
n: "\n",
|
|
462
|
+
r: "\r",
|
|
463
|
+
t: "\t",
|
|
464
|
+
};
|
|
465
|
+
function isDelimiter(char) {
|
|
466
|
+
return (char === " " ||
|
|
467
|
+
char === "\t" ||
|
|
468
|
+
char === "\n" ||
|
|
469
|
+
char === "\r" ||
|
|
470
|
+
char === "," ||
|
|
471
|
+
char === ":" ||
|
|
472
|
+
char === "{" ||
|
|
473
|
+
char === "}" ||
|
|
474
|
+
char === "[" ||
|
|
475
|
+
char === "]" ||
|
|
476
|
+
char === '"');
|
|
477
|
+
}
|
|
478
|
+
function isHexDigit(char) {
|
|
479
|
+
return (char >= "0" && char <= "9") || (char >= "a" && char <= "f") || (char >= "A" && char <= "F");
|
|
480
|
+
}
|
|
481
|
+
/** The value of a complete number or literal token; `undefined` for anything else, which is dropped. */
|
|
482
|
+
function scalarValue(token) {
|
|
483
|
+
for (const [literal, value] of LITERALS)
|
|
484
|
+
if (token === literal)
|
|
485
|
+
return value;
|
|
486
|
+
return NUMBER.test(token) ? Number(token) : undefined;
|
|
487
|
+
}
|
|
488
|
+
/** The value an unterminated number or literal token shows. */
|
|
489
|
+
function pendingScalarValue(token) {
|
|
490
|
+
for (const [literal, value] of LITERALS)
|
|
491
|
+
if (literal.startsWith(token))
|
|
492
|
+
return value;
|
|
493
|
+
if (NUMBER.test(token))
|
|
494
|
+
return Number(token);
|
|
495
|
+
const mantissa = NUMBER_WITHOUT_EXPONENT_DIGITS.exec(token);
|
|
496
|
+
return mantissa === null ? undefined : Number(mantissa[1]);
|
|
497
|
+
}
|
|
498
|
+
function setSlot(container, slot, value) {
|
|
499
|
+
if (slot.kind === "element")
|
|
500
|
+
container[slot.index] = value;
|
|
501
|
+
else if (slot.kind === "member")
|
|
502
|
+
setMember(container, slot.key, value);
|
|
503
|
+
}
|
|
504
|
+
function setMember(members, key, value) {
|
|
505
|
+
// `__proto__` must become an own member, as in JSON.parse(), not the prototype.
|
|
506
|
+
if (key === "__proto__") {
|
|
507
|
+
Object.defineProperty(members, key, { value, writable: true, enumerable: true, configurable: true });
|
|
508
|
+
}
|
|
509
|
+
else {
|
|
510
|
+
members[key] = value;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
//# sourceMappingURL=streaming-json.js.map
|