1use std::sync::{Arc, Mutex};
10
11use serde::{Deserialize, Serialize};
12
13use super::{Action, Emitter, Observation, Stage, Subject, Witness};
14
15pub fn scrub_diagnostic(value: &str, secrets: &[String]) -> String {
19 scrub::text(value, secrets)
20}
21
22pub fn diagnostic_url_secrets(url: &str) -> Vec<String> {
27 scrub::url_secrets(url)
28}
29
30#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
32pub struct AdapterObservation {
33 pub operation: String,
35 pub attempt: Option<u64>,
37 #[serde(default, skip_serializing_if = "Option::is_none")]
40 pub host_attempt: Option<std::num::NonZeroU64>,
41 pub event: AdapterEvent,
43 #[serde(default, skip_serializing_if = "Option::is_none")]
45 pub analysis: Option<AdapterAnalysis>,
46}
47
48#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
50#[serde(tag = "event", rename_all = "snake_case")]
51pub enum AdapterEvent {
52 Provider {
54 verdict: AdapterVerdict,
56 },
57 ErrorEnvelope {
59 error: AdapterErrorEnvelope,
61 },
62 Usage {
64 usage: AdapterUsage,
66 },
67 Started {
69 method: String,
71 route: String,
73 },
74 Response {
76 status: u16,
78 },
79 TransportEof {
81 after: usize,
83 partial_bytes: usize,
85 },
86 Finished {
88 ending: AdapterEnding,
90 },
91 Corrupt {
93 frame: usize,
96 },
97 IdentityExhausted,
99}
100
101#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
103pub struct AdapterVerdict {
104 pub finish_reason: Option<String>,
106 pub block_reason: Option<String>,
108 pub detail: Option<String>,
110 pub model: Option<String>,
112}
113
114#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
116pub struct AdapterAnalysis {
117 pub response_id: Option<String>,
119 pub headers: Option<std::collections::BTreeMap<String, String>>,
122}
123
124#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
126pub struct AdapterErrorEnvelope {
127 pub code: Option<String>,
129 pub status: Option<String>,
131 pub message: Option<String>,
133}
134
135#[derive(Default)]
138pub struct ObservedError {
139 pub code: Option<serde_json::Value>,
140 pub kind: Option<String>,
141 pub message: Option<String>,
142}
143
144impl ObservedError {
145 pub fn of(doc: &serde_json::Value) -> Option<Self> {
150 let (error, nested) = crate::provider_response::located_error(doc)?;
151 let text = |key: &str| error.get(key).and_then(serde_json::Value::as_str);
152 let kind = nested.then(|| text("type").or_else(|| text("status")));
153 Some(Self {
154 code: error.get("code").filter(|code| !code.is_null()).cloned(),
155 kind: kind.flatten().map(str::to_owned),
156 message: error
157 .as_str()
158 .or_else(|| text("message"))
159 .map(str::to_owned),
160 })
161 }
162
163 pub fn emit(self, sink: &mut ObservationSink<'_>) {
165 let code = self.code.map(|code| match code {
166 serde_json::Value::String(code) => sink.scrub(&code),
167 serde_json::Value::Number(code) => code.to_string(),
168 _ => "[invalid]".to_owned(),
169 });
170 sink.error(AdapterErrorEnvelope {
171 code,
172 status: self.kind.map(|value| sink.scrub(&value)),
173 message: self.message.map(|value| sink.scrub(&value)),
174 });
175 }
176}
177
178#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
182pub struct AdapterUsage {
183 pub input_tokens: Option<u64>,
185 pub output_tokens: Option<u64>,
187 pub total_tokens: Option<u64>,
190 pub cached_input_tokens: Option<u64>,
192 pub reasoning_tokens: Option<u64>,
194 pub tool_input_tokens: Option<u64>,
196}
197
198impl From<&crate::completion::Usage> for AdapterUsage {
199 fn from(usage: &crate::completion::Usage) -> Self {
200 let crate::completion::Usage {
202 input_tokens,
203 output_tokens,
204 total_tokens: _,
205 cached_input_tokens,
206 cache_creation_input_tokens: _,
207 tool_use_prompt_tokens,
208 reasoning_tokens,
209 cost: _,
210 } = *usage;
211 Self {
212 input_tokens,
213 output_tokens,
214 total_tokens: None,
215 cached_input_tokens,
216 reasoning_tokens,
217 tool_input_tokens: tool_use_prompt_tokens,
218 }
219 }
220}
221
222#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
224#[serde(rename_all = "snake_case")]
225pub enum AdapterErrorBoundary {
226 Request,
228 ProviderResponse,
230 Decode,
232 Transport,
234 #[default]
236 Unknown,
237}
238
239impl AdapterErrorBoundary {
240 pub(crate) fn from_http(error: &crate::http_client::Error) -> Self {
241 use crate::http_client::Error as H;
242 match error {
243 H::Protocol(_) | H::InvalidHeaderValue(_) | H::NoHeaders | H::Unbuilt(_) => {
244 Self::Request
245 }
246 H::InvalidContentType(_) => Self::Decode,
247 H::StreamEnded => Self::Transport,
248 H::InvalidStatusCodeWithDetails { .. } => Self::ProviderResponse,
249 _ => Self::Unknown,
250 }
251 }
252}
253
254#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
256#[serde(tag = "ending", rename_all = "snake_case")]
257pub enum AdapterEnding {
258 Decoded,
260 Error {
262 #[serde(default)]
264 boundary: AdapterErrorBoundary,
265 kind: String,
267 status: Option<u16>,
269 retryable: bool,
271 },
272 Terminal,
274 Eof {
276 after: usize,
278 },
279 PartialFrame {
281 byte_count: usize,
283 after: usize,
285 },
286 Dropped,
288}
289
290#[derive(Clone)]
297pub struct AdapterContext {
298 inner: Arc<AdapterContextInner>,
299}
300
301struct AdapterContextInner {
302 sink: Arc<dyn Witness + Send + Sync>,
303 subject: Subject,
304 operation: String,
305 next: Arc<Mutex<Option<u64>>>,
306 host_attempt: Option<std::num::NonZeroU64>,
307}
308
309impl std::fmt::Debug for AdapterContext {
310 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
311 f.debug_struct("AdapterContext").finish_non_exhaustive()
313 }
314}
315
316impl AdapterContext {
317 pub fn operation(&self) -> &str {
319 &self.inner.operation
320 }
321
322 pub fn new(
325 sink: Arc<dyn Witness + Send + Sync>,
326 subject: Subject,
327 operation: impl Into<String>,
328 ) -> Self {
329 Self {
330 inner: Arc::new(AdapterContextInner {
331 sink,
332 subject,
333 operation: operation.into(),
334 next: Arc::new(Mutex::new(Some(1))),
335 host_attempt: None,
336 }),
337 }
338 }
339
340 pub fn for_host_attempt(&self, subject: Subject, attempt: std::num::NonZeroU64) -> Self {
348 Self {
349 inner: Arc::new(AdapterContextInner {
350 sink: self.inner.sink.clone(),
351 subject,
352 operation: self.inner.operation.clone(),
353 next: self.inner.next.clone(),
354 host_attempt: Some(attempt),
355 }),
356 }
357 }
358
359 pub(crate) fn attempt_for<B>(
363 &self,
364 request: &http::Request<B>,
365 route: &str,
366 ) -> Option<AdapterAttempt> {
367 let mut attempt = self.begin(request.method(), route)?;
368 attempt.secrets = scrub::request_secrets(request);
369 Some(attempt)
370 }
371
372 fn emit(&self, attempt: Option<u64>, event: AdapterEvent) {
373 self.emit_with_analysis(attempt, event, None);
374 }
375
376 fn emit_with_analysis(
377 &self,
378 attempt: Option<u64>,
379 event: AdapterEvent,
380 analysis: Option<AdapterAnalysis>,
381 ) {
382 self.inner.sink.observe(Observation::new(
383 self.inner.subject.clone(),
384 Stage::Handler,
385 Emitter::named("rig-core/adapter"),
386 Action::Adapter {
387 observation: AdapterObservation {
388 operation: self.inner.operation.clone(),
389 attempt,
390 host_attempt: self.inner.host_attempt,
391 event,
392 analysis,
393 },
394 },
395 ));
396 }
397
398 pub(crate) fn begin(&self, method: &http::Method, route: &str) -> Option<AdapterAttempt> {
402 let number = {
403 let mut next = self
404 .inner
405 .next
406 .lock()
407 .unwrap_or_else(std::sync::PoisonError::into_inner);
408 let number = (*next)?;
409 *next = number.checked_add(1);
410 number
411 };
412 self.emit(
413 Some(number),
414 AdapterEvent::Started {
415 method: method.to_string(),
416 route: route.to_owned(),
417 },
418 );
419 if number == u64::MAX {
420 self.emit(None, AdapterEvent::IdentityExhausted);
421 }
422 Some(AdapterAttempt {
423 context: self.clone(),
424 number,
425 closed: false,
426 response_seen: false,
427 unframed: 0,
428 secrets: Vec::new(),
429 pending_response_id: None,
430 error_boundary: None,
431 })
432 }
433}
434
435pub(crate) struct AdapterAttempt {
437 context: AdapterContext,
438 number: u64,
439 closed: bool,
440 response_seen: bool,
441 unframed: usize,
443 secrets: Vec<String>,
444 pending_response_id: Option<String>,
445 error_boundary: Option<AdapterErrorBoundary>,
446}
447
448impl AdapterAttempt {
449 pub(crate) fn text(&self, text: &str) -> String {
450 scrub::text(text, &self.secrets)
451 }
452
453 fn emit_with_analysis(&self, event: AdapterEvent, analysis: AdapterAnalysis) {
454 let analysis = (analysis != AdapterAnalysis::default()).then_some(analysis);
455 self.context
456 .emit_with_analysis(Some(self.number), event, analysis);
457 }
458
459 fn emit(&self, event: AdapterEvent) {
460 self.context.emit(Some(self.number), event);
461 }
462
463 pub(crate) fn project(&mut self, project: impl FnOnce(&mut ObservationSink<'_>)) {
465 project(&mut ObservationSink { attempt: self });
466 }
467
468 pub(crate) fn provider(&mut self, verdict: AdapterVerdict, response_id: Option<String>) {
469 if response_id.is_some() {
470 self.pending_response_id = response_id;
471 }
472 if verdict != AdapterVerdict::default() {
475 let response_id = self.pending_response_id.take();
476 self.emit_with_analysis(
477 AdapterEvent::Provider { verdict },
478 AdapterAnalysis {
479 response_id,
480 ..AdapterAnalysis::default()
481 },
482 );
483 }
484 }
485
486 pub(crate) fn response_with_headers(
487 &mut self,
488 status: http::StatusCode,
489 headers: Option<&http::HeaderMap>,
490 ) {
491 if !self.response_seen {
492 self.response_seen = true;
493 self.emit_with_analysis(
494 AdapterEvent::Response {
495 status: status.as_u16(),
496 },
497 AdapterAnalysis {
498 headers: headers.map(|h| scrub::headers(h, &self.secrets)),
499 ..AdapterAnalysis::default()
500 },
501 );
502 }
503 }
504
505 pub(crate) fn finish(&mut self, ending: AdapterEnding) {
506 if !self.closed {
507 self.closed = true;
508 let response_id = self.pending_response_id.take();
509 self.emit_with_analysis(
510 AdapterEvent::Finished { ending },
511 AdapterAnalysis {
512 response_id,
513 ..AdapterAnalysis::default()
514 },
515 );
516 }
517 }
518}
519
520impl Drop for AdapterAttempt {
521 fn drop(&mut self) {
522 self.finish(AdapterEnding::Dropped);
523 }
524}
525
526pub struct ObservationSink<'a> {
546 attempt: &'a mut AdapterAttempt,
547}
548
549impl ObservationSink<'_> {
550 pub fn error(&mut self, error: AdapterErrorEnvelope) {
552 self.attempt.emit(AdapterEvent::ErrorEnvelope { error });
553 }
554
555 pub fn provider(&mut self, verdict: AdapterVerdict, response_id: Option<String>) {
557 self.attempt.provider(verdict, response_id);
558 }
559
560 pub fn scrub(&self, value: &str) -> String {
562 self.attempt.text(value)
563 }
564}
565
566#[derive(Clone, Default)]
568pub(crate) struct AdapterSlot(Arc<Mutex<Option<AdapterAttempt>>>);
569
570impl AdapterSlot {
571 pub(crate) fn transport_eof(&self, after: usize) {
572 if let Some(attempt) = self
573 .0
574 .lock()
575 .unwrap_or_else(std::sync::PoisonError::into_inner)
576 .as_ref()
577 {
578 attempt.emit(AdapterEvent::TransportEof {
579 after,
580 partial_bytes: attempt.unframed,
581 });
582 }
583 }
584
585 pub(crate) fn project(&self, project: impl FnOnce(&mut ObservationSink<'_>)) {
588 if let Some(attempt) = self
589 .0
590 .lock()
591 .unwrap_or_else(std::sync::PoisonError::into_inner)
592 .as_mut()
593 {
594 attempt.project(project);
595 }
596 }
597
598 pub(crate) fn terminal(&self, usage: Option<&crate::completion::Usage>) {
601 let mut attempt = self
602 .0
603 .lock()
604 .unwrap_or_else(std::sync::PoisonError::into_inner);
605 if let Some(attempt) = attempt.as_mut().filter(|attempt| !attempt.closed) {
606 let usage = usage.map(AdapterUsage::from);
607 if let Some(usage) = usage.filter(|usage| *usage != AdapterUsage::default()) {
608 attempt.emit(AdapterEvent::Usage { usage });
609 }
610 attempt.finish(AdapterEnding::Terminal);
611 }
612 }
613
614 pub(crate) fn install(&self, attempt: Option<AdapterAttempt>) {
616 *self
617 .0
618 .lock()
619 .unwrap_or_else(std::sync::PoisonError::into_inner) = attempt;
620 }
621
622 pub(crate) fn response(&self, status: http::StatusCode) {
623 self.response_with_headers(status, None);
624 }
625
626 pub(crate) fn response_with_headers(
627 &self,
628 status: http::StatusCode,
629 headers: Option<&http::HeaderMap>,
630 ) {
631 if let Some(attempt) = self
632 .0
633 .lock()
634 .unwrap_or_else(std::sync::PoisonError::into_inner)
635 .as_mut()
636 {
637 attempt.response_with_headers(status, headers);
638 }
639 }
640
641 fn finish(&self, ending: AdapterEnding) {
642 if let Some(attempt) = self
643 .0
644 .lock()
645 .unwrap_or_else(std::sync::PoisonError::into_inner)
646 .as_mut()
647 {
648 attempt.finish(ending);
649 }
650 }
651
652 pub(crate) fn error_boundary(&self, boundary: AdapterErrorBoundary) {
654 if let Some(attempt) = self
655 .0
656 .lock()
657 .unwrap_or_else(std::sync::PoisonError::into_inner)
658 .as_mut()
659 {
660 attempt.error_boundary = Some(boundary);
661 }
662 }
663
664 pub(crate) fn fail(&self, error: &crate::error::ProviderError) {
665 if let Some(status) = error.provider_response_status() {
666 self.response(status);
667 }
668 let report = error.report();
669 let boundary = self
670 .0
671 .lock()
672 .unwrap_or_else(std::sync::PoisonError::into_inner)
673 .as_ref()
674 .and_then(|attempt| attempt.error_boundary)
675 .unwrap_or_else(|| error.boundary());
676 self.finish(AdapterEnding::Error {
677 boundary,
678 kind: report.kind.code().to_owned(),
679 status: report.http_status,
680 retryable: report.is_retryable(),
681 });
682 }
683
684 pub(crate) fn unframed(&self, bytes: usize) {
687 if let Some(attempt) = self
688 .0
689 .lock()
690 .unwrap_or_else(std::sync::PoisonError::into_inner)
691 .as_mut()
692 {
693 attempt.unframed = bytes;
694 }
695 }
696
697 pub(crate) fn eof(&self, after: usize) {
698 if let Some(attempt) = self
699 .0
700 .lock()
701 .unwrap_or_else(std::sync::PoisonError::into_inner)
702 .as_mut()
703 {
704 let byte_count = attempt.unframed;
705 let ending = if byte_count == 0 {
706 AdapterEnding::Eof { after }
707 } else {
708 AdapterEnding::PartialFrame { byte_count, after }
709 };
710 attempt.finish(ending);
711 }
712 }
713
714 pub(crate) fn corrupt(&self, frame: usize) {
715 if let Some(attempt) = self
716 .0
717 .lock()
718 .unwrap_or_else(std::sync::PoisonError::into_inner)
719 .as_ref()
720 {
721 attempt
722 .context
723 .emit(Some(attempt.number), AdapterEvent::Corrupt { frame });
724 }
725 }
726}
727
728mod scrub;
729#[cfg(test)]
730mod tests;