use crate::ModelIden;
use crate::chat::StreamEnd;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::sync::atomic::{AtomicU64, Ordering};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RawFrame {
pub index: u32,
pub event: Option<String>,
pub data: RawFrameData,
pub elapsed_us: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RawFrameData {
Json(Value),
Text(String),
}
impl RawFrameData {
pub fn as_json(&self) -> Option<&Value> {
match self {
RawFrameData::Json(value) => Some(value),
RawFrameData::Text(_) => None,
}
}
pub fn as_text(&self) -> Option<&str> {
match self {
RawFrameData::Json(_) => None,
RawFrameData::Text(text) => Some(text),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct RawFrameRef<'a> {
pub index: u32,
pub event: Option<&'a str>,
pub data: &'a str,
pub elapsed_us: u64,
}
impl RawFrameRef<'_> {
pub fn json(&self) -> Option<Value> {
serde_json::from_str(self.data).ok()
}
pub fn to_owned_frame(&self) -> RawFrame {
let data = match serde_json::from_str::<Value>(self.data) {
Ok(value) => RawFrameData::Json(value),
Err(_) => RawFrameData::Text(self.data.to_string()),
};
RawFrame {
index: self.index,
event: self.event.map(String::from),
data,
elapsed_us: self.elapsed_us,
}
}
}
#[derive(Debug, Clone)]
pub struct FrameCtx {
pub stream_id: u64,
pub model_iden: ModelIden,
}
impl FrameCtx {
pub(crate) fn new(model_iden: ModelIden) -> Self {
static NEXT_STREAM_ID: AtomicU64 = AtomicU64::new(1);
Self {
stream_id: NEXT_STREAM_ID.fetch_add(1, Ordering::Relaxed),
model_iden,
}
}
}
pub trait ChatFrameSink: Send + Sync {
fn on_frame(&self, ctx: &FrameCtx, frame: RawFrameRef<'_>);
fn on_end(&self, _ctx: &FrameCtx, _end: &StreamEnd) {}
fn on_error(&self, _ctx: &FrameCtx, _err: &crate::Error) {}
}
impl std::fmt::Debug for dyn ChatFrameSink {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "ChatFrameSink")
}
}
pub struct FnSink<F>(F);
impl<F> FnSink<F>
where
F: Fn(&FrameCtx, RawFrameRef<'_>) + Send + Sync + 'static,
{
pub fn new(f: F) -> Self {
Self(f)
}
}
impl<F> ChatFrameSink for FnSink<F>
where
F: Fn(&FrameCtx, RawFrameRef<'_>) + Send + Sync + 'static,
{
fn on_frame(&self, ctx: &FrameCtx, frame: RawFrameRef<'_>) {
(self.0)(ctx, frame)
}
}
#[derive(Debug, Default)]
pub struct CollectorSink {
frames: std::sync::Mutex<Vec<RawFrame>>,
}
impl CollectorSink {
pub fn new() -> Self {
Self::default()
}
pub fn frames(&self) -> Vec<RawFrame> {
self.frames.lock().map(|frames| frames.clone()).unwrap_or_default()
}
pub fn len(&self) -> usize {
self.frames.lock().map(|frames| frames.len()).unwrap_or(0)
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn take(&self) -> Vec<RawFrame> {
self.frames
.lock()
.map(|mut frames| std::mem::take(&mut *frames))
.unwrap_or_default()
}
}
impl ChatFrameSink for CollectorSink {
fn on_frame(&self, _ctx: &FrameCtx, frame: RawFrameRef<'_>) {
if let Ok(mut frames) = self.frames.lock() {
frames.push(frame.to_owned_frame());
}
}
}
#[derive(Debug)]
pub struct ChannelSink {
tx: tokio::sync::mpsc::Sender<RawFrame>,
dropped: AtomicU64,
}
impl ChannelSink {
pub fn new(tx: tokio::sync::mpsc::Sender<RawFrame>) -> Self {
Self {
tx,
dropped: AtomicU64::new(0),
}
}
pub fn dropped(&self) -> u64 {
self.dropped.load(Ordering::Relaxed)
}
}
impl ChatFrameSink for ChannelSink {
fn on_frame(&self, _ctx: &FrameCtx, frame: RawFrameRef<'_>) {
if self.tx.try_send(frame.to_owned_frame()).is_err() {
self.dropped.fetch_add(1, Ordering::Relaxed);
}
}
}
#[cfg(test)]
mod tests {
type Result<T> = core::result::Result<T, Box<dyn std::error::Error>>;
use super::*;
use crate::adapter::AdapterKind;
use std::sync::Arc;
fn frame_ref<'a>(index: u32, event: Option<&'a str>, data: &'a str) -> RawFrameRef<'a> {
RawFrameRef {
index,
event,
data,
elapsed_us: 42,
}
}
fn ctx() -> FrameCtx {
FrameCtx::new(ModelIden::new(AdapterKind::OpenAI, "gpt-test"))
}
#[test]
fn test_frame_sink_owned_frame_json() -> Result<()> {
let frame = frame_ref(1, Some("content_block_delta"), r#"{"a":1}"#).to_owned_frame();
assert_eq!(frame.index, 1);
assert_eq!(frame.event.as_deref(), Some("content_block_delta"));
assert_eq!(
frame.data.as_json().and_then(|v| v.get("a")).and_then(|v| v.as_i64()),
Some(1)
);
Ok(())
}
#[test]
fn test_frame_sink_owned_frame_text_fallback() -> Result<()> {
let frame = frame_ref(0, None, "[DONE]").to_owned_frame();
assert_eq!(frame.data.as_text(), Some("[DONE]"));
assert!(frame.data.as_json().is_none());
Ok(())
}
#[test]
fn test_frame_sink_raw_frame_serde_roundtrip() -> Result<()> {
let frames = vec![
frame_ref(0, Some("message"), r#"{"a":1}"#).to_owned_frame(),
frame_ref(1, None, "[DONE]").to_owned_frame(),
];
let json = serde_json::to_string(&frames)?;
let back: Vec<RawFrame> = serde_json::from_str(&json)?;
assert_eq!(back.len(), 2);
assert!(back[0].data.as_json().is_some(), "first frame should stay json");
assert_eq!(
back[1].data.as_text(),
Some("[DONE]"),
"text frame should not become json"
);
Ok(())
}
#[test]
fn test_frame_sink_collector_collects() -> Result<()> {
let sink = CollectorSink::new();
let ctx = ctx();
sink.on_frame(&ctx, frame_ref(0, Some("a"), r#"{"n":1}"#));
sink.on_frame(&ctx, frame_ref(1, Some("b"), r#"{"n":2}"#));
assert_eq!(sink.len(), 2);
let frames = sink.take();
assert_eq!(frames.iter().map(|f| f.index).collect::<Vec<_>>(), vec![0, 1]);
assert!(sink.is_empty(), "take should leave the collector empty");
Ok(())
}
#[test]
fn test_frame_sink_fn_sink_calls_closure() -> Result<()> {
let count = Arc::new(AtomicU64::new(0));
let count_clone = count.clone();
let sink = FnSink::new(move |_ctx: &FrameCtx, _frame: RawFrameRef<'_>| {
count_clone.fetch_add(1, Ordering::Relaxed);
});
sink.on_frame(&ctx(), frame_ref(0, None, "{}"));
assert_eq!(count.load(Ordering::Relaxed), 1);
Ok(())
}
}