Skip to main content

openai_compat/
pagination.rs

1//! Cursor pagination, mirroring `pagination.py::SyncCursorPage`: pages carry
2//! `data` + `has_more`, and the next page is requested with
3//! `after = <last item id>`.
4
5use serde::de::DeserializeOwned;
6use serde::Deserialize;
7
8use crate::client::Client;
9use crate::error::OpenAIError;
10use crate::request::RequestOptions;
11
12/// A single page of results from a list endpoint.
13#[derive(Debug, Clone, Deserialize)]
14#[non_exhaustive]
15pub struct List<T> {
16    pub data: Vec<T>,
17    #[serde(default)]
18    pub object: Option<String>,
19    #[serde(default)]
20    pub has_more: Option<bool>,
21    #[serde(default)]
22    pub first_id: Option<String>,
23    #[serde(default)]
24    pub last_id: Option<String>,
25}
26
27impl<T> List<T> {
28    /// Whether another page may exist, mirroring
29    /// `SyncCursorPage::has_next_page`.
30    pub fn has_next_page(&self) -> bool {
31        !self.data.is_empty() && self.has_more != Some(false)
32    }
33}
34
35/// Items that expose an `id` usable as an `after` cursor.
36pub trait HasId {
37    fn id(&self) -> Option<&str>;
38}
39
40/// Build the cursor query pairs (`after`/`before`/`limit`/`order`) shared by
41/// list endpoints; module-specific params are appended by the caller.
42pub(crate) fn cursor_query(
43    after: Option<&str>,
44    before: Option<&str>,
45    limit: Option<u32>,
46    order: Option<&str>,
47) -> Vec<(String, String)> {
48    let mut query = Vec::new();
49    if let Some(after) = after {
50        query.push(("after".to_string(), after.to_string()));
51    }
52    if let Some(before) = before {
53        query.push(("before".to_string(), before.to_string()));
54    }
55    if let Some(limit) = limit {
56        query.push(("limit".to_string(), limit.to_string()));
57    }
58    if let Some(order) = order {
59        query.push(("order".to_string(), order.to_string()));
60    }
61    query
62}
63
64impl Client {
65    /// Fetch every item from a cursor-paginated list endpoint, following
66    /// `after` cursors until `has_more` is false or a page is empty.
67    pub(crate) async fn paginate_all<T: HasId + DeserializeOwned>(
68        &self,
69        path: &str,
70        query: Vec<(String, String)>,
71    ) -> Result<Vec<T>, OpenAIError> {
72        self.paginate_all_with_headers(path, query, Vec::new())
73            .await
74    }
75
76    /// [`Client::paginate_all`] with extra headers sent on every page request
77    /// (used by beta endpoints that require `OpenAI-Beta: assistants=v2`).
78    pub(crate) async fn paginate_all_with_headers<T: HasId + DeserializeOwned>(
79        &self,
80        path: &str,
81        mut query: Vec<(String, String)>,
82        extra_headers: Vec<(String, String)>,
83    ) -> Result<Vec<T>, OpenAIError> {
84        let mut items: Vec<T> = Vec::new();
85        let mut previous_cursor: Option<String> = None;
86        loop {
87            let options = RequestOptions {
88                query: query.clone(),
89                extra_headers: extra_headers.clone(),
90                ..RequestOptions::default()
91            };
92            let page: List<T> = self.execute(reqwest::Method::GET, path, options).await?;
93            let has_next = page.has_next_page();
94            items.extend(page.data);
95
96            if !has_next {
97                return Ok(items);
98            }
99            let Some(cursor) = items.last().and_then(HasId::id).map(str::to_owned) else {
100                return Ok(items);
101            };
102            // Guard against providers that ignore `after` (or omit
103            // `has_more`): a cursor that does not advance would loop forever.
104            if previous_cursor.as_deref() == Some(cursor.as_str()) {
105                return Ok(items);
106            }
107            previous_cursor = Some(cursor.clone());
108            query.retain(|(k, _)| k != "after");
109            query.push(("after".into(), cursor));
110        }
111    }
112}