Skip to main content

xz_provider/
config.rs

1use std::collections::HashMap;
2
3use serde::{Deserialize, Serialize};
4
5use crate::protocol::AuthMethod;
6use crate::types::{ModelCapabilities, ModelInfo, ModelLimits, ModelPricing};
7
8/// Provider 类型
9#[derive(Debug, Clone, PartialEq)]
10pub enum ProviderType {
11    Claude,
12    OpenAiCompatible,
13    /// Generic provider for custom configurations.
14    Generic,
15}
16
17impl Serialize for ProviderType {
18    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
19        let s = match self {
20            ProviderType::Claude => "claude",
21            ProviderType::OpenAiCompatible => "open_ai_compatible",
22            ProviderType::Generic => "generic",
23        };
24        serializer.serialize_str(s)
25    }
26}
27
28impl<'de> Deserialize<'de> for ProviderType {
29    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
30        let s = String::deserialize(deserializer)?;
31        match s.as_str() {
32            "claude" => Ok(ProviderType::Claude),
33            // "open_ai" and "local" map to OpenAiCompatible for backward compatibility
34            "open_ai" | "open_ai_compatible" | "local" => Ok(ProviderType::OpenAiCompatible),
35            "generic" => Ok(ProviderType::Generic),
36            _ => Err(serde::de::Error::unknown_variant(
37                &s,
38                &["claude", "open_ai", "open_ai_compatible", "local", "generic"],
39            )),
40        }
41    }
42}
43
44/// 模型配置
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct ModelConfig {
47    /// 模型名称
48    pub name: String,
49
50    /// 可读名称(可选)
51    #[serde(default)]
52    pub display_name: Option<String>,
53
54    /// 能力声明
55    #[serde(default)]
56    pub capabilities: ModelCapabilities,
57
58    /// 价格信息
59    #[serde(default)]
60    pub pricing: ModelPricing,
61
62    /// 速率限制
63    #[serde(default)]
64    pub limits: ModelLimits,
65}
66
67impl From<ModelConfig> for ModelInfo {
68    fn from(cfg: ModelConfig) -> Self {
69        ModelInfo {
70            name: cfg.name,
71            display_name: cfg.display_name,
72            provider: None,
73            capabilities: cfg.capabilities,
74            pricing: cfg.pricing,
75            limits: cfg.limits,
76        }
77    }
78}
79
80/// API protocol variant for OpenAI-compatible endpoints.
81///
82/// Determines which endpoint path and request/response format the provider uses.
83///
84/// ```
85/// use xz_provider::config::ApiProtocol;
86///
87/// let protocol = ApiProtocol::ChatCompletions;
88/// assert_eq!(protocol, ApiProtocol::ChatCompletions);
89/// ```
90#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
91#[serde(rename_all = "snake_case")]
92pub enum ApiProtocol {
93    /// Chat Completions protocol — POST `/v1/chat/completions`.
94    ///
95    /// The standard OpenAI chat completion format with `messages` array.
96    #[default]
97    ChatCompletions,
98    /// Responses API protocol — POST `/v1/responses`.
99    ///
100    /// Uses `input`/`instructions` fields. Supported by newer OpenAI-compatible endpoints.
101    Responses,
102    /// Anthropic Messages protocol — POST `/v1/messages`.
103    ///
104    /// Uses Anthropic's own request/response format.
105    AnthropicMessages,
106}
107
108/// Provider 定义
109#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct ProviderDefinition {
111    pub provider_type: ProviderType,
112
113    #[serde(skip_serializing_if = "Option::is_none")]
114    pub api_key: Option<String>,
115
116    #[serde(skip_serializing_if = "Option::is_none")]
117    pub base_url: Option<String>,
118
119    /// Custom HTTP headers to include in every request to this provider.
120    #[serde(default, skip_serializing_if = "Option::is_none")]
121    pub headers: Option<HashMap<String, String>>,
122
123    /// API protocol variant: ChatCompletions (default), Responses, or AnthropicMessages.
124    #[serde(default)]
125    pub protocol: ApiProtocol,
126
127    /// Authentication method for this provider.
128    ///
129    /// If `None`, the provider uses the global API key from the router.
130    #[serde(default, skip_serializing_if = "Option::is_none")]
131    pub auth_method: Option<AuthMethod>,
132
133    /// Anthropic API version string (e.g. "2023-06-01").
134    #[serde(default, skip_serializing_if = "Option::is_none")]
135    pub anthropic_version: Option<String>,
136
137    /// Anthropic beta feature flags (e.g. ["prompt-caching-2025-02-19"]).
138    #[serde(default, skip_serializing_if = "Option::is_none")]
139    pub anthropic_beta: Option<Vec<String>>,
140
141    /// Default thinking mode for this provider (e.g. "extended").
142    #[serde(default, skip_serializing_if = "Option::is_none")]
143    pub default_thinking_type: Option<String>,
144
145    /// Default effort level for thinking (e.g. "high", "medium", "low").
146    #[serde(default, skip_serializing_if = "Option::is_none")]
147    pub default_effort: Option<String>,
148
149    pub models: Vec<ModelConfig>,
150}
151
152/// 路由规则 — 按用途将请求映射到指定 Provider + 模型
153#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct RouteRule {
155    pub model: String,
156    pub provider: Option<String>,
157
158    #[serde(default)]
159    pub temperature: Option<f32>,
160
161    #[serde(default)]
162    pub max_tokens: Option<usize>,
163
164    /// 回退链:主模型失败后按顺序尝试
165    #[serde(default)]
166    pub fallback: Vec<FallbackEntry>,
167}
168
169/// 回退条目
170#[derive(Debug, Clone, Serialize, Deserialize)]
171pub struct FallbackEntry {
172    pub model: String,
173    pub provider: Option<String>,
174
175    /// 回退触发条件
176    #[serde(default)]
177    pub condition: FallbackCondition,
178}
179
180/// 回退触发条件
181#[derive(Debug, Clone, Default, Serialize, Deserialize)]
182pub enum FallbackCondition {
183    /// 总是回退
184    #[serde(rename = "always")]
185    #[default]
186    Always,
187    /// 仅限限流时回退
188    #[serde(rename = "rate_limit_only")]
189    RateLimitOnly,
190    /// 特定 HTTP 状态码时回退
191    #[serde(rename = "error_status")]
192    ErrorStatus(Vec<u16>),
193}
194
195/// Provider 配置(v2)
196#[derive(Debug, Clone, Serialize, Deserialize)]
197pub struct ProviderConfig {
198    /// 默认模型名称
199    pub default_model: Option<String>,
200
201    /// 各 Provider 定义
202    pub providers: HashMap<String, ProviderDefinition>,
203
204    /// 按用途的命名路由
205    #[serde(default)]
206    pub routing: HashMap<String, RouteRule>,
207}
208
209impl ProviderConfig {
210    /// 从 JSON 字符串加载
211    pub fn from_json(json: &str) -> Result<Self, crate::error::ProviderError> {
212        let config: Self = serde_json::from_str(json)
213            .map_err(|e| crate::error::ProviderError::Config(e.to_string()))?;
214        config.validate()
215    }
216
217    /// 从 YAML 字符串加载
218    pub fn from_yaml(yaml: &str) -> Result<Self, crate::error::ProviderError> {
219        let yaml_interpolated = Self::interpolate_env(yaml);
220        let config: Self = serde_yaml::from_str(&yaml_interpolated)
221            .map_err(|e| crate::error::ProviderError::Config(e.to_string()))?;
222        config.validate()
223    }
224
225    /// 从 JSON 文件加载
226    pub async fn from_file(
227        path: impl AsRef<std::path::Path>,
228    ) -> Result<Self, crate::error::ProviderError> {
229        let content = tokio::fs::read_to_string(path.as_ref())
230            .await
231            .map_err(|e| crate::error::ProviderError::Config(format!("读取配置文件失败: {e}")))?;
232        Self::from_json(&content)
233    }
234
235    /// 从 YAML 文件加载
236    pub async fn from_yaml_file(
237        path: impl AsRef<std::path::Path>,
238    ) -> Result<Self, crate::error::ProviderError> {
239        let content = tokio::fs::read_to_string(path.as_ref())
240            .await
241            .map_err(|e| crate::error::ProviderError::Config(format!("读取配置文件失败: {e}")))?;
242        Self::from_yaml(&content)
243    }
244
245    /// 展开环境变量引用 ${VAR_NAME}
246    fn interpolate_env(input: &str) -> String {
247        let mut result = input.to_string();
248        // 匹配 ${VAR_NAME} 或 ${VAR_NAME:-default}
249        let re = regex_lite::Regex::new(r"\$\{([^:}]+)(?::-(.*?))?\}").ok();
250        if let Some(re) = re {
251            for caps in re.captures_iter(input) {
252                let var_name = caps.get(1).map(|m| m.as_str()).unwrap_or("");
253                let default_val = caps.get(2).map(|m| m.as_str());
254                let value = std::env::var(var_name)
255                    .ok()
256                    .or_else(|| default_val.map(|s| s.to_string()))
257                    .unwrap_or_default();
258                result = result.replace(caps.get(0).map(|m| m.as_str()).unwrap_or(""), &value);
259            }
260        }
261        result
262    }
263
264    /// 验证配置合法性
265    fn validate(self) -> Result<Self, crate::error::ProviderError> {
266        for (name, def) in &self.providers {
267            if def.models.is_empty() {
268                return Err(crate::error::ProviderError::Config(format!(
269                    "Provider '{name}' 没有定义任何模型"
270                )));
271            }
272            if def.provider_type == ProviderType::Claude
273                && def.api_key.as_ref().is_none_or(|k| k.is_empty())
274            {
275                return Err(crate::error::ProviderError::Config(format!(
276                    "Provider '{name}' 缺少 api_key"
277                )));
278            }
279        }
280        Ok(self)
281    }
282
283    /// 收集所有模型信息(用于路由层注册)
284    pub fn collect_models(&self) -> Vec<ModelInfo> {
285        let mut models = Vec::new();
286        for (provider_name, def) in &self.providers {
287            for mc in &def.models {
288                let mut info = ModelInfo::from(mc.clone());
289                info.provider = Some(provider_name.clone());
290                models.push(info);
291            }
292        }
293        models
294    }
295}
296
297/// 配置热更新监听器
298pub trait ConfigWatcher: Send + Sync {
299    /// 返回配置变更流
300    fn watch(&self) -> futures::stream::BoxStream<'static, ProviderConfig>;
301}
302
303#[cfg(test)]
304mod tests {
305    use super::*;
306
307    fn valid_openai_json() -> &'static str {
308        r#"{
309            "default_model": "gpt-4",
310            "providers": {
311                "openai": {
312                    "provider_type": "open_ai",
313                    "api_key": "sk-test",
314                    "models": [
315                        {
316                            "name": "gpt-4",
317                            "capabilities": {
318                                "context_window": 128000,
319                                "max_output_tokens": 4096,
320                                "supports_tool_calling": true
321                            }
322                        }
323                    ]
324                }
325            }
326        }"#
327    }
328
329    #[test]
330    fn test_from_json_valid() {
331        let config = ProviderConfig::from_json(valid_openai_json()).unwrap();
332        assert_eq!(config.default_model.unwrap(), "gpt-4");
333        assert!(config.providers.contains_key("openai"));
334        assert_eq!(config.providers["openai"].provider_type, ProviderType::OpenAiCompatible);
335        assert_eq!(config.providers["openai"].models.len(), 1);
336        assert_eq!(config.providers["openai"].models[0].name, "gpt-4");
337    }
338
339    #[test]
340    fn test_from_json_invalid_syntax() {
341        let result = ProviderConfig::from_json("not valid json");
342        assert!(result.is_err());
343    }
344
345    #[test]
346    fn test_from_json_empty_models() {
347        let json = r#"{
348            "providers": {
349                "test": {
350                    "provider_type": "open_ai",
351                    "api_key": "sk-test",
352                    "models": []
353                }
354            }
355        }"#;
356        let result = ProviderConfig::from_json(json);
357        assert!(result.is_err());
358        let err = result.unwrap_err();
359        assert!(format!("{}", err).contains("没有定义任何模型"));
360    }
361
362    #[test]
363    fn test_from_json_missing_api_key() {
364        let json = r#"{
365            "providers": {
366                "test": {
367                    "provider_type": "claude",
368                    "models": [{"name": "claude-3", "capabilities": {"context_window": 200000, "max_output_tokens": 4096}}]
369                }
370            }
371        }"#;
372        let result = ProviderConfig::from_json(json);
373        assert!(result.is_err());
374        let err = format!("{}", result.unwrap_err());
375        assert!(err.contains("api_key"), "error: {}", err);
376    }
377
378    #[test]
379    fn test_from_json_no_api_key_needed() {
380        let json = r#"{
381            "providers": {
382                "custom": {
383                    "provider_type": "open_ai_compatible",
384                    "base_url": "http://localhost:11434",
385                    "models": [{"name": "local-model", "capabilities": {"context_window": 4096, "max_output_tokens": 2048}}]
386                }
387            }
388        }"#;
389        let config = ProviderConfig::from_json(json).unwrap();
390        assert!(config.providers.contains_key("custom"));
391    }
392
393    #[test]
394    fn test_from_yaml_valid() {
395        let yaml = r#"
396default_model: gpt-4
397providers:
398  openai:
399    provider_type: open_ai
400    api_key: sk-test
401    models:
402      - name: gpt-4
403        capabilities:
404          context_window: 128000
405          max_output_tokens: 4096
406"#;
407        let config = ProviderConfig::from_yaml(yaml).unwrap();
408        assert_eq!(config.default_model.unwrap(), "gpt-4");
409        assert!(config.providers.contains_key("openai"));
410    }
411
412    #[test]
413    fn test_from_yaml_invalid() {
414        let result = ProviderConfig::from_yaml(": bad yaml : :");
415        assert!(result.is_err());
416    }
417
418    #[test]
419    fn test_collect_models() {
420        let config = ProviderConfig::from_json(valid_openai_json()).unwrap();
421        let models = config.collect_models();
422        assert_eq!(models.len(), 1);
423        assert_eq!(models[0].name, "gpt-4");
424        assert_eq!(models[0].provider.as_deref(), Some("openai"));
425        assert!(models[0].capabilities.supports_tool_calling);
426    }
427
428    #[test]
429    fn test_collect_models_multiple_providers() {
430        let json = r#"{
431            "providers": {
432                "p1": {
433                    "provider_type": "open_ai",
434                    "api_key": "k1",
435                    "models": [{"name": "m1", "capabilities": {"context_window": 100, "max_output_tokens": 100}}]
436                },
437                "p2": {
438                    "provider_type": "open_ai_compatible",
439                    "models": [{"name": "m2", "capabilities": {"context_window": 100, "max_output_tokens": 100}}, {"name": "m3", "capabilities": {"context_window": 100, "max_output_tokens": 100}}]
440                }
441            }
442        }"#;
443        let config = ProviderConfig::from_json(json).unwrap();
444        let models = config.collect_models();
445        assert_eq!(models.len(), 3);
446        assert!(models.iter().any(|m| m.provider.as_deref() == Some("p1")));
447        assert!(models.iter().any(|m| m.provider.as_deref() == Some("p2")));
448    }
449
450    #[test]
451    fn test_interpolate_env_no_vars() {
452        let input = "hello world";
453        let result = ProviderConfig::interpolate_env(input);
454        assert_eq!(result, "hello world");
455    }
456
457    #[test]
458    fn test_interpolate_env_with_default() {
459        let input = r#"api_key: ${MY_KEY:-default_key}"#;
460        let result = ProviderConfig::interpolate_env(input);
461        assert_eq!(result, "api_key: default_key");
462    }
463
464    #[test]
465    fn test_model_config_to_model_info() {
466        let cfg = ModelConfig {
467            name: "gpt-4".into(),
468            display_name: Some("GPT-4".into()),
469            capabilities: ModelCapabilities {
470                context_window: 8192,
471                max_output_tokens: 4096,
472                ..Default::default()
473            },
474            pricing: ModelPricing {
475                input_per_million: 30.0,
476                output_per_million: 60.0,
477                ..Default::default()
478            },
479            limits: ModelLimits::default(),
480        };
481        let info: ModelInfo = cfg.into();
482        assert_eq!(info.name, "gpt-4");
483        assert_eq!(info.display_name.unwrap(), "GPT-4");
484        assert_eq!(info.capabilities.context_window, 8192);
485        assert_eq!(info.pricing.input_per_million, 30.0);
486        assert!(info.provider.is_none());
487    }
488
489    #[test]
490    fn test_route_rule_default_fallback() {
491        let rule = RouteRule {
492            model: "gpt-4".into(),
493            provider: Some("openai".into()),
494            temperature: None,
495            max_tokens: None,
496            fallback: vec![],
497        };
498        assert_eq!(rule.model, "gpt-4");
499    }
500
501    #[test]
502    fn test_fallback_condition_default() {
503        let cond: FallbackCondition = Default::default();
504        assert!(matches!(cond, FallbackCondition::Always));
505    }
506
507    #[test]
508    fn test_config_routing() {
509        let json = r#"{
510            "default_model": "gpt-4",
511            "providers": {
512                "openai": {
513                    "provider_type": "open_ai",
514                    "api_key": "sk-test",
515                    "models": [{"name": "gpt-4", "capabilities": {"context_window": 100, "max_output_tokens": 100}}]
516                }
517            },
518            "routing": {
519                "chat": {
520                    "model": "gpt-4",
521                    "provider": "openai"
522                }
523            }
524        }"#;
525        let config = ProviderConfig::from_json(json).unwrap();
526        assert!(config.routing.contains_key("chat"));
527        assert_eq!(config.routing["chat"].model, "gpt-4");
528    }
529
530    #[test]
531    fn test_provider_type_serde() {
532        assert_eq!(serde_json::to_string(&ProviderType::Claude).unwrap(), r#""claude""#);
533        assert_eq!(
534            serde_json::to_string(&ProviderType::OpenAiCompatible).unwrap(),
535            r#""open_ai_compatible""#
536        );
537        assert_eq!(serde_json::to_string(&ProviderType::Generic).unwrap(), r#""generic""#);
538        // Backward compat: "open_ai" deserializes to OpenAiCompatible
539        let deserialized: ProviderType = serde_json::from_str(r#""open_ai""#).unwrap();
540        assert_eq!(deserialized, ProviderType::OpenAiCompatible);
541    }
542
543    #[test]
544    fn test_parse_anthropic_messages_protocol() {
545        let json = r#"{
546            "providers": {
547                "anthropic": {
548                    "provider_type": "claude",
549                    "api_key": "sk-ant-xxx",
550                    "protocol": "anthropic_messages",
551                    "models": [{"name": "claude-3-opus", "capabilities": {"context_window": 200000, "max_output_tokens": 4096}}]
552                }
553            }
554        }"#;
555        let config = ProviderConfig::from_json(json).unwrap();
556        assert_eq!(config.providers["anthropic"].protocol, ApiProtocol::AnthropicMessages);
557    }
558
559    #[test]
560    fn test_parse_open_ai_compatible_backward_compat() {
561        let json = r#"{
562            "providers": {
563                "deepseek": {
564                    "provider_type": "open_ai_compatible",
565                    "api_key": "sk-ds",
566                    "models": [{"name": "deepseek-chat", "capabilities": {"context_window": 64000, "max_output_tokens": 8192}}]
567                }
568            }
569        }"#;
570        let config = ProviderConfig::from_json(json).unwrap();
571        assert_eq!(config.providers["deepseek"].provider_type, ProviderType::OpenAiCompatible);
572    }
573
574    #[test]
575    fn test_parse_auth_method_bearer() {
576        let json = r#"{
577            "providers": {
578                "custom": {
579                    "provider_type": "open_ai_compatible",
580                    "base_url": "https://custom.example.com",
581                    "auth_method": {"type": "bearer", "token": "sk-xxx"},
582                    "models": [{"name": "model-1", "capabilities": {"context_window": 4096, "max_output_tokens": 1024}}]
583                }
584            }
585        }"#;
586        let config = ProviderConfig::from_json(json).unwrap();
587        let auth = config.providers["custom"].auth_method.as_ref().unwrap();
588        assert_eq!(*auth, AuthMethod::Bearer { token: "sk-xxx".into() });
589    }
590
591    #[test]
592    fn test_parse_auth_method_api_key() {
593        let json = r#"{
594            "providers": {
595                "custom": {
596                    "provider_type": "open_ai_compatible",
597                    "base_url": "https://custom.example.com",
598                    "auth_method": {"type": "api_key", "header_name": "x-api-key", "key": "sk-ant-xxx"},
599                    "models": [{"name": "model-1", "capabilities": {"context_window": 4096, "max_output_tokens": 1024}}]
600                }
601            }
602        }"#;
603        let config = ProviderConfig::from_json(json).unwrap();
604        let auth = config.providers["custom"].auth_method.as_ref().unwrap();
605        assert_eq!(
606            *auth,
607            AuthMethod::ApiKey { header_name: "x-api-key".into(), key: "sk-ant-xxx".into() }
608        );
609    }
610
611    #[test]
612    fn test_parse_no_auth_method_defaults_none() {
613        // Old config format without auth_method should still parse, defaulting to None.
614        let json = r#"{
615            "providers": {
616                "openai": {
617                    "provider_type": "open_ai",
618                    "api_key": "sk-test",
619                    "models": [{"name": "gpt-4", "capabilities": {"context_window": 128000, "max_output_tokens": 4096}}]
620                }
621            }
622        }"#;
623        let config = ProviderConfig::from_json(json).unwrap();
624        assert!(config.providers["openai"].auth_method.is_none());
625    }
626
627    #[test]
628    fn test_parse_generic_provider_type() {
629        let json = r#"{
630            "providers": {
631                "my_custom": {
632                    "provider_type": "generic",
633                    "base_url": "https://my-custom.api.com",
634                    "auth_method": {"type": "bearer", "token": "sk-custom"},
635                    "protocol": "anthropic_messages",
636                    "models": [{"name": "custom-model", "capabilities": {"context_window": 4096, "max_output_tokens": 1024}}]
637                }
638            }
639        }"#;
640        let config = ProviderConfig::from_json(json).unwrap();
641        assert_eq!(config.providers["my_custom"].provider_type, ProviderType::Generic);
642    }
643
644    #[test]
645    fn test_api_protocol_default() {
646        assert_eq!(ApiProtocol::default(), ApiProtocol::ChatCompletions);
647    }
648
649    #[test]
650    fn test_api_protocol_serde() {
651        assert_eq!(
652            serde_json::to_string(&ApiProtocol::ChatCompletions).unwrap(),
653            r#""chat_completions""#
654        );
655        assert_eq!(serde_json::to_string(&ApiProtocol::Responses).unwrap(), r#""responses""#);
656        assert_eq!(
657            serde_json::to_string(&ApiProtocol::AnthropicMessages).unwrap(),
658            r#""anthropic_messages""#
659        );
660    }
661
662    #[test]
663    fn test_provider_type_generic_serde() {
664        assert_eq!(serde_json::to_string(&ProviderType::Generic).unwrap(), r#""generic""#);
665    }
666
667    #[test]
668    fn test_auth_method_serde_roundtrip() {
669        let original = AuthMethod::Bearer { token: "sk-test".into() };
670        let json = serde_json::to_string(&original).unwrap();
671        let deserialized: AuthMethod = serde_json::from_str(&json).unwrap();
672        assert_eq!(original, deserialized);
673
674        let original = AuthMethod::ApiKey { header_name: "x-custom".into(), key: "key-123".into() };
675        let json = serde_json::to_string(&original).unwrap();
676        let deserialized: AuthMethod = serde_json::from_str(&json).unwrap();
677        assert_eq!(original, deserialized);
678    }
679
680    #[test]
681    fn test_provider_definition_new_fields_roundtrip() {
682        // Config with all new fields — confirms serde round-trip.
683        let json = r#"{
684            "providers": {
685                "anthropic": {
686                    "provider_type": "claude",
687                    "api_key": "sk-ant-xxx",
688                    "anthropic_version": "2023-06-01",
689                    "anthropic_beta": ["prompt-caching-2025-02-19", "tools-2025-04-01"],
690                    "default_thinking_type": "extended",
691                    "default_effort": "high",
692                    "models": [{"name": "claude-3-opus", "capabilities": {"context_window": 200000, "max_output_tokens": 4096}}]
693                }
694            }
695        }"#;
696        let config = ProviderConfig::from_json(json).unwrap();
697        let def = &config.providers["anthropic"];
698
699        assert_eq!(def.anthropic_version.as_deref(), Some("2023-06-01"));
700        let expected_beta: &[String] =
701            &["prompt-caching-2025-02-19".into(), "tools-2025-04-01".into()];
702        assert_eq!(def.anthropic_beta.as_deref(), Some(expected_beta));
703        assert_eq!(def.default_thinking_type.as_deref(), Some("extended"));
704        assert_eq!(def.default_effort.as_deref(), Some("high"));
705
706        // Round-trip: serialize and deserialize again
707        let serialized = serde_json::to_string_pretty(&config).unwrap();
708        let deserialized: ProviderConfig = serde_json::from_str(&serialized).unwrap();
709        let def2 = &deserialized.providers["anthropic"];
710        assert_eq!(def2.anthropic_version, def.anthropic_version);
711        assert_eq!(def2.anthropic_beta, def.anthropic_beta);
712        assert_eq!(def2.default_thinking_type, def.default_thinking_type);
713        assert_eq!(def2.default_effort, def.default_effort);
714    }
715
716    #[test]
717    fn test_provider_definition_without_new_fields_backward_compat() {
718        // Config without new fields — confirms backward compatibility.
719        let json = r#"{
720            "providers": {
721                "openai": {
722                    "provider_type": "open_ai",
723                    "api_key": "sk-test",
724                    "models": [{"name": "gpt-4", "capabilities": {"context_window": 128000, "max_output_tokens": 4096}}]
725                }
726            }
727        }"#;
728        let config = ProviderConfig::from_json(json).unwrap();
729        let def = &config.providers["openai"];
730
731        // All new fields should default to None
732        assert!(def.anthropic_version.is_none());
733        assert!(def.anthropic_beta.is_none());
734        assert!(def.default_thinking_type.is_none());
735        assert!(def.default_effort.is_none());
736    }
737}