Skip to content

fix(vector-io): return the page right before the cursor when paginating with before - #6813

Merged
mattf merged 3 commits into
ogx-ai:mainfrom
nicoloangileri:fix/vector-store-before-pagination
Nov 6, 2026
Merged

mattf merged 3 commits into
ogx-ai:mainfrom
nicoloangileri:fix/vector-store-before-pagination

Conversation

@nicoloangileri

Copy link
Copy Markdown
Contributor

What does this PR do?

before pagination on the vector store list endpoints always returns the first page of the list, not the page right before the cursor.

The listings filter out everything from the before cursor onwards and then slice [:limit], so they keep the head of what's left instead of the items closest to the cursor. With six vector stores and limit=2, paging forward with after gives [vs_1, vs_2], [vs_3, vs_4], [vs_5, vs_6], but before=vs_5 returns [vs_1, vs_2] instead of [vs_3, vs_4]. A client walking backwards jumps straight to the start. The OpenAI docs describe before=obj_foo as the way "to fetch the previous page of the list", and anthropic_list_models in core/routing_tables/models.py already does it that way (start_index = max(0, end_index - page_limit)).

When the before cursor is found, the page is now [-limit:] of the remaining items. has_more is unchanged: it was already len(remaining) > limit, which is correct for the backward direction. An unknown before cursor keeps today's behavior and returns the head of the list.

Affected listings (same pattern in each):

  • VectorIORouter.openai_list_vector_stores (the code path behind GET /v1/vector_stores)
  • OpenAIVectorStoreMixin.openai_list_vector_stores
  • OpenAIVectorStoreMixin.openai_list_files_in_vector_store (behind GET /v1/vector_stores/{id}/files)
  • OpenAIVectorStoreMixin.openai_list_files_in_vector_store_file_batch (behind GET /v1/vector_stores/{id}/file_batches/{batch_id}/files)

Not changed here: ResponsesStore.list_response_input_items has the same slicing, but test_responses_store_input_items_before_pagination asserts the current output (before="before-5", limit=2 in ascending order -> [First, Second]), so it seemed better to ask before changing that behavior. I can open a follow-up if you agree it should match.

Test Plan

New unit tests, one per fixed listing. All of them fail on main with the head-of-list page (for example assert ['vs_1', 'vs_2'] == ['vs_3', 'vs_4']) and pass with this change:

  • tests/unit/core/routers/test_vector_io.py::test_list_vector_stores_before_cursor_returns_previous_page
  • tests/unit/providers/utils/memory/test_openai_vector_store_mixin.py::TestBeforeCursorPagination (vector stores and vector store files)
  • tests/unit/providers/vector_io/test_vector_io_stores_batches.py::test_file_batch_pagination_before_cursor (sqlite_vec, faiss, pgvector, qdrant)
$ uv run pytest tests/unit/core/routers/test_vector_io.py tests/unit/providers/utils/memory/test_openai_vector_store_mixin.py tests/unit/providers/vector_io/test_vector_io_stores_batches.py -k before
# main:        7 failed, 89 deselected
# this branch: 7 passed, 89 deselected

$ uv run pytest tests/unit/
4499 passed, 28 skipped, 3 xfailed

Reproduction against the router and the mixin (mocked storage only):

== VectorIORouter.openai_list_vector_stores (order=asc, limit=2)
forward pages via after: [['vs_1', 'vs_2'], ['vs_3', 'vs_4'], ['vs_5', 'vs_6']]
main:        before='vs_5': got ['vs_1', 'vs_2'] has_more=True; expected ['vs_3', 'vs_4']
this branch: before='vs_5': got ['vs_3', 'vs_4'] has_more=True; expected ['vs_3', 'vs_4']
== OpenAIVectorStoreMixin.openai_list_files_in_vector_store (order=asc, limit=2)
forward pages via after: [['file_1', 'file_2'], ['file_3', 'file_4'], ['file_5', 'file_6']]
main:        before='file_5': got ['file_1', 'file_2'] has_more=True; expected ['file_3', 'file_4']
this branch: before='file_5': got ['file_3', 'file_4'] has_more=True; expected ['file_3', 'file_4']

pre-commit run --files <changed files> passes except mypy, which reports the same 4 errors on untouched main in this environment (src/ogx_api/filters.py:73 and three in openai_responses.py). This diff adds no new ones.

…ng with before

The vector store list endpoints dropped everything from the `before` cursor
onwards and then kept the head of what was left, so a `before` request always
returned the first page of the list instead of the page immediately preceding
the cursor. Walking back from the third page therefore jumped to the first one.
When the cursor is found, the page is now taken from the items closest to it,
matching the OpenAI "fetch the previous page" semantics and the existing
`anthropic_list_models` implementation. An unknown `before` cursor keeps its
current behavior.

The fix covers the router's vector store listing and the mixin's vector store,
vector store file and file batch listings, with unit tests for each.

Signed-off-by: Nicoló Angileri <[email protected]>

@VANDRANKI VANDRANKI left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ran this and it fixes the paging. The PR head was behind main when I looked, so I applied the PR diff (gh pr diff) cleanly onto current main (fetched 2026-10-16) and compared that with a clean main checkout. Python 3.13.3.

Property probe on OpenAIVectorStoreMixin.openai_list_vector_stores (a small mixin subclass, no mocks of the list code). For n = 1 to 8 stores, limit = 1 to 4 and both orders (64 cases), I paged forward with after=last_id to get the pages. Then I walked backwards from the first id of the last page with before=first_id until has_more was false, and compared the pages.

  • main: 24 of 64 cases do not match. Example n=3, limit=1, asc: forward pages are [vs_1] [vs_2] [vs_3], the backward walk gives [vs_1] [vs_3].
  • PR on main: 0 of 64 mismatches.

Other cursor cases (5 stores, limit 2, asc), the same on main and the PR:

  • unknown before="nope": returns vs_1, vs_2 with has_more=True (behaves like no cursor)
  • before="vs_1": empty, has_more=False
  • after="vs_2", before="vs_5": vs_3, vs_4

Tests: with the PR diff on main, the three files test_openai_vector_store_mixin.py, test_vector_io.py (router) and test_vector_io_stores_batches.py give 81 passed and 15 errors. All 15 errors are ModuleNotFoundError: faiss in the batches file, which includes the new test_file_batch_pagination_before_cursor, so I did not run that test. When I copy the PR's versions of the first two test files onto main, 3 fail (the router test and the two mixin tests) and 33 pass, so those tests catch the bug.

Limits: the batch file listing (openai_list_files_in_vector_store_file_batch) is read-only, I only read the same two line change there. My property probe covered the vector store listing in the mixin. The files in a store listing and the router listing were run only through the PR's own tests.

@nicoloangileri

Copy link
Copy Markdown
Contributor Author

Hi, a gentle ping on this one when someone has a moment. @VANDRANKI tested the fix on current main and approved it, and it still merges cleanly. I'm happy to rebase or adjust if needed. Thanks!

@nicoloangileri

Copy link
Copy Markdown
Contributor Author

Hi @skamenan7, would you have time to review this one? before pagination on the vector store list endpoints returns the first page instead of the page right before the cursor. @VANDRANKI tested the fix and approved it, and it still merges cleanly. Happy to rebase or adjust anything. Thanks!

@mattf mattf left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nicoloangileri thank you

@mattf
mattf added this pull request to the merge queue Nov 6, 2026
Merged via the queue into ogx-ai:main with commit 3cb4030 Nov 6, 2026
80 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants