Skip to content

Commit 512ac55

Browse files
authored
add some logging to help debug the sporadic JSONDecodeError (#7)
1 parent 5eeabae commit 512ac55

File tree

3 files changed

+409
-2
lines changed

3 files changed

+409
-2
lines changed
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
"""Utility for formatting JSON decode errors with visual context."""
2+
3+
from __future__ import annotations
4+
5+
import json
6+
from dataclasses import dataclass
7+
from typing import Any
8+
9+
10+
@dataclass
11+
class JsonErrorInfo:
12+
"""Extracted information from a JSONDecodeError."""
13+
14+
msg: str
15+
pos: int
16+
lineno: int
17+
colno: int
18+
doc: str
19+
error_line: str
20+
lines: list[str]
21+
22+
23+
def extract_json_error_info(error: json.JSONDecodeError) -> JsonErrorInfo:
24+
"""Extract structured information from a JSONDecodeError.
25+
26+
Args:
27+
error: The JSONDecodeError to extract from
28+
29+
Returns:
30+
JsonErrorInfo with extracted data
31+
"""
32+
doc = error.doc or ''
33+
lines = doc.splitlines()
34+
35+
# Get the problematic line (lineno is 1-based)
36+
error_line = ''
37+
if error.lineno >= 1 and error.lineno <= len(lines):
38+
error_line = lines[error.lineno - 1]
39+
40+
return JsonErrorInfo(
41+
msg=error.msg,
42+
pos=error.pos,
43+
lineno=error.lineno,
44+
colno=error.colno,
45+
doc=doc,
46+
error_line=error_line,
47+
lines=lines,
48+
)
49+
50+
51+
def format_json_error_visual(error_info: JsonErrorInfo) -> str:
52+
"""Format JsonErrorInfo with visual context similar to compiler errors.
53+
54+
Args:
55+
error_info: The extracted error information
56+
57+
Returns:
58+
A formatted string showing the error location with visual indicators
59+
"""
60+
if not error_info.doc:
61+
return f'{error_info.msg} at position {error_info.pos}'
62+
63+
# If we don't have valid line/col info, fall back to basic error
64+
if error_info.lineno < 1 or error_info.lineno > len(error_info.lines):
65+
return f'{error_info.msg} at position {error_info.pos}'
66+
67+
# Create the visual indicator
68+
# colno is 1-based, so we need colno-1 spaces before the caret
69+
caret_pos = max(0, error_info.colno - 1)
70+
visual_indicator = ' ' * caret_pos + '^'
71+
72+
# Build the formatted error message
73+
parts = [
74+
f'JSON parsing error, line {error_info.lineno}:',
75+
f' {error_info.error_line}',
76+
f' {visual_indicator}',
77+
f'JSONDecodeError: {error_info.msg}',
78+
]
79+
80+
return '\n'.join(parts)
81+
82+
83+
def format_json_decode_error(error: json.JSONDecodeError) -> str:
84+
"""Format a JSONDecodeError with visual context similar to compiler errors.
85+
86+
Args:
87+
error: The JSONDecodeError to format
88+
89+
Returns:
90+
A formatted string showing the error location with visual indicators
91+
"""
92+
error_info = extract_json_error_info(error)
93+
return format_json_error_visual(error_info)
94+
95+
96+
def create_json_error_context(error: json.JSONDecodeError, model_name: str, chunk_count: int) -> dict[str, Any]:
97+
"""Create structured context for JSON decode errors.
98+
99+
Args:
100+
error: The JSONDecodeError
101+
model_name: Name of the model that failed
102+
chunk_count: Number of chunks processed before failure
103+
104+
Returns:
105+
Dictionary with structured error context
106+
"""
107+
error_info = extract_json_error_info(error)
108+
formatted_error = format_json_error_visual(error_info)
109+
110+
return {
111+
'model_name': model_name,
112+
'chunk_count': chunk_count,
113+
'json_error_msg': error_info.msg,
114+
'json_error_pos': error_info.pos,
115+
'json_error_lineno': error_info.lineno,
116+
'json_error_colno': error_info.colno,
117+
'formatted_error': formatted_error,
118+
'problematic_content_preview': error_info.doc[:500] + '...'
119+
if len(error_info.doc) > 500
120+
else error_info.doc or 'N/A',
121+
}

pydantic_ai_slim/pydantic_ai/models/google.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from typing_extensions import assert_never
1414

1515
from .. import UnexpectedModelBehavior, _utils, usage
16+
from .._json_error_formatter import create_json_error_context, format_json_decode_error
1617
from .._output import OutputObjectDefinition
1718
from ..exceptions import UserError
1819
from ..messages import (
@@ -497,10 +498,34 @@ class GeminiStreamedResponse(StreamedResponse):
497498
_response: AsyncIterator[GenerateContentResponse]
498499
_timestamp: datetime
499500

501+
async def _iter_chunks_with_json_error_handling(self) -> AsyncIterator[GenerateContentResponse]:
502+
"""Iterator wrapper that provides enhanced JSON error handling."""
503+
chunk_count = 0
504+
try:
505+
async for chunk in self._response:
506+
chunk_count += 1
507+
yield chunk
508+
except json.JSONDecodeError as e:
509+
# Create rich error context with visual formatting
510+
error_context = create_json_error_context(e, self._model_name, chunk_count)
511+
formatted_json_error = format_json_decode_error(e)
512+
513+
error_msg = (
514+
'Google Gemini streaming response JSON parsing failed. '
515+
f'Model: {self._model_name}, processed {chunk_count} chunks before failure.\n\n'
516+
f'{formatted_json_error}\n\n'
517+
'This typically indicates: (1) Network interruption causing partial JSON chunks, '
518+
'(2) Google API server issues returning malformed responses, '
519+
'(3) Authentication/authorization problems causing HTML error pages instead of JSON, '
520+
'(4) Rate limiting or quota exceeded responses in non-JSON format.\n\n'
521+
f'Diagnostic context: {error_context}'
522+
)
523+
524+
raise UnexpectedModelBehavior(error_msg) from e
525+
500526
async def _get_event_iterator(self) -> AsyncIterator[ModelResponseStreamEvent]:
501-
async for chunk in self._response:
527+
async for chunk in self._iter_chunks_with_json_error_handling():
502528
self._usage = _metadata_as_usage(chunk)
503-
504529
assert chunk.candidates is not None
505530
candidate = chunk.candidates[0]
506531
if candidate.content is None:

0 commit comments

Comments
 (0)