gproxy_protocol/adapt/models/
collect.rs1use super::{ModelDirectory, ModelListError, ModelListFailure, ModelListLimits, query};
2use crate::{
3 WireRequest,
4 adapt::{JsonInvocation, invoke_empty},
5 capability::Upstream,
6 codec,
7 transform::{TransformError, TransformErrorKind},
8 wire::{DeclaredFields, claude::models as c, gemini::models as g, openai::models as o},
9};
10use serde::{Serialize, de::DeserializeOwned};
11use std::collections::BTreeSet;
12
13struct Progress {
14 calls: usize,
15 models: usize,
16 bytes: u64,
17 ids: BTreeSet<String>,
18 limits: ModelListLimits,
19}
20
21impl Progress {
22 fn new(request: &WireRequest<()>, limits: ModelListLimits) -> Result<Self, ModelListError> {
23 query::template(request)?;
24
25 Ok(Self {
26 calls: 0,
27 models: 0,
28 bytes: 0,
29 ids: BTreeSet::new(),
30 limits,
31 })
32 }
33 fn error(&self, error: TransformError) -> ModelListError {
34 ModelListError {
35 completed_calls: self.calls,
36 completed_models: self.models,
37 failure: ModelListFailure::Transform(error),
38 }
39 }
40 fn invalid(&self, path: &str, detail: &str) -> ModelListError {
41 self.error(TransformError::new(
42 TransformErrorKind::InvalidResult,
43 path,
44 detail,
45 ))
46 }
47 async fn page<U: Upstream, O: DeserializeOwned + DeclaredFields + Serialize>(
48 &mut self,
49 upstream: &U,
50 target: &U::Target,
51 request: WireRequest<()>,
52 ) -> Result<O, ModelListError> {
53 let result = invoke_empty(upstream, target, request, self.limits.codec)
54 .await
55 .map_err(|e| self.error(e))?;
56 let response = match result {
57 JsonInvocation::Success(response) => response,
58 JsonInvocation::Rejected(response) => {
59 return Err(ModelListError {
60 completed_calls: self.calls,
61 completed_models: self.models,
62 failure: ModelListFailure::Rejected(Box::new(response)),
63 });
64 }
65 };
66 self.calls += 1;
67 let encoded = codec::encode_json(&response.body, self.limits.codec).map_err(|e| {
68 self.error(TransformError::new(
69 TransformErrorKind::Limit,
70 "pagination.page",
71 e.to_string(),
72 ))
73 })?;
74 self.bytes = self
75 .bytes
76 .checked_add(encoded.len() as u64)
77 .ok_or_else(|| self.invalid("pagination.bytes", "byte counter overflow"))?;
78 if self.bytes > self.limits.max_declared_bytes {
79 return Err(self.error(TransformError::new(
80 TransformErrorKind::Limit,
81 "pagination.max_declared_bytes",
82 "aggregate declared page bytes exceeded",
83 )));
84 }
85 Ok(response.body)
86 }
87 fn item(&mut self, id: &str) -> Result<(), ModelListError> {
88 if id.is_empty() || !self.ids.insert(id.to_owned()) {
89 return Err(self.invalid("models.id", "empty or repeated model identity across pages"));
90 }
91
92 self.models += 1;
93 Ok(())
94 }
95}
96
97pub async fn collect_claude<U: Upstream>(
99 upstream: &U,
100 target: &U::Target,
101 request: WireRequest<()>,
102 query: c::ListModelsQuery,
103 limits: ModelListLimits,
104) -> Result<ModelDirectory<c::ListModelsResponseBody>, ModelListError> {
105 let query = query.into_declared();
106 query::page_size(query.limit, 1000)?;
107 if query.after_id.is_some() || query.before_id.is_some() {
108 return Err(TransformError::shape(
109 "query.cursor",
110 "full-directory collection must begin at the first page",
111 )
112 .into());
113 }
114 let mut progress = Progress::new(&request, limits)?;
115 let mut cursor = None;
116 let mut cursors = BTreeSet::new();
117 let mut data = Vec::new();
118 let mut first_id = None;
119 loop {
120 let mut pairs = Vec::new();
121 if let Some(limit) = query.limit {
122 pairs.push(("limit", limit.to_string()));
123 }
124 if let Some(cursor) = &cursor {
125 pairs.push(("after_id", String::clone(cursor)));
126 }
127 let mut next = WireRequest {
128 method: request.method.clone(),
129 path: request.path.clone(),
130 query: None,
131 headers: request.headers.clone(),
132 body: (),
133 };
134 next.query = query::encode(&pairs);
135 let page: c::ListModelsResponseBody = progress.page(upstream, target, next).await?;
136 if let (Some(first), Some(last)) = (page.data.first(), page.data.last()) {
137 if page.first_id.as_deref() != Some(first.id.as_str())
138 || page.last_id.as_deref() != Some(last.id.as_str())
139 {
140 return Err(progress.invalid(
141 "pagination",
142 "Claude page boundary IDs disagree with model data",
143 ));
144 }
145 } else if page.has_more {
146 return Err(progress.invalid("pagination", "empty Claude page claims continuation"));
147 }
148 if first_id.is_none() {
149 first_id = page.first_id;
150 }
151 for model in page.data {
152 progress.item(&model.id)?;
153 data.push(model);
154 }
155 if !page.has_more {
156 let last_id = data.last().map(|model| model.id.clone()).or(page.last_id);
157 return Ok(ModelDirectory {
158 value: c::ListModelsResponseBody {
159 data,
160 first_id,
161 last_id,
162 has_more: false,
163 rest: Default::default(),
164 },
165 completed_calls: progress.calls,
166 });
167 }
168 let next_cursor = page.last_id.filter(|id| !id.is_empty()).ok_or_else(|| {
169 progress.invalid("pagination.last_id", "missing or empty continuation cursor")
170 })?;
171 if !cursors.insert(next_cursor.clone()) {
172 return Err(progress.invalid(
173 "pagination.last_id",
174 "empty or repeated continuation cursor",
175 ));
176 }
177 cursor = Some(next_cursor);
178 }
179}
180
181pub async fn collect_gemini<U: Upstream>(
183 upstream: &U,
184 target: &U::Target,
185 request: WireRequest<()>,
186 query: g::ListModelsQuery,
187 limits: ModelListLimits,
188) -> Result<ModelDirectory<g::ListModelsResponseBody>, ModelListError> {
189 let query = query.into_declared();
190 query::page_size(query.page_size, i64::MAX)?;
191 if query.page_token.is_some() {
192 return Err(TransformError::shape(
193 "query.page_token",
194 "full-directory collection must begin at the first page",
195 )
196 .into());
197 }
198 let mut progress = Progress::new(&request, limits)?;
199 let mut cursor = None;
200 let mut cursors = BTreeSet::new();
201 let mut models = Vec::new();
202 loop {
203 let mut pairs = Vec::new();
204 if let Some(size) = query.page_size {
205 pairs.push(("pageSize", size.to_string()));
206 }
207 if let Some(cursor) = &cursor {
208 pairs.push(("pageToken", String::clone(cursor)));
209 }
210 let mut next = WireRequest {
211 method: request.method.clone(),
212 path: request.path.clone(),
213 query: None,
214 headers: request.headers.clone(),
215 body: (),
216 };
217 next.query = query::encode(&pairs);
218 let page: g::ListModelsResponseBody = progress.page(upstream, target, next).await?;
219 for model in page.models.unwrap_or_default() {
220 progress.item(&model.name)?;
221 models.push(model);
222 }
223 match page.next_page_token.filter(|token| !token.is_empty()) {
225 None => {
226 return Ok(ModelDirectory {
227 value: g::ListModelsResponseBody {
228 models: Some(models),
229 next_page_token: None,
230 rest: Default::default(),
231 },
232 completed_calls: progress.calls,
233 });
234 }
235 Some(token) => {
236 if !cursors.insert(token.clone()) {
237 return Err(
238 progress.invalid("pagination.nextPageToken", "repeated continuation token")
239 );
240 }
241 cursor = Some(token);
242 }
243 }
244 }
245}
246
247pub async fn collect_openai<U: Upstream>(
249 upstream: &U,
250 target: &U::Target,
251 request: WireRequest<()>,
252 limits: ModelListLimits,
253) -> Result<ModelDirectory<o::ListModelsResponseBody>, ModelListError> {
254 let mut progress = Progress::new(&request, limits)?;
255 let page: o::ListModelsResponseBody = progress.page(upstream, target, request).await?;
256 let page = crate::transform::models::normalize_openai_list(page)
257 .map_err(|error| progress.error(error))?;
258 for model in &page.data {
259 progress.item(&model.id)?;
260 }
261 Ok(ModelDirectory {
262 value: page,
263 completed_calls: progress.calls,
264 })
265}