@oh-my-pi/pi-utils 16.3.6 → 16.3.7

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.
Files changed (41) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/types/env.d.ts +1 -1
  3. package/dist/types/fetch-retry.d.ts +2 -0
  4. package/dist/types/index.d.ts +34 -34
  5. package/dist/types/json-parse.d.ts +26 -0
  6. package/dist/types/mermaid-ascii.d.ts +1 -1
  7. package/dist/types/vendor/mermaid-ascii/ascii/ansi.d.ts +1 -1
  8. package/dist/types/vendor/mermaid-ascii/ascii/canvas.d.ts +1 -1
  9. package/dist/types/vendor/mermaid-ascii/ascii/class-diagram.d.ts +1 -1
  10. package/dist/types/vendor/mermaid-ascii/ascii/converter.d.ts +2 -2
  11. package/dist/types/vendor/mermaid-ascii/ascii/draw.d.ts +1 -1
  12. package/dist/types/vendor/mermaid-ascii/ascii/edge-bundling.d.ts +1 -1
  13. package/dist/types/vendor/mermaid-ascii/ascii/edge-routing.d.ts +1 -1
  14. package/dist/types/vendor/mermaid-ascii/ascii/er-diagram.d.ts +1 -1
  15. package/dist/types/vendor/mermaid-ascii/ascii/grid.d.ts +1 -1
  16. package/dist/types/vendor/mermaid-ascii/ascii/index.d.ts +3 -3
  17. package/dist/types/vendor/mermaid-ascii/ascii/multiline-utils.d.ts +1 -1
  18. package/dist/types/vendor/mermaid-ascii/ascii/pathfinder.d.ts +1 -1
  19. package/dist/types/vendor/mermaid-ascii/ascii/sequence.d.ts +1 -1
  20. package/dist/types/vendor/mermaid-ascii/ascii/shapes/circle.d.ts +1 -1
  21. package/dist/types/vendor/mermaid-ascii/ascii/shapes/corners.d.ts +1 -1
  22. package/dist/types/vendor/mermaid-ascii/ascii/shapes/diamond.d.ts +1 -1
  23. package/dist/types/vendor/mermaid-ascii/ascii/shapes/hexagon.d.ts +1 -1
  24. package/dist/types/vendor/mermaid-ascii/ascii/shapes/index.d.ts +2 -2
  25. package/dist/types/vendor/mermaid-ascii/ascii/shapes/rectangle.d.ts +3 -3
  26. package/dist/types/vendor/mermaid-ascii/ascii/shapes/rounded.d.ts +1 -1
  27. package/dist/types/vendor/mermaid-ascii/ascii/shapes/special.d.ts +1 -1
  28. package/dist/types/vendor/mermaid-ascii/ascii/shapes/stadium.d.ts +1 -1
  29. package/dist/types/vendor/mermaid-ascii/ascii/shapes/state.d.ts +1 -1
  30. package/dist/types/vendor/mermaid-ascii/ascii/shapes/types.d.ts +1 -1
  31. package/dist/types/vendor/mermaid-ascii/ascii/types.d.ts +1 -1
  32. package/dist/types/vendor/mermaid-ascii/ascii/xychart.d.ts +1 -1
  33. package/dist/types/vendor/mermaid-ascii/class/parser.d.ts +1 -1
  34. package/dist/types/vendor/mermaid-ascii/er/parser.d.ts +1 -1
  35. package/dist/types/vendor/mermaid-ascii/index.d.ts +1 -1
  36. package/dist/types/vendor/mermaid-ascii/parser.d.ts +1 -1
  37. package/dist/types/vendor/mermaid-ascii/sequence/parser.d.ts +1 -1
  38. package/dist/types/vendor/mermaid-ascii/xychart/parser.d.ts +1 -1
  39. package/package.json +2 -2
  40. package/src/fetch-retry.ts +18 -0
  41. package/src/json-parse.ts +209 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [16.3.7] - 2026-07-05
6
+
7
+ ### Added
8
+
9
+ - Added `classifyJsonPrefix`, a strict RFC 8259 streaming-buffer classifier (`"complete" | "prefix" | "invalid"`). Providers use it to disambiguate identifierless streaming tool-call deltas: a `{`-prefixed chunk only advances to a sibling call when the current argument buffer cannot absorb it.
10
+
5
11
  ## [16.3.1] - 2026-07-02
