open-standards/eoc/eoc-rs/crates/eoc-streaming/src/openai.rs
Transaction Science 7ef14c2ea9 ingest wave B: 3 crates · op-payouts + smart-byte-nostr + eoc-streaming
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]>
2026-05-24 13:39:09 -04:00

174 lines
6 KiB
Rust

//! OpenAI Chat Completions streaming delta mapper.
//!
//! Reference frame shape:
//!
//! ```jsonc
//! {
//! "id": "chatcmpl-abc",
//! "object": "chat.completion.chunk",
//! "choices": [{
//! "index": 0,
//! "delta": { "role": "assistant", "content": "Hi" },
//! "finish_reason": null
//! }]
//! }
//! ```
//!
//! The terminating event is the literal SSE payload `[DONE]`, which the
//! caller should detect *before* invoking this mapper (it isn't valid
//! JSON). Tool calls are emitted as `delta.tool_calls[].function.{name,
//! arguments}` fragments; we emit `ToolCallStart` the first time a call
//! id is seen and `ToolCallDelta` for each argument fragment.
use serde_json::Value;
use std::collections::HashSet;
use std::sync::Mutex;
use crate::error::StreamResult;
use crate::stream::{Event, FinishReason, Role};
/// OpenAI → [`Event`] mapper.
///
/// Holds a small amount of state to distinguish the first appearance of
/// a tool call (which gets a `ToolCallStart`) from subsequent argument
/// fragments (`ToolCallDelta`).
#[derive(Debug, Default)]
pub struct OpenAiMapper {
seen_tool_ids: Mutex<HashSet<String>>,
started: Mutex<bool>,
}
impl OpenAiMapper {
/// Construct a fresh mapper.
pub fn new() -> Self {
Self::default()
}
/// Map a parsed OpenAI chunk (the JSON object after `data: `).
pub fn map(&self, data: &str) -> StreamResult<Vec<Event>> {
if data.trim() == "[DONE]" {
return Ok(vec![Event::MessageStop {
reason: FinishReason::EndTurn,
}]);
}
let v: Value = serde_json::from_str(data)?;
let id = v.get("id").and_then(|s| s.as_str()).map(String::from);
let mut out = Vec::new();
let mut started_guard = self.started.lock().map_err(poisoned)?;
if !*started_guard {
*started_guard = true;
out.push(Event::MessageStart {
id: id.clone(),
role: Role::Assistant,
});
}
drop(started_guard);
let choices = v.get("choices").and_then(|c| c.as_array());
let Some(choices) = choices else {
return Ok(out);
};
for choice in choices {
let index = choice
.get("index")
.and_then(|i| i.as_u64())
.unwrap_or(0) as u32;
if let Some(delta) = choice.get("delta") {
if let Some(content) = delta.get("content").and_then(|c| c.as_str())
&& !content.is_empty()
{
out.push(Event::TextDelta {
index,
delta: content.to_string(),
});
}
if let Some(calls) = delta.get("tool_calls").and_then(|c| c.as_array()) {
for call in calls {
let call_idx = call
.get("index")
.and_then(|i| i.as_u64())
.unwrap_or(0) as u32;
let call_id = call
.get("id")
.and_then(|s| s.as_str())
.unwrap_or_default()
.to_string();
let function = call.get("function");
let name = function
.and_then(|f| f.get("name"))
.and_then(|s| s.as_str())
.unwrap_or_default()
.to_string();
let args = function
.and_then(|f| f.get("arguments"))
.and_then(|s| s.as_str())
.unwrap_or_default()
.to_string();
if !call_id.is_empty() {
let mut seen = self.seen_tool_ids.lock().map_err(poisoned)?;
if !seen.contains(&call_id) {
seen.insert(call_id.clone());
out.push(Event::ToolCallStart {
index: call_idx,
id: call_id.clone(),
name: name.clone(),
});
}
drop(seen);
}
if !args.is_empty() {
out.push(Event::ToolCallDelta {
index: call_idx,
id: call_id,
arguments_delta: args,
});
}
}
}
}
if let Some(reason) = choice.get("finish_reason").and_then(|r| r.as_str()) {
out.push(Event::MessageStop {
reason: map_reason(reason),
});
}
}
Ok(out)
}
}
fn map_reason(s: &str) -> FinishReason {
match s {
"stop" => FinishReason::EndTurn,
"length" => FinishReason::MaxTokens,
"tool_calls" | "function_call" => FinishReason::ToolUse,
"content_filter" => FinishReason::ContentFilter,
other => FinishReason::Other(other.to_string()),
}
}
fn poisoned<T>(_: T) -> crate::error::StreamError {
crate::error::StreamError::Backend("mapper state poisoned".into())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn maps_content_delta() {
let m = OpenAiMapper::new();
let frame =
r#"{"id":"x","choices":[{"index":0,"delta":{"content":"Hi"},"finish_reason":null}]}"#;
let out = m.map(frame).unwrap();
assert!(matches!(out[0], Event::MessageStart { .. }));
assert!(matches!(out[1], Event::TextDelta { .. }));
}
#[test]
fn maps_done() {
let m = OpenAiMapper::new();
let out = m.map("[DONE]").unwrap();
assert!(matches!(out[0], Event::MessageStop { .. }));
}
}