Repository navigation
Conversation
21b3fca to
387108b
Compare
|
Rebased this fix onto current Validation on Python 3.9: focused regression |
|
Independent offline verification (AI-assisted): I extended the status check to 27 constructed cases: three outer response statuses × three message statuses × valid JSON/truncated JSON/refusal. Main passed 15/27; this head passed 27/27. The useful boundaries are that a completed message is still parsed even when the outer response differs; incomplete/in-progress messages retain their text and leave Compared main Runnable separately against each source tree: import importlib,itertools,json
from pydantic import BaseModel
from openai._types import omit
from openai._models import construct_type_unchecked
from openai.types.responses import Response
source=importlib.import_module('openai.lib._parsing._responses')
class Result(BaseModel):name:str
rows=[]
for overall,message_status,content_kind in itertools.product(['completed','incomplete','failed'],['completed','in_progress','incomplete'],['valid','truncated','refusal']):
text='{"name":"fictional"}' if content_kind=='valid' else '{"name":'
item={'type':'refusal','refusal':'fictional refusal'} if content_kind=='refusal' else {'type':'output_text','text':text,'annotations':[]}
raw=construct_type_unchecked(type_=Response,value={'id':'fictional','object':'response','created_at':0,'model':'fictional','status':overall,'output':[{'id':'fictional-message','type':'message','role':'assistant','status':message_status,'content':[item]}]})
expected_error=content_kind=='truncated' and message_status=='completed';expected_parsed={'name':'fictional'} if content_kind=='valid' and message_status=='completed' else None
try:
result=source.parse_response(text_format=Result,input_tools=omit,response=raw);parsed=result.output_parsed;actual=parsed.model_dump() if parsed else None;error=None
preserved=result.status==overall and result.output[0].status==message_status and (result.output[0].content[0].refusal=='fictional refusal' if content_kind=='refusal' else result.output[0].content[0].text==text)
except Exception as exc:error=type(exc).__name__;actual=None;preserved=None
ok=error=='ValidationError' if expected_error else error is None and actual==expected_parsed and preserved is True
rows.append({'overall':overall,'message_status':message_status,'content_kind':content_kind,'error':error,'actual_parsed':actual,'expected_parsed':expected_parsed,'preserved':preserved,'pass':ok})
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':'Constructed Responses objects; message completion controls parsing even when outer response differs. Invalid JSON in completed messages still fails; no provider emission claim.'})) |
Summary
parsedasNoneparse_responseFixes #2486.
Root cause
parse_responseattempted to parse everymessage.output_textitem with the requested structured output type. For incomplete responses, that text can contain partial JSON, so Pydantic raises a validation error instead of returning the incomplete response payload.Validation
PYTHONPATH=src uv run --frozen --python 3.9 --with pytest --with pytest-asyncio --with respx --with inline-snapshot pytest tests/lib/responses/test_responses.py::test_parse_incomplete_output_text -q -o addopts=""(1 passed)env -u HTTP_PROXY -u HTTPS_PROXY -u ALL_PROXY -u http_proxy -u https_proxy -u all_proxy PYTHONPATH=src uv run --frozen --python 3.9 --with pytest --with pytest-asyncio --with respx --with inline-snapshot pytest tests/lib/responses/test_responses.py -q -o addopts=""(8 passed)uv run --frozen --python 3.9 --with ruff ruff format --check src/openai/lib/_parsing/_responses.py tests/lib/responses/test_responses.pyuv run --frozen --python 3.9 --with ruff ruff check src/openai/lib/_parsing/_responses.py tests/lib/responses/test_responses.pyuv run --frozen --python 3.9 --with pyright==1.1.399 --with pytest --with pytest-asyncio --with respx --with inline-snapshot pyright src/openai/lib/_parsing/_responses.py tests/lib/responses/test_responses.py(0 errors)