6
12
 
7
13
  ### Fixed
@@ -1,4 +1,4 @@
1
- export * from "./worker-host";
1
+ export * from "./worker-host.js";
2
2
  /**
3
3
  * Strict shell-identifier shape. Used for dotenv keys we accept into
4
4
  * `Bun.env` — those should be referenceable as `$NAME` from POSIX shells,
@@ -3,7 +3,9 @@
3
3
  * by the OpenAI Codex and Google Gemini retry helpers.
4
4
  *
5
5
  * Header sources (checked in order):
6
+ * - `retry-after-ms` (milliseconds)
6
7
  * - `Retry-After` (numeric seconds, or HTTP date)
8
+ * - `x-ratelimit-reset-ms` (delta ms, or Unix epoch ms/s for large values)
7
9
  * - `x-ratelimit-reset` (Unix epoch seconds)
8
10
  * - `x-ratelimit-reset-after` (seconds)
9
11
  *
@@ -1,35 +1,35 @@
1
- export { once, untilAborted } from "./abortable";
2
- export * from "./async";
3
- export * from "./binary";
4
- export * from "./color";
5
- export * from "./dirs";
6
- export * from "./env";
7
- export * from "./fetch-retry";
8
- export * from "./format";
9
- export * from "./frontmatter";
10
- export * from "./fs-error";
11
- export * from "./glob";
12
- export * from "./json";
13
- export * from "./json-parse";
14
- export * as logger from "./logger";
15
- export * from "./loop-phase";
16
- export * from "./mermaid-ascii";
17
- export * from "./mime";
18
- export * from "./path";
19
- export * from "./path-tree";
20
- export * from "./peek-file";
21
- export * as postmortem from "./postmortem";
22
- export * as procmgr from "./procmgr";
23
- export * as prompt from "./prompt";
24
- export * as ptree from "./ptree";
25
- export { AbortError, ChildProcess, Exception, NonZeroExitError } from "./ptree";
26
- export * from "./runtime-install";
27
- export * from "./sanitize-text";
28
- export * from "./snowflake";
29
- export * from "./stream";
30
- export * from "./tab-spacing";
31
- export * from "./temp";
32
- export * from "./tls-fetch";
33
- export * from "./type-guards";
34
- export * from "./which";
1
+ export { once, untilAborted } from "./abortable.js";
2
+ export * from "./async.js";
3
+ export * from "./binary.js";
4
+ export * from "./color.js";
5
+ export * from "./dirs.js";
6
+ export * from "./env.js";
7
+ export * from "./fetch-retry.js";
8
+ export * from "./format.js";
9
+ export * from "./frontmatter.js";
10
+ export * from "./fs-error.js";
11
+ export * from "./glob.js";
12
+ export * from "./json.js";
13
+ export * from "./json-parse.js";
14
+ export * as logger from "./logger.js";
15
+ export * from "./loop-phase.js";
16
+ export * from "./mermaid-ascii.js";
17
+ export * from "./mime.js";
18
+ export * from "./path.js";
19
+ export * from "./path-tree.js";
20
+ export * from "./peek-file.js";
21
+ export * as postmortem from "./postmortem.js";
22
+ export * as procmgr from "./procmgr.js";
23
+ export * as prompt from "./prompt.js";
24
+ export * as ptree from "./ptree.js";
25
+ export { AbortError, ChildProcess, Exception, NonZeroExitError } from "./ptree.js";
26
+ export * from "./runtime-install.js";
27
+ export * from "./sanitize-text.js";
28
+ export * from "./snowflake.js";
29
+ export * from "./stream.js";
30
+ export * from "./tab-spacing.js";
31
+ export * from "./temp.js";
32
+ export * from "./tls-fetch.js";
33
+ export * from "./type-guards.js";
34
+ export * from "./which.js";
35
35
  export declare function structuredCloneJSON<T>(value: T): T;
@@ -46,3 +46,29 @@ export declare function parseStreamingJsonThrottled<T = Record<string, unknown>>
46
46
  value: T;
47
47
  parsedLen: number;
48
48
  } | null;
49
+ /**
50
+ * Classification of a streaming buffer against strict JSON (RFC 8259):
51
+ * - `"complete"`: exactly one whole JSON value (plus surrounding whitespace).
52
+ * - `"prefix"`: a proper prefix of some valid JSON value — more bytes can
53
+ * still complete it.
54
+ * - `"invalid"`: no suffix can ever make it valid strict JSON (e.g. a raw
55
+ * control character inside a string, or a second top-level value).
56
+ */
57
+ export type JsonPrefixState = "complete" | "prefix" | "invalid";
58
+ /**
59
+ * Classify `text` as a strict-JSON value, prefix, or dead end.
60
+ *
61
+ * Providers use this to disambiguate identifierless streaming tool-call
62
+ * deltas: a chunk starting with `{` is a *new* sibling call only if the
63
+ * current call's argument buffer cannot absorb it — the buffer is already a
64
+ * complete value, already unsalvageable (lossy hosts abandon buffers
65
+ * mid-string, leaving raw control characters strict JSON forbids), or the
66
+ * concatenation would break it. Unlike {@link parseStreamingJson} this is
67
+ * deliberately strict: forgiving repair would mask exactly the corruption
68
+ * signals the caller needs.
69
+ *
70
+ * A top-level number at end-of-input classifies as `"complete"` even though
71
+ * more digits could extend it; tool-argument buffers are always objects, so
72
+ * the ambiguity is immaterial here.
73
+ */
74
+ export declare function classifyJsonPrefix(text: string): JsonPrefixState;
@@ -1,4 +1,4 @@
1
- import { type AsciiRenderOptions } from "./vendor/mermaid-ascii";
1
+ import { type AsciiRenderOptions } from "./vendor/mermaid-ascii/index.js";
2
2
  export type { AsciiRenderOptions as MermaidAsciiRenderOptions };
