1use std::collections::BTreeMap;
7use std::sync::Arc;
8
9use serde::{Deserialize, Serialize};
10use serde_json::{Map, Value, json};
11
12use crate::LlmResponseChunk;
13use crate::codecs::anthropic::AnthropicMessagesStreamCodec;
14use crate::codecs::openai_chat::OpenAiChatStreamCodec;
15use crate::codecs::responses::OpenAiResponsesStreamCodec;
16use crate::engine::{FormatRegistry, TranslationEngine};
17use crate::error::{Result, TranslationError};
18use crate::format::{FormatId, WireFormat};
19use crate::llm::Usage;
20
21#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
23pub struct StreamTranslationState {
24 pub source: Option<FormatId>,
25 pub target: Option<FormatId>,
26 pub model: Option<String>,
28 pub message_id: Option<String>,
30 pub target_model: Option<String>,
33 pub target_message_id: Option<String>,
35 pub saw_message_start: bool,
36 pub emitted_message_start: bool,
37 pub finished: bool,
38 pub errored: bool,
40 pub usage: Usage,
41
42 pub(crate) output_tokens_seen: u64,
43 pub(crate) saw_backend_usage: bool,
44 pub(crate) stop_reason: Option<String>,
45 pub(crate) stop_details: Option<Value>,
48 pub(crate) emitted_message_delta: bool,
49
50 pub(crate) next_content_index: usize,
51 pub(crate) text_block_index: Option<usize>,
52 pub(crate) text_block_started: bool,
53 pub(crate) emitted_content_block: bool,
54 pub(crate) tool_states: BTreeMap<usize, StreamToolState>,
55 #[serde(default)]
56 pub(crate) pending_chat_tool_names: BTreeMap<usize, String>,
57 #[serde(default)]
58 pub(crate) active_anthropic_tool: Option<usize>,
59 #[serde(default)]
60 pub(crate) deferred_anthropic_tools: Vec<usize>,
61 #[serde(default)]
63 pub(crate) empty_anthropic_tool_inputs: std::collections::BTreeSet<usize>,
64 pub(crate) decoded_reasoning: BTreeMap<usize, String>,
67 pub(crate) decoded_reasoning_encrypted: std::collections::BTreeSet<usize>,
69 pub(crate) decoded_tool_call: bool,
72 #[serde(default)]
74 pub(crate) decoded_response_text: BTreeMap<usize, BTreeMap<usize, String>>,
75
76 pub(crate) response_created: bool,
77 pub(crate) response_text_started: bool,
78 pub(crate) response_text_output_index: Option<usize>,
79 pub(crate) response_text: String,
80 pub(crate) response_reasoning: BTreeMap<usize, ResponseReasoningState>,
84 pub(crate) decoded_reasoning_ids: std::collections::BTreeSet<usize>,
86 pub(crate) next_response_output_index: usize,
87 pub(crate) response_sequence_number: u64,
88 pub(crate) next_chat_tool_index: usize,
89
90 pub(crate) reasoning_block_index: Option<usize>,
91 pub(crate) reasoning_block_started: bool,
92}
93
94#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
96pub(crate) struct ResponseReasoningState {
97 pub(crate) started: bool,
99 pub(crate) summary_started: bool,
102 pub(crate) output_index: Option<usize>,
103 pub(crate) item_id: Option<String>,
106 pub(crate) text: String,
107 pub(crate) encrypted: Option<String>,
110 #[serde(default)]
111 pub(crate) anthropic_signature: Option<String>,
112 #[serde(default)]
115 pub(crate) pending_text: String,
116}
117
118#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
120pub(crate) struct StreamToolState {
121 pub(crate) id: Option<String>,
122 pub(crate) name: Option<String>,
123 pub(crate) arguments: String,
124 pub(crate) decoded_arguments: String,
132 #[serde(default)]
133 pub(crate) has_decoded_identity: bool,
134 #[serde(default)]
138 pub(crate) decoded_custom_call: bool,
139 pub(crate) pending_arguments: String,
140 pub(crate) started: bool,
141 pub(crate) content_index: Option<usize>,
142 pub(crate) chat_tool_index: Option<usize>,
146 pub(crate) response_output_index: Option<usize>,
147 pub(crate) response_item_id: Option<String>,
148}
149
150impl StreamTranslationState {
151 pub fn new(source: impl Into<FormatId>, target: impl Into<FormatId>) -> Self {
153 Self {
154 source: Some(source.into()),
155 target: Some(target.into()),
156 ..Self::default()
157 }
158 }
159}
160
161#[derive(Default)]
163pub struct StreamTranslationEngine {
164 engine: TranslationEngine,
165}
166
167pub trait StreamCodec: Send + Sync {
169 fn format(&self) -> FormatId;
171
172 fn is_terminal_event(&self, event: &Value) -> bool;
174
175 fn stops_at_terminal_event(&self) -> bool {
179 false
180 }
181
182 fn sse_done_is_terminal(&self) -> bool;
187
188 fn stamp_streamed_response_model(&self, event: &mut Value, served_model: &str);
192
193 fn decode_event(
195 &self,
196 state: &mut StreamTranslationState,
197 event: &Value,
198 ) -> Vec<LlmResponseChunk>;
199
200 fn encode_event(
202 &self,
203 state: &mut StreamTranslationState,
204 event: LlmResponseChunk,
205 ) -> Vec<Value>;
206
207 fn observe_replayed_event(
215 &self,
216 state: &mut StreamTranslationState,
217 _raw: &Value,
218 normalized: Vec<LlmResponseChunk>,
219 ) {
220 let replayed_terminal = normalized
221 .iter()
222 .any(|chunk| matches!(chunk, LlmResponseChunk::MessageStop { .. }));
223 for chunk in normalized {
224 drop(self.encode_event(state, chunk));
225 }
226 if replayed_terminal {
227 state.finished = true;
228 }
229 }
230
231 fn finish(&self, state: &mut StreamTranslationState) -> Vec<Value>;
240}
241
242#[derive(Default)]
244pub struct StreamCodecRegistry {
245 codecs: BTreeMap<FormatId, Arc<dyn StreamCodec>>,
246}
247
248impl StreamCodecRegistry {
249 pub fn new() -> Self {
251 Self::default()
252 }
253
254 pub fn with_builtins() -> Self {
256 let mut registry = Self::new();
257 registry.register(OpenAiChatStreamCodec);
258 registry.register(AnthropicMessagesStreamCodec);
259 registry.register(OpenAiResponsesStreamCodec);
260 registry
261 }
262
263 pub fn register(&mut self, codec: impl StreamCodec + 'static) {
265 self.codecs.insert(codec.format(), Arc::new(codec));
266 }
267
268 pub fn codec(&self, format: impl Into<FormatId>) -> Result<Arc<dyn StreamCodec>> {
270 let format = format.into();
271 self.codecs.get(&format).cloned().ok_or_else(|| {
272 TranslationError::Other(format!("no stream codec registered for {format}"))
273 })
274 }
275}
276
277impl StreamTranslationEngine {
278 pub fn new(registry: StreamCodecRegistry) -> Self {
280 Self {
281 engine: TranslationEngine::with_registries(FormatRegistry::with_builtins(), registry),
282 }
283 }
284
285 pub fn translate_event(
287 &self,
288 state: &mut StreamTranslationState,
289 source: impl Into<FormatId>,
290 target: impl Into<FormatId>,
291 event: &Value,
292 ) -> Result<Vec<Value>> {
293 self.engine.translate_event(state, source, target, event)
294 }
295
296 pub fn finish(
298 &self,
299 state: &mut StreamTranslationState,
300 target: impl Into<FormatId>,
301 ) -> Result<Vec<Value>> {
302 self.engine.finish_stream(state, target)
303 }
304
305 pub fn translate_event_with_builtins(
307 state: &mut StreamTranslationState,
308 source: impl Into<FormatId>,
309 target: impl Into<FormatId>,
310 event: &Value,
311 ) -> Vec<Value> {
312 Self::default()
313 .translate_event(state, source, target, event)
314 .unwrap_or_else(|error| vec![json!({"error": {"message": error.to_string()}})])
315 }
316}
317
318pub fn decode_stream_event(
320 state: &mut StreamTranslationState,
321 source: impl Into<FormatId>,
322 event: &Value,
323) -> Vec<LlmResponseChunk> {
324 let source = source.into();
325 StreamCodecRegistry::with_builtins()
326 .codec(source)
327 .map(|codec| codec.decode_event(state, event))
328 .unwrap_or_else(|error| {
329 vec![LlmResponseChunk::DecodeError {
332 message: error.to_string(),
333 }]
334 })
335}
336
337pub(crate) fn encode_response_stream_event(
338 state: &mut StreamTranslationState,
339 target_codec: &dyn StreamCodec,
340 target: &FormatId,
341 event: crate::LlmResponseStreamEvent,
342) -> Vec<Value> {
343 if state.errored {
344 return Vec::new();
345 }
346 let (preservation, normalized) = event.into_parts();
347 if let Some(preservation) = preservation {
348 let (source, raw) = preservation.into_parts();
349 if let Err(error) = super::responses::validate_stream_output(&source, target, &raw) {
350 return target_codec.encode_event(
351 state,
352 LlmResponseChunk::DecodeError {
353 message: error.to_string(),
354 },
355 );
356 }
357 let has_decode_error = normalized
359 .iter()
360 .any(|chunk| matches!(chunk, LlmResponseChunk::DecodeError { .. }));
361 if &source == target && !has_decode_error {
362 target_codec.observe_replayed_event(state, &raw, normalized);
367 return vec![raw];
368 }
369 }
370
371 normalized
372 .into_iter()
373 .flat_map(|chunk| target_codec.encode_event(state, chunk))
374 .collect()
375}
376
377pub fn encode_stream_event(
379 state: &mut StreamTranslationState,
380 target: impl Into<FormatId>,
381 event: LlmResponseChunk,
382) -> Vec<Value> {
383 StreamCodecRegistry::with_builtins()
384 .codec(target)
385 .map(|codec| codec.encode_event(state, event))
386 .unwrap_or_else(|error| vec![json!({"error": {"message": error.to_string()}})])
387}
388
389pub(crate) fn record_source_identity(
391 state: &mut StreamTranslationState,
392 id: Option<String>,
393 model: Option<String>,
394) {
395 if id.is_some() {
396 state.message_id = id;
397 }
398 if model.is_some() {
399 state.model = model;
400 }
401}
402
403pub(crate) fn target_model_or_source_model(state: &StreamTranslationState) -> String {
405 state
406 .target_model
407 .clone()
408 .or_else(|| state.model.clone())
409 .unwrap_or_else(|| "unknown".to_string())
410}
411
412pub(crate) fn target_message_id_or_source_message_id(
414 state: &StreamTranslationState,
415) -> Option<&str> {
416 state
417 .target_message_id
418 .as_deref()
419 .or(state.message_id.as_deref())
420}
421
422pub(crate) fn state_source_is(state: &StreamTranslationState, format: WireFormat) -> bool {
424 let format_id: FormatId = format.into();
425 match &state.source {
426 Some(source) => source == &format_id,
427 None => false,
428 }
429}
430
431pub(crate) fn string_field(object: &Map<String, Value>, key: &str) -> Option<String> {
433 object
434 .get(key)
435 .and_then(Value::as_str)
436 .filter(|value| !value.is_empty())
437 .map(ToOwned::to_owned)
438}