1use std::collections::HashMap;
2
3use serde::{Deserialize, Serialize};
4
5use crate::protocol::AuthMethod;
6use crate::types::{ModelCapabilities, ModelInfo, ModelLimits, ModelPricing};
7
8#[derive(Debug, Clone, PartialEq)]
10pub enum ProviderType {
11 Claude,
12 OpenAiCompatible,
13 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" | "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#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct ModelConfig {
47 pub name: String,
49
50 #[serde(default)]
52 pub display_name: Option<String>,
53
54 #[serde(default)]
56 pub capabilities: ModelCapabilities,
57
58 #[serde(default)]
60 pub pricing: ModelPricing,
61
62 #[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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
91#[serde(rename_all = "snake_case")]
92pub enum ApiProtocol {
93 #[default]
97 ChatCompletions,
98 Responses,
102 AnthropicMessages,
106}
107
108#[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 #[serde(default, skip_serializing_if = "Option::is_none")]
121 pub headers: Option<HashMap<String, String>>,
122
123 #[serde(default)]
125 pub protocol: ApiProtocol,
126
127 #[serde(default, skip_serializing_if = "Option::is_none")]
131 pub auth_method: Option<AuthMethod>,
132
133 #[serde(default, skip_serializing_if = "Option::is_none")]
135 pub anthropic_version: Option<String>,
136
137 #[serde(default, skip_serializing_if = "Option::is_none")]
139 pub anthropic_beta: Option<Vec<String>>,
140
141 #[serde(default, skip_serializing_if = "Option::is_none")]
143 pub default_thinking_type: Option<String>,
144
145 #[serde(default, skip_serializing_if = "Option::is_none")]
147 pub default_effort: Option<String>,
148
149 pub models: Vec<ModelConfig>,
150}
151
152#[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 #[serde(default)]
166 pub fallback: Vec<FallbackEntry>,
167}
168
169#[derive(Debug, Clone, Serialize, Deserialize)]
171pub struct FallbackEntry {
172 pub model: String,
173 pub provider: Option<String>,
174
175 #[serde(default)]
177 pub condition: FallbackCondition,
178}
179
180#[derive(Debug, Clone, Default, Serialize, Deserialize)]
182pub enum FallbackCondition {
183 #[serde(rename = "always")]
185 #[default]
186 Always,
187 #[serde(rename = "rate_limit_only")]
189 RateLimitOnly,
190 #[serde(rename = "error_status")]
192 ErrorStatus(Vec<u16>),
193}
194
195#[derive(Debug, Clone, Serialize, Deserialize)]
197pub struct ProviderConfig {
198 pub default_model: Option<String>,
200
201 pub providers: HashMap<String, ProviderDefinition>,
203
204 #[serde(default)]
206 pub routing: HashMap<String, RouteRule>,
207}
208
209impl ProviderConfig {
210 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 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 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 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 fn interpolate_env(input: &str) -> String {
247 let mut result = input.to_string();
248 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 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 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
297pub trait ConfigWatcher: Send + Sync {
299 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 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 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 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 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 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 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}