3
3
  export declare function renderMermaidAscii(source: string, options?: AsciiRenderOptions): string;
4
4
  export declare function renderMermaidAsciiSafe(source: string, options?: AsciiRenderOptions): string | null;
@@ -1,4 +1,4 @@
1
- import type { CharRole, AsciiTheme, ColorMode } from './types';
1
+ import type { CharRole, AsciiTheme, ColorMode } from './types.js';
2
2
  /**
3
3
  * Default ASCII theme derived from the SVG renderer's color palette.
4
4
  * Uses the same mixing ratios to maintain visual consistency.
@@ -1,4 +1,4 @@
1
- import type { Canvas, DrawingCoord, RoleCanvas, CharRole, AsciiTheme, ColorMode } from './types';
1
+ import type { Canvas, DrawingCoord, RoleCanvas, CharRole, AsciiTheme, ColorMode } from './types.js';
2
2
  /**
3
3
  * Create a blank canvas filled with spaces.
4
4
  * Dimensions are inclusive: mkCanvas(3, 2) creates a 4x3 grid (indices 0..3, 0..2).
@@ -1,4 +1,4 @@
1
- import type { AsciiConfig, AsciiTheme, ColorMode } from './types';
1
+ import type { AsciiConfig, AsciiTheme, ColorMode } from './types.js';
2
2
  /**
3
3
  * Render a Mermaid class diagram to ASCII/Unicode text.
4
4
  *
@@ -1,5 +1,5 @@
1
- import type { MermaidGraph } from '../types';
2
- import type { AsciiGraph, AsciiConfig } from './types';
1
+ import type { MermaidGraph } from '../types.js';
2
+ import type { AsciiGraph, AsciiConfig } from './types.js';
3
3
  /**
4
4
  * Convert a parsed MermaidGraph into an AsciiGraph ready for grid layout.
5
5
  *
@@ -1,4 +1,4 @@
1
- import type { Canvas, DrawingCoord, AsciiGraph, AsciiNode, AsciiEdge, AsciiSubgraph, AsciiEdgeStyle } from './types';
1
+ import type { Canvas, DrawingCoord, AsciiGraph, AsciiNode, AsciiEdge, AsciiSubgraph, AsciiEdgeStyle } from './types.js';
2
2
  /**
3
3
  * Draw a node using its shape type.
4
4
  * Returns a standalone canvas containing the rendered shape.
@@ -1,4 +1,4 @@
1
- import type { AsciiGraph, EdgeBundle, GridCoord } from './types';
1
+ import type { AsciiGraph, EdgeBundle, GridCoord } from './types.js';
2
2
  /**
3
3
  * Analyze graph edges and create bundles for parallel links.
4
4
  *
@@ -1,4 +1,4 @@
1
- import type { Direction, AsciiEdge, AsciiGraph } from './types';
1
+ import type { Direction, AsciiEdge, AsciiGraph } from './types.js';
2
2
  export declare function getOpposite(d: Direction): Direction;
3
3
  /** Compare directions by value (not reference). */
