switchyard_translation/codecs/
stream.rs1use std::collections::BTreeMap;
7use std::sync::Arc;
8
9use serde::{Deserialize, Serialize};
10use serde_json::{json, Map, Value};
11
12use crate::codecs::anthropic::AnthropicMessagesStreamCodec;
13use crate::codecs::openai_chat::OpenAiChatStreamCodec;
14use crate::codecs::responses::OpenAiResponsesStreamCodec;
15use crate::engine::{FormatRegistry, TranslationEngine};
16use crate::error::{Result, TranslationError};
17use crate::format::{FormatId, WireFormat};
18use crate::llm::{LlmStreamEvent, Usage};
19
20#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
22pub struct StreamTranslationState {
23 pub source: Option<FormatId>,
24 pub target: Option<FormatId>,
25 pub model: Option<String>,
27 pub message_id: Option<String>,
29 pub target_model: Option<String>,
31 pub target_message_id: Option<String>,
33 pub saw_message_start: bool,
34 pub emitted_message_start: bool,
35 pub finished: bool,
36 pub usage: Usage,
37
38 pub(crate) output_tokens_seen: u64,
39 pub(crate) saw_backend_usage: bool,
40 pub(crate) usage_extras: BTreeMap<String, u64>,
41 pub(crate) stop_reason: Option<String>,
42
43 pub(crate) next_content_index: usize,
44 pub(crate) text_block_index: Option<usize>,
45 pub(crate) text_block_started: bool,
46 pub(crate) emitted_content_block: bool,
47 pub(crate) tool_states: BTreeMap<usize, StreamToolState>,
48
49 pub(crate) response_created: bool,
50 pub(crate) response_text_started: bool,
51 pub(crate) response_text_output_index: Option<usize>,
52 pub(crate) response_text: String,
53 pub(crate) response_reasoning_started: bool,
54 pub(crate) response_reasoning_output_index: Option<usize>,
55 pub(crate) response_reasoning_text: String,
56 pub(crate) next_response_output_index: usize,
57
58 pub(crate) reasoning_block_index: Option<usize>,
59 pub(crate) reasoning_block_started: bool,
60}
61
62#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
64pub(crate) struct StreamToolState {
65 pub(crate) id: Option<String>,
66 pub(crate) name: Option<String>,
67 pub(crate) arguments: String,
68 pub(crate) pending_arguments: String,
69 pub(crate) started: bool,
70 pub(crate) content_index: Option<usize>,
71 pub(crate) response_output_index: Option<usize>,
72 pub(crate) response_item_id: Option<String>,
73}
74
75impl StreamTranslationState {
76 pub fn new(source: impl Into<FormatId>, target: impl Into<FormatId>) -> Self {
78 Self {
79 source: Some(source.into()),
80 target: Some(target.into()),
81 ..Self::default()
82 }
83 }
84}
85
86#[derive(Default)]
88pub struct StreamTranslationEngine {
89 engine: TranslationEngine,
90}
91
92pub trait StreamCodec: Send + Sync {
94 fn format(&self) -> FormatId;
96
97 fn decode_event(
99 &self,
100 state: &mut StreamTranslationState,
101 event: &Value,
102 ) -> Vec<LlmStreamEvent>;
103
104 fn encode_event(&self, state: &mut StreamTranslationState, event: LlmStreamEvent)
106 -> Vec<Value>;
107
108 fn finish(&self, state: &mut StreamTranslationState) -> Vec<Value>;
117}
118
119#[derive(Default)]
121pub struct StreamCodecRegistry {
122 codecs: BTreeMap<FormatId, Arc<dyn StreamCodec>>,
123}
124
125impl StreamCodecRegistry {
126 pub fn new() -> Self {
128 Self::default()
129 }
130
131 pub fn with_builtins() -> Self {
133 let mut registry = Self::new();
134 registry.register(OpenAiChatStreamCodec);
135 registry.register(AnthropicMessagesStreamCodec);
136 registry.register(OpenAiResponsesStreamCodec);
137 registry
138 }
139
140 pub fn register(&mut self, codec: impl StreamCodec + 'static) {
142 self.codecs.insert(codec.format(), Arc::new(codec));
143 }
144
145 pub fn codec(&self, format: impl Into<FormatId>) -> Result<Arc<dyn StreamCodec>> {
147 let format = format.into();
148 self.codecs.get(&format).cloned().ok_or_else(|| {
149 TranslationError::Other(format!("no stream codec registered for {format}"))
150 })
151 }
152}
153
154impl StreamTranslationEngine {
155 pub fn new(registry: StreamCodecRegistry) -> Self {
157 Self {
158 engine: TranslationEngine::with_registries(FormatRegistry::with_builtins(), registry),
159 }
160 }
161
162 pub fn translate_event(
164 &self,
165 state: &mut StreamTranslationState,
166 source: impl Into<FormatId>,
167 target: impl Into<FormatId>,
168 event: &Value,
169 ) -> Result<Vec<Value>> {
170 self.engine.translate_event(state, source, target, event)
171 }
172
173 pub fn finish(
175 &self,
176 state: &mut StreamTranslationState,
177 target: impl Into<FormatId>,
178 ) -> Result<Vec<Value>> {
179 self.engine.finish_stream(state, target)
180 }
181
182 pub fn translate_event_with_builtins(
184 state: &mut StreamTranslationState,
185 source: impl Into<FormatId>,
186 target: impl Into<FormatId>,
187 event: &Value,
188 ) -> Vec<Value> {
189 Self::default()
190 .translate_event(state, source, target, event)
191 .unwrap_or_else(|error| vec![json!({"error": {"message": error.to_string()}})])
192 }
193}
194
195pub fn decode_stream_event(
197 state: &mut StreamTranslationState,
198 source: impl Into<FormatId>,
199 event: &Value,
200) -> Vec<LlmStreamEvent> {
201 let source = source.into();
202 StreamCodecRegistry::with_builtins()
203 .codec(source)
204 .map(|codec| codec.decode_event(state, event))
205 .unwrap_or_else(|error| {
206 vec![LlmStreamEvent::Error {
207 message: error.to_string(),
208 }]
209 })
210}
211
212pub fn encode_stream_event(
214 state: &mut StreamTranslationState,
215 target: impl Into<FormatId>,
216 event: LlmStreamEvent,
217) -> Vec<Value> {
218 StreamCodecRegistry::with_builtins()
219 .codec(target)
220 .map(|codec| codec.encode_event(state, event))
221 .unwrap_or_else(|error| vec![json!({"error": {"message": error.to_string()}})])
222}
223
224pub(crate) fn record_source_identity(
226 state: &mut StreamTranslationState,
227 id: Option<String>,
228 model: Option<String>,
229) {
230 if id.is_some() {
231 state.message_id = id;
232 }
233 if model.is_some() {
234 state.model = model;
235 }
236}
237
238pub(crate) fn source_model_or_unknown(state: &StreamTranslationState) -> String {
240 state.model.clone().unwrap_or_else(|| "unknown".to_string())
241}
242
243pub(crate) fn target_model_or_source_model(state: &StreamTranslationState) -> String {
245 state
246 .target_model
247 .clone()
248 .or_else(|| state.model.clone())
249 .unwrap_or_else(|| "unknown".to_string())
250}
251
252pub(crate) fn target_message_id_or_source_message_id(
254 state: &StreamTranslationState,
255) -> Option<&str> {
256 state
257 .target_message_id
258 .as_deref()
259 .or(state.message_id.as_deref())
260}
261
262pub(crate) fn state_source_is(state: &StreamTranslationState, format: WireFormat) -> bool {
264 let format_id: FormatId = format.into();
265 match &state.source {
266 Some(source) => source == &format_id,
267 None => false,
268 }
269}
270
271pub(crate) fn string_field(object: &Map<String, Value>, key: &str) -> Option<String> {
273 object
274 .get(key)
275 .and_then(Value::as_str)
276 .filter(|value| !value.is_empty())
277 .map(ToOwned::to_owned)
278}