use std::sync::{Arc, Mutex};
use serde::{Deserialize, Serialize};
use super::{Action, Emitter, Observation, Stage, Subject, Witness};
pub fn scrub_diagnostic(value: &str, secrets: &[String]) -> String {
scrub::text(value, secrets)
}
pub fn diagnostic_url_secrets(url: &str) -> Vec<String> {
scrub::url_secrets(url)
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AdapterObservation {
pub operation: String,
pub attempt: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub host_attempt: Option<std::num::NonZeroU64>,
pub event: AdapterEvent,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub analysis: Option<AdapterAnalysis>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "event", rename_all = "snake_case")]
pub enum AdapterEvent {
Provider {
verdict: AdapterVerdict,
},
ErrorEnvelope {
error: AdapterErrorEnvelope,
},
Usage {
usage: AdapterUsage,
},
Started {
method: String,
route: String,
},
Response {
status: u16,
},
TransportEof {
after: usize,
partial_bytes: usize,
},
Finished {
ending: AdapterEnding,
},
Corrupt {
frame: usize,
},
IdentityExhausted,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct AdapterVerdict {
pub finish_reason: Option<String>,
pub block_reason: Option<String>,
pub detail: Option<String>,
pub model: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct AdapterAnalysis {
pub response_id: Option<String>,
pub headers: Option<std::collections::BTreeMap<String, String>>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct AdapterErrorEnvelope {
pub code: Option<String>,
pub status: Option<String>,
pub message: Option<String>,
}
#[derive(Default, Deserialize)]
pub struct ObservedError {
pub code: Option<serde_json::Value>,
#[serde(rename = "type", alias = "status")]
pub kind: Option<String>,
pub message: Option<String>,
}
impl ObservedError {
pub fn emit(self, sink: &mut ObservationSink<'_>) {
let code = self.code.map(|code| match code {
serde_json::Value::String(code) => sink.scrub(&code),
serde_json::Value::Number(code) => code.to_string(),
_ => "[invalid]".to_owned(),
});
sink.emit(AdapterEvent::ErrorEnvelope {
error: AdapterErrorEnvelope {
code,
status: self.kind.map(|value| sink.scrub(&value)),
message: self.message.map(|value| sink.scrub(&value)),
},
});
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct AdapterUsage {
pub input_tokens: Option<u64>,
pub output_tokens: Option<u64>,
pub total_tokens: Option<u64>,
pub cached_input_tokens: Option<u64>,
pub reasoning_tokens: Option<u64>,
pub tool_input_tokens: Option<u64>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AdapterErrorBoundary {
Request,
ProviderResponse,
Decode,
Transport,
#[default]
Unknown,
}
impl AdapterErrorBoundary {
pub(crate) fn from_http(error: &crate::http_client::Error) -> Self {
use crate::http_client::Error as H;
match error {
H::Protocol(_) | H::InvalidHeaderValue(_) | H::NoHeaders => Self::Request,
H::InvalidContentType(_) => Self::Decode,
H::StreamEnded => Self::Transport,
H::InvalidStatusCodeWithDetails { .. } => Self::ProviderResponse,
H::Instance(_) => Self::Unknown,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "ending", rename_all = "snake_case")]
pub enum AdapterEnding {
Decoded,
Error {
#[serde(default)]
boundary: AdapterErrorBoundary,
kind: String,
status: Option<u16>,
retryable: bool,
},
Terminal,
Eof {
after: usize,
},
PartialFrame {
byte_count: usize,
after: usize,
},
Dropped,
}
#[derive(Clone)]
pub struct AdapterContext {
inner: Arc<AdapterContextInner>,
}
struct AdapterContextInner {
sink: Arc<dyn Witness + Send + Sync>,
subject: Subject,
operation: String,
next: Arc<Mutex<Option<u64>>>,
host_attempt: Option<std::num::NonZeroU64>,
}
impl std::fmt::Debug for AdapterContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AdapterContext").finish_non_exhaustive()
}
}
impl AdapterContext {
pub fn operation(&self) -> &str {
&self.inner.operation
}
pub fn new(
sink: Arc<dyn Witness + Send + Sync>,
subject: Subject,
operation: impl Into<String>,
) -> Self {
Self {
inner: Arc::new(AdapterContextInner {
sink,
subject,
operation: operation.into(),
next: Arc::new(Mutex::new(Some(1))),
host_attempt: None,
}),
}
}
pub fn for_host_attempt(&self, subject: Subject, attempt: std::num::NonZeroU64) -> Self {
Self {
inner: Arc::new(AdapterContextInner {
sink: self.inner.sink.clone(),
subject,
operation: self.inner.operation.clone(),
next: self.inner.next.clone(),
host_attempt: Some(attempt),
}),
}
}
pub(crate) fn attempt_for<B>(
&self,
request: &http::Request<B>,
route: &str,
) -> Option<AdapterAttempt> {
let mut attempt = self.begin(request.method(), route)?;
attempt.secrets = scrub::request_secrets(request);
Some(attempt)
}
fn emit(&self, attempt: Option<u64>, event: AdapterEvent) {
self.emit_with_analysis(attempt, event, None);
}
fn emit_with_analysis(
&self,
attempt: Option<u64>,
event: AdapterEvent,
analysis: Option<AdapterAnalysis>,
) {
self.inner.sink.observe(Observation::new(
self.inner.subject.clone(),
Stage::Handler,
Emitter::named("rig-core/adapter"),
Action::Adapter {
observation: AdapterObservation {
operation: self.inner.operation.clone(),
attempt,
host_attempt: self.inner.host_attempt,
event,
analysis,
},
},
));
}
pub(crate) fn begin(&self, method: &http::Method, route: &str) -> Option<AdapterAttempt> {
let number = {
let mut next = self
.inner
.next
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let number = (*next)?;
*next = number.checked_add(1);
number
};
self.emit(
Some(number),
AdapterEvent::Started {
method: method.to_string(),
route: route.to_owned(),
},
);
if number == u64::MAX {
self.emit(None, AdapterEvent::IdentityExhausted);
}
Some(AdapterAttempt {
context: self.clone(),
number,
closed: false,
response_seen: false,
sse_tail: super::sse_tail::SseTail::default(),
secrets: Vec::new(),
pending_response_id: None,
error_boundary: None,
})
}
}
pub(crate) struct AdapterAttempt {
context: AdapterContext,
number: u64,
closed: bool,
response_seen: bool,
sse_tail: super::sse_tail::SseTail,
secrets: Vec<String>,
pending_response_id: Option<String>,
error_boundary: Option<AdapterErrorBoundary>,
}
impl AdapterAttempt {
pub(crate) fn text(&self, text: &str) -> String {
scrub::text(text, &self.secrets)
}
pub(crate) fn emit_with_analysis(&self, event: AdapterEvent, analysis: AdapterAnalysis) {
let analysis = (analysis != AdapterAnalysis::default()).then_some(analysis);
self.context
.emit_with_analysis(Some(self.number), event, analysis);
}
pub(crate) fn emit(&self, event: AdapterEvent) {
self.context.emit(Some(self.number), event);
}
pub(crate) fn project(&mut self, project: impl FnOnce(&mut ObservationSink<'_>)) {
project(&mut ObservationSink { attempt: self });
}
pub(crate) fn provider(&mut self, verdict: AdapterVerdict, response_id: Option<String>) {
if response_id.is_some() {
self.pending_response_id = response_id;
}
if verdict != AdapterVerdict::default() {
let response_id = self.pending_response_id.take();
self.emit_with_analysis(
AdapterEvent::Provider { verdict },
AdapterAnalysis {
response_id,
..AdapterAnalysis::default()
},
);
}
}
pub(crate) fn response_with_headers(
&mut self,
status: http::StatusCode,
headers: Option<&http::HeaderMap>,
) {
if !self.response_seen {
self.response_seen = true;
self.emit_with_analysis(
AdapterEvent::Response {
status: status.as_u16(),
},
AdapterAnalysis {
headers: headers.map(|h| scrub::headers(h, &self.secrets)),
..AdapterAnalysis::default()
},
);
}
}
pub(crate) fn finish(&mut self, ending: AdapterEnding) {
if !self.closed {
self.closed = true;
let response_id = self.pending_response_id.take();
self.emit_with_analysis(
AdapterEvent::Finished { ending },
AdapterAnalysis {
response_id,
..AdapterAnalysis::default()
},
);
}
}
}
impl Drop for AdapterAttempt {
fn drop(&mut self) {
self.finish(AdapterEnding::Dropped);
}
}
pub struct ObservationSink<'a> {
attempt: &'a mut AdapterAttempt,
}
impl ObservationSink<'_> {
pub fn emit(&mut self, event: AdapterEvent) {
self.attempt.emit(event);
}
pub fn provider(&mut self, verdict: AdapterVerdict, response_id: Option<String>) {
self.attempt.provider(verdict, response_id);
}
pub fn scrub(&self, value: &str) -> String {
self.attempt.text(value)
}
}
#[derive(Clone, Default)]
pub(crate) struct AdapterSlot(Arc<Mutex<Option<AdapterAttempt>>>);
impl AdapterSlot {
pub(crate) fn transport_eof(&self, after: usize) {
if let Some(attempt) = self
.0
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.as_ref()
{
attempt.emit(AdapterEvent::TransportEof {
after,
partial_bytes: attempt.sse_tail.pending(),
});
}
}
pub(crate) fn project(&self, project: impl FnOnce(&mut ObservationSink<'_>)) {
if let Some(attempt) = self
.0
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.as_mut()
{
attempt.project(project);
}
}
pub(crate) fn install(&self, attempt: Option<AdapterAttempt>) {
*self
.0
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = attempt;
}
pub(crate) fn response(&self, status: http::StatusCode) {
self.response_with_headers(status, None);
}
pub(crate) fn response_with_headers(
&self,
status: http::StatusCode,
headers: Option<&http::HeaderMap>,
) {
if let Some(attempt) = self
.0
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.as_mut()
{
attempt.response_with_headers(status, headers);
}
}
pub(crate) fn finish(&self, ending: AdapterEnding) {
if let Some(attempt) = self
.0
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.as_mut()
{
attempt.finish(ending);
}
}
pub(crate) fn error_boundary(&self, boundary: AdapterErrorBoundary) {
if let Some(attempt) = self
.0
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.as_mut()
{
attempt.error_boundary = Some(boundary);
}
}
pub(crate) fn fail(&self, error: &crate::error::ProviderError) {
if let Some(status) = error.provider_response_status() {
self.response(status);
}
let report = error.report();
let boundary = self
.0
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.as_ref()
.and_then(|attempt| attempt.error_boundary)
.unwrap_or_else(|| error.boundary());
self.finish(AdapterEnding::Error {
boundary,
kind: report.kind.code().to_owned(),
status: report.http_status,
retryable: report.is_retryable(),
});
}
pub(crate) fn bytes(&self, bytes: &[u8]) {
if let Some(attempt) = self
.0
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.as_mut()
{
attempt.sse_tail.feed(bytes);
}
}
pub(crate) fn eof(&self, after: usize) {
if let Some(attempt) = self
.0
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.as_mut()
{
let byte_count = attempt.sse_tail.pending();
let ending = if byte_count == 0 {
AdapterEnding::Eof { after }
} else {
AdapterEnding::PartialFrame { byte_count, after }
};
attempt.finish(ending);
}
}
pub(crate) fn corrupt(&self, frame: usize) {
if let Some(attempt) = self
.0
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.as_ref()
{
attempt
.context
.emit(Some(attempt.number), AdapterEvent::Corrupt { frame });
}
}
}
mod scrub;
#[cfg(test)]
mod tests;