Repository navigation
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f33b811813
ℹ️ About Codex in GitHub
Your team has set up Codex to 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 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| # mirror the streaming accumulator (`ChatCompletionStreamState`), which guards these | ||
| # errors with `has_parseable_input`, and leave the completion untouched — matching | ||
| # `chat.completions.create()`. | ||
| raise_on_incomplete = has_parseable_input(response_format=response_format, input_tools=input_tools) |
There was a problem hiding this comment.
Preserve finish errors for one-shot tool iterables
When tools is a generator (which the public Iterable annotation permits), both sync and async chat.completions.parse() first consume it in _validate_input_tools (completions.py:179 and completions.py:1790) and then pass the exhausted iterator here. Consequently input_tools becomes empty and raise_on_incomplete is false, so a request that supplied a strict parseable tool but finishes with length or content_filter now silently returns an incomplete completion instead of raising. Materialize the tools before validation or otherwise preserve their parseability state.
Useful? React with 👍 / 👎.
|
Independent offline verification (AI-assisted): I checked seven input modes (plain, text/json-object/JSON-schema format dictionaries, Pydantic model, strict tool, non-strict tool) against The added boundary evidence is that both terminal errors remain intact for Pydantic models and strict tools; non-strict tools and format dictionaries do not activate Pydantic parsing. Successful plain paths preserve content and finish reason. Format dictionaries here are helper-policy controls, not claims about provider-supported schemas. Python 3.14.7 with shared existing dependencies plus isolated legacy HTTPX compatibility dependencies; not a historical lockfile matrix. Network-denied, fictional objects only, no provider/client calls. Focused evidence only, not full SDK or merge approval. Runnable separately against each source tree: import importlib,itertools,json
from pydantic import BaseModel
from openai._types import omit
from openai.types.chat import ChatCompletion,ChatCompletionChunk
from openai.lib.streaming.chat import ChatCompletionStreamState
source=importlib.import_module('openai.lib._parsing._completions')
class Result(BaseModel):name:str
fn={'type':'function','function':{'name':'fictional_tool','strict':True,'parameters':{'type':'object','properties':{},'additionalProperties':False}}}
loose={'type':'function','function':{**fn['function'],'strict':False}}
variants=[('plain',omit,[],False),('text',{'type':'text'},[],False),('json_object',{'type':'json_object'},[],False),('json_schema',{'type':'json_schema','json_schema':{'name':'fictional','schema':{'type':'object'},'strict':True}},[],False),('model',Result,[],True),('strict_tool',omit,[fn],True),('nonstrict_tool',omit,[loose],False)]
rows=[]
for (name,format_,tools,parseable),reason,path in itertools.product(variants,['stop','length','content_filter'],['direct','stream_final']):
expected=('LengthFinishReasonError' if reason=='length' else 'ContentFilterFinishReasonError') if parseable and reason!='stop' else None
try:
if path=='direct':
completion=ChatCompletion(id='fictional',object='chat.completion',created=0,model='fictional',choices=[{'index':0,'message':{'role':'assistant','content':'{"name":"fictional"}'},'finish_reason':reason}])
parsed=source.parse_chat_completion(response_format=format_,input_tools=tools,chat_completion=completion)
else:
state=ChatCompletionStreamState(response_format=format_,input_tools=tools)
for delta,finish in [({'role':'assistant','content':'{"name":"fictional"}'},None),({},reason)]:
chunk=ChatCompletionChunk(id='fictional',object='chat.completion.chunk',created=0,model='fictional',choices=[{'index':0,'delta':delta,'finish_reason':finish}]);list(state.handle_chunk(chunk))
parsed=state.get_final_completion()
actual=None;preserved=parsed.choices[0].finish_reason==reason and parsed.choices[0].message.content=='{"name":"fictional"}'
except Exception as exc:actual=type(exc).__name__;preserved=None
rows.append({'variant':name,'finish_reason':reason,'path':path,'expected_error':expected,'actual_error':actual,'content_and_finish_preserved':preserved,'pass':actual==expected and (expected is not None or preserved is True)})
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 parser and stream state with fictional objects; response-format dicts are non-Pydantic parse controls, not provider support claims.'})) |
`parse_chat_completion` raised `LengthFinishReasonError` / `ContentFilterFinishReasonError` for any choice whose `finish_reason` was `length` / `content_filter`, regardless of whether structured-output parsing was requested. This is inconsistent with the streaming accumulator (`ChatCompletionStreamState._accumulate_chunk`), which only raises these when `has_parseable_input` is true. As a result a plain `client.chat.completions.stream(...)` (no `response_format` and no parseable tools) that stops at the token limit would iterate to completion without raising, but then raise `LengthFinishReasonError` from `get_final_completion()` — even though `client.chat.completions.create()` never raises for the same response and the length/content-filter restriction is documented only for `.parse()` (structured outputs). Guard both raises with `has_parseable_input`, mirroring the accumulator, so the errors are only raised when there is actually something to parse.
f33b811 to
226630f
Compare
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 226630fe57
ℹ️ About Codex in GitHub
Your team has set up Codex to 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 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| # mirror the streaming accumulator (`ChatCompletionStreamState`), which guards these | ||
| # errors with `has_parseable_input`, and leave the completion untouched — matching | ||
| # `chat.completions.create()`. | ||
| raise_on_incomplete = has_parseable_input(response_format=response_format, input_tools=input_tools) |
There was a problem hiding this comment.
Update the documented finish-reason contract
When chat.completions.parse() is called without a response format or tools and the API returns length or content_filter, this guard now returns the completion instead of raising. helpers.md:142-147 still promises unconditionally that .parse() raises LengthFinishReasonError or ContentFilterFinishReasonError, so callers relying on that documented failure signal can silently accept truncated or filtered output; condition the documentation on parseable input (or retain the old behavior for .parse()).
Useful? React with 👍 / 👎.
Changes being requested
parse_chat_completion(src/openai/lib/_parsing/_completions.py) raisesLengthFinishReasonError/ContentFilterFinishReasonErrorfor any choice whosefinish_reasonislength/content_filter, regardless of whether structured-outputparsing was actually requested.
That is inconsistent with the streaming accumulator
(
ChatCompletionStreamState._accumulate_chunk), which only raises these errors whenhas_parseable_input(...)is true:Because
get_final_completion()funnels throughparse_chat_completion, a plainclient.chat.completions.stream(...)— noresponse_format, no parseable tools — thatstops at the token limit iterates to completion without raising (the accumulator
suppresses it), but then raises
LengthFinishReasonErrorfromget_final_completion():This is surprising for two reasons:
client.chat.completions.create()never raises for thesame response — it just returns a completion with
finish_reason="length".helpers.mddocuments the length / content-filter raise only under "Differencesfrom
.create()" forchat.completions.parse()(structured outputs); it is not adocumented behavior of a plain
.stream().Fix
Compute
has_parseable_input(...)once inparse_chat_completionand guard both raiseswith it, mirroring the accumulator. Behavior is unchanged whenever there is something to
parse:
.parse(response_format=Model)atlength.parse(tools=[strict fn])atlength.stream(response_format=Model)atlength.stream()→get_final_completion()atlength.parse()(no format/tools) atlength.create())has_parseable_inputis already defined in the same module, so no new import is needed.Tests
tests/lib/chat/test_completions_streaming.py:test_non_parse_stream_terminal_finish_reason_does_not_raise[length|content_filter]—drives
ChatCompletionStreamState(no network) with a content chunk + a terminalfinish chunk; asserts
get_final_completion()returns the accumulated completion withthe content and
finish_reasonpreserved. Fails onmainfor both parameters.test_parse_stream_length_finish_still_raises— with aresponse_format, the terminallengthchunk still raisesLengthFinishReasonError(guards the unchanged path).Validation
Run in an isolated worktree off
upstream/main@0c09a3fe(Python 3.9, pydantic 2.12.5,pyright 1.1.399):
pytest tests/lib/chat tests/lib/test_pydantic.py -o addopts=""→ all pass; the threenew tests fail on
mainfor the right reason and pass after the change.pytest tests/lib -o addopts=""→ 258 passed. The single failure(
test_bedrock_auth_conformance.py::test_retry_signing_fixture) is pre-existing andnetwork-dependent — it fails identically on unmodified
upstream/mainand is unrelatedto this change.
ruff check+ruff format --checkon both files → clean.pyrightonsrc/openai/lib/_parsing/_completions.py→ 0 errors; the test file'sstrict-mode error count is unchanged by this PR.
No API key, paid call, or live service was used.
Additional context & links
unconditional
parse_chat_completionraises both landed together in the structuredoutputs commit, so the two paths have disagreed since then.