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::{
45    BufferedResponse, ResponseCollector, ResponseStream, synthesize_error, synthesize_keepalive,
46    synthesize_response,
47};
48pub use error::TransformError;
49pub use gproxy_protocol as protocol;
50
51pub fn can_transform(source: OperationKey, target: OperationKey) -> bool {
52    envelope::is_promotion(source, target) || registry::resolve(source, target).is_some()
53}
54
55pub fn request(
56    source: OperationKey,
57    target: OperationKey,
58    mut body: Bytes,
59    upstream_model: &str,
60    stream: bool,
61) -> Result<Bytes, TransformError> {
62    if envelope::is_promotion(source, target) {
63        return envelope::promotion_request(source, target, body);
64    }
65    let original_target = target;
66    let semantic_source = semantic_responses_key(source);
67    let semantic_target = semantic_responses_key(target);
68    if semantic_source != source {
69        body = envelope::promotion_request(source, semantic_source, body)?;
70    }
71    let pair = registry::resolve(semantic_source, semantic_target).ok_or(
72        TransformError::UnsupportedPair {
73            source_key: source,
74            target_key: target,
75        },
76    )?;
77    let body = registry::request(pair, body, upstream_model, stream)?;
78    if semantic_target != original_target {
79        envelope::promotion_request(semantic_target, original_target, body)
80    } else {
81        Ok(body)
82    }
83}
84
85pub fn response(
86    source: OperationKey,
87    target: OperationKey,
88    body: Bytes,
89) -> Result<Bytes, TransformError> {
90    if envelope::is_promotion(source, target) {
91        return envelope::promotion_response(body);
92    }
93    let semantic_source = semantic_responses_key(source);
94    let semantic_target = semantic_responses_key(target);
95    let pair = registry::resolve(semantic_source, semantic_target).ok_or(
96        TransformError::UnsupportedPair {
97            source_key: source,
98            target_key: target,
99        },
100    )?;
101    registry::response(pair, body)
102}
103
104fn semantic_responses_key(key: OperationKey) -> OperationKey {
105    if key.kind()
106        == gproxy_protocol::OperationKind::ContentGeneration(
107            gproxy_protocol::ContentGenerationKind::OpenAiResponsesWebSocket,
108        )
109    {
110        return OperationKey::content(
111            key.operation(),
112            gproxy_protocol::ContentGenerationKind::OpenAiResponses,
113        );
114    }
115    key
116}
117
118pub fn response_stream(
119    source: OperationKey,
120    target: OperationKey,
121) -> Result<ResponseStream, TransformError> {
122    ResponseStream::new(source, target)
123}
124
125pub fn response_stream_framed(
126    source: OperationKey,
127    target: OperationKey,
128    source_framing: StreamFraming,
129    target_framing: StreamFraming,
130) -> Result<ResponseStream, TransformError> {
131    ResponseStream::new_framed(source, target, source_framing, target_framing)
132}
133
134pub fn request_query(
135    source: OperationKey,
136    target: OperationKey,
137    query: Option<&str>,
138) -> Result<Option<String>, TransformError> {
139    if source.operation() != gproxy_protocol::Operation::ListModels {
140        return Ok(query.map(str::to_owned));
141    }
142    use gproxy_protocol::{OperationKind::Family, WireFamily};
143    Ok(match (source.kind(), target.kind()) {
144        (Family(WireFamily::Claude), Family(WireFamily::Gemini)) => {
145            let values = query_pairs(query);
146            let mut output = Vec::new();
147            copy_query(&values, "limit", "pageSize", &mut output);
148            copy_query(&values, "after_id", "pageToken", &mut output);
149            joined_query(output)
150        }
151        (Family(WireFamily::Gemini), Family(WireFamily::Claude)) => {
152            let values = query_pairs(query);
153            let mut output = Vec::new();
154            copy_query(&values, "pageSize", "limit", &mut output);
155            copy_query(&values, "pageToken", "after_id", &mut output);
156            joined_query(output)
157        }
158        (Family(WireFamily::Claude | WireFamily::Gemini), Family(WireFamily::OpenAi))
159        | (Family(WireFamily::OpenAi), Family(WireFamily::Claude | WireFamily::Gemini)) => None,
160        _ => query.map(str::to_owned),
161    })
162}
163
164fn query_pairs(query: Option<&str>) -> Vec<(String, String)> {
165    form_urlencoded::parse(query.unwrap_or_default().as_bytes())
166        .map(|(key, value)| (key.into_owned(), value.into_owned()))
167        .collect()
168}
169
170fn copy_query(
171    values: &[(String, String)],
172    source: &str,
173    target: &str,
174    output: &mut Vec<(String, String)>,
175) {
176    if let Some((_, value)) = values.iter().find(|(key, _)| key == source) {
177        output.push((target.into(), value.clone()));
178    }
179}
180
181fn joined_query(values: Vec<(String, String)>) -> Option<String> {
182    if values.is_empty() {
183        None
184    } else {
185        Some(
186            form_urlencoded::Serializer::new(String::new())
187                .extend_pairs(values)
188                .finish(),
189        )
190    }
191}
192
193#[cfg(test)]
194mod tests;