4
4
  export declare function dirEquals(a: Direction, b: Direction): boolean;
@@ -1,4 +1,4 @@
1
- import type { AsciiConfig, AsciiTheme, ColorMode } from './types';
1
+ import type { AsciiConfig, AsciiTheme, ColorMode } from './types.js';
2
2
  /**
3
3
  * Render a Mermaid ER diagram to ASCII/Unicode text.
4
4
  *
@@ -1,4 +1,4 @@
1
- import type { GridCoord, DrawingCoord, Direction, AsciiGraph, AsciiNode, AsciiSubgraph } from './types';
1
+ import type { GridCoord, DrawingCoord, Direction, AsciiGraph, AsciiNode, AsciiSubgraph } from './types.js';
2
2
  /**
3
3
  * Convert a grid coordinate to a drawing (character) coordinate.
4
4
  * Sums column widths up to the target column, and row heights up to the target row,
@@ -1,6 +1,6 @@
1
- import { detectColorMode, DEFAULT_ASCII_THEME } from './ansi';
2
- import type { AsciiTheme, ColorMode } from './types';
3
- import type { Direction } from '../types';
1
+ import { detectColorMode, DEFAULT_ASCII_THEME } from './ansi.js';
2
+ import type { AsciiTheme, ColorMode } from './types.js';
3
+ import type { Direction } from '../types.js';
4
4
  export type { AsciiTheme, ColorMode };
5
5
  export { DEFAULT_ASCII_THEME, detectColorMode };
6
6
  export interface AsciiRenderOptions {
@@ -1,4 +1,4 @@
1
- import type { Canvas } from './types';
1
+ import type { Canvas } from './types.js';
2
2
  /**
3
3
  * Split a label into lines.
4
4
  * Labels are already normalized by parsers (br tags → \n).
@@ -1,4 +1,4 @@
1
- import type { GridCoord, AsciiNode } from './types';
1
+ import type { GridCoord, AsciiNode } from './types.js';
2
2
  /**
3
3
  * Manhattan distance with a +1 penalty when both dx and dy are non-zero.
4
4
  * This encourages the pathfinder to prefer straight lines and minimize corners.
@@ -1,4 +1,4 @@
1
- import type { AsciiConfig, AsciiTheme, ColorMode } from './types';
1
+ import type { AsciiConfig, AsciiTheme, ColorMode } from './types.js';
2
2
  /**
3
3
  * Render a Mermaid sequence diagram to ASCII/Unicode text.
4
4
  *
@@ -1,4 +1,4 @@
1
- import type { ShapeRenderer } from './types';
1
+ import type { ShapeRenderer } from './types.js';
2
2
  /**
3
3
  * Circle shape renderer.
4
4
  * Uses circle markers (◯) at corners to indicate circular shape semantics.
@@ -1,4 +1,4 @@
1
- import type { AsciiNodeShape } from '../types';
1
+ import type { AsciiNodeShape } from '../types.js';
2
2
  /**
3
3
  * Corner characters for a shape in both Unicode and ASCII modes.
4
4
  */
