pub(crate) mod anthropic;
pub(crate) mod openai;
use crate::chat::config::{ChatRuntimeConfig, ProviderKind};
use crate::chat::message::{ContentBlock, Message, Usage};
use crate::chat::tools::ToolSchema;
use async_trait::async_trait;
use bytes::{Bytes, BytesMut};
use futures::stream::{BoxStream, Stream};
use std::collections::VecDeque;
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll};
#[derive(Debug, Clone)]
pub(crate) struct SystemBlock {
pub text: String,
pub cache: bool,
}
#[derive(Debug, Clone)]
pub(crate) struct ChatRequest {
pub model: String,
pub system: Vec<SystemBlock>,
pub messages: Vec<Message>,
pub tools: Vec<ToolSchema>,
pub max_tokens: u32,
}
#[derive(Debug, Clone)]
pub(crate) enum ProviderEvent {
TextDelta(String),
ToolUseStart { id: String, name: String },
#[allow(dead_code)]
ToolUseEnd {
id: String,
name: String,
input: serde_json::Value,
},
MessageEnd {
stop_reason: String,
usage: Usage,
content: Vec<ContentBlock>,
},
}
#[derive(Debug, thiserror::Error)]
#[allow(dead_code)]
pub(crate) enum ProviderError {
#[error("network error: {0}")]
Network(String),
#[error("auth error: {0}")]
Auth(String),
#[error("api error ({status}): {message}")]
Api { status: u16, message: String },
#[error("decode error: {0}")]
Decode(String),
#[error("provider misconfigured: {0}")]
Config(&'static str),
}
#[async_trait]
pub(crate) trait Provider: Send + Sync {
#[allow(dead_code)]
fn kind(&self) -> ProviderKind;
async fn stream(
&self,
request: ChatRequest,
) -> Result<BoxStream<'static, Result<ProviderEvent, ProviderError>>, ProviderError>;
}
pub(crate) fn build(cfg: ChatRuntimeConfig) -> Arc<dyn Provider> {
match cfg.provider {
ProviderKind::Anthropic => Arc::new(anthropic::AnthropicProvider::new(cfg)),
ProviderKind::OpenAI => Arc::new(openai::OpenAiProvider::new(cfg)),
}
}
pub(super) type EventQueue = VecDeque<Result<ProviderEvent, ProviderError>>;
pub(super) fn scrub_credentials(msg: &str, api_key: &str) -> String {
let mut out = msg.to_string();
if api_key.len() >= 8 {
out = out.replace(api_key, "[redacted]");
}
mask_credential_shapes(&out)
}
fn mask_credential_shapes(s: &str) -> String {
use std::sync::OnceLock;
static RE: OnceLock<regex::Regex> = OnceLock::new();
let re = RE.get_or_init(|| {
regex::Regex::new(
r"(?ix)
( (?:x-api-key|authorization)\s*[:=]\s*[^\r\n,;]+
| bearer\s+[A-Za-z0-9_\-\.=]{16,}
| sk-ant-[A-Za-z0-9_\-]{8,}
| sk-[A-Za-z0-9_\-]{16,}
)",
)
.expect("scrub regex must compile")
});
re.replace_all(s, "[redacted]").into_owned()
}
pub(super) struct SseStreamDriver<S, State> {
upstream: Pin<Box<S>>,
buf: BytesMut,
queue: EventQueue,
state: State,
on_chunk: fn(&str, &mut State, &mut EventQueue),
on_eof: fn(&mut State, &mut EventQueue),
is_finished: fn(&State) -> bool,
upstream_done: bool,
eof_dispatched: bool,
}
impl<S, State> SseStreamDriver<S, State>
where
S: Stream<Item = Result<Bytes, ProviderError>> + Send + 'static,
{
pub(super) fn new(
upstream: S,
state: State,
on_chunk: fn(&str, &mut State, &mut EventQueue),
on_eof: fn(&mut State, &mut EventQueue),
is_finished: fn(&State) -> bool,
) -> Self {
Self {
upstream: Box::pin(upstream),
buf: BytesMut::new(),
queue: VecDeque::new(),
state,
on_chunk,
on_eof,
is_finished,
upstream_done: false,
eof_dispatched: false,
}
}
}
impl<S, State> Stream for SseStreamDriver<S, State>
where
S: Stream<Item = Result<Bytes, ProviderError>> + Send + 'static,
State: Unpin,
{
type Item = Result<ProviderEvent, ProviderError>;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let this = self.get_mut();
loop {
if let Some(ev) = this.queue.pop_front() {
return Poll::Ready(Some(ev));
}
if (this.is_finished)(&this.state) {
return Poll::Ready(None);
}
if let Some((pos, term_len)) = find_event_end(&this.buf) {
let raw = this.buf.split_to(pos + term_len);
let chunk = String::from_utf8_lossy(&raw).to_string();
(this.on_chunk)(&chunk, &mut this.state, &mut this.queue);
continue;
}
if this.upstream_done {
if !this.buf.is_empty() {
let chunk = String::from_utf8_lossy(&this.buf).to_string();
this.buf.clear();
(this.on_chunk)(&chunk, &mut this.state, &mut this.queue);
continue;
}
if !this.eof_dispatched {
this.eof_dispatched = true;
(this.on_eof)(&mut this.state, &mut this.queue);
continue;
}
return Poll::Ready(None);
}
match this.upstream.as_mut().poll_next(cx) {
Poll::Ready(Some(Ok(bytes))) => {
this.buf.extend_from_slice(&bytes);
continue;
}
Poll::Ready(Some(Err(e))) => {
return Poll::Ready(Some(Err(e)));
}
Poll::Ready(None) => {
this.upstream_done = true;
continue;
}
Poll::Pending => return Poll::Pending,
}
}
}
}
pub(super) fn find_event_end(buf: &[u8]) -> Option<(usize, usize)> {
let mut i = 0;
while i + 1 < buf.len() {
if buf[i] == b'\n' && buf[i + 1] == b'\n' {
return Some((i, 2));
}
if i + 3 < buf.len()
&& buf[i] == b'\r'
&& buf[i + 1] == b'\n'
&& buf[i + 2] == b'\r'
&& buf[i + 3] == b'\n'
{
return Some((i, 4));
}
i += 1;
}
None
}
#[cfg(test)]
mod scrub_tests {
use super::scrub_credentials;
#[test]
fn redacts_exact_key_anywhere_in_message() {
let msg = "request failed: x-api-key was sk-ant-api03-EXAMPLEKEY123, please retry";
let out = scrub_credentials(msg, "sk-ant-api03-EXAMPLEKEY123");
assert!(!out.contains("sk-ant-api03-EXAMPLEKEY123"));
assert!(out.contains("[redacted]"));
assert!(out.contains("please retry"));
}
#[test]
fn redacts_bearer_token_shape() {
let msg = "upstream said: Authorization: Bearer abcdefghijklmnop1234";
let out = scrub_credentials(msg, "different-key");
assert!(!out.contains("abcdefghijklmnop1234"));
}
#[test]
fn redacts_unknown_sk_prefix() {
let msg = "leaked sk-proj-AAAA1111BBBB2222 in body";
let out = scrub_credentials(msg, "");
assert!(!out.contains("sk-proj-AAAA1111BBBB2222"));
}
#[test]
fn empty_api_key_does_not_panic_or_overmatch() {
let msg = "hello world";
assert_eq!(scrub_credentials(msg, ""), "hello world");
}
#[test]
fn short_api_key_is_ignored_to_avoid_collisions() {
let msg = "abc happens in many words";
assert_eq!(scrub_credentials(msg, "abc"), "abc happens in many words");
}
}