1use 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}