gproxy_transform/transform/dispatch/
mod.rs1mod content;
7mod other;
8
9use serde::Serialize;
10use serde::de::DeserializeOwned;
11
12use super::{TransformContext, TransformError, TransformPair};
13
14pub fn is_wired(pair: TransformPair) -> bool {
16 content::is_content(pair) || other::is_wired(pair)
17}
18
19pub fn request_bytes(
21 pair: TransformPair,
22 ctx: &TransformContext,
23 body: &[u8],
24) -> Result<Vec<u8>, TransformError> {
25 if content::is_content(pair) {
26 content::request_bytes(pair, ctx, body)
27 } else {
28 other::request_bytes(pair, ctx, body)
29 }
30}
31
32pub fn response_bytes(
35 pair: TransformPair,
36 ctx: &TransformContext,
37 body: &[u8],
38) -> Result<Vec<u8>, TransformError> {
39 if content::is_content(pair) {
40 content::response_bytes(pair, ctx, body)
41 } else {
42 other::response_bytes(pair, ctx, body)
43 }
44}
45
46pub enum StreamEventOut {
50 Encoded { event: Option<String>, data: String },
51 Responses(Box<crate::protocol::openai::ResponseStreamEvent>),
52}
53
54pub fn stream_event(
58 pair: TransformPair,
59 ctx: &TransformContext,
60 data: &str,
61) -> Result<StreamEventOut, TransformError> {
62 if content::is_content(pair) {
63 content::stream_event(pair, ctx, data)
64 } else {
65 Err(not_wired(pair))
66 }
67}
68
69fn run<S, T>(
70 f: impl Fn(S, &TransformContext) -> Result<T, TransformError>,
71 ctx: &TransformContext,
72 body: &[u8],
73) -> Result<Vec<u8>, TransformError>
74where
75 S: DeserializeOwned,
76 T: Serialize,
77{
78 let input: S = serde_json::from_slice(body).map_err(|e| TransformError::InvalidInput {
79 reason: format!("decode source body: {e}"),
80 })?;
81 let out = f(input, ctx)?;
82 serde_json::to_vec(&out).map_err(|e| TransformError::Serialization {
83 reason: e.to_string(),
84 })
85}
86
87fn run_ok<S, T>(
89 f: impl Fn(S, &TransformContext) -> T,
90 ctx: &TransformContext,
91 body: &[u8],
92) -> Result<Vec<u8>, TransformError>
93where
94 S: DeserializeOwned,
95 T: Serialize,
96{
97 run(
98 |input, ctx| Ok::<_, TransformError>(f(input, ctx)),
99 ctx,
100 body,
101 )
102}
103
104fn not_wired(pair: TransformPair) -> TransformError {
105 TransformError::InvalidInput {
106 reason: format!("bytes dispatch not wired for {pair:?}"),
107 }
108}
109
110#[cfg(test)]
111mod tests {
112 use serde_json::Value;
113
114 use super::*;
115 use crate::protocol::{ContentGenerationKind, Operation, OperationKey, Provider};
116
117 #[test]
118 fn claude_to_openai_chat_request_roundtrip() {
119 let source = OperationKey::content_generation(
120 Operation::GenerateContent,
121 ContentGenerationKind::ClaudeMessages,
122 );
123 let target = OperationKey::content_generation(
124 Operation::GenerateContent,
125 ContentGenerationKind::OpenAiChatCompletions,
126 );
127 let ctx = TransformContext::new(source, target);
128 let body = br#"{"model":"m","max_tokens":16,"messages":[{"role":"user","content":"hi"}]}"#;
129 let out = request_bytes(TransformPair::ClaudeMessagesToOpenAiChat, &ctx, body).unwrap();
130 let v: Value = serde_json::from_slice(&out).unwrap();
131 assert_eq!(v["messages"][0]["role"], "user");
132 assert!(v.get("max_tokens").is_some() || v.get("max_completion_tokens").is_some());
133 }
134
135 #[test]
136 fn openai_responses_to_websocket_request_roundtrip() {
137 let source = OperationKey::content_generation(
138 Operation::GenerateContent,
139 ContentGenerationKind::OpenAiResponses,
140 );
141 let target = OperationKey::content_generation(
142 Operation::GenerateContent,
143 ContentGenerationKind::OpenAiResponsesWebSocket,
144 );
145 let pair = crate::transform::resolve(source, target).unwrap();
146 assert!(is_wired(pair));
147
148 let ctx = TransformContext::new(source, target);
149 let body = br#"{"model":"m","input":"hi","stream":true}"#;
150 let out = request_bytes(pair, &ctx, body).unwrap();
151 let v: Value = serde_json::from_slice(&out).unwrap();
152
153 assert_eq!(v["type"], "response.create");
154 assert_eq!(v["model"], "m");
155 assert_eq!(v["stream"], true);
156 }
157
158 #[test]
159 fn claude_to_openai_responses_websocket_request_roundtrip() {
160 let source = OperationKey::content_generation(
161 Operation::GenerateContent,
162 ContentGenerationKind::ClaudeMessages,
163 );
164 let target = OperationKey::content_generation(
165 Operation::GenerateContent,
166 ContentGenerationKind::OpenAiResponsesWebSocket,
167 );
168 let pair = crate::transform::resolve(source, target).unwrap();
169 assert!(is_wired(pair));
170
171 let ctx = TransformContext::new(source, target);
172 let body = br#"{"model":"m","max_tokens":16,"messages":[{"role":"user","content":"hi"}]}"#;
173 let out = request_bytes(pair, &ctx, body).unwrap();
174 let v: Value = serde_json::from_slice(&out).unwrap();
175
176 assert_eq!(v["type"], "response.create");
177 assert_eq!(v["model"], "m");
178 assert!(v.get("input").is_some());
179 }
180
181 #[test]
182 fn claude_to_openai_count_tokens_request_roundtrip() {
183 let source = OperationKey::provider(Operation::CountTokens, Provider::Claude);
184 let target = OperationKey::provider(Operation::CountTokens, Provider::OpenAi);
185 let ctx = TransformContext::new(source, target);
186 let body = br#"{"model":"m","messages":[{"role":"user","content":"hi"}]}"#;
187 let out = request_bytes(TransformPair::ClaudeToOpenAiCountTokens, &ctx, body).unwrap();
188 let v: Value = serde_json::from_slice(&out).unwrap();
189 assert_eq!(v["model"], "m");
190 assert!(v.get("input").is_some());
191 }
192
193 #[test]
194 fn compact_to_responses_is_resolved_and_wired() {
195 let source = OperationKey::provider(Operation::CompactContent, Provider::OpenAi);
196 let target = OperationKey::content_generation(
197 Operation::GenerateContent,
198 ContentGenerationKind::OpenAiResponses,
199 );
200 let pair = crate::transform::resolve(source, target).unwrap();
201 assert_eq!(pair, TransformPair::OpenAiCompactToOpenAiResponses);
202 assert!(is_wired(pair));
203
204 let ctx = TransformContext::new(source, target);
205 let body = br#"{"model":"m","input":"summarize this"}"#;
206 let out = request_bytes(pair, &ctx, body).unwrap();
207 let value: Value = serde_json::from_slice(&out).unwrap();
208 assert_eq!(value["model"], "m");
209 assert!(value.get("input").is_some());
210 }
211
212 #[test]
213 fn openai_to_claude_models_list_response_roundtrip() {
214 let source = OperationKey::provider(Operation::ListModels, Provider::OpenAi);
215 let target = OperationKey::provider(Operation::ListModels, Provider::Claude);
216 let ctx = TransformContext::new(source, target);
217 let body = br#"{"object":"list","data":[{"id":"gpt-x","created":1,"object":"model","owned_by":"openai"}]}"#;
218 let out = response_bytes(TransformPair::OpenAiToClaudeModels, &ctx, body).unwrap();
219 let v: Value = serde_json::from_slice(&out).unwrap();
220 assert_eq!(v["data"][0]["id"], "gpt-x");
221 }
222}