AI & Agent Dev Bug Sandbox logo
AI & Agent Dev Bug Sandbox
Back to Radar

Optimize Parse_partial_json To Avoid O(N^2) Re-Parsing During Large Streaming Tool Calls

Streaming large tool call arguments through (prompt | model).stream() or with_structured_output(...).stream() triggers repeated parse_partial_json calls on every AIMessageChunk addition, causing quadratic CPU time as arguments grow.

mediumConfidence 85%Langchain-CoreAffected V1.6.5

Origin Analysis

AIMessageChunk.__add__ invokes parse_partial_json on the accumulated tool call args for each new chunk. The current parse_partial_json walks backward one character at a time and retries json.loads on each prefix, causing O(n^2) behavior for large argument strings.
Run the provided ToolCallStreamer with sizes 5000, 10000, 20000 and compare model.stream() vs (prompt | model).stream(). Observe model.stream() stays fast while the chained stream degrades superlinearly (e.g. 0.11s vs 2.06s for 20k chars).

Fixing Code Block

import json import re from typing import Any _STRING_RE = re.compile(r'"(?:[^"\\]|\\.)*"') def parse_partial_json(s: str, *, strict: bool = False) -> Any: new_s = s.strip() if new_s.startswith('[') and not new_s.endswith(']'): new_s += ']' if new_s.startswith('{') and not new_s.endswith('}'): new_s += '}' try: return json.loads(new_s, strict=strict) except json.JSONDecodeError as e: error_pos = e.pos - 1 for match in _STRING_RE.finditer(new_s): if match.start() <= error_pos < match.end(): error_pos = match.start() break candidate = new_s[:error_pos].rstrip() try: return json.loads(candidate, strict=strict) except json.JSONDecodeError: pass for i in range(error_pos, 0, -1): candidate = new_s[:i].rstrip() try: return json.loads(candidate, strict=strict) except json.JSONDecodeError: continue raise
The hotfix uses json.JSONDecodeError.pos to jump to the first invalid character instead of trimming from the end. It also scans string literals with a compiled regex so an unterminated string is cut at its opening quote, preserving the longest valid prefix. The fallback loop only examines the small prefix before the error position, avoiding O(n^2) retries.

Edge Case Audit

This changes the internal trimming strategy; in rare malformed JSON cases the fallback may still degrade or return a slightly different partial object than the legacy loop. Add unit tests for partial objects, arrays, escaped quotes, and unterminated strings before deploying. If regressions appear, revert to the previous character-trim implementation. The regex is module-level and immutable, so no threading concerns, but performance for deeply nested or extremely large JSON may still require further chunk-merging optimizations.

Ecosystem Topology