Attempt to parse a potentially incomplete JSON string into the most complete valid partial object possible. Strategy: try parsing as-is first. If that fails, progressively close open braces/brackets and try again. This handles the common case where the LLM has output `{"name": "foo", "items": [1, 2` — we close it to get a partial.
(text: &str)
| 492 | /// braces/brackets and try again. This handles the common case where the LLM |
| 493 | /// has output `{"name": "foo", "items": [1, 2` — we close it to get a partial. |
| 494 | fn try_parse_partial_json(text: &str) -> Option<Value> { |
| 495 | let trimmed = text.trim(); |
| 496 | if trimmed.is_empty() { |
| 497 | return None; |
| 498 | } |
| 499 | |
| 500 | // Fast path: already valid |
| 501 | if let Ok(v) = serde_json::from_str::<Value>(trimmed) { |
| 502 | if v.is_object() || v.is_array() { |
| 503 | return Some(v); |
| 504 | } |
| 505 | } |
| 506 | |
| 507 | // Count unclosed brackets/braces (respecting strings) |
| 508 | let mut closers = Vec::new(); |
| 509 | let mut in_string = false; |
| 510 | let mut escape_next = false; |
| 511 | // Track if we're mid-value (after a colon or comma, before the value is complete) |
| 512 | let mut last_significant: Option<u8> = None; |
| 513 | |
| 514 | for &b in trimmed.as_bytes() { |
| 515 | if escape_next { |
| 516 | escape_next = false; |
| 517 | continue; |
| 518 | } |
| 519 | match b { |
| 520 | b'\\' if in_string => { |
| 521 | escape_next = true; |
| 522 | } |
| 523 | b'"' => { |
| 524 | in_string = !in_string; |
| 525 | if !in_string { |
| 526 | last_significant = Some(b'"'); |
| 527 | } |
| 528 | } |
| 529 | _ if in_string => {} |
| 530 | b'{' => { |
| 531 | closers.push(b'}'); |
| 532 | last_significant = Some(b'{'); |
| 533 | } |
| 534 | b'[' => { |
| 535 | closers.push(b']'); |
| 536 | last_significant = Some(b'['); |
| 537 | } |
| 538 | b'}' | b']' => { |
| 539 | closers.pop(); |
| 540 | last_significant = Some(b); |
| 541 | } |
| 542 | b':' | b',' => { |
| 543 | last_significant = Some(b); |
| 544 | } |
| 545 | b if !b.is_ascii_whitespace() => { |
| 546 | last_significant = Some(b); |
| 547 | } |
| 548 | _ => {} |
| 549 | } |
| 550 | } |
| 551 |