1use std::collections::BTreeMap;
7use std::pin::Pin;
8use std::sync::Arc;
9
10use async_stream::try_stream;
11use futures::io::AsyncBufReadExt;
12use futures::{Stream, StreamExt, TryStreamExt};
13use serde_json::Value;
14use switchyard_protocol::LlmClientError;
15
16use crate::codecs::FormatCodec;
17use crate::codecs::anthropic::AnthropicMessagesCodec;
18use crate::codecs::openai_chat::OpenAiChatCodec;
19use crate::codecs::responses::OpenAiResponsesCodec;
20use crate::codecs::stream::{
21 StreamCodecRegistry, StreamTranslationState, encode_response_stream_event,
22};
23use crate::diagnostic::TranslationDiagnostic;
24use crate::error::{Result, TranslationError};
25use crate::format::FormatId;
26use crate::llm::{AggLlmResponse, LlmRequest, ProviderExtensions};
27use crate::policy::TranslationPolicy;
28use crate::sse;
29use crate::{LlmResponseChunk, LlmResponseStream, LlmResponseStreamEvent, LlmStreamError};
30
31pub type RawEventStream =
35 Pin<Box<dyn Stream<Item = std::result::Result<Value, LlmStreamError>> + Send>>;
36
37#[derive(Debug)]
39pub struct TranslationOutput {
40 pub body: Value,
41 pub diagnostics: Vec<TranslationDiagnostic>,
42}
43
44#[derive(Debug)]
46pub struct RequestIrOutput {
47 pub request: LlmRequest,
48 pub diagnostics: Vec<TranslationDiagnostic>,
49}
50
51#[derive(Debug)]
53pub struct ResponseIrOutput {
54 pub response: AggLlmResponse,
55 pub diagnostics: Vec<TranslationDiagnostic>,
56}
57
58#[derive(Default)]
60pub struct FormatRegistry {
61 codecs: BTreeMap<FormatId, Arc<dyn FormatCodec>>,
62}
63
64impl FormatRegistry {
65 pub fn new() -> Self {
67 Self::default()
68 }
69
70 pub fn with_builtins() -> Self {
72 let mut registry = Self::new();
73 registry.register(OpenAiChatCodec);
74 registry.register(AnthropicMessagesCodec);
75 registry.register(OpenAiResponsesCodec);
76 registry
77 }
78
79 pub fn register(&mut self, codec: impl FormatCodec + 'static) {
81 self.codecs.insert(codec.format(), Arc::new(codec));
82 }
83
84 pub fn codec(&self, format: impl Into<FormatId>) -> Result<Arc<dyn FormatCodec>> {
86 let format = format.into();
87 self.codecs
88 .get(&format)
89 .cloned()
90 .ok_or_else(|| TranslationError::Other(format!("no codec registered for {format}")))
91 }
92}
93
94pub struct TranslationEngine {
96 registry: FormatRegistry,
97 stream_registry: StreamCodecRegistry,
98}
99
100impl Default for TranslationEngine {
101 fn default() -> Self {
102 Self {
103 registry: FormatRegistry::with_builtins(),
104 stream_registry: StreamCodecRegistry::with_builtins(),
105 }
106 }
107}
108
109impl TranslationEngine {
110 pub fn new(registry: FormatRegistry) -> Self {
112 Self {
113 registry,
114 stream_registry: StreamCodecRegistry::with_builtins(),
115 }
116 }
117
118 pub fn with_registries(registry: FormatRegistry, stream_registry: StreamCodecRegistry) -> Self {
120 Self {
121 registry,
122 stream_registry,
123 }
124 }
125
126 pub fn decode_request(
128 &self,
129 source: impl Into<FormatId>,
130 body: &Value,
131 policy: &TranslationPolicy,
132 ) -> Result<RequestIrOutput> {
133 let source = source.into();
134 let mut decoded = self.registry.codec(source)?.decode_request(body, policy)?;
135 crate::normalize_compaction(&mut decoded.request, None);
136 Ok(RequestIrOutput {
137 request: decoded.request,
138 diagnostics: decoded.diagnostics,
139 })
140 }
141
142 pub fn encode_request(
148 &self,
149 target: impl Into<FormatId>,
150 request: &LlmRequest,
151 policy: &TranslationPolicy,
152 ) -> Result<TranslationOutput> {
153 let target = target.into();
154 let encoded = self
155 .registry
156 .codec(target)?
157 .encode_request(request, policy)?;
158 Ok(TranslationOutput {
159 body: encoded.body,
160 diagnostics: encoded.diagnostics,
161 })
162 }
163
164 pub fn translate_request(
166 &self,
167 source: impl Into<FormatId>,
168 target: impl Into<FormatId>,
169 body: &Value,
170 policy: &TranslationPolicy,
171 ) -> Result<TranslationOutput> {
172 let source = source.into();
173 let target = target.into();
174 let decoded = self
175 .registry
176 .codec(source.clone())?
177 .decode_request(body, policy)?;
178 let encoded = self
179 .registry
180 .codec(target.clone())?
181 .encode_request(&decoded.request, policy)?;
182 Ok(TranslationOutput {
183 body: encoded.body,
184 diagnostics: with_formats(decoded.diagnostics, encoded.diagnostics, source, target),
185 })
186 }
187
188 pub fn decode_response(
190 &self,
191 source: impl Into<FormatId>,
192 body: &Value,
193 policy: &TranslationPolicy,
194 ) -> Result<ResponseIrOutput> {
195 let source = source.into();
196 let decoded = self.registry.codec(source)?.decode_response(body, policy)?;
197 Ok(ResponseIrOutput {
198 response: decoded.response,
199 diagnostics: decoded.diagnostics,
200 })
201 }
202
203 pub fn encode_response(
205 &self,
206 target: impl Into<FormatId>,
207 response: &AggLlmResponse,
208 policy: &TranslationPolicy,
209 ) -> Result<TranslationOutput> {
210 let target = target.into();
211 let encoded = self
212 .registry
213 .codec(target)?
214 .encode_response(response, policy)?;
215 Ok(TranslationOutput {
216 body: encoded.body,
217 diagnostics: encoded.diagnostics,
218 })
219 }
220
221 pub fn encode_response_with_extensions(
226 &self,
227 target: impl Into<FormatId>,
228 response: &AggLlmResponse,
229 request_extensions: &ProviderExtensions,
230 policy: &TranslationPolicy,
231 ) -> Result<TranslationOutput> {
232 let mut output = self.encode_response(target, response, policy)?;
233 if !request_extensions.fields.is_empty() {
234 crate::codex_namespaces::restore_qualified_tool_names(
235 &mut output.body,
236 &crate::codex_namespaces::qualified_tool_origins(request_extensions),
237 );
238 crate::codex_custom_tools::restore_custom_tool_calls(
239 &mut output.body,
240 &crate::codex_custom_tools::custom_tool_names(request_extensions),
241 );
242 }
243 Ok(output)
244 }
245
246 pub fn translate_response(
248 &self,
249 source: impl Into<FormatId>,
250 target: impl Into<FormatId>,
251 body: &Value,
252 policy: &TranslationPolicy,
253 ) -> Result<TranslationOutput> {
254 self.translate_response_with_extensions(
255 source,
256 target,
257 body,
258 &ProviderExtensions::default(),
259 policy,
260 )
261 }
262
263 pub fn translate_response_with_extensions(
267 &self,
268 source: impl Into<FormatId>,
269 target: impl Into<FormatId>,
270 body: &Value,
271 request_extensions: &ProviderExtensions,
272 policy: &TranslationPolicy,
273 ) -> Result<TranslationOutput> {
274 let source = source.into();
275 let target = target.into();
276 let decoded = self
277 .registry
278 .codec(source.clone())?
279 .decode_response(body, policy)?;
280 let encoded = self.encode_response_with_extensions(
281 target.clone(),
282 &decoded.response,
283 request_extensions,
284 policy,
285 )?;
286 Ok(TranslationOutput {
287 body: encoded.body,
288 diagnostics: with_formats(decoded.diagnostics, encoded.diagnostics, source, target),
289 })
290 }
291
292 pub fn translate_event(
294 &self,
295 state: &mut StreamTranslationState,
296 source: impl Into<FormatId>,
297 target: impl Into<FormatId>,
298 event: &Value,
299 ) -> Result<Vec<Value>> {
300 let source = source.into();
301 let target = target.into();
302 let source_codec = self.stream_registry.codec(source.clone())?;
303 let target_codec = self.stream_registry.codec(target.clone())?;
304 if let Err(error) =
305 crate::codecs::responses::validate_stream_output(&source, &target, event)
306 {
307 return Ok(target_codec.encode_event(
308 state,
309 crate::LlmResponseChunk::DecodeError {
310 message: error.to_string(),
311 },
312 ));
313 }
314 let canonical = source_codec.decode_event(state, event);
315 state.source = Some(source);
316 state.target = Some(target);
317 Ok(canonical
318 .into_iter()
319 .flat_map(|event| target_codec.encode_event(state, event))
320 .collect())
321 }
322
323 pub fn decode_stream_event(
328 &self,
329 state: &mut StreamTranslationState,
330 source: impl Into<FormatId>,
331 event: Value,
332 ) -> Result<LlmResponseStreamEvent> {
333 let source = source.into();
334 let source_codec = self.stream_registry.codec(source.clone())?;
335 state.source = Some(source.clone());
336 let normalized = source_codec.decode_event(state, &event);
337 Ok(LlmResponseStreamEvent::preserved(source, event, normalized))
338 }
339
340 pub fn encode_stream_event(
346 &self,
347 state: &mut StreamTranslationState,
348 target: impl Into<FormatId>,
349 event: LlmResponseStreamEvent,
350 ) -> Result<Vec<Value>> {
351 let target = target.into();
352 let target_codec = self.stream_registry.codec(target.clone())?;
353 state.target = Some(target.clone());
354 Ok(encode_response_stream_event(
355 state,
356 target_codec.as_ref(),
357 &target,
358 event,
359 ))
360 }
361
362 pub fn finish_stream(
364 &self,
365 state: &mut StreamTranslationState,
366 target: impl Into<FormatId>,
367 ) -> Result<Vec<Value>> {
368 let target = target.into();
369 let target_codec = self.stream_registry.codec(target)?;
370 Ok(target_codec.finish(state))
371 }
372
373 pub fn encode_stream(
380 &self,
381 target: impl Into<FormatId>,
382 chunks: LlmResponseStream,
383 served_model: Option<String>,
384 ) -> std::result::Result<RawEventStream, LlmClientError> {
385 self.encode_stream_with_extensions(
386 target,
387 chunks,
388 served_model,
389 &ProviderExtensions::default(),
390 )
391 }
392
393 pub fn encode_stream_with_extensions(
398 &self,
399 target: impl Into<FormatId>,
400 chunks: LlmResponseStream,
401 served_model: Option<String>,
402 request_extensions: &ProviderExtensions,
403 ) -> std::result::Result<RawEventStream, LlmClientError> {
404 let origins = crate::codex_namespaces::qualified_tool_origins(request_extensions);
405 let custom_tools = crate::codex_custom_tools::custom_tool_names(request_extensions);
406 let mut custom_state = crate::codex_custom_tools::CustomToolCallStreamState::default();
407 let target_format: FormatId = target.into();
408 let codec = self
409 .stream_registry
410 .codec(target_format.clone())
411 .map_err(|err| LlmClientError::Configuration {
413 message: err.to_string(),
414 })?;
415
416 let served_model_for_events = served_model.clone();
417 let mut state = StreamTranslationState {
418 target: Some(target_format.clone()),
419 target_model: served_model,
420 ..Default::default()
421 };
422 let mut chunks = chunks;
423
424 let events = try_stream! {
425 while let Some(item) = chunks.next().await {
426 let event = item.map_err(LlmStreamError::Client)?;
427 let mut encoded =
428 encode_response_stream_event(&mut state, codec.as_ref(), &target_format, event);
429 for value in &mut encoded {
430 if let Some(model) = served_model_for_events.as_deref() {
431 codec.stamp_streamed_response_model(value, model);
432 }
433 crate::codex_namespaces::restore_qualified_tool_names(value, &origins);
434 }
435 let mut encoded: Vec<Value> = encoded
438 .into_iter()
439 .filter_map(|mut value| {
440 crate::codex_custom_tools::restore_custom_tool_calls_in_event(
441 &mut value,
442 &custom_tools,
443 &mut custom_state,
444 )
445 .then_some(value)
446 })
447 .collect();
448 let terminal = encoded.pop();
449 for value in encoded {
450 yield value;
451 }
452 match (state.errored, terminal) {
453 (false, Some(value)) => yield value,
454 (true, Some(value)) => Err(LlmStreamError::Upstream(value))?,
455 (true, None) => Err(LlmStreamError::Client(LlmClientError::ResponseTranslation(
456 "stream ended after an error without a terminal event".to_string(),
457 )))?,
458 _ => ()
459 }
460 }
461 for mut value in codec.finish(&mut state) {
462 if let Some(model) = served_model_for_events.as_deref() {
463 codec.stamp_streamed_response_model(&mut value, model);
464 }
465 crate::codex_namespaces::restore_qualified_tool_names(&mut value, &origins);
466 if crate::codex_custom_tools::restore_custom_tool_calls_in_event(
467 &mut value,
468 &custom_tools,
469 &mut custom_state,
470 ) {
471 yield value;
472 }
473 }
474 };
475
476 Ok(Box::pin(events))
477 }
478
479 pub fn decode_stream<S>(
490 &self,
491 source: impl Into<FormatId>,
492 bytes: S,
493 ) -> std::result::Result<LlmResponseStream, LlmClientError>
494 where
495 S: Stream<Item = std::result::Result<Vec<u8>, LlmClientError>> + Send + 'static,
496 {
497 let marker = sse::done_marker();
498 let source_format: FormatId = source.into();
499 let codec = self
500 .stream_registry
501 .codec(source_format.clone())
502 .map_err(|error| LlmClientError::ResponseTranslation(error.to_string()))?;
503 let io_bytes: Pin<Box<dyn Stream<Item = std::io::Result<Vec<u8>>> + Send>> =
510 Box::pin(bytes.map(|item| item.map_err(std::io::Error::other)));
511 let lines = futures::io::BufReader::new(io_bytes.into_async_read()).lines();
512
513 let mut state = StreamTranslationState {
514 source: Some(source_format.clone()),
515 ..StreamTranslationState::default()
516 };
517 let mut frame = String::new();
518 let mut saw_terminal = false;
519 let mut saw_error = false;
520 let stream = Box::pin(try_stream! {
521 futures::pin_mut!(lines);
522 while let Some(line) = lines.next().await {
523 let line = line.map_err(llm_client_error_from_io)?;
524 if line.trim_end().is_empty() {
526 let parsed = sse::parse_json_sse_frame(&frame, marker)
527 .map_err(|error| LlmClientError::ResponseTranslation(error.to_string()))?;
528 frame.clear();
529 match parsed {
530 sse::SseFrame::Empty => {}
531 sse::SseFrame::Done => {
532 saw_terminal |= codec.sse_done_is_terminal();
533 break;
534 }
535 sse::SseFrame::Data(value) => {
536 saw_terminal |= codec.is_terminal_event(&value);
537 let normalized = codec.decode_event(&mut state, &value);
538 saw_error |= normalized.iter().any(|chunk| matches!(
539 chunk,
540 LlmResponseChunk::DecodeError { .. }
541 | LlmResponseChunk::StreamError { .. }
542 ));
543 yield LlmResponseStreamEvent::preserved(
544 source_format.clone(),
545 value,
546 normalized,
547 );
548 }
549 }
550 } else {
551 frame.push_str(&line);
552 frame.push('\n');
553 }
554 }
555
556 #[allow(clippy::collapsible_if)]
559 if !frame.trim_end().is_empty() {
560 let parsed = sse::parse_json_sse_frame(&frame, marker)
561 .map_err(|error| LlmClientError::ResponseTranslation(error.to_string()))?;
562 match parsed {
563 sse::SseFrame::Done => {
564 saw_terminal |= codec.sse_done_is_terminal();
565 }
566 sse::SseFrame::Data(value) => {
567 saw_terminal |= codec.is_terminal_event(&value);
568 let normalized = codec.decode_event(&mut state, &value);
569 saw_error |= normalized.iter().any(|chunk| matches!(
570 chunk,
571 LlmResponseChunk::DecodeError { .. }
572 | LlmResponseChunk::StreamError { .. }
573 ));
574 yield LlmResponseStreamEvent::preserved(source_format.clone(), value, normalized);
575 }
576 sse::SseFrame::Empty => {}
577 }
578 }
579
580 if !saw_terminal && !saw_error {
581 Err(LlmClientError::ResponseTranslation(format!(
582 "{source_format} stream ended before a terminal event"
583 )))?;
584 }
585 });
586 Ok(stream)
587 }
588}
589
590fn llm_client_error_from_io(error: std::io::Error) -> LlmClientError {
593 let kind = error.kind();
594 let message = error.to_string();
595 match error.into_inner() {
596 Some(source) => match source.downcast::<LlmClientError>() {
597 Ok(error) => *error,
598 Err(source) => LlmClientError::InvalidResponse { source },
599 },
600 None => LlmClientError::InvalidResponse {
601 source: Box::new(std::io::Error::new(kind, message)),
602 },
603 }
604}
605
606fn with_formats(
608 decoded: Vec<TranslationDiagnostic>,
609 encoded: Vec<TranslationDiagnostic>,
610 source: FormatId,
611 target: FormatId,
612) -> Vec<TranslationDiagnostic> {
613 decoded
614 .into_iter()
615 .chain(encoded)
616 .map(|diagnostic| diagnostic.with_formats(source.clone(), target.clone()))
617 .collect()
618}