@@ -1,4 +1,4 @@
1
- import type { ShapeRenderer } from './types';
1
+ import type { ShapeRenderer } from './types.js';
2
2
  /**
3
3
  * Diamond shape renderer.
4
4
  * Uses diamond markers (◇) at corners to indicate decision node semantics.
@@ -1,4 +1,4 @@
1
- import type { ShapeRenderer } from './types';
1
+ import type { ShapeRenderer } from './types.js';
2
2
  /**
3
3
  * Hexagon shape renderer.
4
4
  * Uses hexagon markers (⬡) at corners to indicate process node semantics.
@@ -1,5 +1,5 @@
1
- import type { AsciiNodeShape, Canvas, DrawingCoord, Direction } from '../types';
2
- import type { ShapeRenderer, ShapeDimensions, ShapeRenderOptions, ShapeRegistry } from './types';
1
+ import type { AsciiNodeShape, Canvas, DrawingCoord, Direction } from '../types.js';
2
+ import type { ShapeRenderer, ShapeDimensions, ShapeRenderOptions, ShapeRegistry } from './types.js';
3
3
  export type { ShapeRenderer, ShapeDimensions, ShapeRenderOptions, ShapeRegistry };
4
4
  /**
5
5
  * Global shape registry — maps shape types to their renderers.
@@ -1,6 +1,6 @@
1
- import type { Canvas, DrawingCoord, Direction } from '../types';
2
- import type { ShapeRenderer, ShapeDimensions, ShapeRenderOptions } from './types';
3
- import { type CornerChars } from './corners';
1
+ import type { Canvas, DrawingCoord, Direction } from '../types.js';
2
+ import type { ShapeRenderer, ShapeDimensions, ShapeRenderOptions } from './types.js';
3
+ import { type CornerChars } from './corners.js';
4
4
  /**
5
5
  * Calculate standard box dimensions for any rectangular shape.
6
6
  * Used by rectangle, circle, diamond, hexagon, etc.
@@ -1,4 +1,4 @@
1
- import type { ShapeRenderer } from './types';
1
+ import type { ShapeRenderer } from './types.js';
2
2
  /**
3
3
  * Rounded rectangle shape renderer.
4
4
  * Uses rounded corner markers (╭╮╰╯) to indicate soft edges.
@@ -1,4 +1,4 @@
1
- import type { ShapeRenderer } from './types';
1
+ import type { ShapeRenderer } from './types.js';
2
2
  /**
3
3
  * Subroutine shape renderer — double-bordered rectangle.
4
4
  * Renders as:
@@ -1,4 +1,4 @@
1
- import type { ShapeRenderer } from './types';
1
+ import type { ShapeRenderer } from './types.js';
2
2
  /**
3
3
  * Stadium (pill) shape renderer.
4
4
  *
@@ -1,4 +1,4 @@
1
- import type { ShapeRenderer } from './types';
1
+ import type { ShapeRenderer } from './types.js';
2
2
  /**
3
3
  * State start pseudo-state renderer — filled circle in rounded box.
4
4
  * Renders as:
@@ -1,4 +1,4 @@
1
- import type { Canvas, DrawingCoord, Direction, AsciiNodeShape } from '../types';
1
+ import type { Canvas, DrawingCoord, Direction, AsciiNodeShape } from '../types.js';
2
2
  /**
3
3
  * Dimensions calculated for a shape, used by layout and rendering.
4
4
  */
@@ -1,4 +1,4 @@
1
- import type { NodeShape } from '../types';
1
+ import type { NodeShape } from '../types.js';
2
2
  export type { NodeShape };
