1mod content;
7mod other;
8
9use serde::Serialize;
10use serde::de::DeserializeOwned;
11
12use super::{TransformContext, TransformError, TransformOutput, 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 validate_pair(pair, ctx)?;
26 ctx.scope(|| {
27 if content::is_content(pair) {
28 content::request_bytes(pair, ctx, body)
29 } else {
30 other::request_bytes(pair, ctx, body)
31 }
32 })
33}
34
35pub fn request_bytes_detailed(
37 pair: TransformPair,
38 ctx: &TransformContext,
39 body: &[u8],
40) -> Result<TransformOutput<Vec<u8>>, TransformError> {
41 let scoped = ctx.isolated();
42 let value = request_bytes(pair, &scoped, body)?;
43 Ok(TransformOutput::new(value, scoped.take_diagnostics()))
44}
45
46pub fn response_bytes(
49 pair: TransformPair,
50 ctx: &TransformContext,
51 body: &[u8],
52) -> Result<Vec<u8>, TransformError> {
53 validate_pair(pair, ctx)?;
54 ctx.scope(|| {
55 if content::is_content(pair) {
56 content::response_bytes(pair, ctx, body)
57 } else {
58 other::response_bytes(pair, ctx, body)
59 }
60 })
61}
62
63pub fn response_bytes_detailed(
65 pair: TransformPair,
66 ctx: &TransformContext,
67 body: &[u8],
68) -> Result<TransformOutput<Vec<u8>>, TransformError> {
69 let scoped = ctx.isolated();
70 let value = response_bytes(pair, &scoped, body)?;
71 Ok(TransformOutput::new(value, scoped.take_diagnostics()))
72}
73
74pub enum StreamEventOut {
78 Encoded { event: Option<String>, data: String },
79 Responses(Box<crate::protocol::openai::ResponseStreamEvent>),
80}
81
82pub struct StreamConverter {
88 inner: content::ContentStreamConverter,
89}
90
91impl StreamConverter {
92 pub fn new(pair: TransformPair, ctx: TransformContext) -> Result<Self, TransformError> {
93 validate_pair(pair, &ctx)?;
94 Ok(Self {
95 inner: content::ContentStreamConverter::new(pair, ctx)?,
96 })
97 }
98
99 pub fn push(&mut self, data: &str) -> Result<Vec<StreamEventOut>, TransformError> {
101 Ok(self.push_detailed(data)?.value)
102 }
103
104 pub fn push_detailed(
106 &mut self,
107 data: &str,
108 ) -> Result<TransformOutput<Vec<StreamEventOut>>, TransformError> {
109 let value = self.inner.push(data)?;
110 Ok(TransformOutput::new(value, self.inner.take_diagnostics()))
111 }
112
113 pub fn finish(&mut self) -> Result<Vec<StreamEventOut>, TransformError> {
115 Ok(self.finish_detailed()?.value)
116 }
117
118 pub fn finish_detailed(
120 &mut self,
121 ) -> Result<TransformOutput<Vec<StreamEventOut>>, TransformError> {
122 let value = self.inner.finish()?;
123 Ok(TransformOutput::new(value, self.inner.take_diagnostics()))
124 }
125}
126
127pub fn stream_event(
132 pair: TransformPair,
133 ctx: &TransformContext,
134 data: &str,
135) -> Result<Vec<StreamEventOut>, TransformError> {
136 if content::is_content(pair) {
137 let mut converter = StreamConverter::new(pair, ctx.clone())?;
138 converter.push(data)
139 } else {
140 Err(not_wired(pair))
141 }
142}
143
144fn validate_pair(pair: TransformPair, ctx: &TransformContext) -> Result<(), TransformError> {
145 let resolved = super::resolve(ctx.source, ctx.target)?;
146 if resolved == pair {
147 Ok(())
148 } else {
149 Err(TransformError::InvalidInput {
150 reason: format!(
151 "transform pair {pair:?} does not match context {:?} -> {:?} (resolved {resolved:?})",
152 ctx.source, ctx.target
153 ),
154 })
155 }
156}
157
158fn run<S, T>(
159 f: impl Fn(S, &TransformContext) -> Result<T, TransformError>,
160 ctx: &TransformContext,
161 body: &[u8],
162) -> Result<Vec<u8>, TransformError>
163where
164 S: DeserializeOwned,
165 T: Serialize,
166{
167 let input: S = serde_json::from_slice(body).map_err(|e| TransformError::InvalidInput {
168 reason: format!("decode source body: {e}"),
169 })?;
170 let out = f(input, ctx)?;
171 serde_json::to_vec(&out).map_err(|e| TransformError::Serialization {
172 reason: e.to_string(),
173 })
174}
175
176fn run_ok<S, T>(
178 f: impl Fn(S, &TransformContext) -> T,
179 ctx: &TransformContext,
180 body: &[u8],
181) -> Result<Vec<u8>, TransformError>
182where
183 S: DeserializeOwned,
184 T: Serialize,
185{
186 run(
187 |input, ctx| Ok::<_, TransformError>(f(input, ctx)),
188 ctx,
189 body,
190 )
191}
192
193fn not_wired(pair: TransformPair) -> TransformError {
194 TransformError::InvalidInput {
195 reason: format!("bytes dispatch not wired for {pair:?}"),
196 }
197}
198
199#[cfg(test)]
200mod tests {
201 use serde_json::Value;
202
203 use super::*;
204 use crate::protocol::{ContentGenerationKind, Operation, OperationKey, Provider};
205 use crate::transform::TransformDiagnosticKind;
206
207 #[test]
208 fn claude_to_openai_chat_request_roundtrip() {
209 let source = OperationKey::content_generation(
210 Operation::GenerateContent,
211 ContentGenerationKind::ClaudeMessages,
212 );
213 let target = OperationKey::content_generation(
214 Operation::GenerateContent,
215 ContentGenerationKind::OpenAiChatCompletions,
216 );
217 let ctx = TransformContext::new(source, target);
218 let body = br#"{"model":"m","max_tokens":16,"messages":[{"role":"user","content":"hi"}]}"#;
219 let out = request_bytes(TransformPair::ClaudeMessagesToOpenAiChat, &ctx, body).unwrap();
220 let v: Value = serde_json::from_slice(&out).unwrap();
221 assert_eq!(v["messages"][0]["role"], "user");
222 assert!(v.get("max_tokens").is_some() || v.get("max_completion_tokens").is_some());
223 }
224
225 #[test]
226 fn detailed_request_returns_structured_semantic_loss() {
227 let source = OperationKey::content_generation(
228 Operation::GenerateContent,
229 ContentGenerationKind::OpenAiChatCompletions,
230 );
231 let target = OperationKey::content_generation(
232 Operation::GenerateContent,
233 ContentGenerationKind::ClaudeMessages,
234 );
235 let pair = crate::transform::resolve(source, target).unwrap();
236 let ctx = TransformContext::new(source, target);
237 let body = br#"{
238 "model":"m",
239 "messages":[{"role":"user","content":[{
240 "type":"text",
241 "text":"",
242 "prompt_cache_breakpoint":{"mode":"explicit"}
243 }]}]
244 }"#;
245
246 let output = request_bytes_detailed(pair, &ctx, body).unwrap();
247
248 assert_eq!(output.diagnostics.len(), 1);
249 assert_eq!(
250 output.diagnostics[0].kind,
251 TransformDiagnosticKind::LossyField
252 );
253 assert_eq!(
254 output.diagnostics[0].field,
255 "messages[].content[].text.prompt_cache_breakpoint"
256 );
257 assert!(ctx.diagnostics().is_empty(), "detailed calls are isolated");
258 }
259
260 #[test]
261 fn openai_responses_to_websocket_request_roundtrip() {
262 let source = OperationKey::content_generation(
263 Operation::GenerateContent,
264 ContentGenerationKind::OpenAiResponses,
265 );
266 let target = OperationKey::content_generation(
267 Operation::GenerateContent,
268 ContentGenerationKind::OpenAiResponsesWebSocket,
269 );
270 let pair = crate::transform::resolve(source, target).unwrap();
271 assert!(is_wired(pair));
272
273 let ctx = TransformContext::new(source, target);
274 let body = br#"{"model":"m","input":"hi","stream":true}"#;
275 let out = request_bytes(pair, &ctx, body).unwrap();
276 let v: Value = serde_json::from_slice(&out).unwrap();
277
278 assert_eq!(v["type"], "response.create");
279 assert_eq!(v["model"], "m");
280 assert_eq!(v["stream"], true);
281 }
282
283 #[test]
284 fn claude_to_openai_responses_websocket_request_roundtrip() {
285 let source = OperationKey::content_generation(
286 Operation::GenerateContent,
287 ContentGenerationKind::ClaudeMessages,
288 );
289 let target = OperationKey::content_generation(
290 Operation::GenerateContent,
291 ContentGenerationKind::OpenAiResponsesWebSocket,
292 );
293 let pair = crate::transform::resolve(source, target).unwrap();
294 assert!(is_wired(pair));
295
296 let ctx = TransformContext::new(source, target);
297 let body = br#"{"model":"m","max_tokens":16,"messages":[{"role":"user","content":"hi"}]}"#;
298 let out = request_bytes(pair, &ctx, body).unwrap();
299 let v: Value = serde_json::from_slice(&out).unwrap();
300
301 assert_eq!(v["type"], "response.create");
302 assert_eq!(v["model"], "m");
303 assert!(v.get("input").is_some());
304 }
305
306 #[test]
307 fn claude_to_openai_count_tokens_request_roundtrip() {
308 let source = OperationKey::provider(Operation::CountTokens, Provider::Claude);
309 let target = OperationKey::provider(Operation::CountTokens, Provider::OpenAi);
310 let ctx = TransformContext::new(source, target);
311 let body = br#"{"model":"m","messages":[{"role":"user","content":"hi"}]}"#;
312 let out = request_bytes(TransformPair::ClaudeToOpenAiCountTokens, &ctx, body).unwrap();
313 let v: Value = serde_json::from_slice(&out).unwrap();
314 assert_eq!(v["model"], "m");
315 assert!(v.get("input").is_some());
316 }
317
318 #[test]
319 fn compact_to_responses_is_resolved_and_wired() {
320 let source = OperationKey::provider(Operation::CompactContent, Provider::OpenAi);
321 let target = OperationKey::content_generation(
322 Operation::GenerateContent,
323 ContentGenerationKind::OpenAiResponses,
324 );
325 let pair = crate::transform::resolve(source, target).unwrap();
326 assert_eq!(pair, TransformPair::OpenAiCompactToOpenAiResponses);
327 assert!(is_wired(pair));
328
329 let ctx = TransformContext::new(source, target);
330 let body = br#"{"model":"m","input":"summarize this"}"#;
331 let out = request_bytes(pair, &ctx, body).unwrap();
332 let value: Value = serde_json::from_slice(&out).unwrap();
333 assert_eq!(value["model"], "m");
334 assert!(value.get("input").is_some());
335 }
336
337 #[test]
338 fn openai_to_claude_models_list_response_roundtrip() {
339 let source = OperationKey::provider(Operation::ListModels, Provider::OpenAi);
340 let target = OperationKey::provider(Operation::ListModels, Provider::Claude);
341 let ctx = TransformContext::new(source, target);
342 let body = br#"{"object":"list","data":[{"id":"gpt-x","created":1,"object":"model","owned_by":"openai"}]}"#;
343 let out = response_bytes(TransformPair::OpenAiToClaudeModels, &ctx, body).unwrap();
344 let v: Value = serde_json::from_slice(&out).unwrap();
345 assert_eq!(v["data"][0]["id"], "gpt-x");
346 }
347}