Repository navigation
Conversation
BasePage.has_next_page() short-circuits on an empty `data` list, but the cursor for the envelope-paginated page classes comes from `last_id` / `next`, which their next_page_info() reads without looking at `data`. A page that the server marks `has_more: true` with a usable cursor and no items therefore reported has_next_page() False while next_page_info() returned a PageInfo and get_next_page() would fetch it, so iter_pages() and auto_paging_iter() stopped early and dropped the remaining pages. Delegate has_next_page() to next_page_info() for those six classes. The `has_more: false` short-circuit and SyncCursorPage/AsyncCursorPage, whose cursor really is `data[-1].id`, are unchanged.
|
Independent offline verification (AI-assisted): I independently checked the six sync/async envelope-cursor page classes across empty/populated data, absent/empty/nonempty cursor, and absent/false/true Compared main Reproducer, runnable separately against each source tree: import json,itertools
from pydantic import BaseModel
import openai.pagination as source
class Item(BaseModel):id:str
rows=[]
for name in ['SyncConversationCursorPage','AsyncConversationCursorPage','SyncNextCursorPage','AsyncNextCursorPage','SyncTokenPage','AsyncTokenPage']:
cls=getattr(source,name)[Item];field='last_id' if 'Conversation' in name else 'next'
for populated,cursor,has_more in itertools.product([False,True],[None,'','fictional-cursor'],[None,False,True]):
page=cls(data=[Item(id='fictional')] if populated else [],has_more=has_more,**{field:cursor})
expected=has_more is not False and bool(cursor);actual=page.has_next_page()
rows.append({'class':name,'populated':populated,'cursor':cursor,'has_more':has_more,'expected_has_next':expected,'actual_has_next':actual,'pass':actual==expected})
for name in ['SyncCursorPage','AsyncCursorPage']:
for populated in [False,True]:
page=getattr(source,name)[Item](data=[Item(id='fictional')] if populated else [])
rows.append({'class':name,'populated':populated,'expected_has_next':populated,'actual_has_next':page.has_next_page(),'pass':page.has_next_page()==populated})
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 page objects and cursor/has_more policy, not transport, actual iter_pages traversal or provider contract.'})) |
src/openai/pagination.py).Changes being requested
The bug
For the six envelope-cursor page classes,
has_next_page()can returnFalsewhilenext_page_info()on the same object returns a usablePageInfo. A response withdata: [],has_more: trueand a cursor (last_id/next) therefore ends the iteration even though there is a next page:iter_pages()andauto_paging_iter()build onhas_next_page(), so the remaining pages are dropped without any error. Affected classes:Sync/AsyncConversationCursorPage,Sync/AsyncNextCursorPage,Sync/AsyncTokenPage— i.e.conversations.items.list,videos.list, the adminroles/groupslistings andbeta.agents.environments.files.list.Cause
Those six overrides end with
return super().has_next_page(), andBasePage.has_next_page()(src/openai/_base_client.py:205-209) short-circuits whenself._get_page_items()is empty. That is correct forSyncCursorPage/AsyncCursorPage, whose cursor is literallydata[-1].idand thus cannot exist without items — but the envelope classes read their cursor from a response field, so the emptiness ofdatasays nothing about whether the server has more to give.Change
For exactly those six classes, delegate to the cursor itself:
The
has_more is Falseshort-circuit above it is kept, so a server that says it is done still wins, and no-cursor responses still reportFalse.SyncCursorPage/AsyncCursorPageandBasePageare untouched.The change
For the six envelope-cursor classes,
has_next_page()now asks the cursor instead of the row list:The
has_more is Falseshort-circuit above it is kept, so a server that says it is done still wins, and no-cursor responses still reportFalse.SyncCursorPage/AsyncCursorPageandBasePageare untouched.Additional context & links
Testing
New
tests/test_pagination_envelope_cursor.pycovers each of the six classes plusSyncCursorPage/AsyncCursorPageas controls, and aniter_pages()case that stops only when the cursor is exhausted:69a2c1db:7 failed, 19 passed— the seven failures are the six empty-page assertions and theiter_pages()case, withAssertionError: assert False is Trueonhas_next_page().26 passed.Neighbouring streaming/pagination files as a regression control (
tests/test_streaming.py,tests/lib/test_streaming_deltas.py,tests/lib/chat/test_completions_streaming.py,tests/lib/chat/test_stream_moderation.py,tests/lib/streaming/agents/test_streams.py), same command:13 failed, 139 passed, 12 skippedat head and identically13 failed, 139 passed, 12 skippedon pristine69a2c1db. Those 13 need live credentials, so they fail the same before and after; no test changed status because of this patch.ruff check→All checks passed;ruff format --check→2 files already formatted. All numbers above are local runs — CI on a fork PR still needs a maintainer to approve the workflow runs.src/openai/pagination.pycarries no# File generated from our OpenAPI spec by Castironheader (comparesrc/openai/types/chat/chat_completion.py, which does), so I read it as the hand-written pagination layer rather than generator output. If the cursor logic in fact lives in a template I cannot see, the same one-line change belongs there and I would defer to that..castiron-ratchet.jsonchange: this replaces 6 existing lines and adds no new custom-code surface beyond the test file.BasePage.has_next_page()shape (fix(pagination): continue past an empty page that carries a cursor anthropics/anthropic-sdk-python#1943). Different repository and different code — mentioning it only so you can see the pattern is not specific to one SDK.