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 crate::codex_namespaces::restore_qualified_tool_names(
234 &mut output.body,
235 &crate::codex_namespaces::qualified_tool_origins(request_extensions),
236 );
237 crate::codex_custom_tools::restore_custom_tool_calls(
238 &mut output.body,
239 &crate::codex_custom_tools::custom_tool_names(request_extensions),
240 );
241 Ok(output)
242 }
243
244 pub fn translate_response(
246 &self,
247 source: impl Into<FormatId>,
248 target: impl Into<FormatId>,
249 body: &Value,
250 policy: &TranslationPolicy,
251 ) -> Result<TranslationOutput> {
252 let source = source.into();
253 let target = target.into();
254 let decoded = self
255 .registry
256 .codec(source.clone())?
257 .decode_response(body, policy)?;
258 let encoded = self
259 .registry
260 .codec(target.clone())?
261 .encode_response(&decoded.response, policy)?;
262 Ok(TranslationOutput {
263 body: encoded.body,
264 diagnostics: with_formats(decoded.diagnostics, encoded.diagnostics, source, target),
265 })
266 }
267
268 pub fn translate_event(
270 &self,
271 state: &mut StreamTranslationState,
272 source: impl Into<FormatId>,
273 target: impl Into<FormatId>,
274 event: &Value,
275 ) -> Result<Vec<Value>> {
276 let source = source.into();
277 let target = target.into();
278 let source_codec = self.stream_registry.codec(source.clone())?;
279 let target_codec = self.stream_registry.codec(target.clone())?;
280 if let Err(error) =
281 crate::codecs::responses::validate_stream_output(&source, &target, event)
282 {
283 return Ok(target_codec.encode_event(
284 state,
285 crate::LlmResponseChunk::DecodeError {
286 message: error.to_string(),
287 },
288 ));
289 }
290 let canonical = source_codec.decode_event(state, event);
291 state.source = Some(source);
292 state.target = Some(target);
293 Ok(canonical
294 .into_iter()
295 .flat_map(|event| target_codec.encode_event(state, event))
296 .collect())
297 }
298
299 pub fn decode_stream_event(
304 &self,
305 state: &mut StreamTranslationState,
306 source: impl Into<FormatId>,
307 event: Value,
308 ) -> Result<LlmResponseStreamEvent> {
309 let source = source.into();
310 let source_codec = self.stream_registry.codec(source.clone())?;
311 state.source = Some(source.clone());
312 let normalized = source_codec.decode_event(state, &event);
313 Ok(LlmResponseStreamEvent::preserved(source, event, normalized))
314 }
315
316 pub fn encode_stream_event(
322 &self,
323 state: &mut StreamTranslationState,
324 target: impl Into<FormatId>,
325 event: LlmResponseStreamEvent,
326 ) -> Result<Vec<Value>> {
327 let target = target.into();
328 let target_codec = self.stream_registry.codec(target.clone())?;
329 state.target = Some(target.clone());
330 Ok(encode_response_stream_event(
331 state,
332 target_codec.as_ref(),
333 &target,
334 event,
335 ))
336 }
337
338 pub fn finish_stream(
340 &self,
341 state: &mut StreamTranslationState,
342 target: impl Into<FormatId>,
343 ) -> Result<Vec<Value>> {
344 let target = target.into();
345 let target_codec = self.stream_registry.codec(target)?;
346 Ok(target_codec.finish(state))
347 }
348
349 pub fn encode_stream(
356 &self,
357 target: impl Into<FormatId>,
358 chunks: LlmResponseStream,
359 served_model: Option<String>,
360 ) -> std::result::Result<RawEventStream, LlmClientError> {
361 self.encode_stream_with_extensions(
362 target,
363 chunks,
364 served_model,
365 &ProviderExtensions::default(),
366 )
367 }
368
369 pub fn encode_stream_with_extensions(
374 &self,
375 target: impl Into<FormatId>,
376 chunks: LlmResponseStream,
377 served_model: Option<String>,
378 request_extensions: &ProviderExtensions,
379 ) -> std::result::Result<RawEventStream, LlmClientError> {
380 let origins = crate::codex_namespaces::qualified_tool_origins(request_extensions);
381 let custom_tools = crate::codex_custom_tools::custom_tool_names(request_extensions);
382 let mut custom_state = crate::codex_custom_tools::CustomToolCallStreamState::default();
383 let target_format: FormatId = target.into();
384 let codec = self
385 .stream_registry
386 .codec(target_format.clone())
387 .map_err(|err| LlmClientError::Configuration {
389 message: err.to_string(),
390 })?;
391
392 let served_model_for_events = served_model.clone();
393 let mut state = StreamTranslationState {
394 target: Some(target_format.clone()),
395 target_model: served_model,
396 ..Default::default()
397 };
398 let mut chunks = chunks;
399
400 let events = try_stream! {
401 while let Some(item) = chunks.next().await {
402 let event = item.map_err(LlmStreamError::Client)?;
403 let mut encoded =
404 encode_response_stream_event(&mut state, codec.as_ref(), &target_format, event);
405 for value in &mut encoded {
406 if let Some(model) = served_model_for_events.as_deref() {
407 codec.stamp_streamed_response_model(value, model);
408 }
409 crate::codex_namespaces::restore_qualified_tool_names(value, &origins);
410 }
411 let mut encoded: Vec<Value> = encoded
414 .into_iter()
415 .filter_map(|mut value| {
416 crate::codex_custom_tools::restore_custom_tool_calls_in_event(
417 &mut value,
418 &custom_tools,
419 &mut custom_state,
420 )
421 .then_some(value)
422 })
423 .collect();
424 let terminal = encoded.pop();
425 for value in encoded {
426 yield value;
427 }
428 match (state.errored, terminal) {
429 (false, Some(value)) => yield value,
430 (true, Some(value)) => Err(LlmStreamError::Upstream(value))?,
431 (true, None) => Err(LlmStreamError::Client(LlmClientError::ResponseTranslation(
432 "stream ended after an error without a terminal event".to_string(),
433 )))?,
434 _ => ()
435 }
436 }
437 for mut value in codec.finish(&mut state) {
438 if let Some(model) = served_model_for_events.as_deref() {
439 codec.stamp_streamed_response_model(&mut value, model);
440 }
441 crate::codex_namespaces::restore_qualified_tool_names(&mut value, &origins);
442 if crate::codex_custom_tools::restore_custom_tool_calls_in_event(
443 &mut value,
444 &custom_tools,
445 &mut custom_state,
446 ) {
447 yield value;
448 }
449 }
450 };
451
452 Ok(Box::pin(events))
453 }
454
455 pub fn decode_stream<S>(
466 &self,
467 source: impl Into<FormatId>,
468 bytes: S,
469 ) -> std::result::Result<LlmResponseStream, LlmClientError>
470 where
471 S: Stream<Item = std::result::Result<Vec<u8>, LlmClientError>> + Send + 'static,
472 {
473 let marker = sse::done_marker();
474 let source_format: FormatId = source.into();
475 let codec = self
476 .stream_registry
477 .codec(source_format.clone())
478 .map_err(|error| LlmClientError::ResponseTranslation(error.to_string()))?;
479 let io_bytes: Pin<Box<dyn Stream<Item = std::io::Result<Vec<u8>>> + Send>> =
486 Box::pin(bytes.map(|item| item.map_err(std::io::Error::other)));
487 let lines = futures::io::BufReader::new(io_bytes.into_async_read()).lines();
488
489 let mut state = StreamTranslationState {
490 source: Some(source_format.clone()),
491 ..StreamTranslationState::default()
492 };
493 let mut frame = String::new();
494 let mut saw_terminal = false;
495 let mut saw_error = false;
496 let stream = Box::pin(try_stream! {
497 futures::pin_mut!(lines);
498 while let Some(line) = lines.next().await {
499 let line = line.map_err(llm_client_error_from_io)?;
500 if line.trim_end().is_empty() {
502 let parsed = sse::parse_json_sse_frame(&frame, marker)
503 .map_err(|error| LlmClientError::ResponseTranslation(error.to_string()))?;
504 frame.clear();
505 match parsed {
506 sse::SseFrame::Empty => {}
507 sse::SseFrame::Done => {
508 saw_terminal |= codec.sse_done_is_terminal();
509 break;
510 }
511 sse::SseFrame::Data(value) => {
512 saw_terminal |= codec.is_terminal_event(&value);
513 let normalized = codec.decode_event(&mut state, &value);
514 saw_error |= normalized.iter().any(|chunk| matches!(
515 chunk,
516 LlmResponseChunk::DecodeError { .. }
517 | LlmResponseChunk::StreamError { .. }
518 ));
519 yield LlmResponseStreamEvent::preserved(
520 source_format.clone(),
521 value,
522 normalized,
523 );
524 }
525 }
526 } else {
527 frame.push_str(&line);
528 frame.push('\n');
529 }
530 }
531
532 #[allow(clippy::collapsible_if)]
535 if !frame.trim_end().is_empty() {
536 let parsed = sse::parse_json_sse_frame(&frame, marker)
537 .map_err(|error| LlmClientError::ResponseTranslation(error.to_string()))?;
538 match parsed {
539 sse::SseFrame::Done => {
540 saw_terminal |= codec.sse_done_is_terminal();
541 }
542 sse::SseFrame::Data(value) => {
543 saw_terminal |= codec.is_terminal_event(&value);
544 let normalized = codec.decode_event(&mut state, &value);
545 saw_error |= normalized.iter().any(|chunk| matches!(
546 chunk,
547 LlmResponseChunk::DecodeError { .. }
548 | LlmResponseChunk::StreamError { .. }
549 ));
550 yield LlmResponseStreamEvent::preserved(source_format.clone(), value, normalized);
551 }
552 sse::SseFrame::Empty => {}
553 }
554 }
555
556 if !saw_terminal && !saw_error {
557 Err(LlmClientError::ResponseTranslation(format!(
558 "{source_format} stream ended before a terminal event"
559 )))?;
560 }
561 });
562 Ok(stream)
563 }
564}
565
566fn llm_client_error_from_io(error: std::io::Error) -> LlmClientError {
569 let kind = error.kind();
570 let message = error.to_string();
571 match error.into_inner() {
572 Some(source) => match source.downcast::<LlmClientError>() {
573 Ok(error) => *error,
574 Err(source) => LlmClientError::InvalidResponse { source },
575 },
576 None => LlmClientError::InvalidResponse {
577 source: Box::new(std::io::Error::new(kind, message)),
578 },
579 }
580}
581
582fn with_formats(
584 decoded: Vec<TranslationDiagnostic>,
585 encoded: Vec<TranslationDiagnostic>,
586 source: FormatId,
587 target: FormatId,
588) -> Vec<TranslationDiagnostic> {
589 decoded
590 .into_iter()
591 .chain(encoded)
592 .map(|diagnostic| diagnostic.with_formats(source.clone(), target.clone()))
593 .collect()
594}