Skip to main content

ai_lib_core/protocol/
config.rs

1//! Protocol configuration structures
2//!
3//! This module contains all the configuration-related structures used in protocol manifests.
4
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8/// Structured endpoint definition (v1.1+ extension)
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct EndpointDefinition {
11    pub base_url: String,
12    #[serde(skip_serializing_if = "Option::is_none")]
13    pub protocol: Option<String>, // https, http, ws, wss
14    #[serde(skip_serializing_if = "Option::is_none")]
15    pub timeout_ms: Option<u32>,
16}
17
18/// Endpoint configuration for specific operations
19#[derive(Debug, Clone, Serialize)]
20pub struct EndpointConfig {
21    pub path: String,
22    pub method: String,
23    #[serde(skip_serializing_if = "Option::is_none")]
24    pub adapter: Option<String>,
25}
26
27impl<'de> Deserialize<'de> for EndpointConfig {
28    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
29    where
30        D: serde::Deserializer<'de>,
31    {
32        #[derive(Deserialize)]
33        #[serde(untagged)]
34        enum Input {
35            // Shorthand: endpoint: "/v1/chat/completions"
36            Path(String),
37            // Full form
38            Obj {
39                path: String,
40                #[serde(default = "default_method")]
41                method: String,
42                #[serde(default)]
43                adapter: Option<String>,
44            },
45        }
46
47        match Input::deserialize(deserializer)? {
48            Input::Path(path) => Ok(EndpointConfig {
49                path,
50                method: default_method(),
51                adapter: None,
52            }),
53            Input::Obj {
54                path,
55                method,
56                adapter,
57            } => Ok(EndpointConfig {
58                path,
59                method,
60                adapter,
61            }),
62        }
63    }
64}
65
66fn default_method() -> String {
67    "POST".to_string()
68}
69
70/// Service configuration for auxiliary endpoints
71#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct ServiceConfig {
73    pub path: String,
74    #[serde(default = "default_method_get")]
75    pub method: String,
76    #[serde(skip_serializing_if = "Option::is_none")]
77    pub headers: Option<HashMap<String, String>>,
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub query_params: Option<HashMap<String, String>>,
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub response_binding: Option<String>,
82}
83
84fn default_method_get() -> String {
85    "GET".to_string()
86}
87
88/// Capabilities object format (v1.1+ legacy + v2 required/optional shape)
89#[derive(Debug, Clone, Serialize)]
90pub struct Capabilities {
91    pub streaming: bool,
92    pub tools: bool,
93    pub vision: bool,
94    #[serde(default, skip_serializing_if = "is_false")]
95    pub agentic: bool,
96    #[serde(default, skip_serializing_if = "is_false")]
97    pub parallel_tools: bool,
98    #[serde(default, skip_serializing_if = "is_false")]
99    pub reasoning: bool,
100    #[serde(default, skip_serializing_if = "is_false")]
101    pub multimodal: bool,
102    #[serde(default, skip_serializing_if = "is_false")]
103    pub audio: bool,
104    /// Structured output / JSON mode (capability or `feature_flags.structured_output`).
105    #[serde(default, skip_serializing_if = "is_false")]
106    pub structured_output: bool,
107    /// MCP client tool-bridge (`mcp_client` in V2 optional/required lists).
108    #[serde(default, skip_serializing_if = "is_false")]
109    pub mcp_client: bool,
110}
111
112impl<'de> Deserialize<'de> for Capabilities {
113    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
114    where
115        D: serde::Deserializer<'de>,
116    {
117        #[derive(Deserialize)]
118        struct LegacyCaps {
119            streaming: bool,
120            tools: bool,
121            vision: bool,
122            #[serde(default)]
123            agentic: bool,
124            #[serde(default)]
125            parallel_tools: bool,
126            #[serde(default)]
127            reasoning: bool,
128            #[serde(default)]
129            multimodal: bool,
130            #[serde(default)]
131            audio: bool,
132        }
133
134        #[derive(Deserialize, Default)]
135        struct FeatureFlags {
136            #[serde(default)]
137            parallel_tool_calls: bool,
138            #[serde(default)]
139            extended_thinking: bool,
140            #[serde(default)]
141            structured_output: bool,
142        }
143
144        #[derive(Deserialize)]
145        struct V2Caps {
146            required: Vec<String>,
147            #[serde(default)]
148            optional: Vec<String>,
149            #[serde(default)]
150            feature_flags: Option<FeatureFlags>,
151        }
152
153        #[derive(Deserialize)]
154        #[serde(untagged)]
155        enum Input {
156            /// Shorthand list e.g. `[chat, streaming, tools]` in compliance fixtures.
157            TagList(Vec<String>),
158            Legacy(LegacyCaps),
159            V2(V2Caps),
160        }
161
162        fn from_capability_tags(tags: &[String]) -> Capabilities {
163            let mut c = Capabilities {
164                streaming: false,
165                tools: false,
166                vision: false,
167                agentic: false,
168                parallel_tools: false,
169                reasoning: false,
170                multimodal: false,
171                audio: false,
172                structured_output: false,
173                mcp_client: false,
174            };
175            for t in tags {
176                match t.as_str() {
177                    "chat" | "text" => {}
178                    "streaming" => c.streaming = true,
179                    "tools" => c.tools = true,
180                    "vision" => {
181                        c.vision = true;
182                        c.multimodal = true;
183                    }
184                    "audio" => {
185                        c.audio = true;
186                        c.multimodal = true;
187                    }
188                    "video" => c.multimodal = true,
189                    "agentic" => c.agentic = true,
190                    "parallel_tools" => c.parallel_tools = true,
191                    "reasoning" => c.reasoning = true,
192                    "structured_output" => c.structured_output = true,
193                    "mcp_client" => c.mcp_client = true,
194                    _ => {}
195                }
196            }
197            c
198        }
199
200        match Input::deserialize(deserializer)? {
201            Input::TagList(tags) => Ok(from_capability_tags(&tags)),
202            Input::Legacy(v) => Ok(Capabilities {
203                streaming: v.streaming,
204                tools: v.tools,
205                vision: v.vision,
206                agentic: v.agentic,
207                parallel_tools: v.parallel_tools,
208                reasoning: v.reasoning,
209                multimodal: v.multimodal,
210                audio: v.audio,
211                structured_output: false,
212                mcp_client: false,
213            }),
214            Input::V2(v) => {
215                let has = |name: &str| {
216                    v.required.iter().any(|c| c == name) || v.optional.iter().any(|c| c == name)
217                };
218                let flags = v.feature_flags.unwrap_or_default();
219                Ok(Capabilities {
220                    streaming: has("streaming"),
221                    tools: has("tools"),
222                    vision: has("vision"),
223                    agentic: has("agentic"),
224                    parallel_tools: has("parallel_tools") || flags.parallel_tool_calls,
225                    reasoning: has("reasoning") || flags.extended_thinking,
226                    multimodal: has("vision") || has("audio") || has("video"),
227                    audio: has("audio"),
228                    structured_output: has("structured_output") || flags.structured_output,
229                    mcp_client: has("mcp_client"),
230                })
231            }
232        }
233    }
234}
235
236fn is_false(b: &bool) -> bool {
237    !*b
238}
239
240/// Authentication configuration
241#[derive(Debug, Clone, Serialize, Deserialize)]
242pub struct AuthConfig {
243    #[serde(rename = "type")]
244    pub auth_type: String,
245    #[serde(default, skip_serializing_if = "Option::is_none")]
246    pub token_env: Option<String>,
247    #[serde(default, skip_serializing_if = "Option::is_none")]
248    pub key_env: Option<String>,
249    #[serde(default, skip_serializing_if = "Option::is_none")]
250    pub param_name: Option<String>,
251    #[serde(default, skip_serializing_if = "Option::is_none")]
252    pub header_name: Option<String>,
253    #[serde(default, skip_serializing_if = "Option::is_none")]
254    pub extra_headers: Option<Vec<HeaderConfig>>,
255}
256
257/// Header configuration for extra headers
258#[derive(Debug, Clone, Serialize, Deserialize)]
259pub struct HeaderConfig {
260    pub name: String,
261    pub value: String,
262}
263
264/// Streaming configuration
265#[derive(Debug, Clone, Serialize, Deserialize)]
266pub struct StreamingConfig {
267    #[serde(skip_serializing_if = "Option::is_none")]
268    pub event_format: Option<String>,
269    #[serde(skip_serializing_if = "Option::is_none")]
270    pub decoder: Option<DecoderConfig>,
271    #[serde(skip_serializing_if = "Option::is_none")]
272    pub frame_selector: Option<String>,
273    /// Common path for content delta in streaming frames (provider-specific)
274    #[serde(default, skip_serializing_if = "Option::is_none")]
275    pub content_path: Option<String>,
276    /// Common path for tool call delta in streaming frames (provider-specific)
277    #[serde(default, skip_serializing_if = "Option::is_none")]
278    pub tool_call_path: Option<String>,
279    /// Common path for usage metadata in streaming frames (provider-specific)
280    #[serde(default, skip_serializing_if = "Option::is_none")]
281    pub usage_path: Option<String>,
282    #[serde(skip_serializing_if = "Option::is_none")]
283    pub candidate: Option<CandidateConfig>,
284    #[serde(skip_serializing_if = "Option::is_none")]
285    pub accumulator: Option<AccumulatorConfig>,
286    #[serde(default)]
287    pub event_map: Vec<EventMapRule>,
288    #[serde(skip_serializing_if = "Option::is_none")]
289    pub stop_condition: Option<String>,
290}
291
292/// Decoder configuration for streaming
293#[derive(Debug, Clone, Serialize, Deserialize)]
294pub struct DecoderConfig {
295    pub format: String,
296    #[serde(skip_serializing_if = "Option::is_none")]
297    pub strategy: Option<String>,
298    #[serde(skip_serializing_if = "Option::is_none")]
299    pub delimiter: Option<String>,
300    #[serde(skip_serializing_if = "Option::is_none")]
301    pub prefix: Option<String>,
302    #[serde(skip_serializing_if = "Option::is_none")]
303    pub done_signal: Option<String>,
304}
305
306/// Candidate configuration for multi-candidate responses
307#[derive(Debug, Clone, Serialize, Deserialize)]
308pub struct CandidateConfig {
309    #[serde(skip_serializing_if = "Option::is_none")]
310    pub candidate_id_path: Option<String>,
311    #[serde(skip_serializing_if = "Option::is_none")]
312    pub fan_out: Option<bool>,
313}
314
315/// Accumulator configuration for stateful parsing
316#[derive(Debug, Clone, Serialize, Deserialize)]
317pub struct AccumulatorConfig {
318    #[serde(default)]
319    pub stateful_tool_parsing: bool,
320    #[serde(skip_serializing_if = "Option::is_none")]
321    pub key_path: Option<String>,
322    #[serde(skip_serializing_if = "Option::is_none")]
323    pub flush_on: Option<String>,
324}
325
326/// Event mapping rule for streaming events
327#[derive(Debug, Clone, Serialize, Deserialize)]
328pub struct EventMapRule {
329    #[serde(rename = "match")]
330    pub match_expr: String,
331    pub emit: String,
332    #[serde(default, skip_serializing_if = "Option::is_none")]
333    pub fields: Option<HashMap<String, String>>,
334}
335
336/// Features configuration
337#[derive(Debug, Clone, Serialize, Deserialize)]
338pub struct FeaturesConfig {
339    #[serde(default, skip_serializing_if = "Option::is_none")]
340    pub multi_candidate: Option<MultiCandidateConfig>,
341    #[serde(default, skip_serializing_if = "Option::is_none")]
342    pub response_mapping: Option<ResponseMappingConfig>,
343}
344
345/// Multi-candidate configuration
346#[derive(Debug, Clone, Serialize, Deserialize)]
347pub struct MultiCandidateConfig {
348    pub support_type: String,
349    #[serde(default, skip_serializing_if = "Option::is_none")]
350    pub param_name: Option<String>,
351    #[serde(default, skip_serializing_if = "Option::is_none")]
352    pub max_concurrent: Option<u32>,
353}
354
355/// Response mapping configuration
356#[derive(Debug, Clone, Serialize, Deserialize)]
357pub struct ResponseMappingConfig {
358    #[serde(default, skip_serializing_if = "Option::is_none")]
359    pub tool_calls: Option<ToolCallsMapping>,
360    #[serde(default, skip_serializing_if = "Option::is_none")]
361    pub error: Option<ErrorMapping>,
362}
363
364/// Tool calls mapping configuration
365#[derive(Debug, Clone, Serialize, Deserialize)]
366pub struct ToolCallsMapping {
367    pub path: String,
368    #[serde(default, skip_serializing_if = "Option::is_none")]
369    pub filter: Option<String>,
370    pub fields: HashMap<String, String>,
371    #[serde(default, skip_serializing_if = "Option::is_none")]
372    pub array_fan_out: Option<bool>,
373}
374
375/// Error mapping configuration
376#[derive(Debug, Clone, Serialize, Deserialize)]
377pub struct ErrorMapping {
378    #[serde(default, skip_serializing_if = "Option::is_none")]
379    pub message_path: Option<String>,
380    #[serde(default, skip_serializing_if = "Option::is_none")]
381    pub code_path: Option<String>,
382    #[serde(default, skip_serializing_if = "Option::is_none")]
383    pub type_path: Option<String>,
384}
385
386/// Termination configuration
387#[derive(Debug, Clone, Serialize, Deserialize)]
388pub struct TerminationConfig {
389    pub source_field: String,
390    #[serde(default, skip_serializing_if = "Option::is_none")]
391    pub mapping: Option<HashMap<String, String>>,
392}
393
394/// Tooling configuration
395#[derive(Debug, Clone, Serialize, Deserialize)]
396pub struct ToolingConfig {
397    pub source_model: String,
398    #[serde(default, skip_serializing_if = "Option::is_none")]
399    pub tool_use: Option<ToolUseMapping>,
400    #[serde(default, skip_serializing_if = "Option::is_none")]
401    pub tool_result: Option<ToolResultMapping>,
402}
403
404/// Tool use mapping
405#[derive(Debug, Clone, Serialize, Deserialize)]
406pub struct ToolUseMapping {
407    #[serde(default, skip_serializing_if = "Option::is_none")]
408    pub id_path: Option<String>,
409    #[serde(default, skip_serializing_if = "Option::is_none")]
410    pub name_path: Option<String>,
411    #[serde(default, skip_serializing_if = "Option::is_none")]
412    pub input_path: Option<String>,
413    #[serde(default, skip_serializing_if = "Option::is_none")]
414    pub input_format: Option<String>,
415}
416
417/// Tool result mapping
418#[derive(Debug, Clone, Serialize, Deserialize)]
419pub struct ToolResultMapping {
420    #[serde(default, skip_serializing_if = "Option::is_none")]
421    pub id_path: Option<String>,
422    #[serde(default, skip_serializing_if = "Option::is_none")]
423    pub name_path: Option<String>,
424    #[serde(default, skip_serializing_if = "Option::is_none")]
425    pub response_path: Option<String>,
426}
427
428/// Retry policy configuration
429#[derive(Debug, Clone, Serialize, Deserialize)]
430pub struct RetryPolicy {
431    pub strategy: String,
432    #[serde(default, skip_serializing_if = "Option::is_none")]
433    pub max_retries: Option<u32>,
434    #[serde(default, skip_serializing_if = "Option::is_none")]
435    pub min_delay_ms: Option<u32>,
436    #[serde(default, skip_serializing_if = "Option::is_none")]
437    pub max_delay_ms: Option<u32>,
438    #[serde(default, skip_serializing_if = "Option::is_none")]
439    pub jitter: Option<String>,
440    #[serde(default, skip_serializing_if = "Option::is_none")]
441    pub retry_on_http_status: Option<Vec<u16>>,
442    #[serde(default, skip_serializing_if = "Option::is_none")]
443    pub retry_on_error_status: Option<Vec<String>>,
444}
445
446/// Error classification configuration
447#[derive(Debug, Clone, Serialize, Deserialize)]
448pub struct ErrorClassification {
449    #[serde(default, skip_serializing_if = "Option::is_none")]
450    pub by_http_status: Option<HashMap<String, String>>,
451    #[serde(default, skip_serializing_if = "Option::is_none")]
452    pub by_error_status: Option<HashMap<String, String>>,
453}
454
455/// Availability and health checking configuration (v1.1+ extension)
456/// Required fields: required, regions, check
457#[derive(Debug, Clone, Serialize, Deserialize)]
458pub struct AvailabilityConfig {
459    pub required: bool,
460    pub regions: Vec<String>, // cn, global, us, eu
461    pub check: HealthCheckConfig,
462    #[serde(skip_serializing_if = "Option::is_none")]
463    pub notes: Option<Vec<String>>,
464}
465
466/// Health check endpoint configuration
467/// Required fields: method, path, expected_status
468#[derive(Debug, Clone, Serialize, Deserialize)]
469pub struct HealthCheckConfig {
470    pub method: String, // HEAD, GET
471    pub path: String,
472    pub expected_status: Vec<u16>,
473    #[serde(skip_serializing_if = "Option::is_none")]
474    pub timeout_ms: Option<u32>,
475}
476
477/// Rate limit headers configuration
478#[derive(Debug, Clone, Serialize, Deserialize)]
479pub struct RateLimitHeaders {
480    #[serde(default, skip_serializing_if = "Option::is_none")]
481    pub requests_limit: Option<String>,
482    #[serde(default, skip_serializing_if = "Option::is_none")]
483    pub requests_remaining: Option<String>,
484    #[serde(default, skip_serializing_if = "Option::is_none")]
485    pub requests_reset: Option<String>,
486    #[serde(default, skip_serializing_if = "Option::is_none")]
487    pub tokens_limit: Option<String>,
488    #[serde(default, skip_serializing_if = "Option::is_none")]
489    pub tokens_remaining: Option<String>,
490    #[serde(default, skip_serializing_if = "Option::is_none")]
491    pub tokens_reset: Option<String>,
492    #[serde(default, skip_serializing_if = "Option::is_none")]
493    pub retry_after: Option<String>,
494}