3
3
  /**
4
4
  * Shape type for ASCII rendering — maps parser shapes to ASCII renderers.
@@ -1,2 +1,2 @@
1
- import type { AsciiConfig, AsciiTheme, ColorMode } from './types';
1
+ import type { AsciiConfig, AsciiTheme, ColorMode } from './types.js';
2
2
  export declare function renderXYChartAscii(text: string, config: AsciiConfig, colorMode: ColorMode, theme: AsciiTheme): string;
@@ -1,4 +1,4 @@
1
- import type { ClassDiagram } from './types';
1
+ import type { ClassDiagram } from './types.js';
2
2
  /**
3
3
  * Parse a Mermaid class diagram.
4
4
  * Expects the first line to be "classDiagram".
@@ -1,4 +1,4 @@
1
- import type { ErDiagram } from './types';
1
+ import type { ErDiagram } from './types.js';
2
2
  /**
3
3
  * Parse a Mermaid ER diagram.
4
4
  * Expects the first line to be "erDiagram".
@@ -1 +1 @@
1
- export * from './ascii/index';
1
+ export * from './ascii/index.js';
@@ -1,4 +1,4 @@
1
- import type { MermaidGraph } from './types';
1
+ import type { MermaidGraph } from './types.js';
2
2
  /**
3
3
  * Parse Mermaid text into a logical graph structure.
4
4
  * Auto-detects diagram type (flowchart or state diagram).
@@ -1,4 +1,4 @@
1
- import type { SequenceDiagram } from './types';
1
+ import type { SequenceDiagram } from './types.js';
2
2
  /**
3
3
  * Parse a Mermaid sequence diagram.
4
4
  * Expects the first line to be "sequenceDiagram".
@@ -1,4 +1,4 @@
1
- import type { XYChart } from './types';
1
+ import type { XYChart } from './types.js';
2
2
  /**
3
3
  * Parse a Mermaid xychart-beta diagram from preprocessed lines.
4
4
  * Lines should already be trimmed and comment-stripped.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-utils",
4
- "version": "16.3.6",
4
+ "version": "16.3.7",
5
5
  "description": "Shared utilities for pi packages",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -31,7 +31,7 @@
31
31
  "fmt": "biome format --write ."
32
32
  },
33
33
  "dependencies": {
34
- "@oh-my-pi/pi-natives": "16.3.6",
34
+ "@oh-my-pi/pi-natives": "16.3.7",
35
35
  "handlebars": "^4.7.9",
36
36
  "winston": "^3.19.0",
37
37
  "winston-daily-rotate-file": "^5.0.0"
@@ -16,7 +16,9 @@ const TRY_AGAIN_PATTERN = /try again in\s+~?\s*([0-9.]+)\s*(ms|sec|s|minutes?|mi
16
16
  * by the OpenAI Codex and Google Gemini retry helpers.
17
17
  *
18
18
  * Header sources (checked in order):
19
+ * - `retry-after-ms` (milliseconds)
19
20
  * - `Retry-After` (numeric seconds, or HTTP date)
21
+ * - `x-ratelimit-reset-ms` (delta ms, or Unix epoch ms/s for large values)
20
22
  * - `x-ratelimit-reset` (Unix epoch seconds)
21
23
  * - `x-ratelimit-reset-after` (seconds)
22
24
  *
@@ -31,6 +33,11 @@ const TRY_AGAIN_PATTERN = /try again in\s+~?\s*([0-9.]+)\s*(ms|sec|s|minutes?|mi
31
33
  export function extractRetryHint(source: Response | Headers | null | undefined, body?: string): number | undefined {
32
34
  const headers = source instanceof Headers ? source : (source?.headers ?? undefined);
33
35
  if (headers) {
36
+ const retryAfterMs = headers.get("retry-after-ms");
37
+ if (retryAfterMs) {
38
+ const ms = Number(retryAfterMs);
39
+ if (Number.isFinite(ms) && ms >= 0) return ms;
40
+ }
34
41
  const retryAfter = headers.get("retry-after");
35
42
  if (retryAfter) {
36
43
  const seconds = Number(retryAfter);
@@ -38,6 +45,17 @@ export function extractRetryHint(source: Response | Headers | null | undefined,
38
45
  const parsedDate = Date.parse(retryAfter);
39
46
  if (!Number.isNaN(parsedDate)) return Math.max(0, parsedDate - Date.now());
40
47
  }
48
+ const rateLimitResetMs = headers.get("x-ratelimit-reset-ms");
49
+ if (rateLimitResetMs) {
50
+ const value = Number(rateLimitResetMs);
51
+ if (Number.isFinite(value) && value > 0) {
52
+ // > 1e12 → epoch ms; > 1e9 → epoch s; otherwise a delta in ms.
53
+ const targetMs = value > 1e12 ? value : value > 1e9 ? value * 1000 : undefined;
54
+ if (targetMs === undefined) return value;
55
+ const delta = targetMs - Date.now();
56
+ if (delta > 0) return delta;
57
+ }
58
+ }
41
59
  const rateLimitReset = headers.get("x-ratelimit-reset");
42
60
  if (rateLimitReset) {
43
61
  const resetSeconds = Number.parseInt(rateLimitReset, 10);
package/src/json-parse.ts CHANGED
@@ -610,3 +610,212 @@ export function parseStreamingJsonThrottled<T = Record<string, unknown>>(
610
610
  if (len === 0 || (lastParsedLen > 0 && len - lastParsedLen < minGrowthBytes)) return null;
611
611
  return { value: parseStreamingJson<T>(partialJson), parsedLen: len };
612
612
  }
613
+
614
+ /**
615
+ * Classification of a streaming buffer against strict JSON (RFC 8259):
616
+ * - `"complete"`: exactly one whole JSON value (plus surrounding whitespace).
617
+ * - `"prefix"`: a proper prefix of some valid JSON value — more bytes can
618
+ * still complete it.
619
+ * - `"invalid"`: no suffix can ever make it valid strict JSON (e.g. a raw
620
+ * control character inside a string, or a second top-level value).
621
+ */
622
+ export type JsonPrefixState = "complete" | "prefix" | "invalid";
623
+
624
+ /** What the strict-prefix scanner expects at the current position. */
625
+ const enum JsonExpect {
626
+ Value,
627
+ ObjKeyOrEnd,
628
+ ObjKey,
629
+ ObjColon,
630
+ ObjCommaOrEnd,
631
+ ArrValueOrEnd,
632
+ ArrCommaOrEnd,
633
+ End,
634
+ }
635
+
636
+ /**
637
+ * Classify `text` as a strict-JSON value, prefix, or dead end.
638
+ *
639
+ * Providers use this to disambiguate identifierless streaming tool-call
640
+ * deltas: a chunk starting with `{` is a *new* sibling call only if the
641
+ * current call's argument buffer cannot absorb it — the buffer is already a
642
+ * complete value, already unsalvageable (lossy hosts abandon buffers
643
+ * mid-string, leaving raw control characters strict JSON forbids), or the
644
+ * concatenation would break it. Unlike {@link parseStreamingJson} this is
645
+ * deliberately strict: forgiving repair would mask exactly the corruption
646
+ * signals the caller needs.
647
+ *
648
+ * A top-level number at end-of-input classifies as `"complete"` even though
649
+ * more digits could extend it; tool-argument buffers are always objects, so
650
+ * the ambiguity is immaterial here.
651
+ */
652
+ export function classifyJsonPrefix(text: string): JsonPrefixState {
653
+ const n = text.length;
654
+ let i = 0;
655
+ // Container stack: true = object, false = array.
656
+ const stack: boolean[] = [];
657
+ let expect = JsonExpect.Value;
658
+
659
+ /** Consume a string starting at the opening quote. 1 = ok, 0 = prefix, -1 = invalid. */
660
+ const scanString = (): 1 | 0 | -1 => {
661
+ i++; // opening quote
662
+ while (i < n) {
663
+ const c = text.charCodeAt(i);
664
+ if (c === QUOTE) {
665
+ i++;
666
+ return 1;
667
+ }
668
+ if (c === BACKSLASH) {
669
+ i++;
670
+ if (i >= n) return 0;
671
+ const e = text.charCodeAt(i);
672
+ if (e >= 128 || !VALID_ESCAPE_CHAR[e]) return -1;
673
+ i++;
674
+ if (e === U) {
675
+ for (let k = 0; k < 4; k++, i++) {
676
+ if (i >= n) return 0;
677
+ if (!isHexDigit(text.charCodeAt(i))) return -1;
678
+ }
679
+ }
680
+ continue;
681
+ }
682
+ if (c < 0x20) return -1; // raw control char: strict JSON forbids it
683
+ i++;
684
+ }
685
+ return 0;
686
+ };
687
+
688
+ /** Consume a number starting at `-` or a digit. 1 = token done, 0 = prefix, -1 = invalid. */
689
+ const scanNumber = (): 1 | 0 | -1 => {
690
+ if (text.charCodeAt(i) === 0x2d) i++; // -
691
+ if (i >= n) return 0;
692
+ let c = text.charCodeAt(i);
693
+ if (c === 0x30) {
694
+ i++; // 0: no further integer digits allowed
695
+ } else if (c >= 0x31 && c <= 0x39) {
696
+ while (i < n && text.charCodeAt(i) >= 0x30 && text.charCodeAt(i) <= 0x39) i++;
697
+ } else {
698
+ return -1;
699
+ }
700
+ if (i < n && text.charCodeAt(i) === 0x2e) {
701
+ i++; // .
702
+ if (i >= n) return 0;
703
+ if (text.charCodeAt(i) < 0x30 || text.charCodeAt(i) > 0x39) return -1;
704
+ while (i < n && text.charCodeAt(i) >= 0x30 && text.charCodeAt(i) <= 0x39) i++;
705
+ }
706
+ c = i < n ? text.charCodeAt(i) : 0;
707
+ if (c === 0x65 || c === 0x45) {
708
+ i++; // e | E
709
+ if (i < n && (text.charCodeAt(i) === 0x2b || text.charCodeAt(i) === 0x2d)) i++;
710
+ if (i >= n) return 0;
711
+ if (text.charCodeAt(i) < 0x30 || text.charCodeAt(i) > 0x39) return -1;
712
+ while (i < n && text.charCodeAt(i) >= 0x30 && text.charCodeAt(i) <= 0x39) i++;
713
+ }
714
+ return 1;
715
+ };
716
+
717
+ /** Consume `true`/`false`/`null`. 1 = done, 0 = prefix, -1 = invalid. */
718
+ const scanKeyword = (): 1 | 0 | -1 => {
719
+ for (const word of ["true", "false", "null"] as const) {
720
+ if (word.charCodeAt(0) !== text.charCodeAt(i)) continue;
721
+ const available = Math.min(word.length, n - i);
722
+ if (!word.startsWith(text.slice(i, i + available))) return -1;
723
+ i += available;
724
+ return available === word.length ? 1 : 0;
725
+ }
726
+ return -1;
727
+ };
728
+
729
+ /** A value just finished; the next expectation follows from the stack. */
730
+ const valueDone = (): JsonExpect =>
731
+ stack.length === 0
732
+ ? JsonExpect.End
733
+ : stack[stack.length - 1]
734
+ ? JsonExpect.ObjCommaOrEnd
735
+ : JsonExpect.ArrCommaOrEnd;
736
+
737
+ while (i < n) {
738
+ const c = text.charCodeAt(i);
739
+ if (isWhitespace(c)) {
740
+ i++;
741
+ continue;
742
+ }
743
+ switch (expect) {
744
+ case JsonExpect.Value:
745
+ case JsonExpect.ArrValueOrEnd: {
746
+ if (c === 0x5d && expect === JsonExpect.ArrValueOrEnd) {
747
+ stack.pop();
748
+ i++;
749
+ expect = valueDone();
750
+ break;
751
+ }
752
+ if (c === 0x7b) {
753
+ stack.push(true);
754
+ i++;
755
+ expect = JsonExpect.ObjKeyOrEnd;
756
+ break;
757
+ }
758
+ if (c === 0x5b) {
759
+ stack.push(false);
760
+ i++;
761
+ expect = JsonExpect.ArrValueOrEnd;
762
+ break;
763
+ }
764
+ let r: 1 | 0 | -1;
765
+ if (c === QUOTE) r = scanString();
766
+ else if (c === 0x2d || (c >= 0x30 && c <= 0x39)) r = scanNumber();
767
+ else if (c === 0x74 || c === 0x66 || c === 0x6e) r = scanKeyword();
768
+ else return "invalid";
769
+ if (r === -1) return "invalid";
770
+ if (r === 0) return "prefix";
771
+ expect = valueDone();
772
+ break;
773
+ }
774
+ case JsonExpect.ObjKeyOrEnd:
775
+ case JsonExpect.ObjKey: {
776
+ if (c === 0x7d && expect === JsonExpect.ObjKeyOrEnd) {
777
+ stack.pop();
778
+ i++;
779
+ expect = valueDone();
780
+ break;
781
+ }
782
+ if (c !== QUOTE) return "invalid";
783
+ const r = scanString();
784
+ if (r === -1) return "invalid";
785
+ if (r === 0) return "prefix";
786
+ expect = JsonExpect.ObjColon;
787
+ break;
788
+ }
789
+ case JsonExpect.ObjColon:
790
+ if (c !== 0x3a) return "invalid";
791
+ i++;
792
+ expect = JsonExpect.Value;
793
+ break;
794
+ case JsonExpect.ObjCommaOrEnd:
795
+ if (c === 0x7d) {
796
+ stack.pop();
797
+ i++;
798
+ expect = valueDone();
799
+ break;
800
+ }
801
+ if (c !== 0x2c) return "invalid";
802
+ i++;
803
+ expect = JsonExpect.ObjKey;
804
+ break;
805
+ case JsonExpect.ArrCommaOrEnd:
806
+ if (c === 0x5d) {
807
+ stack.pop();
808
+ i++;
809
+ expect = valueDone();
810
+ break;
811
+ }
812
+ if (c !== 0x2c) return "invalid";
813
+ i++;
814
+ expect = JsonExpect.Value;
815
+ break;
816
+ case JsonExpect.End:
817
+ return "invalid"; // trailing non-whitespace after a complete value
818
+ }
819
+ }
820
+ return expect === JsonExpect.End ? "complete" : "prefix";
821
+ }