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>(
365 &self,
366 request: &http::Request<B>,
367 route: &str,
368 request_id_header: Option<&'static str>,
369 ) -> Option<AdapterAttempt> {
370 let mut attempt = self.begin(request.method(), route)?;
371 attempt.secrets = scrub::request_secrets(request);
372 attempt.request_id_header = request_id_header;
373 Some(attempt)
374 }
375
376 fn emit(&self, attempt: Option<u64>, event: AdapterEvent) {
377 self.emit_with_analysis(attempt, event, None);
378 }
379
380 fn emit_with_analysis(
381 &self,
382 attempt: Option<u64>,
383 event: AdapterEvent,
384 analysis: Option<AdapterAnalysis>,
385 ) {
386 self.inner.sink.observe(Observation::new(
387 self.inner.subject.clone(),
388 Stage::Handler,
389 Emitter::named("rig-core/adapter"),
390 Action::Adapter {
391 observation: AdapterObservation {
392 operation: self.inner.operation.clone(),
393 attempt,
394 host_attempt: self.inner.host_attempt,
395 event,
396 analysis,
397 },
398 },
399 ));
400 }
401
402 pub(crate) fn begin(&self, method: &http::Method, route: &str) -> Option<AdapterAttempt> {
406 let number = {
407 let mut next = self
408 .inner
409 .next
410 .lock()
411 .unwrap_or_else(std::sync::PoisonError::into_inner);
412 let number = (*next)?;
413 *next = number.checked_add(1);
414 number
415 };
416 self.emit(
417 Some(number),
418 AdapterEvent::Started {
419 method: method.to_string(),
420 route: route.to_owned(),
421 },
422 );
423 if number == u64::MAX {
424 self.emit(None, AdapterEvent::IdentityExhausted);
425 }
426 Some(AdapterAttempt {
427 context: self.clone(),
428 number,
429 closed: false,
430 response_seen: false,
431 unframed: 0,
432 secrets: Vec::new(),
433 request_id_header: None,
434 pending_response_id: None,
435 error_boundary: None,
436 })
437 }
438}
439
440pub(crate) struct AdapterAttempt {
442 context: AdapterContext,
443 number: u64,
444 closed: bool,
445 response_seen: bool,
446 unframed: usize,
448 secrets: Vec<String>,
449 request_id_header: Option<&'static str>,
451 pending_response_id: Option<String>,
452 error_boundary: Option<AdapterErrorBoundary>,
453}
454
455impl AdapterAttempt {
456 pub(crate) fn text(&self, text: &str) -> String {
457 scrub::text(text, &self.secrets)
458 }
459
460 fn emit_with_analysis(&self, event: AdapterEvent, analysis: AdapterAnalysis) {
461 let analysis = (analysis != AdapterAnalysis::default()).then_some(analysis);
462 self.context
463 .emit_with_analysis(Some(self.number), event, analysis);
464 }
465
466 fn emit(&self, event: AdapterEvent) {
467 self.context.emit(Some(self.number), event);
468 }
469
470 pub(crate) fn project(&mut self, project: impl FnOnce(&mut ObservationSink<'_>)) {
472 project(&mut ObservationSink { attempt: self });
473 }
474
475 pub(crate) fn provider(&mut self, verdict: AdapterVerdict, response_id: Option<String>) {
476 if response_id.is_some() {
477 self.pending_response_id = response_id;
478 }
479 if verdict != AdapterVerdict::default() {
482 let response_id = self.pending_response_id.take();
483 self.emit_with_analysis(
484 AdapterEvent::Provider { verdict },
485 AdapterAnalysis {
486 response_id,
487 ..AdapterAnalysis::default()
488 },
489 );
490 }
491 }
492
493 pub(crate) fn response_with_headers(
494 &mut self,
495 status: http::StatusCode,
496 headers: Option<&http::HeaderMap>,
497 ) {
498 if !self.response_seen {
499 self.response_seen = true;
500 self.emit_with_analysis(
501 AdapterEvent::Response {
502 status: status.as_u16(),
503 },
504 AdapterAnalysis {
505 headers: headers
506 .map(|h| scrub::headers(h, &self.secrets, self.request_id_header)),
507 ..AdapterAnalysis::default()
508 },
509 );
510 }
511 }
512
513 pub(crate) fn finish(&mut self, ending: AdapterEnding) {
514 if !self.closed {
515 self.closed = true;
516 let response_id = self.pending_response_id.take();
517 self.emit_with_analysis(
518 AdapterEvent::Finished { ending },
519 AdapterAnalysis {
520 response_id,
521 ..AdapterAnalysis::default()
522 },
523 );
524 }
525 }
526}
527
528impl Drop for AdapterAttempt {
529 fn drop(&mut self) {
530 self.finish(AdapterEnding::Dropped);
531 }
532}
533
534pub struct ObservationSink<'a> {
554 attempt: &'a mut AdapterAttempt,
555}
556
557impl ObservationSink<'_> {
558 pub fn error(&mut self, error: AdapterErrorEnvelope) {
560 self.attempt.emit(AdapterEvent::ErrorEnvelope { error });
561 }
562
563 pub fn provider(&mut self, verdict: AdapterVerdict, response_id: Option<String>) {
565 self.attempt.provider(verdict, response_id);
566 }
567
568 pub fn scrub(&self, value: &str) -> String {
570 self.attempt.text(value)
571 }
572}
573
574#[derive(Clone, Default)]
576pub(crate) struct AdapterSlot(Arc<Mutex<Option<AdapterAttempt>>>);
577
578impl AdapterSlot {
579 pub(crate) fn transport_eof(&self, after: usize) {
580 if let Some(attempt) = self
581 .0
582 .lock()
583 .unwrap_or_else(std::sync::PoisonError::into_inner)
584 .as_ref()
585 {
586 attempt.emit(AdapterEvent::TransportEof {
587 after,
588 partial_bytes: attempt.unframed,
589 });
590 }
591 }
592
593 pub(crate) fn project(&self, project: impl FnOnce(&mut ObservationSink<'_>)) {
596 if let Some(attempt) = self
597 .0
598 .lock()
599 .unwrap_or_else(std::sync::PoisonError::into_inner)
600 .as_mut()
601 {
602 attempt.project(project);
603 }
604 }
605
606 pub(crate) fn terminal(&self, usage: Option<&crate::completion::Usage>) {
609 let mut attempt = self
610 .0
611 .lock()
612 .unwrap_or_else(std::sync::PoisonError::into_inner);
613 if let Some(attempt) = attempt.as_mut().filter(|attempt| !attempt.closed) {
614 let usage = usage.map(AdapterUsage::from);
615 if let Some(usage) = usage.filter(|usage| *usage != AdapterUsage::default()) {
616 attempt.emit(AdapterEvent::Usage { usage });
617 }
618 attempt.finish(AdapterEnding::Terminal);
619 }
620 }
621
622 pub(crate) fn install(&self, attempt: Option<AdapterAttempt>) {
624 *self
625 .0
626 .lock()
627 .unwrap_or_else(std::sync::PoisonError::into_inner) = attempt;
628 }
629
630 pub(crate) fn response(&self, status: http::StatusCode) {
631 self.response_with_headers(status, None);
632 }
633
634 pub(crate) fn response_with_headers(
635 &self,
636 status: http::StatusCode,
637 headers: Option<&http::HeaderMap>,
638 ) {
639 if let Some(attempt) = self
640 .0
641 .lock()
642 .unwrap_or_else(std::sync::PoisonError::into_inner)
643 .as_mut()
644 {
645 attempt.response_with_headers(status, headers);
646 }
647 }
648
649 fn finish(&self, ending: AdapterEnding) {
650 if let Some(attempt) = self
651 .0
652 .lock()
653 .unwrap_or_else(std::sync::PoisonError::into_inner)
654 .as_mut()
655 {
656 attempt.finish(ending);
657 }
658 }
659
660 pub(crate) fn error_boundary(&self, boundary: AdapterErrorBoundary) {
662 if let Some(attempt) = self
663 .0
664 .lock()
665 .unwrap_or_else(std::sync::PoisonError::into_inner)
666 .as_mut()
667 {
668 attempt.error_boundary = Some(boundary);
669 }
670 }
671
672 pub(crate) fn fail(&self, error: &crate::error::ProviderError) {
673 if let Some(status) = error.provider_response_status() {
674 self.response(status);
675 }
676 let report = error.report();
677 let boundary = self
678 .0
679 .lock()
680 .unwrap_or_else(std::sync::PoisonError::into_inner)
681 .as_ref()
682 .and_then(|attempt| attempt.error_boundary)
683 .unwrap_or_else(|| error.boundary());
684 self.finish(AdapterEnding::Error {
685 boundary,
686 kind: report.kind.code().to_owned(),
687 status: report.http_status,
688 retryable: report.is_retryable(),
689 });
690 }
691
692 pub(crate) fn unframed(&self, bytes: usize) {
695 if let Some(attempt) = self
696 .0
697 .lock()
698 .unwrap_or_else(std::sync::PoisonError::into_inner)
699 .as_mut()
700 {
701 attempt.unframed = bytes;
702 }
703 }
704
705 pub(crate) fn eof(&self, after: usize) {
706 if let Some(attempt) = self
707 .0
708 .lock()
709 .unwrap_or_else(std::sync::PoisonError::into_inner)
710 .as_mut()
711 {
712 let byte_count = attempt.unframed;
713 let ending = if byte_count == 0 {
714 AdapterEnding::Eof { after }
715 } else {
716 AdapterEnding::PartialFrame { byte_count, after }
717 };
718 attempt.finish(ending);
719 }
720 }
721
722 pub(crate) fn corrupt(&self, frame: usize) {
723 if let Some(attempt) = self
724 .0
725 .lock()
726 .unwrap_or_else(std::sync::PoisonError::into_inner)
727 .as_ref()
728 {
729 attempt
730 .context
731 .emit(Some(attempt.number), AdapterEvent::Corrupt { frame });
732 }
733 }
734}
735
736mod scrub;
737#[cfg(test)]
738mod tests;