Skip to content

Commit d251fcb

Browse files
poog26benfrank241
andauthored
Fix _strip_code_fences truncating JSON when content contains inner backticks (#2563)
* Fix _strip_code_fences truncating JSON when content contains inner backticks The old implementation used content.split('')[0] to strip markdown code fences from LLM responses. This finds the FIRST occurrence of ''' after the opening fence — so when the extracted JSON itself contains literal triple-backtick characters (e.g. facts about code fence formatting), the split matches those inner backticks and truncates the JSON mid-string. Replace with line-based fence detection that only matches fences at line boundaries per the markdown spec. Inner backticks inside JSON string values are preserved since they aren't at line boundaries. * test(llm): cover inner-backtick fence stripping regression --------- Co-authored-by: Ben <ben.bartholomew@vectorize.io>
1 parent e839c65 commit d251fcb

2 files changed

Lines changed: 33 additions & 5 deletions

File tree

hindsight-api-slim/hindsight_api/engine/providers/openai_compatible_llm.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -78,12 +78,24 @@ def _strip_code_fences(content: str) -> str:
7878
"""
7979
if "```" not in content:
8080
return content
81-
try:
82-
if "```json" in content:
83-
return content.split("```json")[1].split("```")[0].strip()
84-
return content.split("```")[1].split("```")[0].strip()
85-
except (IndexError, ValueError):
81+
lines = content.split("\n")
82+
# Find first line that starts a code fence (``` optionally followed by language)
83+
fence_start = None
84+
for i, line in enumerate(lines):
85+
if line.startswith("```"):
86+
fence_start = i
87+
break
88+
if fence_start is None:
89+
return content
90+
# Find matching closing fence (``` alone or with trailing whitespace)
91+
fence_end = None
92+
for j in range(fence_start + 1, len(lines)):
93+
if lines[j].strip() == "```":
94+
fence_end = j
95+
break
96+
if fence_end is None:
8697
return content
98+
return "\n".join(lines[fence_start + 1 : fence_end]).strip()
8799

88100

89101
# Reasoning/thinking tags emitted by extended-thinking models. Some providers

hindsight-api-slim/tests/test_strip_code_fences.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,22 @@ def test_fence_with_leading_whitespace(self):
3636
result = _strip_code_fences(content)
3737
assert '{"facts": []}' in result
3838

39+
def test_inner_backticks_preserved(self):
40+
"""Inner triple-backticks inside a JSON string value must not truncate the JSON.
41+
42+
Regression for the fact-extraction case where an extracted fact describes
43+
code-fence behavior, so the JSON payload itself contains a literal
44+
```` ```json ```` — the old split-based stripper matched that inner
45+
occurrence and cut the JSON mid-string.
46+
"""
47+
import json
48+
49+
content = '```json\n{"facts": [{"what": "the model wraps output in ```json fences"}]}\n```'
50+
result = _strip_code_fences(content)
51+
assert result == '{"facts": [{"what": "the model wraps output in ```json fences"}]}'
52+
parsed = json.loads(result)
53+
assert parsed["facts"][0]["what"] == "the model wraps output in ```json fences"
54+
3955
def test_no_fences_no_change(self):
4056
"""Content without any backticks passes through."""
4157
content = "Just some text without fences"

0 commit comments

Comments
 (0)