Repository navigation
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d60d06b1b6
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| def _is_json_parse_error(exc: pydantic.ValidationError) -> bool: | ||
| return any("json" in str(error.get("type", "")).lower() for error in exc.errors()) |
There was a problem hiding this comment.
Distinguish top-level JSON parse failures
When a structured model contains a JSON-validated field, e.g. a Pydantic Json[...] field, Pydantic reports invalid field contents with a json_* error type even though the top-level response text parsed successfully. This helper treats those schema validation failures as truncation and returns None from response.output_text.done, so an incomplete stream with complete-but-invalid structured text can silently skip the validation error that this change intended to preserve. Check that the JSON error is for the top-level parse (for example by inspecting the error location) before suppressing it.
Useful? React with 👍 / 👎.
|
Gentle nudge. The failure mode is still live: when streamed text arrives in fragments, the eager parse throws on incomplete JSON even though more chunks are on the way. The patch defers parsing until the text is complete, which matches how the non-streaming path already behaves. Tests cover the split-mid-string and split-mid-unicode cases. CI green. |
d60d06b to
31298b2
Compare
|
Additional validation boundary (AI-assisted), reproduced at head
I checked eight constructed stream cases using the actual A separate boundary control: after Python 3.14.7 / Pydantic 2.13.5, isolated exact source trees and shared dependencies plus legacy HTTPX compatibility dependencies; network-denied. These are fictional constructed event iterators, not SSE/network tests or a claim that the strict API accepts a nested Pydantic Json-field schema. Reproducer: import importlib,json
from types import SimpleNamespace
from pydantic import BaseModel,Json
from openai._models import construct_type_unchecked
from openai.types.responses import ResponseCreatedEvent,ResponseOutputItemAddedEvent,ResponseContentPartAddedEvent,ResponseTextDoneEvent,ResponseIncompleteEvent,ResponseCompletedEvent
from openai.lib.streaming.responses._responses import ResponseStream
source=importlib.import_module('openai.lib.streaming.responses._responses')
class Result(BaseModel):answer:str
class Nested(BaseModel):
answer:str
payload:Json[list[int]]
def response(status,output):return {'id':'fictional','object':'response','created_at':0,'model':'fictional','status':status,'output':output,'parallel_tool_calls':True,'tool_choice':'auto','tools':[]}
def message(text,status):return {'id':'fictional-message','type':'message','role':'assistant','status':status,'content':[{'type':'output_text','text':text,'annotations':[],'logprobs':[]}]}
class Raw:
def __init__(self,events):self.events=events;self.response=SimpleNamespace(close=lambda:None);self.last_type=None
def __iter__(self):
for event in self.events:self.last_type=event.type;yield event
cases=[('truncated_colon',Result,'{"answer":','incomplete','final','RuntimeError'),('truncated_unicode',Result,'{"answer":"\\u12','incomplete','final','RuntimeError'),('valid_completed',Result,'{"answer":"fictional"}','completed','success',None),('schema_missing_field',Result,'{}','completed','response.output_text.done','ValidationError'),('nested_json_invalid',Nested,'{"answer":"fictional","payload":"not-json"}','completed','response.output_text.done','ValidationError'),('nested_json_wrong_type',Nested,'{"answer":"fictional","payload":42}','completed','response.output_text.done','ValidationError'),('nested_value_invalid',Nested,'{"answer":"fictional","payload":"[1,\\"wrong\\"]"}','completed','response.output_text.done','ValidationError'),('nested_valid',Nested,'{"answer":"fictional","payload":"[1,2]"}','completed','success',None)]
rows=[]
for name,model,text,status,expected_stage,expected_error in cases:
payloads=[(ResponseCreatedEvent,{'type':'response.created','sequence_number':0,'response':response('in_progress',[])}),(ResponseOutputItemAddedEvent,{'type':'response.output_item.added','sequence_number':1,'output_index':0,'item':message('','in_progress')}),(ResponseContentPartAddedEvent,{'type':'response.content_part.added','sequence_number':2,'output_index':0,'content_index':0,'item_id':'fictional-message','part':{'type':'output_text','text':'','annotations':[],'logprobs':[]}}),(ResponseTextDoneEvent,{'type':'response.output_text.done','sequence_number':3,'output_index':0,'content_index':0,'item_id':'fictional-message','text':text,'logprobs':[]}),(ResponseIncompleteEvent if status=='incomplete' else ResponseCompletedEvent,{'type':'response.'+status,'sequence_number':4,'response':response(status,[message(text,status)])})]
raw=Raw([construct_type_unchecked(type_=t,value=p) for t,p in payloads]);stream=ResponseStream(raw_stream=raw,text_format=model,input_tools=[],starting_after=None);seen=[];stage='iteration';error=None;done_parsed=None;final_parsed=None;error_locations=[]
try:
for event in stream:
seen.append(event.type)
if event.type=='response.output_text.done':done_parsed=event.parsed.model_dump() if event.parsed else None
stage='final';final=stream.get_final_response();final_parsed=final.output_parsed.model_dump() if final.output_parsed else None;stage='success'
except Exception as exc:
if stage=='iteration':stage=raw.last_type
error=type(exc).__name__
if hasattr(exc,'errors'):error_locations=[{'type':e['type'],'loc':e['loc']} for e in exc.errors()]
rows.append({'case':name,'stage':stage,'error':error,'error_locations':error_locations,'seen_events':seen,'done_parsed':done_parsed,'final_parsed':final_parsed,'expected_stage':expected_stage,'expected_error':expected_error,'pass':stage==expected_stage and error==expected_error})
print(json.dumps({'source_module':source.__file__,'cases':rows,'passed':sum(r['pass'] for r in rows),'failed':sum(not r['pass'] for r in rows),'scope':'Actual ResponseStream over constructed event iterator; no SSE transport/provider acceptance claim for nested Pydantic Json fields. Final accessor requires response.completed: incomplete RuntimeError is an existing contract control.'})) |
31298b2 to
4eff760
Compare
|
Good catch, and thanks for the harness. Reproduced it exactly as described: on the old head your eight cases go 6/8, with The discriminator was too coarse. Also rebased onto current main (4e152cd). The branch predates the Verification: your harness is 8/8 on this head (the two nested cases went from deferred to raising |
Fixes #3263.
Summary
response.output_text.doneso an incomplete structured stream can reach the terminalresponse.incompleteeventTo verify
PYTHONPATH=src python -m pytest tests/lib/responses/test_responses.py -qPYTHONPATH=src python -m ruff check src/openai/lib/streaming/responses/_responses.py tests/lib/responses/test_responses.pyPYTHONPATH=src python -m ruff format --check src/openai/lib/streaming/responses/_responses.py tests/lib/responses/test_responses.pygit diff --check