OpenPay (1 crate, 2174 LOC, 14 tests)
op-payouts Multi-rail payout engine. Visa Direct, Mastercard Send,
ACH credits (NACHA), Fedwire + SWIFT MT103 + ISO 20022
pacs.008, SEPA SCT + SCT Inst (mod-97 IBAN), UK FPS,
FedNow, RTP, PayPal, Wise. Crypto: USDC/USDT on
ETH/Polygon/Base/Solana + BTC + Lightning with real
ERC-20 transfer(address,uint256) calldata. Offline-pure
drivers (emit wire payloads — operators transmit).
Smart Byte (1 crate, 2605 LOC, 38 tests)
smart-byte-nostr Nostr adapter. NIP-01 canonical event id + BIP-340
Schnorr sign/verify; from-scratch bech32 + NIP-19 TLV
(npub/nsec/note/nprofile/nevent/nrelay); NIP-04 legacy
AES-256-CBC DMs; NIP-44 v2 HKDF-SHA256→ChaCha20+HMAC
with bucketed padding; NIP-17 gift-wrap (kind 1059) +
seal (kind 13) + rumor (kind 14); NIP-05/11/13/26/65.
Relay client message vocabulary (REQ/EVENT/CLOSE/AUTH/
EOSE/OK/NOTICE/CLOSED) + in-memory router.
EOC (1 crate, 2024 LOC, 30 tests)
eoc-streaming Token-by-token + tool-call streaming primitives.
SSE framing (data: lines); Anthropic event mapper
(message_start/content_block_*/message_delta/stop/ping);
OpenAI delta mapper; Vercel AI SDK mapper. Bounded-channel
backpressure with watermarks; CancelToken propagation;
Last-Event-ID resumable streams; per-stream joule meter.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
224 lines
6.6 KiB
Rust
224 lines
6.6 KiB
Rust
//! The normalized event vocabulary every provider mapper targets.
|
|
//!
|
|
//! [`Event`] is the lingua franca: providers emit framed bytes, mappers
|
|
//! turn those into [`Event`]s, and downstream consumers (cascade,
|
|
//! agent, HTTP/WS bridge) only ever see [`Event`]. The [`TokenStream`]
|
|
//! is a thin wrapper around a bounded MPSC channel so the sink end can
|
|
//! exert backpressure and the receiver end can poll like any other
|
|
//! tokio stream.
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use crate::error::{StreamError, StreamResult};
|
|
|
|
/// Speaker role for a content block.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
#[serde(rename_all = "lowercase")]
|
|
pub enum Role {
|
|
/// System / instruction author.
|
|
System,
|
|
/// End user.
|
|
User,
|
|
/// Model output.
|
|
Assistant,
|
|
/// Tool / function response.
|
|
Tool,
|
|
}
|
|
|
|
/// Why the stream stopped.
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum FinishReason {
|
|
/// Model produced an end-of-message token.
|
|
EndTurn,
|
|
/// Model hit `max_tokens` / `max_output_tokens`.
|
|
MaxTokens,
|
|
/// Model emitted a tool call and is awaiting a response.
|
|
ToolUse,
|
|
/// Content was filtered by safety policy.
|
|
ContentFilter,
|
|
/// The stream was cancelled.
|
|
Cancelled,
|
|
/// Provider-specific reason.
|
|
Other(String),
|
|
}
|
|
|
|
/// Normalized stream event.
|
|
///
|
|
/// Every provider mapper produces zero or more of these per upstream
|
|
/// frame. Consumers should treat unknown fields as forward-compatible.
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(tag = "type", rename_all = "snake_case")]
|
|
pub enum Event {
|
|
/// The model started a new message.
|
|
MessageStart {
|
|
/// Optional message id (Anthropic, OpenAI both expose one).
|
|
id: Option<String>,
|
|
/// Speaker role for the message.
|
|
role: Role,
|
|
},
|
|
/// A token-level text delta.
|
|
TextDelta {
|
|
/// Index of the content block this delta belongs to.
|
|
index: u32,
|
|
/// The new text chunk.
|
|
delta: String,
|
|
},
|
|
/// A tool/function call was emitted by the model.
|
|
ToolCallStart {
|
|
/// Index of the content block.
|
|
index: u32,
|
|
/// Provider-issued call id.
|
|
id: String,
|
|
/// Tool name.
|
|
name: String,
|
|
},
|
|
/// Streaming arguments for an in-progress tool call.
|
|
ToolCallDelta {
|
|
/// Index of the content block.
|
|
index: u32,
|
|
/// Provider-issued call id.
|
|
id: String,
|
|
/// Argument JSON fragment.
|
|
arguments_delta: String,
|
|
},
|
|
/// A tool call is complete.
|
|
ToolCallEnd {
|
|
/// Index of the content block.
|
|
index: u32,
|
|
/// Provider-issued call id.
|
|
id: String,
|
|
},
|
|
/// A tool-execution result was injected into the stream.
|
|
ToolResult {
|
|
/// Provider-issued call id this result satisfies.
|
|
id: String,
|
|
/// Result payload (opaque JSON-as-string).
|
|
result: String,
|
|
},
|
|
/// A content block (text or tool call) closed.
|
|
ContentBlockStop {
|
|
/// Index of the content block that just closed.
|
|
index: u32,
|
|
},
|
|
/// Provider liveness ping (Anthropic sends these every ~15s).
|
|
Ping,
|
|
/// The stream is finishing.
|
|
MessageDelta {
|
|
/// Optional partial reason — Anthropic emits this before stop.
|
|
stop_reason: Option<FinishReason>,
|
|
},
|
|
/// Final event — the model has stopped emitting.
|
|
MessageStop {
|
|
/// Why the stream stopped.
|
|
reason: FinishReason,
|
|
},
|
|
/// Provider raised an error mid-stream.
|
|
Error {
|
|
/// Provider-supplied error code.
|
|
code: String,
|
|
/// Human-readable message.
|
|
message: String,
|
|
},
|
|
}
|
|
|
|
/// Anything that can absorb [`Event`]s.
|
|
///
|
|
/// Implementors decide whether to block, error on backpressure, or drop.
|
|
#[async_trait::async_trait]
|
|
pub trait Sink: Send + Sync {
|
|
/// Send an event. Returns `Closed` if the sink is shut down.
|
|
async fn send(&self, event: Event) -> StreamResult<()>;
|
|
}
|
|
|
|
/// A bounded MPSC stream of normalized events.
|
|
///
|
|
/// Construct with [`TokenStream::bounded`]; clone the sink and hand it
|
|
/// to mappers; consume via [`TokenStream::recv`].
|
|
#[derive(Debug)]
|
|
pub struct TokenStream {
|
|
rx: tokio::sync::mpsc::Receiver<Event>,
|
|
tx: tokio::sync::mpsc::Sender<Event>,
|
|
}
|
|
|
|
impl TokenStream {
|
|
/// Construct a bounded stream with the given capacity.
|
|
pub fn bounded(capacity: usize) -> Self {
|
|
let cap = capacity.max(1);
|
|
let (tx, rx) = tokio::sync::mpsc::channel(cap);
|
|
Self { rx, tx }
|
|
}
|
|
|
|
/// Clone a [`Sink`] handle for this stream.
|
|
pub fn sink(&self) -> StreamSink {
|
|
StreamSink {
|
|
tx: self.tx.clone(),
|
|
}
|
|
}
|
|
|
|
/// Receive the next event, or `None` once all sinks are dropped.
|
|
pub async fn recv(&mut self) -> Option<Event> {
|
|
self.rx.recv().await
|
|
}
|
|
|
|
/// Close the receive end. Any further sends will fail with `Closed`.
|
|
pub fn close(&mut self) {
|
|
self.rx.close();
|
|
}
|
|
}
|
|
|
|
/// A clonable sink handle returned by [`TokenStream::sink`].
|
|
#[derive(Clone, Debug)]
|
|
pub struct StreamSink {
|
|
tx: tokio::sync::mpsc::Sender<Event>,
|
|
}
|
|
|
|
impl StreamSink {
|
|
/// Non-blocking send. Returns `Backpressure` if the channel is full.
|
|
pub fn try_send(&self, event: Event) -> StreamResult<()> {
|
|
use tokio::sync::mpsc::error::TrySendError;
|
|
self.tx.try_send(event).map_err(|e| match e {
|
|
TrySendError::Full(_) => StreamError::Backpressure,
|
|
TrySendError::Closed(_) => StreamError::Closed,
|
|
})
|
|
}
|
|
}
|
|
|
|
#[async_trait::async_trait]
|
|
impl Sink for StreamSink {
|
|
async fn send(&self, event: Event) -> StreamResult<()> {
|
|
self.tx
|
|
.send(event)
|
|
.await
|
|
.map_err(|_| StreamError::Closed)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[tokio::test]
|
|
async fn roundtrip_text_delta() {
|
|
let mut s = TokenStream::bounded(4);
|
|
let sink = s.sink();
|
|
sink.send(Event::MessageStart {
|
|
id: Some("m1".into()),
|
|
role: Role::Assistant,
|
|
})
|
|
.await
|
|
.expect("send start");
|
|
sink.send(Event::TextDelta {
|
|
index: 0,
|
|
delta: "hi".into(),
|
|
})
|
|
.await
|
|
.expect("send delta");
|
|
assert!(matches!(s.recv().await, Some(Event::MessageStart { .. })));
|
|
assert!(matches!(s.recv().await, Some(Event::TextDelta { .. })));
|
|
// Closing the receiver forces `recv` to drain to `None` without
|
|
// requiring every cloned sender to be dropped.
|
|
s.close();
|
|
assert!(s.recv().await.is_none());
|
|
}
|
|
}
|