Skip to main content

gproxy_transform/transform/models/list/
query.rs

1//! List-models QUERY-string conversion (inbound wire → target wire).
2//!
3//! Models requests are GET/no-body — their parameters travel in the query, so
4//! the bytes dispatch never sees them. The pipeline calls [`request_query`]
5//! with the inbound query in [`TransformContext::query`] instead. Values are
6//! kept percent-encoded verbatim (parse → typed request transform → re-emit).
7
8use crate::protocol::{claude, gemini};
9use crate::transform::{TransformContext, TransformPair};
10
11/// Convert the inbound ListModels query to the target wire's query string.
12/// `None` = nothing to send (OpenAI's list endpoint takes no parameters, so
13/// every pair into/out of OpenAI drops them). Non-models pairs and GetModel
14/// are a no-op.
15pub fn request_query(pair: TransformPair, ctx: &TransformContext) -> Option<String> {
16    use TransformPair as P;
17    if ctx.source.operation != crate::protocol::Operation::ListModels {
18        return None;
19    }
20    match pair {
21        P::ClaudeToGeminiModels => {
22            write_gemini(super::claude_to_gemini::request(parse_claude(ctx), ctx))
23        }
24        P::GeminiToClaudeModels => {
25            write_claude(super::gemini_to_claude::request(parse_gemini(ctx), ctx))
26        }
27        _ => None,
28    }
29}
30
31fn pairs(ctx: &TransformContext) -> impl Iterator<Item = (&str, &str)> {
32    ctx.query
33        .as_deref()
34        .unwrap_or_default()
35        .split('&')
36        .filter_map(|pair| {
37            let (k, v) = pair.split_once('=').unwrap_or((pair, ""));
38            (!k.is_empty() && !v.is_empty()).then_some((k, v))
39        })
40}
41
42fn parse_claude(ctx: &TransformContext) -> claude::ListModelsQuery {
43    let mut q = claude::ListModelsQuery {
44        after_id: None,
45        before_id: None,
46        limit: None,
47        extra: Default::default(),
48    };
49    for (k, v) in pairs(ctx) {
50        match k {
51            "after_id" => q.after_id = Some(v.to_owned()),
52            "before_id" => q.before_id = Some(v.to_owned()),
53            "limit" => q.limit = v.parse().ok(),
54            _ => {}
55        }
56    }
57    q
58}
59
60fn parse_gemini(ctx: &TransformContext) -> gemini::ListModelsRequest {
61    let mut q = gemini::ListModelsRequest::default();
62    for (k, v) in pairs(ctx) {
63        match k {
64            "pageSize" => q.page_size = v.parse().ok(),
65            "pageToken" => q.page_token = Some(v.to_owned()),
66            _ => {}
67        }
68    }
69    q
70}
71
72fn write_claude(q: claude::ListModelsQuery) -> Option<String> {
73    let mut out = Vec::new();
74    if let Some(limit) = q.limit {
75        out.push(format!("limit={limit}"));
76    }
77    if let Some(after_id) = q.after_id {
78        out.push(format!("after_id={after_id}"));
79    }
80    if let Some(before_id) = q.before_id {
81        out.push(format!("before_id={before_id}"));
82    }
83    (!out.is_empty()).then(|| out.join("&"))
84}
85
86fn write_gemini(q: gemini::ListModelsRequest) -> Option<String> {
87    let mut out = Vec::new();
88    if let Some(page_size) = q.page_size {
89        out.push(format!("pageSize={page_size}"));
90    }
91    if let Some(page_token) = q.page_token {
92        out.push(format!("pageToken={page_token}"));
93    }
94    (!out.is_empty()).then(|| out.join("&"))
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100    use crate::protocol::{Operation, OperationKey, Provider};
101
102    fn ctx(source: Provider, target: Provider, query: &str) -> TransformContext {
103        TransformContext::new(
104            OperationKey::provider(Operation::ListModels, source),
105            OperationKey::provider(Operation::ListModels, target),
106        )
107        .with_request("/v1/models", Some(query))
108    }
109
110    #[test]
111    fn claude_and_gemini_pagination_round_trips() {
112        let out = request_query(
113            TransformPair::ClaudeToGeminiModels,
114            &ctx(
115                Provider::Claude,
116                Provider::Gemini,
117                "limit=25&after_id=m1&x=1",
118            ),
119        );
120        assert_eq!(out.as_deref(), Some("pageSize=25&pageToken=m1"));
121
122        let out = request_query(
123            TransformPair::GeminiToClaudeModels,
124            &ctx(
125                Provider::Gemini,
126                Provider::Claude,
127                "pageSize=25&pageToken=tok",
128            ),
129        );
130        assert_eq!(out.as_deref(), Some("limit=25&after_id=tok"));
131
132        // openai targets take no list parameters
133        let out = request_query(
134            TransformPair::ClaudeToOpenAiModels,
135            &ctx(Provider::Claude, Provider::OpenAi, "limit=25"),
136        );
137        assert_eq!(out, None);
138    }
139}