Skip to main content

aidens_contracts/
provider.rs

1//! Provider route and backend display contracts.
2//!
3//! AiDENs records local provider readiness and routing evidence here; hosted provider authority remains external and unsupported unless separately proven.
4
5use super::*;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
8#[serde(rename_all = "kebab-case")]
9pub enum ProviderRouteKindV1 {
10    Mock,
11    NativeOpenAiResponses,
12    NativeOpenAiChat,
13    NativeAnthropic,
14    NativeOllama,
15    OllamaChat,
16    OpenAiCompatible,
17    ParserFallback,
18    Disabled,
19    Unavailable,
20    Degraded,
21}
22
23impl fmt::Display for ProviderRouteKindV1 {
24    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25        f.write_str(match self {
26            Self::Mock => "mock",
27            Self::NativeOpenAiResponses => "native-openai-responses",
28            Self::NativeOpenAiChat => "native-openai-chat",
29            Self::NativeAnthropic => "native-anthropic",
30            Self::NativeOllama => "native-ollama",
31            Self::OllamaChat => "ollama-chat",
32            Self::OpenAiCompatible => "openai-compatible",
33            Self::ParserFallback => "parser-fallback",
34            Self::Disabled => "disabled",
35            Self::Unavailable => "unavailable",
36            Self::Degraded => "degraded",
37        })
38    }
39}
40
41#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
42pub struct ProviderRouteReportV1 {
43    pub provider_kind: String,
44    pub model: Option<String>,
45    pub route: ProviderRouteKindV1,
46    pub route_label: String,
47    pub native_tool_loop: bool,
48    #[serde(default)]
49    pub degraded: bool,
50    pub degraded_reason: Option<String>,
51    #[serde(default, skip_serializing_if = "Vec::is_empty")]
52    pub reason_codes: Vec<String>,
53}
54
55impl ProviderRouteReportV1 {
56    pub fn new(
57        provider_kind: impl Into<String>,
58        model: Option<String>,
59        route: ProviderRouteKindV1,
60        reason_codes: Vec<String>,
61    ) -> Self {
62        let native_tool_loop = matches!(
63            route,
64            ProviderRouteKindV1::NativeOpenAiResponses
65                | ProviderRouteKindV1::NativeOpenAiChat
66                | ProviderRouteKindV1::NativeAnthropic
67                | ProviderRouteKindV1::NativeOllama
68                | ProviderRouteKindV1::OpenAiCompatible
69        );
70        let degraded = matches!(
71            route,
72            ProviderRouteKindV1::ParserFallback
73                | ProviderRouteKindV1::Unavailable
74                | ProviderRouteKindV1::Degraded
75        );
76        let degraded_reason = degraded.then(|| reason_codes.join(", "));
77        Self {
78            provider_kind: provider_kind.into(),
79            model,
80            route_label: route.to_string(),
81            route,
82            native_tool_loop,
83            degraded,
84            degraded_reason,
85            reason_codes,
86        }
87    }
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
91#[serde(rename_all = "kebab-case")]
92pub enum ProviderBackendStatusV1 {
93    Disabled,
94    Executable,
95    BoundaryUnavailable,
96    Unsupported,
97}
98
99impl fmt::Display for ProviderBackendStatusV1 {
100    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101        f.write_str(match self {
102            Self::Disabled => "disabled",
103            Self::Executable => "executable",
104            Self::BoundaryUnavailable => "boundary-unavailable",
105            Self::Unsupported => "unsupported",
106        })
107    }
108}
109
110#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
111pub struct ProviderBackendMatrixEntryV1 {
112    pub provider_kind: String,
113    pub status: ProviderBackendStatusV1,
114    pub route_label: String,
115    pub api_key_required: bool,
116    pub chat_completion_executable: bool,
117    pub native_tool_loop_executable: bool,
118    pub streaming_executable: bool,
119    pub structured_output_executable: bool,
120    #[serde(default, skip_serializing_if = "Vec::is_empty")]
121    pub reason_codes: Vec<String>,
122}
123
124impl ProviderBackendMatrixEntryV1 {
125    pub fn native_tool_loop_ready(&self) -> bool {
126        self.status == ProviderBackendStatusV1::Executable && self.native_tool_loop_executable
127    }
128}
129
130#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
131pub struct ProviderBackendMatrixV1 {
132    pub matrix_id: ArtifactId,
133    pub generated_at: DateTime<Utc>,
134    pub entries: Vec<ProviderBackendMatrixEntryV1>,
135}
136
137impl ProviderBackendMatrixV1 {
138    pub fn new(entries: Vec<ProviderBackendMatrixEntryV1>) -> Self {
139        Self {
140            matrix_id: display_only_unstable_id("provider-backend-matrix"),
141            generated_at: Utc::now(),
142            entries,
143        }
144    }
145
146    pub fn entry_for(&self, provider_kind: &str) -> Option<&ProviderBackendMatrixEntryV1> {
147        let normalized = provider_kind.trim().to_ascii_lowercase();
148        self.entries
149            .iter()
150            .find(|entry| entry.provider_kind == normalized)
151    }
152}
153
154#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
155pub struct ProviderReadinessReportV1 {
156    pub receipt_id: ArtifactId,
157    pub kind: ArtifactKindV1,
158    pub provider_kind: String,
159    pub model: Option<String>,
160    pub configured: bool,
161    pub executable: bool,
162    pub native_tool_loop_executable: bool,
163    pub route_label: String,
164    #[serde(default, skip_serializing_if = "Vec::is_empty")]
165    pub reason_codes: Vec<String>,
166    pub checked_at: DateTime<Utc>,
167}
168
169#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
170pub struct ProviderReadinessReportDraftV1 {
171    pub provider_kind: String,
172    pub model: Option<String>,
173    pub configured: bool,
174    pub executable: bool,
175    pub native_tool_loop_executable: bool,
176    pub route_label: String,
177    #[serde(default, skip_serializing_if = "Vec::is_empty")]
178    pub reason_codes: Vec<String>,
179}
180
181impl ProviderReadinessReportV1 {
182    pub fn new(draft: ProviderReadinessReportDraftV1) -> Self {
183        Self {
184            receipt_id: display_only_unstable_id("provider-readiness"),
185            kind: ArtifactKindV1::ProviderReadiness,
186            provider_kind: draft.provider_kind,
187            model: draft.model,
188            configured: draft.configured,
189            executable: draft.executable,
190            native_tool_loop_executable: draft.native_tool_loop_executable,
191            route_label: draft.route_label,
192            reason_codes: draft.reason_codes,
193            checked_at: Utc::now(),
194        }
195    }
196}
197
198#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
199pub struct ProviderRouteReportV2 {
200    pub receipt_id: ArtifactId,
201    pub kind: ArtifactKindV1,
202    pub provider_kind: String,
203    pub model: Option<String>,
204    pub route: ProviderRouteKindV1,
205    pub route_label: String,
206    pub chat_completion_executable: bool,
207    pub native_tool_loop: bool,
208    #[serde(default)]
209    pub degraded: bool,
210    pub degraded_reason: Option<String>,
211    #[serde(default, skip_serializing_if = "Vec::is_empty")]
212    pub reason_codes: Vec<String>,
213    pub checked_at: DateTime<Utc>,
214}
215
216#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
217pub struct ProviderRouteReportDraftV2 {
218    pub provider_kind: String,
219    pub model: Option<String>,
220    pub route: ProviderRouteKindV1,
221    pub route_label: String,
222    pub chat_completion_executable: bool,
223    pub native_tool_loop: bool,
224    pub degraded: bool,
225    pub degraded_reason: Option<String>,
226    #[serde(default, skip_serializing_if = "Vec::is_empty")]
227    pub reason_codes: Vec<String>,
228}
229
230impl ProviderRouteReportV2 {
231    pub fn new(draft: ProviderRouteReportDraftV2) -> Self {
232        Self {
233            receipt_id: display_only_unstable_id("provider-route"),
234            kind: ArtifactKindV1::ProviderRoute,
235            provider_kind: draft.provider_kind,
236            model: draft.model,
237            route: draft.route,
238            route_label: draft.route_label,
239            chat_completion_executable: draft.chat_completion_executable,
240            native_tool_loop: draft.native_tool_loop,
241            degraded: draft.degraded,
242            degraded_reason: draft.degraded_reason,
243            reason_codes: draft.reason_codes,
244            checked_at: Utc::now(),
245        }
246    }
247
248    pub fn to_v1(&self) -> ProviderRouteReportV1 {
249        ProviderRouteReportV1 {
250            provider_kind: self.provider_kind.clone(),
251            model: self.model.clone(),
252            route: self.route,
253            route_label: self.route_label.clone(),
254            native_tool_loop: self.native_tool_loop,
255            degraded: self.degraded,
256            degraded_reason: self.degraded_reason.clone(),
257            reason_codes: self.reason_codes.clone(),
258        }
259    }
260}
261
262#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
263pub struct ProviderCertificationFixtureV1 {
264    pub fixture_id: ArtifactId,
265    pub provider_kind: String,
266    pub scenario: String,
267    pub input_config: BTreeMap<String, String>,
268    pub expected_configured: bool,
269    pub expected_executable: bool,
270    pub expected_route_label: String,
271    pub expected_native_tool_loop: bool,
272    #[serde(default, skip_serializing_if = "Vec::is_empty")]
273    pub expected_reason_codes: Vec<String>,
274}
275
276#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
277pub struct ProviderCertificationFixtureDraftV1 {
278    pub provider_kind: String,
279    pub scenario: String,
280    pub input_config: BTreeMap<String, String>,
281    pub expected_configured: bool,
282    pub expected_executable: bool,
283    pub expected_route_label: String,
284    pub expected_native_tool_loop: bool,
285    #[serde(default, skip_serializing_if = "Vec::is_empty")]
286    pub expected_reason_codes: Vec<String>,
287}
288
289impl ProviderCertificationFixtureV1 {
290    pub fn new(draft: ProviderCertificationFixtureDraftV1) -> Self {
291        Self {
292            fixture_id: display_only_unstable_id("provider-certification-fixture"),
293            provider_kind: draft.provider_kind,
294            scenario: draft.scenario,
295            input_config: draft.input_config,
296            expected_configured: draft.expected_configured,
297            expected_executable: draft.expected_executable,
298            expected_route_label: draft.expected_route_label,
299            expected_native_tool_loop: draft.expected_native_tool_loop,
300            expected_reason_codes: draft.expected_reason_codes,
301        }
302    }
303}