Skip to main content

bamboo_domain/
llm_request_diagnostic.rs

1//! A bounded failure receipt, carried by the existing Session checkpoint.
2//! These observations never authorize execution, retries or recovery.
3
4use chrono::{DateTime, Utc};
5use serde::{Deserialize, Deserializer, Serialize};
6use sha2::{Digest, Sha256};
7use uuid::Uuid;
8
9use crate::Session;
10
11pub const LAST_RUN_LLM_DIAGNOSTIC_KEY: &str = "runtime.llm_request_diagnostic";
12const MAX_DIAGNOSTIC_BYTES: usize = 4096;
13const DIAGNOSTIC_RUN_FINGERPRINT_KEY: &str = "runtime.llm_request_diagnostic_run";
14
15#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
16#[serde(rename_all = "snake_case")]
17pub enum LlmRequestStopReason {
18    AttemptsExhausted,
19    TimeBudgetExhausted,
20    RetryDelayExceedsBudget,
21    NonRetryableTransport,
22    UnclassifiedErrorBody,
23    CancelledOrDropped,
24}
25
26impl LlmRequestStopReason {
27    pub fn guidance(self) -> &'static str {
28        match self {
29            Self::AttemptsExhausted => "Automatic retry attempt limit reached",
30            Self::TimeBudgetExhausted => "Automatic retry time budget exhausted",
31            Self::RetryDelayExceedsBudget => {
32                "Required retry delay exceeds the remaining time budget"
33            }
34            Self::NonRetryableTransport => "Transport failure is not automatically retryable",
35            Self::UnclassifiedErrorBody => "Upstream error response could not be safely classified",
36            Self::CancelledOrDropped => "Request cancelled or its caller stopped waiting",
37        }
38    }
39}
40
41// This private wire shape deliberately matches the provider's stop record.
42// Validation applies on every deserialize, including cold canonical reads.
43#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
44#[serde(deny_unknown_fields)]
45struct InitialRequestDiagnosticFields {
46    schema_version: u8,
47    diagnostic_id: Uuid,
48    request_id: Uuid,
49    error_id: Option<Uuid>,
50    observed_at: DateTime<Utc>,
51    provider: String,
52    session_hash: Option<String>,
53    run_id: Option<Uuid>,
54    attempt: u32,
55    attempt_limit: u32,
56    event: String,
57    phase: String,
58    failure: String,
59    status: Option<u16>,
60    attempt_timeout_ms: u64,
61    total_budget_ms: u64,
62    attempt_elapsed_ms: u64,
63    total_elapsed_ms: u64,
64    remaining_budget_ms: u64,
65    retry_delay_ms: Option<u64>,
66    stop_reason: LlmRequestStopReason,
67    upstream_outcome: String,
68}
69
70/// Public watchdog evidence only. The validated wrapper below is required
71/// before these fields may be persisted or attached to a client error event.
72#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
73#[serde(deny_unknown_fields)]
74pub struct StreamTimeoutDiagnostic {
75    pub schema_version: u8,
76    pub kind: String,
77    pub diagnostic_id: Uuid,
78    pub error_id: Uuid,
79    pub observed_at: DateTime<Utc>,
80    pub provider: Option<String>,
81    pub model: Option<String>,
82    pub session_hash: String,
83    pub run_id: Option<Uuid>,
84    pub phase: String,
85    pub deadline_ms: u64,
86    pub last_transport_ms_ago: u64,
87    pub last_semantic_ms_ago: Option<u64>,
88    pub semantic_output_started: bool,
89    pub retry_safe: bool,
90    pub last_http_status: Option<u16>,
91    pub retry_delay_ms: Option<u64>,
92}
93
94impl StreamTimeoutDiagnostic {
95    /// Identifiers are bounded labels, never URLs or credential-like values.
96    /// This same check applies before formatting the timeout message and on
97    /// cold metadata reads, so redaction cannot break exact error binding.
98    pub fn valid_identifier(value: &str) -> bool {
99        !value.is_empty()
100            && value.len() <= 120
101            && value
102                .bytes()
103                .all(|b| b.is_ascii_alphanumeric() || b"-_.:/@".contains(&b))
104            && !value.contains("://")
105            && !value.to_ascii_lowercase().starts_with("sk-")
106            && !value.to_ascii_lowercase().starts_with("bearer")
107    }
108
109    fn valid(&self) -> bool {
110        // JavaScript consumers must not round a correlation-relevant duration.
111        const MAX_SAFE_INTEGER: u64 = 9_007_199_254_740_991;
112        let has_semantic = self.last_semantic_ms_ago.is_some();
113        self.schema_version == 1
114            && self.kind == "stream_timeout"
115            && self.error_id == self.diagnostic_id
116            && valid_session_hash(&self.session_hash)
117            && self.provider.as_deref().is_none_or(Self::valid_identifier)
118            && self.model.as_deref().is_none_or(Self::valid_identifier)
119            && (1_000..=86_400_000).contains(&self.deadline_ms)
120            && self.last_transport_ms_ago <= MAX_SAFE_INTEGER
121            && self.last_semantic_ms_ago.is_none_or(|elapsed| {
122                elapsed <= MAX_SAFE_INTEGER && self.last_transport_ms_ago <= elapsed
123            })
124            && self.semantic_output_started == has_semantic
125            && (!self.retry_safe || !has_semantic)
126            && match self.phase.as_str() {
127                "bootstrap" | "first_semantic" => !has_semantic,
128                "transport_idle" => true,
129                "semantic_idle" => has_semantic,
130                _ => false,
131            }
132            && match (self.last_http_status, self.retry_delay_ms) {
133                (None, None) => true,
134                (Some(status), Some(delay)) => {
135                    self.phase == "bootstrap"
136                        && (100..=599).contains(&status)
137                        && delay <= MAX_SAFE_INTEGER
138                }
139                _ => false,
140            }
141    }
142
143    fn error_message(&self) -> String {
144        let last_semantic = self
145            .last_semantic_ms_ago
146            .map(|elapsed| format!("{elapsed}ms"))
147            .unwrap_or_else(|| "never".into());
148        let mut message = format!(
149            "Stream timed out: phase={}, deadline_ms={}, provider={}, model={}, last_transport_ms_ago={}, \
150             last_semantic_ms_ago={}, semantic_output_started={}, retry_safe={}",
151            self.phase,
152            self.deadline_ms,
153            self.provider.as_deref().unwrap_or("unknown"),
154            self.model.as_deref().unwrap_or("unknown"),
155            self.last_transport_ms_ago,
156            last_semantic,
157            self.semantic_output_started,
158            self.retry_safe,
159        );
160        if let (Some(status), Some(delay)) = (self.last_http_status, self.retry_delay_ms) {
161            use std::fmt::Write;
162            let _ = write!(
163                message,
164                ", last_http_status={status}, retry_delay_ms={delay}"
165            );
166        }
167        format!("{message}. Diagnostic ID: {}.", self.diagnostic_id)
168    }
169}
170
171#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
172#[serde(untagged)]
173enum DiagnosticFields {
174    InitialRequest(InitialRequestDiagnosticFields),
175    StreamTimeout(StreamTimeoutDiagnostic),
176}
177
178impl DiagnosticFields {
179    fn valid(&self) -> bool {
180        match self {
181            Self::InitialRequest(fields) => fields.valid(),
182            Self::StreamTimeout(fields) => fields.valid(),
183        }
184    }
185
186    fn run_id(&self) -> Option<Uuid> {
187        match self {
188            Self::InitialRequest(fields) => fields.run_id,
189            Self::StreamTimeout(fields) => fields.run_id,
190        }
191    }
192
193    fn set_run_id(&mut self, run_id: Option<Uuid>) {
194        match self {
195            Self::InitialRequest(fields) => fields.run_id = run_id,
196            Self::StreamTimeout(fields) => fields.run_id = run_id,
197        }
198    }
199}
200
201fn valid_session_hash(hash: &str) -> bool {
202    hash.len() == 16
203        && hash
204            .bytes()
205            .all(|b| b.is_ascii_hexdigit() && !b.is_ascii_uppercase())
206}
207
208/// A validated, content-free stop record. Unknown fields and free-text values
209/// fail closed instead of being echoed from editable Session metadata.
210#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
211#[serde(transparent)]
212pub struct LlmRequestDiagnostic(DiagnosticFields);
213
214impl<'de> Deserialize<'de> for LlmRequestDiagnostic {
215    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
216        let fields = DiagnosticFields::deserialize(deserializer)?;
217        if !fields.valid() {
218            return Err(serde::de::Error::custom("invalid LLM failure diagnostic"));
219        }
220        Ok(Self(fields))
221    }
222}
223
224impl InitialRequestDiagnosticFields {
225    fn valid(&self) -> bool {
226        self.schema_version == 1
227            && self.provider == "anthropic"
228            && self.event == "stop"
229            && self.request_id == self.diagnostic_id
230            && self.error_id == Some(self.diagnostic_id)
231            && self.session_hash.as_deref().is_some_and(valid_session_hash)
232            && (1..=4).contains(&self.attempt_limit)
233            && self.attempt <= self.attempt_limit
234            && self
235                .status
236                .is_none_or(|status| (100..=599).contains(&status))
237            && self.attempt_timeout_ms <= 60_000
238            && self.total_budget_ms <= 60_000
239            && self.remaining_budget_ms <= self.total_budget_ms
240            && ["send_headers", "error_body", "response_status", "backoff"]
241                .contains(&self.phase.as_str())
242            && ["unknown", "response_received"].contains(&self.upstream_outcome.as_str())
243            && [
244                "none",
245                "request deadline exceeded",
246                "request attempt limit reached",
247                "initial response headers timed out",
248                "timeout",
249                "connect",
250                "request",
251                "body",
252                "decode",
253                "status",
254                "transport",
255                "upstream error body deadline exceeded",
256                "upstream error body read failed",
257                "connector database is temporarily recovering",
258                "temporary upstream failure",
259            ]
260            .contains(&self.failure.as_str())
261    }
262}
263
264/// Same domain-separated identity as provider operational logs; never store
265/// caller-controlled session text in the diagnostic payload.
266pub fn llm_diagnostic_session_hash(session_id: &str) -> String {
267    let mut digest = Sha256::new();
268    digest.update(b"bamboo-provider-operational-log-v1\0");
269    digest.update(session_id.as_bytes());
270    hex::encode(&digest.finalize()[..8])
271}
272
273impl LlmRequestDiagnostic {
274    pub fn from_stream_timeout(fields: StreamTimeoutDiagnostic) -> Option<Self> {
275        fields
276            .valid()
277            .then_some(Self(DiagnosticFields::StreamTimeout(fields)))
278    }
279
280    pub fn from_json(raw: &str) -> Option<Self> {
281        (raw.len() <= MAX_DIAGNOSTIC_BYTES)
282            .then(|| serde_json::from_str(raw).ok())
283            .flatten()
284    }
285
286    pub fn diagnostic_id(&self) -> Uuid {
287        match &self.0 {
288            DiagnosticFields::InitialRequest(fields) => fields.diagnostic_id,
289            DiagnosticFields::StreamTimeout(fields) => fields.diagnostic_id,
290        }
291    }
292
293    pub fn matches_session(&self, session_id: &str) -> bool {
294        let hash = llm_diagnostic_session_hash(session_id);
295        match &self.0 {
296            DiagnosticFields::InitialRequest(fields) => {
297                fields.session_hash.as_deref() == Some(hash.as_str())
298            }
299            DiagnosticFields::StreamTimeout(fields) => fields.session_hash == hash,
300        }
301    }
302
303    /// Exact generated error text only. An arbitrary upstream error containing
304    /// a copied UUID cannot acquire a receipt through substring matching.
305    pub fn matches_error_message(&self, message: &str) -> bool {
306        let message = match &self.0 {
307            DiagnosticFields::InitialRequest(_) => {
308                message.strip_prefix("LLM error: ").unwrap_or(message)
309            }
310            DiagnosticFields::StreamTimeout(_) => message,
311        };
312        message == self.error_message()
313    }
314
315    pub fn error_message(&self) -> String {
316        let f = match &self.0 {
317            DiagnosticFields::InitialRequest(fields) => fields,
318            DiagnosticFields::StreamTimeout(fields) => return fields.error_message(),
319        };
320        let status = f
321            .status
322            .map(|s| format!("HTTP {s}"))
323            .unwrap_or_else(|| "no HTTP response".into());
324        format!(
325            "Anthropic upstream request failed ({status}): {}. {} after {} request(s) (limit {}). Diagnostic ID: {}. Try again later or contact the upstream gateway administrator.",
326            f.failure, f.stop_reason.guidance(), f.attempt, f.attempt_limit, f.diagnostic_id,
327        )
328    }
329}
330
331impl Session {
332    fn llm_diagnostic_run_fingerprint(&self) -> Option<String> {
333        self.agent_runtime_state.as_ref().map(|state| {
334            let mut digest = Sha256::new();
335            digest.update(b"bamboo-llm-diagnostic-run-v1\0");
336            digest.update((state.run_id.len() as u64).to_be_bytes());
337            digest.update(state.run_id.as_bytes());
338            if let Some(execution) = &state.execution_id {
339                digest.update([1]);
340                digest.update((execution.len() as u64).to_be_bytes());
341                digest.update(execution.as_bytes());
342            } else {
343                digest.update([0]);
344            }
345            hex::encode(&digest.finalize()[..16])
346        })
347    }
348
349    pub(crate) fn clear_llm_request_diagnostic(&mut self) {
350        self.metadata.remove(LAST_RUN_LLM_DIAGNOSTIC_KEY);
351        self.metadata.remove(DIAGNOSTIC_RUN_FINGERPRINT_KEY);
352    }
353
354    fn stored_llm_request_diagnostic(&self) -> Option<LlmRequestDiagnostic> {
355        let diagnostic =
356            LlmRequestDiagnostic::from_json(self.metadata.get(LAST_RUN_LLM_DIAGNOSTIC_KEY)?)?;
357        let run_id = self
358            .agent_runtime_state
359            .as_ref()
360            .and_then(|state| Uuid::parse_str(&state.run_id).ok());
361        let expected_run = self.llm_diagnostic_run_fingerprint();
362        (diagnostic.matches_session(&self.id)
363            && diagnostic.0.run_id() == run_id
364            && self.metadata.get(DIAGNOSTIC_RUN_FINGERPRINT_KEY) == expected_run.as_ref())
365        .then_some(diagnostic)
366    }
367
368    /// Capture only the observer for this actual provider call; existing final
369    /// Session persistence carries it without a second store or background job.
370    pub fn record_llm_request_diagnostic(&mut self, mut diagnostic: LlmRequestDiagnostic) -> bool {
371        if !diagnostic.matches_session(&self.id) {
372            return false;
373        }
374        diagnostic.0.set_run_id(
375            self.agent_runtime_state
376                .as_ref()
377                .and_then(|state| Uuid::parse_str(&state.run_id).ok()),
378        );
379        let Ok(raw) = serde_json::to_string(&diagnostic) else {
380            return false;
381        };
382        if raw.len() > MAX_DIAGNOSTIC_BYTES {
383            return false;
384        }
385        self.metadata
386            .insert(LAST_RUN_LLM_DIAGNOSTIC_KEY.into(), raw);
387        match self.llm_diagnostic_run_fingerprint() {
388            Some(fingerprint) => {
389                self.metadata
390                    .insert(DIAGNOSTIC_RUN_FINGERPRINT_KEY.into(), fingerprint);
391            }
392            None => {
393                self.metadata.remove(DIAGNOSTIC_RUN_FINGERPRINT_KEY);
394            }
395        }
396        true
397    }
398
399    pub(crate) fn retain_llm_request_diagnostic_for_error(&mut self, error: &str) {
400        if !self
401            .stored_llm_request_diagnostic()
402            .is_some_and(|d| d.matches_error_message(error))
403        {
404            self.clear_llm_request_diagnostic();
405        }
406    }
407
408    /// Public/cold-read projection requires the same Session, run and exact
409    /// canonical error. Old, foreign, malformed or overwritten evidence is absent.
410    pub fn last_run_diagnostic(&self) -> Option<LlmRequestDiagnostic> {
411        let error = self.last_run_error()?;
412        self.stored_llm_request_diagnostic()
413            .filter(|d| d.matches_error_message(&error))
414    }
415}
416
417#[cfg(test)]
418mod tests {
419    use super::*;
420
421    fn timeout_sample(session: &Session) -> LlmRequestDiagnostic {
422        let id = Uuid::new_v4();
423        LlmRequestDiagnostic::from_stream_timeout(StreamTimeoutDiagnostic {
424            schema_version: 1,
425            kind: "stream_timeout".into(),
426            diagnostic_id: id,
427            error_id: id,
428            observed_at: Utc::now(),
429            provider: Some("a2a3cebf-8abc-45cb-8407-66c90fd9a978".into()),
430            model: Some("gpt-6-astra".into()),
431            session_hash: llm_diagnostic_session_hash(&session.id),
432            run_id: None,
433            phase: "transport_idle".into(),
434            deadline_ms: 120_000,
435            last_transport_ms_ago: 120_002,
436            last_semantic_ms_ago: Some(120_002),
437            semantic_output_started: true,
438            retry_safe: false,
439            last_http_status: None,
440            retry_delay_ms: None,
441        })
442        .unwrap()
443    }
444
445    fn sample(session: &Session) -> LlmRequestDiagnostic {
446        let id = Uuid::new_v4();
447        LlmRequestDiagnostic::from_json(
448            &serde_json::json!({
449                "schema_version":1,"diagnostic_id":id,"request_id":id,"error_id":id,
450                "observed_at":Utc::now(),"provider":"anthropic",
451                "session_hash":llm_diagnostic_session_hash(&session.id),"run_id":null,
452                "attempt":1,"attempt_limit":4,"event":"stop","phase":"send_headers",
453                "failure":"initial response headers timed out","status":null,
454                "attempt_timeout_ms":20000,"total_budget_ms":60000,"attempt_elapsed_ms":20000,
455                "total_elapsed_ms":60000,"remaining_budget_ms":0,"retry_delay_ms":null,
456                "stop_reason":"time_budget_exhausted","upstream_outcome":"unknown",
457            })
458            .to_string(),
459        )
460        .unwrap()
461    }
462
463    #[test]
464    fn failure_receipt_survives_cold_session_round_trip_and_is_bound_to_error_and_session() {
465        let mut session = Session::new("diagnostic-current", "model");
466        let diagnostic = sample(&session);
467        assert!(session.record_llm_request_diagnostic(diagnostic.clone()));
468        assert!(session.last_run_diagnostic().is_none());
469        session.set_last_run_error(format!("LLM error: {}", diagnostic.error_message()));
470        let cold: Session =
471            serde_json::from_str(&serde_json::to_string(&session).unwrap()).unwrap();
472        assert_eq!(cold.last_run_diagnostic(), Some(diagnostic.clone()));
473        let mut foreign = cold.clone();
474        foreign.id = "other".into();
475        assert!(foreign.last_run_diagnostic().is_none());
476        let mut changed = cold.clone();
477        changed.set_last_run_error("another provider failure");
478        assert!(changed.last_run_diagnostic().is_none());
479        assert!(!changed.metadata.contains_key(LAST_RUN_LLM_DIAGNOSTIC_KEY));
480        let mut cleared = cold;
481        cleared.clear_last_run_error();
482        assert!(!cleared.metadata.contains_key(LAST_RUN_LLM_DIAGNOSTIC_KEY));
483    }
484
485    #[test]
486    fn invalid_free_text_unknown_fields_and_stale_run_fail_closed() {
487        let mut session = Session::new("diagnostic-current", "model");
488        let diagnostic = sample(&session);
489        let value = serde_json::to_value(&diagnostic).unwrap();
490        for field in [
491            "provider",
492            "phase",
493            "failure",
494            "upstream_outcome",
495            "session_hash",
496            "future_secret",
497        ] {
498            let mut invalid = value.clone();
499            invalid[field] = serde_json::json!("PRIVATE_CANARY_body_key_path");
500            assert!(LlmRequestDiagnostic::from_json(&invalid.to_string()).is_none());
501        }
502        assert!(!diagnostic.matches_error_message(&format!(
503            "arbitrary upstream body: {}",
504            diagnostic.error_message()
505        )));
506        session.record_llm_request_diagnostic(diagnostic.clone());
507        session.set_last_run_error(diagnostic.error_message());
508        session.agent_runtime_state =
509            Some(crate::AgentRuntimeState::new(Uuid::new_v4().to_string()));
510        assert!(session.last_run_diagnostic().is_none());
511    }
512
513    #[test]
514    fn non_uuid_run_and_execution_replacement_cannot_reuse_the_old_receipt() {
515        let mut session = Session::new("diagnostic-current", "model");
516        let mut runtime = crate::AgentRuntimeState::new("private-non-uuid-run-a");
517        runtime.execution_id = Some("private-execution-a".into());
518        session.agent_runtime_state = Some(runtime);
519        let diagnostic = sample(&session);
520        session.record_llm_request_diagnostic(diagnostic.clone());
521        session.set_last_run_error(diagnostic.error_message());
522        let encoded = serde_json::to_string(&session).unwrap();
523        let mut cold: Session = serde_json::from_str(&encoded).unwrap();
524        assert!(cold.last_run_diagnostic().is_some());
525        let public = serde_json::to_string(&cold.last_run_diagnostic().unwrap()).unwrap();
526        assert!(!public.contains("private-non-uuid-run-a"));
527        assert!(!public.contains("private-execution-a"));
528        cold.agent_runtime_state.as_mut().unwrap().run_id = "private-non-uuid-run-b".into();
529        assert!(cold.last_run_diagnostic().is_none());
530        let mut cold: Session = serde_json::from_str(&encoded).unwrap();
531        cold.agent_runtime_state.as_mut().unwrap().execution_id =
532            Some("private-execution-b".into());
533        assert!(cold.last_run_diagnostic().is_none());
534    }
535
536    #[test]
537    fn stream_timeout_receipt_survives_cold_read_only_for_its_exact_run_and_error() {
538        let mut session = Session::new("timeout-session-private", "model");
539        let mut runtime = crate::AgentRuntimeState::new(Uuid::new_v4().to_string());
540        runtime.execution_id = Some("private-execution".into());
541        session.agent_runtime_state = Some(runtime);
542        let receipt = timeout_sample(&session);
543        assert!(session.record_llm_request_diagnostic(receipt.clone()));
544        assert!(session.last_run_diagnostic().is_none());
545        session.set_last_run_error(receipt.error_message());
546        let encoded = serde_json::to_string(&session).unwrap();
547        let cold: Session = serde_json::from_str(&encoded).unwrap();
548        let actual = cold.last_run_diagnostic().unwrap();
549        assert_eq!(actual.diagnostic_id(), receipt.diagnostic_id());
550        assert_eq!(actual.error_message(), receipt.error_message());
551        let public = serde_json::to_value(&actual).unwrap();
552        assert_eq!(
553            public["run_id"],
554            session.agent_runtime_state.as_ref().unwrap().run_id
555        );
556        assert!(!public.to_string().contains("timeout-session-private"));
557        assert!(!public.to_string().contains("private-execution"));
558
559        let mut wrong_session = cold.clone();
560        wrong_session.id = "other-session".into();
561        assert!(wrong_session.last_run_diagnostic().is_none());
562        let mut wrong_run = cold.clone();
563        wrong_run.agent_runtime_state.as_mut().unwrap().run_id = Uuid::new_v4().to_string();
564        assert!(wrong_run.last_run_diagnostic().is_none());
565        let mut wrong_execution = cold.clone();
566        wrong_execution
567            .agent_runtime_state
568            .as_mut()
569            .unwrap()
570            .execution_id = Some("next-execution".into());
571        assert!(wrong_execution.last_run_diagnostic().is_none());
572        let mut repeated_timeout = cold;
573        repeated_timeout.set_last_run_error(timeout_sample(&session).error_message());
574        assert!(repeated_timeout.last_run_diagnostic().is_none());
575        assert!(!repeated_timeout
576            .metadata
577            .contains_key(LAST_RUN_LLM_DIAGNOSTIC_KEY));
578    }
579
580    #[test]
581    fn timeout_receipt_rejects_inconsistent_observations_unknown_keys_and_unsafe_labels() {
582        let session = Session::new("session", "model");
583        let value = serde_json::to_value(timeout_sample(&session)).unwrap();
584        for (field, invalid) in [
585            ("schema_version", serde_json::json!(2)),
586            ("kind", serde_json::json!("another_timeout")),
587            ("error_id", serde_json::json!(Uuid::new_v4())),
588            ("phase", serde_json::json!("first_semantic")),
589            ("deadline_ms", serde_json::json!(0)),
590            ("deadline_ms", serde_json::json!(86_400_001_u64)),
591            ("last_transport_ms_ago", serde_json::json!(120_003)),
592            (
593                "last_semantic_ms_ago",
594                serde_json::json!(9_007_199_254_740_992_u64),
595            ),
596            ("semantic_output_started", serde_json::json!(false)),
597            ("retry_safe", serde_json::json!(true)),
598            ("last_http_status", serde_json::json!(429)),
599            ("retry_delay_ms", serde_json::json!(5_000)),
600            (
601                "provider",
602                serde_json::json!("https://private.invalid/token"),
603            ),
604            ("model", serde_json::json!("sk-private")),
605            ("model", serde_json::json!("Bearer_private")),
606            ("future_secret", serde_json::json!("PRIVATE_CANARY")),
607        ] {
608            let mut forged = value.clone();
609            forged[field] = invalid;
610            assert!(
611                LlmRequestDiagnostic::from_json(&forged.to_string()).is_none(),
612                "accepted invalid {field}"
613            );
614        }
615        let receipt = timeout_sample(&session);
616        assert!(
617            !receipt.matches_error_message(&format!("upstream said {}", receipt.error_message()))
618        );
619        assert!(!receipt.matches_error_message(&format!("LLM error: {}", receipt.error_message())));
620    }
621}