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 first.id != page.first_id || last.id != page.last_id {
138 return Err(progress.invalid(
139 "pagination",
140 "Claude page boundary IDs disagree with model data",
141 ));
142 }
143 } else if page.has_more {
144 return Err(progress.invalid("pagination", "empty Claude page claims continuation"));
145 }
146 if first_id.is_none() {
147 first_id = Some(page.first_id);
148 }
149 for model in page.data {
150 progress.item(&model.id)?;
151 data.push(model);
152 }
153 if !page.has_more {
154 let last_id = data
155 .last()
156 .map(|model| model.id.clone())
157 .unwrap_or(page.last_id);
158 return Ok(ModelDirectory {
159 value: c::ListModelsResponseBody {
160 data,
161 first_id: first_id.expect("first page was decoded"),
162 last_id,
163 has_more: false,
164 rest: Default::default(),
165 },
166 completed_calls: progress.calls,
167 });
168 }
169 if page.last_id.is_empty() || !cursors.insert(page.last_id.clone()) {
170 return Err(progress.invalid(
171 "pagination.last_id",
172 "empty or repeated continuation cursor",
173 ));
174 }
175 cursor = Some(page.last_id);
176 }
177}
178
179pub async fn collect_gemini<U: Upstream>(
181 upstream: &U,
182 target: &U::Target,
183 request: WireRequest<()>,
184 query: g::ListModelsQuery,
185 limits: ModelListLimits,
186) -> Result<ModelDirectory<g::ListModelsResponseBody>, ModelListError> {
187 let query = query.into_declared();
188 query::page_size(query.page_size, i64::MAX)?;
189 if query.page_token.is_some() {
190 return Err(TransformError::shape(
191 "query.page_token",
192 "full-directory collection must begin at the first page",
193 )
194 .into());
195 }
196 let mut progress = Progress::new(&request, limits)?;
197 let mut cursor = None;
198 let mut cursors = BTreeSet::new();
199 let mut models = Vec::new();
200 loop {
201 let mut pairs = Vec::new();
202 if let Some(size) = query.page_size {
203 pairs.push(("pageSize", size.to_string()));
204 }
205 if let Some(cursor) = &cursor {
206 pairs.push(("pageToken", String::clone(cursor)));
207 }
208 let mut next = WireRequest {
209 method: request.method.clone(),
210 path: request.path.clone(),
211 query: None,
212 headers: request.headers.clone(),
213 body: (),
214 };
215 next.query = query::encode(&pairs);
216 let page: g::ListModelsResponseBody = progress.page(upstream, target, next).await?;
217 for model in page.models.unwrap_or_default() {
218 progress.item(&model.name)?;
219 models.push(model);
220 }
221 match page.next_page_token.filter(|token| !token.is_empty()) {
223 None => {
224 return Ok(ModelDirectory {
225 value: g::ListModelsResponseBody {
226 models: Some(models),
227 next_page_token: None,
228 rest: Default::default(),
229 },
230 completed_calls: progress.calls,
231 });
232 }
233 Some(token) => {
234 if !cursors.insert(token.clone()) {
235 return Err(
236 progress.invalid("pagination.nextPageToken", "repeated continuation token")
237 );
238 }
239 cursor = Some(token);
240 }
241 }
242 }
243}
244
245pub async fn collect_openai<U: Upstream>(
247 upstream: &U,
248 target: &U::Target,
249 request: WireRequest<()>,
250 limits: ModelListLimits,
251) -> Result<ModelDirectory<o::ListModelsResponseBody>, ModelListError> {
252 let mut progress = Progress::new(&request, limits)?;
253 let page: o::ListModelsResponseBody = progress.page(upstream, target, request).await?;
254 for model in &page.data {
255 progress.item(&model.id)?;
256 }
257 Ok(ModelDirectory {
258 value: page,
259 completed_calls: progress.calls,
260 })
261}