Skip to main content

gproxy_transform/
lib.rs

1//! Pure pairwise wire transforms. Routing policy belongs to channels and core.
2//!
3//! Protocol extension bags stop at this crate's input boundary. A transform maps
4//! only fields and enum values whose semantics it understands, emits empty target
5//! extension bags, drops unknown optional objects and values, and rejects unknown
6//! data required to produce a valid target shape. Cross-protocol correlation and
7//! opaque continuation data use typed converter state or documented wire fields,
8//! never private JSON metadata.
9//! Same-wire operation and envelope promotions remain byte-preserving because
10//! they do not perform a semantic conversion.
11
12#[cfg(feature = "exhaustive")]
13macro_rules! wire {
14    ($value:expr) => {
15        $value
16    };
17}
18
19#[cfg(not(feature = "exhaustive"))]
20macro_rules! wire {
21    ($($tokens:tt)*) => {
22        gproxy_protocol::wire!($($tokens)*)
23    };
24}
25
26pub(crate) use wire;
27
28mod common;
29mod compact;
30mod count_tokens;
31mod embeddings;
32mod envelope;
33mod error;
34mod generate_content;
35mod images;
36mod models;
37mod registry;
38pub mod typed;
39mod videos;
40
41use bytes::Bytes;
42use gproxy_protocol::{OperationKey, StreamFraming};
43
44pub use envelope::{BufferedResponse, ResponseCollector, ResponseStream, synthesize_response};
45pub use error::TransformError;
46pub use gproxy_protocol as protocol;
47
48pub fn can_transform(source: OperationKey, target: OperationKey) -> bool {
49    envelope::is_promotion(source, target) || registry::resolve(source, target).is_some()
50}
51
52pub fn request(
53    source: OperationKey,
54    target: OperationKey,
55    mut body: Bytes,
56    upstream_model: &str,
57    stream: bool,
58) -> Result<Bytes, TransformError> {
59    if envelope::is_promotion(source, target) {
60        return envelope::promotion_request(source, target, body);
61    }
62    let original_target = target;
63    let semantic_source = semantic_responses_key(source);
64    let semantic_target = semantic_responses_key(target);
65    if semantic_source != source {
66        body = envelope::promotion_request(source, semantic_source, body)?;
67    }
68    let pair = registry::resolve(semantic_source, semantic_target).ok_or(
69        TransformError::UnsupportedPair {
70            source_key: source,
71            target_key: target,
72        },
73    )?;
74    let body = registry::request(pair, body, upstream_model, stream)?;
75    if semantic_target != original_target {
76        envelope::promotion_request(semantic_target, original_target, body)
77    } else {
78        Ok(body)
79    }
80}
81
82pub fn response(
83    source: OperationKey,
84    target: OperationKey,
85    body: Bytes,
86) -> Result<Bytes, TransformError> {
87    if envelope::is_promotion(source, target) {
88        return envelope::promotion_response(body);
89    }
90    let semantic_source = semantic_responses_key(source);
91    let semantic_target = semantic_responses_key(target);
92    let pair = registry::resolve(semantic_source, semantic_target).ok_or(
93        TransformError::UnsupportedPair {
94            source_key: source,
95            target_key: target,
96        },
97    )?;
98    registry::response(pair, body)
99}
100
101fn semantic_responses_key(key: OperationKey) -> OperationKey {
102    if key.kind()
103        == gproxy_protocol::OperationKind::ContentGeneration(
104            gproxy_protocol::ContentGenerationKind::OpenAiResponsesWebSocket,
105        )
106    {
107        return OperationKey::content(
108            key.operation(),
109            gproxy_protocol::ContentGenerationKind::OpenAiResponses,
110        );
111    }
112    key
113}
114
115pub fn response_stream(
116    source: OperationKey,
117    target: OperationKey,
118) -> Result<ResponseStream, TransformError> {
119    ResponseStream::new(source, target)
120}
121
122pub fn response_stream_framed(
123    source: OperationKey,
124    target: OperationKey,
125    source_framing: StreamFraming,
126    target_framing: StreamFraming,
127) -> Result<ResponseStream, TransformError> {
128    ResponseStream::new_framed(source, target, source_framing, target_framing)
129}
130
131pub fn request_query(
132    source: OperationKey,
133    target: OperationKey,
134    query: Option<&str>,
135) -> Result<Option<String>, TransformError> {
136    if source.operation() != gproxy_protocol::Operation::ListModels {
137        return Ok(query.map(str::to_owned));
138    }
139    use gproxy_protocol::{OperationKind::Family, WireFamily};
140    Ok(match (source.kind(), target.kind()) {
141        (Family(WireFamily::Claude), Family(WireFamily::Gemini)) => {
142            let values = query_pairs(query);
143            let mut output = Vec::new();
144            copy_query(&values, "limit", "pageSize", &mut output);
145            copy_query(&values, "after_id", "pageToken", &mut output);
146            joined_query(output)
147        }
148        (Family(WireFamily::Gemini), Family(WireFamily::Claude)) => {
149            let values = query_pairs(query);
150            let mut output = Vec::new();
151            copy_query(&values, "pageSize", "limit", &mut output);
152            copy_query(&values, "pageToken", "after_id", &mut output);
153            joined_query(output)
154        }
155        (Family(WireFamily::Claude | WireFamily::Gemini), Family(WireFamily::OpenAi))
156        | (Family(WireFamily::OpenAi), Family(WireFamily::Claude | WireFamily::Gemini)) => None,
157        _ => query.map(str::to_owned),
158    })
159}
160
161fn query_pairs(query: Option<&str>) -> Vec<(String, String)> {
162    form_urlencoded::parse(query.unwrap_or_default().as_bytes())
163        .map(|(key, value)| (key.into_owned(), value.into_owned()))
164        .collect()
165}
166
167fn copy_query(
168    values: &[(String, String)],
169    source: &str,
170    target: &str,
171    output: &mut Vec<(String, String)>,
172) {
173    if let Some((_, value)) = values.iter().find(|(key, _)| key == source) {
174        output.push((target.into(), value.clone()));
175    }
176}
177
178fn joined_query(values: Vec<(String, String)>) -> Option<String> {
179    if values.is_empty() {
180        None
181    } else {
182        Some(
183            form_urlencoded::Serializer::new(String::new())
184                .extend_pairs(values)
185                .finish(),
186        )
187    }
188}
189
190#[cfg(test)]
191mod tests;