use std::collections::BTreeMap;
use std::sync::Arc;
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value, json};
use crate::LlmResponseChunk;
use crate::codecs::anthropic::AnthropicMessagesStreamCodec;
use crate::codecs::openai_chat::OpenAiChatStreamCodec;
use crate::codecs::responses::OpenAiResponsesStreamCodec;
use crate::engine::{FormatRegistry, TranslationEngine};
use crate::error::{Result, TranslationError};
use crate::format::{FormatId, WireFormat};
use crate::llm::Usage;
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct StreamTranslationState {
pub source: Option<FormatId>,
pub target: Option<FormatId>,
pub model: Option<String>,
pub message_id: Option<String>,
pub target_model: Option<String>,
pub target_message_id: Option<String>,
pub saw_message_start: bool,
pub emitted_message_start: bool,
pub finished: bool,
pub errored: bool,
pub usage: Usage,
pub(crate) output_tokens_seen: u64,
pub(crate) saw_backend_usage: bool,
pub(crate) stop_reason: Option<String>,
pub(crate) emitted_message_delta: bool,
pub(crate) next_content_index: usize,
pub(crate) text_block_index: Option<usize>,
pub(crate) text_block_started: bool,
pub(crate) emitted_content_block: bool,
pub(crate) tool_states: BTreeMap<usize, StreamToolState>,
pub(crate) response_created: bool,
pub(crate) response_text_started: bool,
pub(crate) response_text_output_index: Option<usize>,
pub(crate) response_text: String,
pub(crate) response_reasoning_started: bool,
pub(crate) response_reasoning_output_index: Option<usize>,
pub(crate) response_reasoning_text: String,
pub(crate) next_response_output_index: usize,
pub(crate) reasoning_block_index: Option<usize>,
pub(crate) reasoning_block_started: bool,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub(crate) struct StreamToolState {
pub(crate) id: Option<String>,
pub(crate) name: Option<String>,
pub(crate) arguments: String,
pub(crate) pending_arguments: String,
pub(crate) started: bool,
pub(crate) content_index: Option<usize>,
pub(crate) response_output_index: Option<usize>,
pub(crate) response_item_id: Option<String>,
}
impl StreamTranslationState {
pub fn new(source: impl Into<FormatId>, target: impl Into<FormatId>) -> Self {
Self {
source: Some(source.into()),
target: Some(target.into()),
..Self::default()
}
}
}
#[derive(Default)]
pub struct StreamTranslationEngine {
engine: TranslationEngine,
}
pub trait StreamCodec: Send + Sync {
fn format(&self) -> FormatId;
fn decode_event(
&self,
state: &mut StreamTranslationState,
event: &Value,
) -> Vec<LlmResponseChunk>;
fn encode_event(
&self,
state: &mut StreamTranslationState,
event: LlmResponseChunk,
) -> Vec<Value>;
fn observe_replayed_event(
&self,
state: &mut StreamTranslationState,
_raw: &Value,
normalized: Vec<LlmResponseChunk>,
) {
let replayed_terminal = normalized
.iter()
.any(|chunk| matches!(chunk, LlmResponseChunk::MessageStop { .. }));
for chunk in normalized {
drop(self.encode_event(state, chunk));
}
if replayed_terminal {
state.finished = true;
}
}
fn finish(&self, state: &mut StreamTranslationState) -> Vec<Value>;
}
#[derive(Default)]
pub struct StreamCodecRegistry {
codecs: BTreeMap<FormatId, Arc<dyn StreamCodec>>,
}
impl StreamCodecRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn with_builtins() -> Self {
let mut registry = Self::new();
registry.register(OpenAiChatStreamCodec);
registry.register(AnthropicMessagesStreamCodec);
registry.register(OpenAiResponsesStreamCodec);
registry
}
pub fn register(&mut self, codec: impl StreamCodec + 'static) {
self.codecs.insert(codec.format(), Arc::new(codec));
}
pub fn codec(&self, format: impl Into<FormatId>) -> Result<Arc<dyn StreamCodec>> {
let format = format.into();
self.codecs.get(&format).cloned().ok_or_else(|| {
TranslationError::Other(format!("no stream codec registered for {format}"))
})
}
}
impl StreamTranslationEngine {
pub fn new(registry: StreamCodecRegistry) -> Self {
Self {
engine: TranslationEngine::with_registries(FormatRegistry::with_builtins(), registry),
}
}
pub fn translate_event(
&self,
state: &mut StreamTranslationState,
source: impl Into<FormatId>,
target: impl Into<FormatId>,
event: &Value,
) -> Result<Vec<Value>> {
self.engine.translate_event(state, source, target, event)
}
pub fn finish(
&self,
state: &mut StreamTranslationState,
target: impl Into<FormatId>,
) -> Result<Vec<Value>> {
self.engine.finish_stream(state, target)
}
pub fn translate_event_with_builtins(
state: &mut StreamTranslationState,
source: impl Into<FormatId>,
target: impl Into<FormatId>,
event: &Value,
) -> Vec<Value> {
Self::default()
.translate_event(state, source, target, event)
.unwrap_or_else(|error| vec![json!({"error": {"message": error.to_string()}})])
}
}
pub fn decode_stream_event(
state: &mut StreamTranslationState,
source: impl Into<FormatId>,
event: &Value,
) -> Vec<LlmResponseChunk> {
let source = source.into();
StreamCodecRegistry::with_builtins()
.codec(source)
.map(|codec| codec.decode_event(state, event))
.unwrap_or_else(|error| {
vec![LlmResponseChunk::DecodeError {
message: error.to_string(),
}]
})
}
pub(crate) fn encode_response_stream_event(
state: &mut StreamTranslationState,
target_codec: &dyn StreamCodec,
target: &FormatId,
event: crate::LlmResponseStreamEvent,
) -> Vec<Value> {
if state.errored {
return Vec::new();
}
let (preservation, normalized) = event.into_parts();
if let Some(preservation) = preservation {
let (source, raw) = preservation.into_parts();
if &source == target {
target_codec.observe_replayed_event(state, &raw, normalized);
return vec![raw];
}
}
normalized
.into_iter()
.flat_map(|chunk| target_codec.encode_event(state, chunk))
.collect()
}
pub fn encode_stream_event(
state: &mut StreamTranslationState,
target: impl Into<FormatId>,
event: LlmResponseChunk,
) -> Vec<Value> {
StreamCodecRegistry::with_builtins()
.codec(target)
.map(|codec| codec.encode_event(state, event))
.unwrap_or_else(|error| vec![json!({"error": {"message": error.to_string()}})])
}
pub(crate) fn record_source_identity(
state: &mut StreamTranslationState,
id: Option<String>,
model: Option<String>,
) {
if id.is_some() {
state.message_id = id;
}
if model.is_some() {
state.model = model;
}
}
pub(crate) fn target_model_or_source_model(state: &StreamTranslationState) -> String {
state
.target_model
.clone()
.or_else(|| state.model.clone())
.unwrap_or_else(|| "unknown".to_string())
}
pub(crate) fn target_message_id_or_source_message_id(
state: &StreamTranslationState,
) -> Option<&str> {
state
.target_message_id
.as_deref()
.or(state.message_id.as_deref())
}
pub(crate) fn state_source_is(state: &StreamTranslationState, format: WireFormat) -> bool {
let format_id: FormatId = format.into();
match &state.source {
Some(source) => source == &format_id,
None => false,
}
}
pub(crate) fn string_field(object: &Map<String, Value>, key: &str) -> Option<String> {
object
.get(key)
.and_then(Value::as_str)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned)
}