Skip to main content

kernel/event/record/
probe.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4// Copyright (c) 2026 2youg1 and the sprawling contributors
5
6//! What a probe of a base URL found before anything was attached: how
7//! far the call got, and what the model list said.
8
9use serde::{Deserialize, Serialize};
10
11use crate::model::Ceiling;
12use crate::reach::Reach;
13
14/// What a model list said about one model.
15///
16/// Everything but the id is optional, because for most providers
17/// everything but the id is missing.
18#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
19#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
20pub struct ModelFacts {
21    pub id: String,
22    /// How many tokens the model reads in one call.
23    pub context_tokens: Option<u64>,
24    /// How many tokens it may write back.
25    pub max_output_tokens: Option<Ceiling>,
26    /// What it accepts, in the provider's own words (`text`, `image`,
27    /// `audio`). Empty means the row said nothing, never "text only".
28    pub input_modalities: Vec<String>,
29    /// The provider's own input price, verbatim, unit included when the
30    /// provider stated one.
31    pub input_price: Option<String>,
32    /// The provider's own output price, verbatim.
33    pub output_price: Option<String>,
34}
35
36/// `endpoint_probed`: a base URL was asked what it serves, and nothing
37/// was attached.
38///
39/// A probe that could not read a model list still writes this line:
40/// `models` and `facts` are then empty rather than absent, and `failed`
41/// tells the two apart.
42#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
43#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
44pub struct EndpointProbed {
45    pub name: String,
46    pub base_url: String,
47    /// How far the call got, stage by stage.
48    pub reach: Reach,
49    pub models: Vec<String>,
50    pub facts: Vec<ModelFacts>,
51    /// Why the model list could not be read, when it could not.
52    #[serde(default, skip_serializing_if = "Option::is_none")]
53    pub failed: Option<ProbeFailure>,
54}
55
56/// The refusal a probe met, as its stable code and subject.
57#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
58#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
59pub struct ProbeFailure {
60    pub code: String,
61    pub subject: String,
62}
63
64#[cfg(test)]
65#[allow(clippy::unwrap_used, reason = "test code")]
66mod tests {
67    use super::*;
68    use crate::event::Payload;
69
70    /// The golden bytes the hand-written writer spelled: a probe that
71    /// could not read a model list keeps both lists, empty, beside
72    /// `failed`.
73    #[test]
74    fn a_probed_line_keeps_the_bytes_its_ledger_already_holds() {
75        let line = concat!(
76            r#"{"base_url":"http://127.0.0.1:1","facts":[],"#,
77            r#""failed":{"code":"E_PROVIDER","subject":"m"},"models":[],"name":"local","#,
78            r#""reach":{"answered":{"detail":"no socket","state":"unreachable"},"#,
79            r#""connected":{"state":"refused"},"elapsed_ms":3,"host":"127.0.0.1","#,
80            r#""named":{"detail":1,"state":"resolved"},"through":{"state":"direct"}}}"#,
81        );
82        let read: EndpointProbed = serde_json::from_str::<Payload>(line)
83            .unwrap()
84            .read()
85            .unwrap();
86        assert_eq!(
87            serde_json::to_string(&Payload::of(&read).unwrap()).unwrap(),
88            line
89        );
90    }
91}