1 line
120 KiB
JSON
1 line
120 KiB
JSON
{"repo": "astercloud/aster-rust", "files": {"crates/aster/src/providers/factory.rs": {"b3099db8fc3b06389621aabf3f465baaf734418a": {"t": 1785745837.5543363, "n": 20700, "h": "8de408888bb2aced", "c": "use std::collections::HashMap;\nuse std::sync::{Arc, RwLock};\n\nuse super::{\n anthropic::AnthropicProvider,\n azure::AzureProvider,\n base::{Provider, ProviderMetadata},\n claude_code::ClaudeCodeProvider,\n codex::CodexProvider,\n codex_stateful::CodexStatefulProvider,\n cursor_agent::CursorAgentProvider,\n databricks::DatabricksProvider,\n gcpvertexai::GcpVertexAIProvider,\n gemini_cli::GeminiCliProvider,\n githubcopilot::GithubCopilotProvider,\n google::GoogleProvider,\n lead_worker::LeadWorkerProvider,\n litellm::LiteLLMProvider,\n ollama::OllamaProvider,\n openai::OpenAiProvider,\n openrouter::OpenRouterProvider,\n provider_registry::ProviderRegistry,\n snowflake::SnowflakeProvider,\n tetrate::TetrateProvider,\n venice::VeniceProvider,\n xai::XaiProvider,\n};\n#[cfg(feature = \"provider-aws\")]\nuse super::{bedrock::BedrockProvider, sagemaker_tgi::SageMakerTgiProvider};\nuse crate::model::ModelConfig;\nuse crate::providers::base::ProviderType;\nuse crate::{\n config::declarative_providers::register_declarative_providers,\n providers::provider_registry::ProviderEntry,\n};\nuse anyhow::Result;\nuse tokio::sync::OnceCell;\n\nconst DEFAULT_LEAD_TURNS: usize = 3;\nconst DEFAULT_FAILURE_THRESHOLD: usize = 2;\nconst DEFAULT_FALLBACK_TURNS: usize = 2;\n\nstatic REGISTRY: OnceCell<RwLock<ProviderRegistry>> = OnceCell::const_new();\n\nasync fn init_registry() -> RwLock<ProviderRegistry> {\n let mut registry = ProviderRegistry::new().with_providers(|registry| {\n registry\n .register::<AnthropicProvider, _>(|m| Box::pin(AnthropicProvider::from_env(m)), true);\n registry.register::<AzureProvider, _>(|m| Box::pin(AzureProvider::from_env(m)), false);\n #[cfg(feature = \"provider-aws\")]\n registry.register::<BedrockProvider, _>(|m| Box::pin(BedrockProvider::from_env(m)), false);\n registry\n .register::<ClaudeCodeProvider, _>(|m| Box::pin(ClaudeCodeProvider::from_env(m)), true);\n registry.register::<CodexProvider, _>(|m| Box::pin(CodexProvider::from_env(m)), true);\n registry.register::<CodexStatefulProvider, _>(\n |m| Box::pin(CodexStatefulProvider::from_env(m)),\n true,\n );\n registry.register::<CursorAgentProvider, _>(\n |m| Box::pin(CursorAgentProvider::from_env(m)),\n false,\n );\n registry\n .register::<DatabricksProvider, _>(|m| Box::pin(DatabricksProvider::from_env(m)), true);\n registry.register::<GcpVertexAIProvider, _>(\n |m| Box::pin(GcpVertexAIProvider::from_env(m)),\n false,\n );\n registry\n .register::<GeminiCliProvider, _>(|m| Box::pin(GeminiCliProvider::from_env(m)), false);\n registry.register::<GithubCopilotProvider, _>(\n |m| Box::pin(GithubCopilotProvider::from_env(m)),\n false,\n );\n registry.register::<GoogleProvider, _>(|m| Box::pin(GoogleProvider::from_env(m)), true);\n registry.register::<LiteLLMProvider, _>(|m| Box::pin(LiteLLMProvider::from_env(m)), false);\n registry.register::<OllamaProvider, _>(|m| Box::pin(OllamaProvider::from_env(m)), true);\n registry.register::<OpenAiProvider, _>(|m| Box::pin(OpenAiProvider::from_env(m)), true);\n registry\n .register::<OpenRouterProvider, _>(|m| Box::pin(OpenRouterProvider::from_env(m)), true);\n #[cfg(feature = \"provider-aws\")]\n registry.register::<SageMakerTgiProvider, _>(\n |m| Box::pin(SageMakerTgiProvider::from_env(m)),\n false,\n );\n registry\n .register::<SnowflakeProvider, _>(|m| Box::pin(SnowflakeProvider::from_env(m)), false);\n registry.register::<TetrateProvider, _>(|m| Box::pin(TetrateProvider::from_env(m)), true);\n registry.register::<VeniceProvider, _>(|m| Box::pin(VeniceProvider::from_env(m)), false);\n registry.register::<XaiProvider, _>(|m| Box::pin(XaiProvider::from_env(m)), false);\n });\n if let Err(e) = load_custom_providers_into_registry(&mut registry) {\n tracing::warn!(\"Failed to load custom providers: {}\", e);\n }\n RwLock::new(registry)\n}\n\nfn load_custom_providers_into_registry(registry: &mut ProviderRegistry) -> Result<()> {\n register_declarative_providers(registry)\n}\n\nasync fn get_registry() -> &'static RwLock<ProviderRegistry> {\n REGISTRY.get_or_init(init_registry).await\n}\n\npub async fn providers() -> Vec<(ProviderMetadata, ProviderType)> {\n get_registry()\n .await\n .read()\n .unwrap()\n .all_metadata_with_types()\n}\n\npub async fn refresh_custom_providers() -> Result<()> {\n let registry = get_registry().await;\n registry.write().unwrap().remove_custom_providers();\n\n if let Err(e) = load_custom_providers_into_registry(&mut registry.write().unwrap()) {\n tracing::warn!(\"Failed to refresh custom providers: {}\", e);\n return Err(e);\n }\n\n tracing::info!(\"Custom providers refreshed\");\n Ok(())\n}\n\nasync fn get_from_registry(name: &str) -> Result<ProviderEntry> {\n // 将各种 Provider 名称映射到 Aster 支持的 Provider\n let mapped_name = map_provider_alias(name);\n\n #[cfg(not(feature = \"provider-aws\"))]\n if mapped_name == \"bedrock\" || mapped_name == \"sagemaker_tgi\" {\n return Err(anyhow::anyhow!(\n \"Provider {} is disabled at compile time; rebuild with feature provider-aws\",\n mapped_name\n ));\n }\n\n let guard = get_registry().await.read().unwrap();\n guard\n .entries\n .get(mapped_name.as_str())\n .ok_or_else(|| anyhow::anyhow!(\"Unknown provider: {} (mapped to: {})\", name, mapped_name))\n .cloned()\n}\n\n/// 将各种 Provider 名称映射到 Aster 支持的 Provider\n///\n/// Aster 原生支持的 Provider:\n/// - openai, anthropic, google, azure, bedrock, ollama, gcpvertexai\n/// - openrouter, litellm, databricks, codex, xai, venice, tetrate\n/// - snowflake, sagemaker_tgi, githubcopilot, gemini_cli, cursor_agent, claude_code\n///\n/// 其他 Provider 会映射到兼容的 Provider\nfn parse_provider_alias_overrides(raw: &str) -> HashMap<String, String> {\n let trimmed = raw.trim();\n if trimmed.is_empty() {\n return HashMap::new();\n }\n\n if let Ok(json_map) = serde_json::from_str::<HashMap<String, String>>(trimmed) {\n return json_map\n .into_iter()\n .map(|(alias, target)| (alias.trim().to_lowercase(), target.trim().to_lowercase()))\n .filter(|(alias, target)| !alias.is_empty() && !target.is_empty())\n .collect();\n }\n\n let mut overrides = HashMap::new();\n for pair in trimmed.split(',') {\n let entry = pair.trim();\n if entry.is_empty() {\n continue;\n }\n\n let parsed = entry.split_once('=').or_else(|| entry.split_once(':'));\n\n if let Some((alias, target)) = parsed {\n let alias = alias.trim().to_lowercase();\n let target = target.trim().to_lowercase();\n if !alias.is_empty() && !target.is_empty() {\n overrides.insert(alias, target);\n }\n }\n }\n\n overrides\n}\n\nfn load_provider_alias_overrides() -> HashMap<String, String> {\n std::env::var(\"ASTER_PROVIDER_ALIAS_OVERRIDES\")\n .ok()\n .map(|raw| parse_provider_alias_overrides(&raw))\n .unwrap_or_default()\n}\n\nfn map_provider_alias(name: &str) -> String {\n let normalized = name.trim().to_lowercase();\n\n if normalized.is_empty() {\n return normalized;\n }\n\n // 自定义 Provider(UUID 格式,如 custom-ba4e7574-dd00-4784-945a-0f383dfa1272)\n // 这些是用户通过 API Key Provider 添加的自定义服务,通常是 OpenAI 兼容的\n if normalized.starts_with(\"custom-\") {\n return \"openai\".to_string();\n }\n\n // 应用层可通过环境变量覆盖别名映射,避免框架层频繁改代码\n if let Some(mapped) = load_provider_alias_overrides().get(normalized.as_str()) {\n return mapped.clone();\n }\n\n let mapped = match normalized.as_str() {\n // ========== OpenAI 兼容格式 ==========\n // 国内 AI 服务\n \"deepseek\" | \"deep_seek\" | \"deep-seek\" => \"openai\",\n \"qwen\" | \"tongyi\" | \"dashscope\" | \"aliyun\" => \"openai\",\n \"zhipu\" | \"glm\" | \"chatglm\" => \"openai\",\n \"baichuan\" => \"openai\",\n \"moonshot\" | \"kimi\" => \"openai\",\n \"minimax\" => \"openai\",\n \"yi\" | \"01ai\" | \"lingyiwanwu\" => \"openai\",\n \"stepfun\" | \"step\" => \"openai\",\n \"bailian\" | \"百炼\" => \"openai\",\n \"doubao\" | \"豆包\" => \"openai\",\n \"spark\" | \"讯飞\" | \"xunfei\" => \"openai\",\n \"hunyuan\" | \"混元\" => \"openai\",\n \"ernie\" | \"文心\" | \"wenxin\" => \"openai\",\n\n // 国际 AI 服务(OpenAI 兼容)\n \"groq\" => \"openai\",\n \"together\" | \"togetherai\" => \"openai\",\n \"fireworks\" | \"fireworksai\" => \"openai\",\n \"perplexity\" => \"openai\",\n \"anyscale\" => \"openai\",\n \"lepton\" | \"leptonai\" => \"openai\",\n \"novita\" | \"novitaai\" => \"openai\",\n \"siliconflow\" => \"openai\",\n \"mistral\" => \"openai\",\n \"cohere\" => \"openai\",\n\n // API 聚合服务\n \"oneapi\" | \"one-api\" | \"one_api\" => \"openai\",\n \"newapi\" | \"new-api\" | \"new_api\" => \"openai\",\n \"vercel\" | \"vercel_ai\" | \"vercel-ai\" => \"openai\",\n\n // 自定义/通用 OpenAI 兼容\n \"custom\" | \"custom_openai\" | \"openai_compatible\" => \"openai\",\n\n // ========== Anthropic 兼容格式 ==========\n \"claude\" => \"anthropic\",\n \"anthropic_compatible\" | \"anthropic-compatible\" => \"anthropic\",\n\n // ========== Google/Gemini 格式 ==========\n \"gemini\" | \"gemini_api_key\" => \"google\",\n \"antigravity\" => \"google\",\n\n // ========== 其他已支持的 Provider(保持原名) ==========\n \"azure\" | \"azure_openai\" | \"azure-openai\" => \"azure\",\n \"vertex\" | \"vertexai\" | \"vertex_ai\" => \"gcpvertexai\",\n \"aws_bedrock\" | \"aws-bedrock\" => \"bedrock\",\n \"kiro\" => \"bedrock\", // Kiro 使用 CodeWhisperer API\n\n // 默认返回小写原名称(让 Aster 原生处理)\n _ => normalized.as_str(),\n };\n\n mapped.to_string()\n}\n\npub async fn create(name: &str, model: ModelConfig) -> Result<Arc<dyn Provider>> {\n let config = crate::config::Config::global();\n\n if let Ok(lead_model_name) = config.get_param::<String>(\"ASTER_LEAD_MODEL\") {\n tracing::info!(\"Creating lead/worker provider from environment variables\");\n return create_lead_worker_from_env(name, &model, &lead_model_name).await;\n }\n\n let constructor = get_from_registry(name).await?.constructor.clone();\n constructor(model).await\n}\n\npub async fn create_with_default_model(name: impl AsRef<str>) -> Result<Arc<dyn Provider>> {\n get_from_registry(name.as_ref())\n .await?\n .create_with_default_model()\n .await\n}\n\npub async fn create_with_named_model(\n provider_name: &str,\n model_name: &str,\n) -> Result<Arc<dyn Provider>> {\n let config = ModelConfig::new(model_name)?;\n create(provider_name, config).await\n}\n\nasync fn create_lead_worker_from_env(\n default_provider_name: &str,\n default_model: &ModelConfig,\n lead_model_name: &str,\n) -> Result<Arc<dyn Provider>> {\n let config = crate::config::Config::global();\n\n let lead_provider_name_raw = config\n .get_param::<String>(\"ASTER_LEAD_PROVIDER\")\n .unwrap_or_else(|_| default_provider_name.to_string());\n let lead_provider_name = map_provider_alias(&lead_provider_name_raw);\n let worker_provider_name = map_provider_alias(default_provider_name);\n\n let lead_turns = config\n .get_param::<usize>(\"ASTER_LEAD_TURNS\")\n .unwrap_or(DEFAULT_LEAD_TURNS);\n let failure_threshold = config\n .get_param::<usize>(\"ASTER_LEAD_FAILURE_THRESHOLD\")\n .unwrap_or(DEFAULT_FAILURE_THRESHOLD);\n let fallback_turns = config\n .get_param::<usize>(\"ASTER_LEAD_FALLBACK_TURNS\")\n .unwrap_or(DEFAULT_FALLBACK_TURNS);\n\n let lead_model_config = ModelConfig::new_with_context_env(\n lead_model_name.to_string(),\n Some(\"ASTER_LEAD_CONTEXT_LIMIT\"),\n )?;\n\n let worker_model_config = create_worker_model_config(default_model)?;\n\n let registry = get_registry().await;\n\n let lead_constructor = {\n let guard = registry.read().unwrap();\n guard\n .entries\n .get(lead_provider_name.as_str())\n .ok_or_else(|| {\n anyhow::anyhow!(\n \"Unknown provider: {} (mapped to: {})\",\n lead_provider_name_raw,\n lead_provider_name\n )\n })?\n .constructor\n .clone()\n };\n\n let worker_constructor = {\n let guard = registry.read().unwrap();\n guard\n .entries\n .get(worker_provider_name.as_str())\n .ok_or_else(|| {\n anyhow::anyhow!(\n \"Unknown provider: {} (mapped to: {})\",\n default_provider_name,\n worker_provider_name\n )\n })?\n .constructor\n .clone()\n };\n\n let lead_provider = lead_constructor(lead_model_config).await?;\n let worker_provider = worker_constructor(worker_model_config).await?;\n\n Ok(Arc::new(LeadWorkerProvider::new_with_settings(\n lead_provider,\n worker_provider,\n lead_turns,\n failure_threshold,\n fallback_turns,\n )))\n}\n\nfn create_worker_model_config(default_model: &ModelConfig) -> Result<ModelConfig> {\n let mut worker_config = ModelConfig::new_or_fail(&default_model.model_name)\n .with_context_limit(default_model.context_limit)\n .with_temperature(default_model.temperature)\n .with_max_tokens(default_model.max_tokens)\n .with_toolshim(default_model.toolshim)\n .with_toolshim_model(default_model.toolshim_model.clone());\n\n let global_config = crate::config::Config::global();\n\n if let Ok(limit) = global_config.get_param::<usize>(\"ASTER_WORKER_CONTEXT_LIMIT\") {\n worker_config = worker_config.with_context_limit(Some(limit));\n } else if let Ok(limit) = global_config.get_param::<usize>(\"ASTER_CONTEXT_LIMIT\") {\n worker_config = worker_config.with_context_limit(Some(limit));\n }\n\n Ok(worker_config)\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test_case::test_case(None, None, None, DEFAULT_LEAD_TURNS, DEFAULT_FAILURE_THRESHOLD, DEFAULT_FALLBACK_TURNS ; \"defaults\")]\n #[test_case::test_case(Some(\"7\"), Some(\"4\"), Some(\"3\"), 7, 4, 3 ; \"custom\")]\n #[tokio::test]\n async fn test_create_lead_worker_provider(\n lead_turns: Option<&str>,\n failure_threshold: Option<&str>,\n fallback_turns: Option<&str>,\n expected_turns: usize,\n expected_failure: usize,\n expected_fallback: usize,\n ) {\n let _guard = env_lock::lock_env([\n (\"ASTER_LEAD_MODEL\", Some(\"gpt-4o\")),\n (\"ASTER_LEAD_PROVIDER\", None),\n (\"ASTER_LEAD_TURNS\", lead_turns),\n (\"ASTER_LEAD_FAILURE_THRESHOLD\", failure_threshold),\n (\"ASTER_LEAD_FALLBACK_TURNS\", fallback_turns),\n (\"OPENAI_API_KEY\", Some(\"fake-openai-no-keyring\")),\n ]);\n\n let provider = create(\"openai\", ModelConfig::new_or_fail(\"gpt-4o-mini\"))\n .await\n .unwrap();\n let lw = provider.as_lead_worker().unwrap();\n let (lead, worker) = lw.get_model_info();\n assert_eq!(lead, \"gpt-4o\");\n assert_eq!(worker, \"gpt-4o-mini\");\n assert_eq!(\n lw.get_settings(),\n (expected_turns, expected_failure, expected_fallback)\n );\n }\n\n #[tokio::test]\n async fn test_create_regular_provider_without_lead_config() {\n let _guard = env_lock::lock_env([\n (\"ASTER_LEAD_MODEL\", None),\n (\"ASTER_LEAD_PROVIDER\", None),\n (\"ASTER_LEAD_TURNS\", None),\n (\"ASTER_LEAD_FAILURE_THRESHOLD\", None),\n (\"ASTER_LEAD_FALLBACK_TURNS\", None),\n (\"OPENAI_API_KEY\", Some(\"fake-openai-no-keyring\")),\n ]);\n\n let provider = create(\"openai\", ModelConfig::new_or_fail(\"gpt-4o-mini\"))\n .await\n .unwrap();\n assert!(provider.as_lead_worker().is_none());\n assert_eq!(provider.get_model_config().model_name, \"gpt-4o-mini\");\n }\n\n #[test_case::test_case(None, None, 16_000 ; \"no overrides uses default\")]\n #[test_case::test_case(Some(\"32000\"), None, 32_000 ; \"worker limit overrides default\")]\n #[test_case::test_case(Some(\"32000\"), Some(\"64000\"), 32_000 ; \"worker limit takes priority over global\")]\n fn test_worker_model_context_limit(\n worker_limit: Option<&str>,\n global_limit: Option<&str>,\n expected_limit: usize,\n ) {\n let _guard = env_lock::lock_env([\n (\"ASTER_WORKER_CONTEXT_LIMIT\", worker_limit),\n (\"ASTER_CONTEXT_LIMIT\", global_limit),\n ]);\n\n let default_model =\n ModelConfig::new_or_fail(\"gpt-3.5-turbo\").with_context_limit(Some(16_000));\n\n let result = create_worker_model_config(&default_model).unwrap();\n assert_eq!(result.context_limit, Some(expected_limit));\n }\n\n #[tokio::test]\n async fn test_openai_compatible_providers_config_keys() {\n let providers_list = providers().await;\n let cases = vec![\n (\"openai\", \"OPENAI_API_KEY\"),\n (\"groq\", \"GROQ_API_KEY\"),\n (\"mistral\", \"MISTRAL_API_KEY\"),\n (\"custom_deepseek\", \"DEEPSEEK_API_KEY\"),\n ];\n for (name, expected_key) in cases {\n if let Some((meta, _)) = providers_list.iter().find(|(m, _)| m.name == name) {\n assert!(\n !meta.config_keys.is_empty(),\n \"{name} provider should have config keys\"\n );\n assert_eq!(\n meta.config_keys[0].name, expected_key,\n \"First config key for {name} should be {expected_key}, got {}\",\n meta.config_keys[0].name\n );\n assert!(\n meta.config_keys[0].required,\n \"{expected_key} should be required\"\n );\n assert!(\n meta.config_keys[0].secret,\n \"{expected_key} should be secret\"\n );\n } else {\n // Provider not registered; skip test for this provider\n continue;\n }\n }\n }\n\n #[test]\n fn test_map_provider_alias_custom_uuid() {\n let _guard = env_lock::lock_env([(\"ASTER_PROVIDER_ALIAS_OVERRIDES\", None::<&str>)]);\n\n // 自定义 Provider UUID 格式应该映射到 openai\n assert_eq!(\n map_provider_alias(\"custom-ba4e7574-dd00-4784-945a-0f383dfa1272\"),\n \"openai\"\n );\n assert_eq!(\n map_provider_alias(\"custom-12345678-1234-1234-1234-123456789abc\"),\n \"openai\"\n );\n // 普通 custom 也应该映射到 openai\n assert_eq!(map_provider_alias(\"custom\"), \"openai\");\n assert_eq!(map_provider_alias(\"custom_openai\"), \"openai\");\n }\n\n #[test]\n fn test_map_provider_alias_known_providers() {\n let _guard = env_lock::lock_env([(\"ASTER_PROVIDER_ALIAS_OVERRIDES\", None::<&str>)]);\n\n // 已知的 Provider 应该正确映射\n assert_eq!(map_provider_alias(\"deepseek\"), \"openai\");\n assert_eq!(map_provider_alias(\"qwen\"), \"openai\");\n assert_eq!(map_provider_alias(\"claude\"), \"anthropic\");\n assert_eq!(map_provider_alias(\"gemini\"), \"google\");\n assert_eq!(map_provider_alias(\"kiro\"), \"bedrock\");\n // 原生支持的 Provider 应该保持原名\n assert_eq!(map_provider_alias(\"openai\"), \"openai\");\n assert_eq!(map_provider_alias(\"anthropic\"), \"anthropic\");\n assert_eq!(map_provider_alias(\"google\"), \"google\");\n }\n\n #[test]\n fn test_map_provider_alias_fallback_to_lowercase() {\n let _guard = env_lock::lock_env([(\"ASTER_PROVIDER_ALIAS_OVERRIDES\", None::<&str>)]);\n\n assert_eq!(map_provider_alias(\"OpenAI\"), \"openai\");\n assert_eq!(\n map_provider_alias(\"My-Custom-Provider\"),\n \"my-custom-provider\"\n );\n }\n\n #[test]\n fn test_map_provider_alias_env_override_json() {\n let _guard = env_lock::lock_env([(\n \"ASTER_PROVIDER_ALIAS_OVERRIDES\",\n Some(r#\"{\"moonshotai\":\"openrouter\",\"gemini\":\"google\"}\"#),\n )]);\n\n assert_eq!(map_provider_alias(\"moonshotai\"), \"openrouter\");\n assert_eq!(map_provider_alias(\"gemini\"), \"google\");\n }\n\n #[test]\n fn test_map_provider_alias_env_override_kv() {\n let _guard = env_lock::lock_env([(\n \"ASTER_PROVIDER_ALIAS_OVERRIDES\",\n Some(\"deepseek=openrouter,claude=openai\"),\n )]);\n\n assert_eq!(map_provider_alias(\"deepseek\"), \"openrouter\");\n assert_eq!(map_provider_alias(\"claude\"), \"openai\");\n }\n}\n"}, "c4a9c40b48bcb0c77375b10ccb1313366fb4e7ba": {"t": 1785757073.241318, "n": 20700, "h": "8de408888bb2aced", "c": "use std::collections::HashMap;\nuse std::sync::{Arc, RwLock};\n\nuse super::{\n anthropic::AnthropicProvider,\n azure::AzureProvider,\n base::{Provider, ProviderMetadata},\n claude_code::ClaudeCodeProvider,\n codex::CodexProvider,\n codex_stateful::CodexStatefulProvider,\n cursor_agent::CursorAgentProvider,\n databricks::DatabricksProvider,\n gcpvertexai::GcpVertexAIProvider,\n gemini_cli::GeminiCliProvider,\n githubcopilot::GithubCopilotProvider,\n google::GoogleProvider,\n lead_worker::LeadWorkerProvider,\n litellm::LiteLLMProvider,\n ollama::OllamaProvider,\n openai::OpenAiProvider,\n openrouter::OpenRouterProvider,\n provider_registry::ProviderRegistry,\n snowflake::SnowflakeProvider,\n tetrate::TetrateProvider,\n venice::VeniceProvider,\n xai::XaiProvider,\n};\n#[cfg(feature = \"provider-aws\")]\nuse super::{bedrock::BedrockProvider, sagemaker_tgi::SageMakerTgiProvider};\nuse crate::model::ModelConfig;\nuse crate::providers::base::ProviderType;\nuse crate::{\n config::declarative_providers::register_declarative_providers,\n providers::provider_registry::ProviderEntry,\n};\nuse anyhow::Result;\nuse tokio::sync::OnceCell;\n\nconst DEFAULT_LEAD_TURNS: usize = 3;\nconst DEFAULT_FAILURE_THRESHOLD: usize = 2;\nconst DEFAULT_FALLBACK_TURNS: usize = 2;\n\nstatic REGISTRY: OnceCell<RwLock<ProviderRegistry>> = OnceCell::const_new();\n\nasync fn init_registry() -> RwLock<ProviderRegistry> {\n let mut registry = ProviderRegistry::new().with_providers(|registry| {\n registry\n .register::<AnthropicProvider, _>(|m| Box::pin(AnthropicProvider::from_env(m)), true);\n registry.register::<AzureProvider, _>(|m| Box::pin(AzureProvider::from_env(m)), false);\n #[cfg(feature = \"provider-aws\")]\n registry.register::<BedrockProvider, _>(|m| Box::pin(BedrockProvider::from_env(m)), false);\n registry\n .register::<ClaudeCodeProvider, _>(|m| Box::pin(ClaudeCodeProvider::from_env(m)), true);\n registry.register::<CodexProvider, _>(|m| Box::pin(CodexProvider::from_env(m)), true);\n registry.register::<CodexStatefulProvider, _>(\n |m| Box::pin(CodexStatefulProvider::from_env(m)),\n true,\n );\n registry.register::<CursorAgentProvider, _>(\n |m| Box::pin(CursorAgentProvider::from_env(m)),\n false,\n );\n registry\n .register::<DatabricksProvider, _>(|m| Box::pin(DatabricksProvider::from_env(m)), true);\n registry.register::<GcpVertexAIProvider, _>(\n |m| Box::pin(GcpVertexAIProvider::from_env(m)),\n false,\n );\n registry\n .register::<GeminiCliProvider, _>(|m| Box::pin(GeminiCliProvider::from_env(m)), false);\n registry.register::<GithubCopilotProvider, _>(\n |m| Box::pin(GithubCopilotProvider::from_env(m)),\n false,\n );\n registry.register::<GoogleProvider, _>(|m| Box::pin(GoogleProvider::from_env(m)), true);\n registry.register::<LiteLLMProvider, _>(|m| Box::pin(LiteLLMProvider::from_env(m)), false);\n registry.register::<OllamaProvider, _>(|m| Box::pin(OllamaProvider::from_env(m)), true);\n registry.register::<OpenAiProvider, _>(|m| Box::pin(OpenAiProvider::from_env(m)), true);\n registry\n .register::<OpenRouterProvider, _>(|m| Box::pin(OpenRouterProvider::from_env(m)), true);\n #[cfg(feature = \"provider-aws\")]\n registry.register::<SageMakerTgiProvider, _>(\n |m| Box::pin(SageMakerTgiProvider::from_env(m)),\n false,\n );\n registry\n .register::<SnowflakeProvider, _>(|m| Box::pin(SnowflakeProvider::from_env(m)), false);\n registry.register::<TetrateProvider, _>(|m| Box::pin(TetrateProvider::from_env(m)), true);\n registry.register::<VeniceProvider, _>(|m| Box::pin(VeniceProvider::from_env(m)), false);\n registry.register::<XaiProvider, _>(|m| Box::pin(XaiProvider::from_env(m)), false);\n });\n if let Err(e) = load_custom_providers_into_registry(&mut registry) {\n tracing::warn!(\"Failed to load custom providers: {}\", e);\n }\n RwLock::new(registry)\n}\n\nfn load_custom_providers_into_registry(registry: &mut ProviderRegistry) -> Result<()> {\n register_declarative_providers(registry)\n}\n\nasync fn get_registry() -> &'static RwLock<ProviderRegistry> {\n REGISTRY.get_or_init(init_registry).await\n}\n\npub async fn providers() -> Vec<(ProviderMetadata, ProviderType)> {\n get_registry()\n .await\n .read()\n .unwrap()\n .all_metadata_with_types()\n}\n\npub async fn refresh_custom_providers() -> Result<()> {\n let registry = get_registry().await;\n registry.write().unwrap().remove_custom_providers();\n\n if let Err(e) = load_custom_providers_into_registry(&mut registry.write().unwrap()) {\n tracing::warn!(\"Failed to refresh custom providers: {}\", e);\n return Err(e);\n }\n\n tracing::info!(\"Custom providers refreshed\");\n Ok(())\n}\n\nasync fn get_from_registry(name: &str) -> Result<ProviderEntry> {\n // 将各种 Provider 名称映射到 Aster 支持的 Provider\n let mapped_name = map_provider_alias(name);\n\n #[cfg(not(feature = \"provider-aws\"))]\n if mapped_name == \"bedrock\" || mapped_name == \"sagemaker_tgi\" {\n return Err(anyhow::anyhow!(\n \"Provider {} is disabled at compile time; rebuild with feature provider-aws\",\n mapped_name\n ));\n }\n\n let guard = get_registry().await.read().unwrap();\n guard\n .entries\n .get(mapped_name.as_str())\n .ok_or_else(|| anyhow::anyhow!(\"Unknown provider: {} (mapped to: {})\", name, mapped_name))\n .cloned()\n}\n\n/// 将各种 Provider 名称映射到 Aster 支持的 Provider\n///\n/// Aster 原生支持的 Provider:\n/// - openai, anthropic, google, azure, bedrock, ollama, gcpvertexai\n/// - openrouter, litellm, databricks, codex, xai, venice, tetrate\n/// - snowflake, sagemaker_tgi, githubcopilot, gemini_cli, cursor_agent, claude_code\n///\n/// 其他 Provider 会映射到兼容的 Provider\nfn parse_provider_alias_overrides(raw: &str) -> HashMap<String, String> {\n let trimmed = raw.trim();\n if trimmed.is_empty() {\n return HashMap::new();\n }\n\n if let Ok(json_map) = serde_json::from_str::<HashMap<String, String>>(trimmed) {\n return json_map\n .into_iter()\n .map(|(alias, target)| (alias.trim().to_lowercase(), target.trim().to_lowercase()))\n .filter(|(alias, target)| !alias.is_empty() && !target.is_empty())\n .collect();\n }\n\n let mut overrides = HashMap::new();\n for pair in trimmed.split(',') {\n let entry = pair.trim();\n if entry.is_empty() {\n continue;\n }\n\n let parsed = entry.split_once('=').or_else(|| entry.split_once(':'));\n\n if let Some((alias, target)) = parsed {\n let alias = alias.trim().to_lowercase();\n let target = target.trim().to_lowercase();\n if !alias.is_empty() && !target.is_empty() {\n overrides.insert(alias, target);\n }\n }\n }\n\n overrides\n}\n\nfn load_provider_alias_overrides() -> HashMap<String, String> {\n std::env::var(\"ASTER_PROVIDER_ALIAS_OVERRIDES\")\n .ok()\n .map(|raw| parse_provider_alias_overrides(&raw))\n .unwrap_or_default()\n}\n\nfn map_provider_alias(name: &str) -> String {\n let normalized = name.trim().to_lowercase();\n\n if normalized.is_empty() {\n return normalized;\n }\n\n // 自定义 Provider(UUID 格式,如 custom-ba4e7574-dd00-4784-945a-0f383dfa1272)\n // 这些是用户通过 API Key Provider 添加的自定义服务,通常是 OpenAI 兼容的\n if normalized.starts_with(\"custom-\") {\n return \"openai\".to_string();\n }\n\n // 应用层可通过环境变量覆盖别名映射,避免框架层频繁改代码\n if let Some(mapped) = load_provider_alias_overrides().get(normalized.as_str()) {\n return mapped.clone();\n }\n\n let mapped = match normalized.as_str() {\n // ========== OpenAI 兼容格式 ==========\n // 国内 AI 服务\n \"deepseek\" | \"deep_seek\" | \"deep-seek\" => \"openai\",\n \"qwen\" | \"tongyi\" | \"dashscope\" | \"aliyun\" => \"openai\",\n \"zhipu\" | \"glm\" | \"chatglm\" => \"openai\",\n \"baichuan\" => \"openai\",\n \"moonshot\" | \"kimi\" => \"openai\",\n \"minimax\" => \"openai\",\n \"yi\" | \"01ai\" | \"lingyiwanwu\" => \"openai\",\n \"stepfun\" | \"step\" => \"openai\",\n \"bailian\" | \"百炼\" => \"openai\",\n \"doubao\" | \"豆包\" => \"openai\",\n \"spark\" | \"讯飞\" | \"xunfei\" => \"openai\",\n \"hunyuan\" | \"混元\" => \"openai\",\n \"ernie\" | \"文心\" | \"wenxin\" => \"openai\",\n\n // 国际 AI 服务(OpenAI 兼容)\n \"groq\" => \"openai\",\n \"together\" | \"togetherai\" => \"openai\",\n \"fireworks\" | \"fireworksai\" => \"openai\",\n \"perplexity\" => \"openai\",\n \"anyscale\" => \"openai\",\n \"lepton\" | \"leptonai\" => \"openai\",\n \"novita\" | \"novitaai\" => \"openai\",\n \"siliconflow\" => \"openai\",\n \"mistral\" => \"openai\",\n \"cohere\" => \"openai\",\n\n // API 聚合服务\n \"oneapi\" | \"one-api\" | \"one_api\" => \"openai\",\n \"newapi\" | \"new-api\" | \"new_api\" => \"openai\",\n \"vercel\" | \"vercel_ai\" | \"vercel-ai\" => \"openai\",\n\n // 自定义/通用 OpenAI 兼容\n \"custom\" | \"custom_openai\" | \"openai_compatible\" => \"openai\",\n\n // ========== Anthropic 兼容格式 ==========\n \"claude\" => \"anthropic\",\n \"anthropic_compatible\" | \"anthropic-compatible\" => \"anthropic\",\n\n // ========== Google/Gemini 格式 ==========\n \"gemini\" | \"gemini_api_key\" => \"google\",\n \"antigravity\" => \"google\",\n\n // ========== 其他已支持的 Provider(保持原名) ==========\n \"azure\" | \"azure_openai\" | \"azure-openai\" => \"azure\",\n \"vertex\" | \"vertexai\" | \"vertex_ai\" => \"gcpvertexai\",\n \"aws_bedrock\" | \"aws-bedrock\" => \"bedrock\",\n \"kiro\" => \"bedrock\", // Kiro 使用 CodeWhisperer API\n\n // 默认返回小写原名称(让 Aster 原生处理)\n _ => normalized.as_str(),\n };\n\n mapped.to_string()\n}\n\npub async fn create(name: &str, model: ModelConfig) -> Result<Arc<dyn Provider>> {\n let config = crate::config::Config::global();\n\n if let Ok(lead_model_name) = config.get_param::<String>(\"ASTER_LEAD_MODEL\") {\n tracing::info!(\"Creating lead/worker provider from environment variables\");\n return create_lead_worker_from_env(name, &model, &lead_model_name).await;\n }\n\n let constructor = get_from_registry(name).await?.constructor.clone();\n constructor(model).await\n}\n\npub async fn create_with_default_model(name: impl AsRef<str>) -> Result<Arc<dyn Provider>> {\n get_from_registry(name.as_ref())\n .await?\n .create_with_default_model()\n .await\n}\n\npub async fn create_with_named_model(\n provider_name: &str,\n model_name: &str,\n) -> Result<Arc<dyn Provider>> {\n let config = ModelConfig::new(model_name)?;\n create(provider_name, config).await\n}\n\nasync fn create_lead_worker_from_env(\n default_provider_name: &str,\n default_model: &ModelConfig,\n lead_model_name: &str,\n) -> Result<Arc<dyn Provider>> {\n let config = crate::config::Config::global();\n\n let lead_provider_name_raw = config\n .get_param::<String>(\"ASTER_LEAD_PROVIDER\")\n .unwrap_or_else(|_| default_provider_name.to_string());\n let lead_provider_name = map_provider_alias(&lead_provider_name_raw);\n let worker_provider_name = map_provider_alias(default_provider_name);\n\n let lead_turns = config\n .get_param::<usize>(\"ASTER_LEAD_TURNS\")\n .unwrap_or(DEFAULT_LEAD_TURNS);\n let failure_threshold = config\n .get_param::<usize>(\"ASTER_LEAD_FAILURE_THRESHOLD\")\n .unwrap_or(DEFAULT_FAILURE_THRESHOLD);\n let fallback_turns = config\n .get_param::<usize>(\"ASTER_LEAD_FALLBACK_TURNS\")\n .unwrap_or(DEFAULT_FALLBACK_TURNS);\n\n let lead_model_config = ModelConfig::new_with_context_env(\n lead_model_name.to_string(),\n Some(\"ASTER_LEAD_CONTEXT_LIMIT\"),\n )?;\n\n let worker_model_config = create_worker_model_config(default_model)?;\n\n let registry = get_registry().await;\n\n let lead_constructor = {\n let guard = registry.read().unwrap();\n guard\n .entries\n .get(lead_provider_name.as_str())\n .ok_or_else(|| {\n anyhow::anyhow!(\n \"Unknown provider: {} (mapped to: {})\",\n lead_provider_name_raw,\n lead_provider_name\n )\n })?\n .constructor\n .clone()\n };\n\n let worker_constructor = {\n let guard = registry.read().unwrap();\n guard\n .entries\n .get(worker_provider_name.as_str())\n .ok_or_else(|| {\n anyhow::anyhow!(\n \"Unknown provider: {} (mapped to: {})\",\n default_provider_name,\n worker_provider_name\n )\n })?\n .constructor\n .clone()\n };\n\n let lead_provider = lead_constructor(lead_model_config).await?;\n let worker_provider = worker_constructor(worker_model_config).await?;\n\n Ok(Arc::new(LeadWorkerProvider::new_with_settings(\n lead_provider,\n worker_provider,\n lead_turns,\n failure_threshold,\n fallback_turns,\n )))\n}\n\nfn create_worker_model_config(default_model: &ModelConfig) -> Result<ModelConfig> {\n let mut worker_config = ModelConfig::new_or_fail(&default_model.model_name)\n .with_context_limit(default_model.context_limit)\n .with_temperature(default_model.temperature)\n .with_max_tokens(default_model.max_tokens)\n .with_toolshim(default_model.toolshim)\n .with_toolshim_model(default_model.toolshim_model.clone());\n\n let global_config = crate::config::Config::global();\n\n if let Ok(limit) = global_config.get_param::<usize>(\"ASTER_WORKER_CONTEXT_LIMIT\") {\n worker_config = worker_config.with_context_limit(Some(limit));\n } else if let Ok(limit) = global_config.get_param::<usize>(\"ASTER_CONTEXT_LIMIT\") {\n worker_config = worker_config.with_context_limit(Some(limit));\n }\n\n Ok(worker_config)\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test_case::test_case(None, None, None, DEFAULT_LEAD_TURNS, DEFAULT_FAILURE_THRESHOLD, DEFAULT_FALLBACK_TURNS ; \"defaults\")]\n #[test_case::test_case(Some(\"7\"), Some(\"4\"), Some(\"3\"), 7, 4, 3 ; \"custom\")]\n #[tokio::test]\n async fn test_create_lead_worker_provider(\n lead_turns: Option<&str>,\n failure_threshold: Option<&str>,\n fallback_turns: Option<&str>,\n expected_turns: usize,\n expected_failure: usize,\n expected_fallback: usize,\n ) {\n let _guard = env_lock::lock_env([\n (\"ASTER_LEAD_MODEL\", Some(\"gpt-4o\")),\n (\"ASTER_LEAD_PROVIDER\", None),\n (\"ASTER_LEAD_TURNS\", lead_turns),\n (\"ASTER_LEAD_FAILURE_THRESHOLD\", failure_threshold),\n (\"ASTER_LEAD_FALLBACK_TURNS\", fallback_turns),\n (\"OPENAI_API_KEY\", Some(\"fake-openai-no-keyring\")),\n ]);\n\n let provider = create(\"openai\", ModelConfig::new_or_fail(\"gpt-4o-mini\"))\n .await\n .unwrap();\n let lw = provider.as_lead_worker().unwrap();\n let (lead, worker) = lw.get_model_info();\n assert_eq!(lead, \"gpt-4o\");\n assert_eq!(worker, \"gpt-4o-mini\");\n assert_eq!(\n lw.get_settings(),\n (expected_turns, expected_failure, expected_fallback)\n );\n }\n\n #[tokio::test]\n async fn test_create_regular_provider_without_lead_config() {\n let _guard = env_lock::lock_env([\n (\"ASTER_LEAD_MODEL\", None),\n (\"ASTER_LEAD_PROVIDER\", None),\n (\"ASTER_LEAD_TURNS\", None),\n (\"ASTER_LEAD_FAILURE_THRESHOLD\", None),\n (\"ASTER_LEAD_FALLBACK_TURNS\", None),\n (\"OPENAI_API_KEY\", Some(\"fake-openai-no-keyring\")),\n ]);\n\n let provider = create(\"openai\", ModelConfig::new_or_fail(\"gpt-4o-mini\"))\n .await\n .unwrap();\n assert!(provider.as_lead_worker().is_none());\n assert_eq!(provider.get_model_config().model_name, \"gpt-4o-mini\");\n }\n\n #[test_case::test_case(None, None, 16_000 ; \"no overrides uses default\")]\n #[test_case::test_case(Some(\"32000\"), None, 32_000 ; \"worker limit overrides default\")]\n #[test_case::test_case(Some(\"32000\"), Some(\"64000\"), 32_000 ; \"worker limit takes priority over global\")]\n fn test_worker_model_context_limit(\n worker_limit: Option<&str>,\n global_limit: Option<&str>,\n expected_limit: usize,\n ) {\n let _guard = env_lock::lock_env([\n (\"ASTER_WORKER_CONTEXT_LIMIT\", worker_limit),\n (\"ASTER_CONTEXT_LIMIT\", global_limit),\n ]);\n\n let default_model =\n ModelConfig::new_or_fail(\"gpt-3.5-turbo\").with_context_limit(Some(16_000));\n\n let result = create_worker_model_config(&default_model).unwrap();\n assert_eq!(result.context_limit, Some(expected_limit));\n }\n\n #[tokio::test]\n async fn test_openai_compatible_providers_config_keys() {\n let providers_list = providers().await;\n let cases = vec![\n (\"openai\", \"OPENAI_API_KEY\"),\n (\"groq\", \"GROQ_API_KEY\"),\n (\"mistral\", \"MISTRAL_API_KEY\"),\n (\"custom_deepseek\", \"DEEPSEEK_API_KEY\"),\n ];\n for (name, expected_key) in cases {\n if let Some((meta, _)) = providers_list.iter().find(|(m, _)| m.name == name) {\n assert!(\n !meta.config_keys.is_empty(),\n \"{name} provider should have config keys\"\n );\n assert_eq!(\n meta.config_keys[0].name, expected_key,\n \"First config key for {name} should be {expected_key}, got {}\",\n meta.config_keys[0].name\n );\n assert!(\n meta.config_keys[0].required,\n \"{expected_key} should be required\"\n );\n assert!(\n meta.config_keys[0].secret,\n \"{expected_key} should be secret\"\n );\n } else {\n // Provider not registered; skip test for this provider\n continue;\n }\n }\n }\n\n #[test]\n fn test_map_provider_alias_custom_uuid() {\n let _guard = env_lock::lock_env([(\"ASTER_PROVIDER_ALIAS_OVERRIDES\", None::<&str>)]);\n\n // 自定义 Provider UUID 格式应该映射到 openai\n assert_eq!(\n map_provider_alias(\"custom-ba4e7574-dd00-4784-945a-0f383dfa1272\"),\n \"openai\"\n );\n assert_eq!(\n map_provider_alias(\"custom-12345678-1234-1234-1234-123456789abc\"),\n \"openai\"\n );\n // 普通 custom 也应该映射到 openai\n assert_eq!(map_provider_alias(\"custom\"), \"openai\");\n assert_eq!(map_provider_alias(\"custom_openai\"), \"openai\");\n }\n\n #[test]\n fn test_map_provider_alias_known_providers() {\n let _guard = env_lock::lock_env([(\"ASTER_PROVIDER_ALIAS_OVERRIDES\", None::<&str>)]);\n\n // 已知的 Provider 应该正确映射\n assert_eq!(map_provider_alias(\"deepseek\"), \"openai\");\n assert_eq!(map_provider_alias(\"qwen\"), \"openai\");\n assert_eq!(map_provider_alias(\"claude\"), \"anthropic\");\n assert_eq!(map_provider_alias(\"gemini\"), \"google\");\n assert_eq!(map_provider_alias(\"kiro\"), \"bedrock\");\n // 原生支持的 Provider 应该保持原名\n assert_eq!(map_provider_alias(\"openai\"), \"openai\");\n assert_eq!(map_provider_alias(\"anthropic\"), \"anthropic\");\n assert_eq!(map_provider_alias(\"google\"), \"google\");\n }\n\n #[test]\n fn test_map_provider_alias_fallback_to_lowercase() {\n let _guard = env_lock::lock_env([(\"ASTER_PROVIDER_ALIAS_OVERRIDES\", None::<&str>)]);\n\n assert_eq!(map_provider_alias(\"OpenAI\"), \"openai\");\n assert_eq!(\n map_provider_alias(\"My-Custom-Provider\"),\n \"my-custom-provider\"\n );\n }\n\n #[test]\n fn test_map_provider_alias_env_override_json() {\n let _guard = env_lock::lock_env([(\n \"ASTER_PROVIDER_ALIAS_OVERRIDES\",\n Some(r#\"{\"moonshotai\":\"openrouter\",\"gemini\":\"google\"}\"#),\n )]);\n\n assert_eq!(map_provider_alias(\"moonshotai\"), \"openrouter\");\n assert_eq!(map_provider_alias(\"gemini\"), \"google\");\n }\n\n #[test]\n fn test_map_provider_alias_env_override_kv() {\n let _guard = env_lock::lock_env([(\n \"ASTER_PROVIDER_ALIAS_OVERRIDES\",\n Some(\"deepseek=openrouter,claude=openai\"),\n )]);\n\n assert_eq!(map_provider_alias(\"deepseek\"), \"openrouter\");\n assert_eq!(map_provider_alias(\"claude\"), \"openai\");\n }\n}\n"}, "39825c4eeacf18b429b3b66492470c88f688f02d": {"t": 1785757075.1523073, "n": 16893, "h": "7432326cc021e319", "c": "use std::sync::{Arc, RwLock};\n\nuse super::{\n anthropic::AnthropicProvider,\n azure::AzureProvider,\n base::{Provider, ProviderMetadata},\n bedrock::BedrockProvider,\n claude_code::ClaudeCodeProvider,\n codex::CodexProvider,\n codex_stateful::CodexStatefulProvider,\n cursor_agent::CursorAgentProvider,\n databricks::DatabricksProvider,\n gcpvertexai::GcpVertexAIProvider,\n gemini_cli::GeminiCliProvider,\n githubcopilot::GithubCopilotProvider,\n google::GoogleProvider,\n lead_worker::LeadWorkerProvider,\n litellm::LiteLLMProvider,\n ollama::OllamaProvider,\n openai::OpenAiProvider,\n openrouter::OpenRouterProvider,\n provider_registry::ProviderRegistry,\n sagemaker_tgi::SageMakerTgiProvider,\n snowflake::SnowflakeProvider,\n tetrate::TetrateProvider,\n venice::VeniceProvider,\n xai::XaiProvider,\n};\nuse crate::model::ModelConfig;\nuse crate::providers::base::ProviderType;\nuse crate::{\n config::declarative_providers::register_declarative_providers,\n providers::provider_registry::ProviderEntry,\n};\nuse anyhow::Result;\nuse tokio::sync::OnceCell;\n\nconst DEFAULT_LEAD_TURNS: usize = 3;\nconst DEFAULT_FAILURE_THRESHOLD: usize = 2;\nconst DEFAULT_FALLBACK_TURNS: usize = 2;\n\nstatic REGISTRY: OnceCell<RwLock<ProviderRegistry>> = OnceCell::const_new();\n\nasync fn init_registry() -> RwLock<ProviderRegistry> {\n let mut registry = ProviderRegistry::new().with_providers(|registry| {\n registry\n .register::<AnthropicProvider, _>(|m| Box::pin(AnthropicProvider::from_env(m)), true);\n registry.register::<AzureProvider, _>(|m| Box::pin(AzureProvider::from_env(m)), false);\n registry.register::<BedrockProvider, _>(|m| Box::pin(BedrockProvider::from_env(m)), false);\n registry\n .register::<ClaudeCodeProvider, _>(|m| Box::pin(ClaudeCodeProvider::from_env(m)), true);\n registry.register::<CodexProvider, _>(|m| Box::pin(CodexProvider::from_env(m)), true);\n registry.register::<CodexStatefulProvider, _>(\n |m| Box::pin(CodexStatefulProvider::from_env(m)),\n true,\n );\n registry.register::<CursorAgentProvider, _>(\n |m| Box::pin(CursorAgentProvider::from_env(m)),\n false,\n );\n registry\n .register::<DatabricksProvider, _>(|m| Box::pin(DatabricksProvider::from_env(m)), true);\n registry.register::<GcpVertexAIProvider, _>(\n |m| Box::pin(GcpVertexAIProvider::from_env(m)),\n false,\n );\n registry\n .register::<GeminiCliProvider, _>(|m| Box::pin(GeminiCliProvider::from_env(m)), false);\n registry.register::<GithubCopilotProvider, _>(\n |m| Box::pin(GithubCopilotProvider::from_env(m)),\n false,\n );\n registry.register::<GoogleProvider, _>(|m| Box::pin(GoogleProvider::from_env(m)), true);\n registry.register::<LiteLLMProvider, _>(|m| Box::pin(LiteLLMProvider::from_env(m)), false);\n registry.register::<OllamaProvider, _>(|m| Box::pin(OllamaProvider::from_env(m)), true);\n registry.register::<OpenAiProvider, _>(|m| Box::pin(OpenAiProvider::from_env(m)), true);\n registry\n .register::<OpenRouterProvider, _>(|m| Box::pin(OpenRouterProvider::from_env(m)), true);\n registry.register::<SageMakerTgiProvider, _>(\n |m| Box::pin(SageMakerTgiProvider::from_env(m)),\n false,\n );\n registry\n .register::<SnowflakeProvider, _>(|m| Box::pin(SnowflakeProvider::from_env(m)), false);\n registry.register::<TetrateProvider, _>(|m| Box::pin(TetrateProvider::from_env(m)), true);\n registry.register::<VeniceProvider, _>(|m| Box::pin(VeniceProvider::from_env(m)), false);\n registry.register::<XaiProvider, _>(|m| Box::pin(XaiProvider::from_env(m)), false);\n });\n if let Err(e) = load_custom_providers_into_registry(&mut registry) {\n tracing::warn!(\"Failed to load custom providers: {}\", e);\n }\n RwLock::new(registry)\n}\n\nfn load_custom_providers_into_registry(registry: &mut ProviderRegistry) -> Result<()> {\n register_declarative_providers(registry)\n}\n\nasync fn get_registry() -> &'static RwLock<ProviderRegistry> {\n REGISTRY.get_or_init(init_registry).await\n}\n\npub async fn providers() -> Vec<(ProviderMetadata, ProviderType)> {\n get_registry()\n .await\n .read()\n .unwrap()\n .all_metadata_with_types()\n}\n\npub async fn refresh_custom_providers() -> Result<()> {\n let registry = get_registry().await;\n registry.write().unwrap().remove_custom_providers();\n\n if let Err(e) = load_custom_providers_into_registry(&mut registry.write().unwrap()) {\n tracing::warn!(\"Failed to refresh custom providers: {}\", e);\n return Err(e);\n }\n\n tracing::info!(\"Custom providers refreshed\");\n Ok(())\n}\n\nasync fn get_from_registry(name: &str) -> Result<ProviderEntry> {\n // 将各种 Provider 名称映射到 Aster 支持的 Provider\n let mapped_name = map_provider_alias(name);\n\n let guard = get_registry().await.read().unwrap();\n guard\n .entries\n .get(mapped_name)\n .ok_or_else(|| anyhow::anyhow!(\"Unknown provider: {} (mapped to: {})\", name, mapped_name))\n .cloned()\n}\n\n/// 将各种 Provider 名称映射到 Aster 支持的 Provider\n///\n/// Aster 原生支持的 Provider:\n/// - openai, anthropic, google, azure, bedrock, ollama, gcpvertexai\n/// - openrouter, litellm, databricks, codex, xai, venice, tetrate\n/// - snowflake, sagemaker_tgi, githubcopilot, gemini_cli, cursor_agent, claude_code\n///\n/// 其他 Provider 会映射到兼容的 Provider\nfn map_provider_alias(name: &str) -> &str {\n match name.to_lowercase().as_str() {\n // ========== OpenAI 兼容格式 ==========\n // 国内 AI 服务\n \"deepseek\" | \"deep_seek\" | \"deep-seek\" => \"openai\",\n \"qwen\" | \"tongyi\" | \"dashscope\" | \"aliyun\" => \"openai\",\n \"zhipu\" | \"glm\" | \"chatglm\" => \"openai\",\n \"baichuan\" => \"openai\",\n \"moonshot\" | \"kimi\" => \"openai\",\n \"minimax\" => \"openai\",\n \"yi\" | \"01ai\" | \"lingyiwanwu\" => \"openai\",\n \"stepfun\" | \"step\" => \"openai\",\n \"bailian\" | \"百炼\" => \"openai\",\n \"doubao\" | \"豆包\" => \"openai\",\n \"spark\" | \"讯飞\" | \"xunfei\" => \"openai\",\n \"hunyuan\" | \"混元\" => \"openai\",\n \"ernie\" | \"文心\" | \"wenxin\" => \"openai\",\n\n // 国际 AI 服务(OpenAI 兼容)\n \"groq\" => \"openai\",\n \"together\" | \"togetherai\" => \"openai\",\n \"fireworks\" | \"fireworksai\" => \"openai\",\n \"perplexity\" => \"openai\",\n \"anyscale\" => \"openai\",\n \"lepton\" | \"leptonai\" => \"openai\",\n \"novita\" | \"novitaai\" => \"openai\",\n \"siliconflow\" => \"openai\",\n \"mistral\" => \"openai\",\n \"cohere\" => \"openai\",\n\n // API 聚合服务\n \"oneapi\" | \"one-api\" | \"one_api\" => \"openai\",\n \"newapi\" | \"new-api\" | \"new_api\" => \"openai\",\n \"vercel\" | \"vercel_ai\" | \"vercel-ai\" => \"openai\",\n\n // 自定义/通用 OpenAI 兼容\n \"custom\" | \"custom_openai\" | \"openai_compatible\" => \"openai\",\n\n // ========== Anthropic 兼容格式 ==========\n \"claude\" => \"anthropic\",\n \"anthropic_compatible\" | \"anthropic-compatible\" => \"anthropic\",\n\n // ========== Google/Gemini 格式 ==========\n \"gemini\" | \"gemini_api_key\" => \"google\",\n \"antigravity\" => \"google\",\n\n // ========== 其他已支持的 Provider(保持原名) ==========\n \"azure\" | \"azure_openai\" | \"azure-openai\" => \"azure\",\n \"vertex\" | \"vertexai\" | \"vertex_ai\" => \"gcpvertexai\",\n \"aws_bedrock\" | \"aws-bedrock\" => \"bedrock\",\n \"kiro\" => \"bedrock\", // Kiro 使用 CodeWhisperer API\n\n // 自定义 Provider(UUID 格式,如 custom-ba4e7574-dd00-4784-945a-0f383dfa1272)\n // 这些是用户通过 API Key Provider 添加的自定义服务,通常是 OpenAI 兼容的\n s if s.starts_with(\"custom-\") => \"openai\",\n\n // 默认返回原名称(让 Aster 原生处理)\n _ => name,\n }\n}\n\npub async fn create(name: &str, model: ModelConfig) -> Result<Arc<dyn Provider>> {\n let config = crate::config::Config::global();\n\n if let Ok(lead_model_name) = config.get_param::<String>(\"ASTER_LEAD_MODEL\") {\n tracing::info!(\"Creating lead/worker provider from environment variables\");\n return create_lead_worker_from_env(name, &model, &lead_model_name).await;\n }\n\n let constructor = get_from_registry(name).await?.constructor.clone();\n constructor(model).await\n}\n\npub async fn create_with_default_model(name: impl AsRef<str>) -> Result<Arc<dyn Provider>> {\n get_from_registry(name.as_ref())\n .await?\n .create_with_default_model()\n .await\n}\n\npub async fn create_with_named_model(\n provider_name: &str,\n model_name: &str,\n) -> Result<Arc<dyn Provider>> {\n let config = ModelConfig::new(model_name)?;\n create(provider_name, config).await\n}\n\nasync fn create_lead_worker_from_env(\n default_provider_name: &str,\n default_model: &ModelConfig,\n lead_model_name: &str,\n) -> Result<Arc<dyn Provider>> {\n let config = crate::config::Config::global();\n\n let lead_provider_name = config\n .get_param::<String>(\"ASTER_LEAD_PROVIDER\")\n .unwrap_or_else(|_| default_provider_name.to_string());\n\n let lead_turns = config\n .get_param::<usize>(\"ASTER_LEAD_TURNS\")\n .unwrap_or(DEFAULT_LEAD_TURNS);\n let failure_threshold = config\n .get_param::<usize>(\"ASTER_LEAD_FAILURE_THRESHOLD\")\n .unwrap_or(DEFAULT_FAILURE_THRESHOLD);\n let fallback_turns = config\n .get_param::<usize>(\"ASTER_LEAD_FALLBACK_TURNS\")\n .unwrap_or(DEFAULT_FALLBACK_TURNS);\n\n let lead_model_config = ModelConfig::new_with_context_env(\n lead_model_name.to_string(),\n Some(\"ASTER_LEAD_CONTEXT_LIMIT\"),\n )?;\n\n let worker_model_config = create_worker_model_config(default_model)?;\n\n let registry = get_registry().await;\n\n let lead_constructor = {\n let guard = registry.read().unwrap();\n guard\n .entries\n .get(&lead_provider_name)\n .ok_or_else(|| anyhow::anyhow!(\"Unknown provider: {}\", lead_provider_name))?\n .constructor\n .clone()\n };\n\n let worker_constructor = {\n let guard = registry.read().unwrap();\n guard\n .entries\n .get(default_provider_name)\n .ok_or_else(|| anyhow::anyhow!(\"Unknown provider: {}\", default_provider_name))?\n .constructor\n .clone()\n };\n\n let lead_provider = lead_constructor(lead_model_config).await?;\n let worker_provider = worker_constructor(worker_model_config).await?;\n\n Ok(Arc::new(LeadWorkerProvider::new_with_settings(\n lead_provider,\n worker_provider,\n lead_turns,\n failure_threshold,\n fallback_turns,\n )))\n}\n\nfn create_worker_model_config(default_model: &ModelConfig) -> Result<ModelConfig> {\n let mut worker_config = ModelConfig::new_or_fail(&default_model.model_name)\n .with_context_limit(default_model.context_limit)\n .with_temperature(default_model.temperature)\n .with_max_tokens(default_model.max_tokens)\n .with_toolshim(default_model.toolshim)\n .with_toolshim_model(default_model.toolshim_model.clone());\n\n let global_config = crate::config::Config::global();\n\n if let Ok(limit) = global_config.get_param::<usize>(\"ASTER_WORKER_CONTEXT_LIMIT\") {\n worker_config = worker_config.with_context_limit(Some(limit));\n } else if let Ok(limit) = global_config.get_param::<usize>(\"ASTER_CONTEXT_LIMIT\") {\n worker_config = worker_config.with_context_limit(Some(limit));\n }\n\n Ok(worker_config)\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test_case::test_case(None, None, None, DEFAULT_LEAD_TURNS, DEFAULT_FAILURE_THRESHOLD, DEFAULT_FALLBACK_TURNS ; \"defaults\")]\n #[test_case::test_case(Some(\"7\"), Some(\"4\"), Some(\"3\"), 7, 4, 3 ; \"custom\")]\n #[tokio::test]\n async fn test_create_lead_worker_provider(\n lead_turns: Option<&str>,\n failure_threshold: Option<&str>,\n fallback_turns: Option<&str>,\n expected_turns: usize,\n expected_failure: usize,\n expected_fallback: usize,\n ) {\n let _guard = env_lock::lock_env([\n (\"ASTER_LEAD_MODEL\", Some(\"gpt-4o\")),\n (\"ASTER_LEAD_PROVIDER\", None),\n (\"ASTER_LEAD_TURNS\", lead_turns),\n (\"ASTER_LEAD_FAILURE_THRESHOLD\", failure_threshold),\n (\"ASTER_LEAD_FALLBACK_TURNS\", fallback_turns),\n (\"OPENAI_API_KEY\", Some(\"fake-openai-no-keyring\")),\n ]);\n\n let provider = create(\"openai\", ModelConfig::new_or_fail(\"gpt-4o-mini\"))\n .await\n .unwrap();\n let lw = provider.as_lead_worker().unwrap();\n let (lead, worker) = lw.get_model_info();\n assert_eq!(lead, \"gpt-4o\");\n assert_eq!(worker, \"gpt-4o-mini\");\n assert_eq!(\n lw.get_settings(),\n (expected_turns, expected_failure, expected_fallback)\n );\n }\n\n #[tokio::test]\n async fn test_create_regular_provider_without_lead_config() {\n let _guard = env_lock::lock_env([\n (\"ASTER_LEAD_MODEL\", None),\n (\"ASTER_LEAD_PROVIDER\", None),\n (\"ASTER_LEAD_TURNS\", None),\n (\"ASTER_LEAD_FAILURE_THRESHOLD\", None),\n (\"ASTER_LEAD_FALLBACK_TURNS\", None),\n (\"OPENAI_API_KEY\", Some(\"fake-openai-no-keyring\")),\n ]);\n\n let provider = create(\"openai\", ModelConfig::new_or_fail(\"gpt-4o-mini\"))\n .await\n .unwrap();\n assert!(provider.as_lead_worker().is_none());\n assert_eq!(provider.get_model_config().model_name, \"gpt-4o-mini\");\n }\n\n #[test_case::test_case(None, None, 16_000 ; \"no overrides uses default\")]\n #[test_case::test_case(Some(\"32000\"), None, 32_000 ; \"worker limit overrides default\")]\n #[test_case::test_case(Some(\"32000\"), Some(\"64000\"), 32_000 ; \"worker limit takes priority over global\")]\n fn test_worker_model_context_limit(\n worker_limit: Option<&str>,\n global_limit: Option<&str>,\n expected_limit: usize,\n ) {\n let _guard = env_lock::lock_env([\n (\"ASTER_WORKER_CONTEXT_LIMIT\", worker_limit),\n (\"ASTER_CONTEXT_LIMIT\", global_limit),\n ]);\n\n let default_model =\n ModelConfig::new_or_fail(\"gpt-3.5-turbo\").with_context_limit(Some(16_000));\n\n let result = create_worker_model_config(&default_model).unwrap();\n assert_eq!(result.context_limit, Some(expected_limit));\n }\n\n #[tokio::test]\n async fn test_openai_compatible_providers_config_keys() {\n let providers_list = providers().await;\n let cases = vec![\n (\"openai\", \"OPENAI_API_KEY\"),\n (\"groq\", \"GROQ_API_KEY\"),\n (\"mistral\", \"MISTRAL_API_KEY\"),\n (\"custom_deepseek\", \"DEEPSEEK_API_KEY\"),\n ];\n for (name, expected_key) in cases {\n if let Some((meta, _)) = providers_list.iter().find(|(m, _)| m.name == name) {\n assert!(\n !meta.config_keys.is_empty(),\n \"{name} provider should have config keys\"\n );\n assert_eq!(\n meta.config_keys[0].name, expected_key,\n \"First config key for {name} should be {expected_key}, got {}\",\n meta.config_keys[0].name\n );\n assert!(\n meta.config_keys[0].required,\n \"{expected_key} should be required\"\n );\n assert!(\n meta.config_keys[0].secret,\n \"{expected_key} should be secret\"\n );\n } else {\n // Provider not registered; skip test for this provider\n continue;\n }\n }\n }\n\n #[test]\n fn test_map_provider_alias_custom_uuid() {\n // 自定义 Provider UUID 格式应该映射到 openai\n assert_eq!(\n map_provider_alias(\"custom-ba4e7574-dd00-4784-945a-0f383dfa1272\"),\n \"openai\"\n );\n assert_eq!(\n map_provider_alias(\"custom-12345678-1234-1234-1234-123456789abc\"),\n \"openai\"\n );\n // 普通 custom 也应该映射到 openai\n assert_eq!(map_provider_alias(\"custom\"), \"openai\");\n assert_eq!(map_provider_alias(\"custom_openai\"), \"openai\");\n }\n\n #[test]\n fn test_map_provider_alias_known_providers() {\n // 已知的 Provider 应该正确映射\n assert_eq!(map_provider_alias(\"deepseek\"), \"openai\");\n assert_eq!(map_provider_alias(\"qwen\"), \"openai\");\n assert_eq!(map_provider_alias(\"claude\"), \"anthropic\");\n assert_eq!(map_provider_alias(\"gemini\"), \"google\");\n assert_eq!(map_provider_alias(\"kiro\"), \"bedrock\");\n // 原生支持的 Provider 应该保持原名\n assert_eq!(map_provider_alias(\"openai\"), \"openai\");\n assert_eq!(map_provider_alias(\"anthropic\"), \"anthropic\");\n assert_eq!(map_provider_alias(\"google\"), \"google\");\n }\n}\n"}, "9fa8562f3d943798ccd6eb5ea8629e06e4a46443": {"t": 1785757077.8975918, "n": 15357, "h": "9df86078348a7355", "c": "use std::sync::{Arc, RwLock};\n\nuse super::{\n anthropic::AnthropicProvider,\n azure::AzureProvider,\n base::{Provider, ProviderMetadata},\n bedrock::BedrockProvider,\n claude_code::ClaudeCodeProvider,\n codex::CodexProvider,\n cursor_agent::CursorAgentProvider,\n databricks::DatabricksProvider,\n gcpvertexai::GcpVertexAIProvider,\n gemini_cli::GeminiCliProvider,\n githubcopilot::GithubCopilotProvider,\n google::GoogleProvider,\n lead_worker::LeadWorkerProvider,\n litellm::LiteLLMProvider,\n ollama::OllamaProvider,\n openai::OpenAiProvider,\n openrouter::OpenRouterProvider,\n provider_registry::ProviderRegistry,\n sagemaker_tgi::SageMakerTgiProvider,\n snowflake::SnowflakeProvider,\n tetrate::TetrateProvider,\n venice::VeniceProvider,\n xai::XaiProvider,\n};\nuse crate::model::ModelConfig;\nuse crate::providers::base::ProviderType;\nuse crate::{\n config::declarative_providers::register_declarative_providers,\n providers::provider_registry::ProviderEntry,\n};\nuse anyhow::Result;\nuse tokio::sync::OnceCell;\n\nconst DEFAULT_LEAD_TURNS: usize = 3;\nconst DEFAULT_FAILURE_THRESHOLD: usize = 2;\nconst DEFAULT_FALLBACK_TURNS: usize = 2;\n\nstatic REGISTRY: OnceCell<RwLock<ProviderRegistry>> = OnceCell::const_new();\n\nasync fn init_registry() -> RwLock<ProviderRegistry> {\n let mut registry = ProviderRegistry::new().with_providers(|registry| {\n registry\n .register::<AnthropicProvider, _>(|m| Box::pin(AnthropicProvider::from_env(m)), true);\n registry.register::<AzureProvider, _>(|m| Box::pin(AzureProvider::from_env(m)), false);\n registry.register::<BedrockProvider, _>(|m| Box::pin(BedrockProvider::from_env(m)), false);\n registry\n .register::<ClaudeCodeProvider, _>(|m| Box::pin(ClaudeCodeProvider::from_env(m)), true);\n registry.register::<CodexProvider, _>(|m| Box::pin(CodexProvider::from_env(m)), true);\n registry.register::<CursorAgentProvider, _>(\n |m| Box::pin(CursorAgentProvider::from_env(m)),\n false,\n );\n registry\n .register::<DatabricksProvider, _>(|m| Box::pin(DatabricksProvider::from_env(m)), true);\n registry.register::<GcpVertexAIProvider, _>(\n |m| Box::pin(GcpVertexAIProvider::from_env(m)),\n false,\n );\n registry\n .register::<GeminiCliProvider, _>(|m| Box::pin(GeminiCliProvider::from_env(m)), false);\n registry.register::<GithubCopilotProvider, _>(\n |m| Box::pin(GithubCopilotProvider::from_env(m)),\n false,\n );\n registry.register::<GoogleProvider, _>(|m| Box::pin(GoogleProvider::from_env(m)), true);\n registry.register::<LiteLLMProvider, _>(|m| Box::pin(LiteLLMProvider::from_env(m)), false);\n registry.register::<OllamaProvider, _>(|m| Box::pin(OllamaProvider::from_env(m)), true);\n registry.register::<OpenAiProvider, _>(|m| Box::pin(OpenAiProvider::from_env(m)), true);\n registry\n .register::<OpenRouterProvider, _>(|m| Box::pin(OpenRouterProvider::from_env(m)), true);\n registry.register::<SageMakerTgiProvider, _>(\n |m| Box::pin(SageMakerTgiProvider::from_env(m)),\n false,\n );\n registry\n .register::<SnowflakeProvider, _>(|m| Box::pin(SnowflakeProvider::from_env(m)), false);\n registry.register::<TetrateProvider, _>(|m| Box::pin(TetrateProvider::from_env(m)), true);\n registry.register::<VeniceProvider, _>(|m| Box::pin(VeniceProvider::from_env(m)), false);\n registry.register::<XaiProvider, _>(|m| Box::pin(XaiProvider::from_env(m)), false);\n });\n if let Err(e) = load_custom_providers_into_registry(&mut registry) {\n tracing::warn!(\"Failed to load custom providers: {}\", e);\n }\n RwLock::new(registry)\n}\n\nfn load_custom_providers_into_registry(registry: &mut ProviderRegistry) -> Result<()> {\n register_declarative_providers(registry)\n}\n\nasync fn get_registry() -> &'static RwLock<ProviderRegistry> {\n REGISTRY.get_or_init(init_registry).await\n}\n\npub async fn providers() -> Vec<(ProviderMetadata, ProviderType)> {\n get_registry()\n .await\n .read()\n .unwrap()\n .all_metadata_with_types()\n}\n\npub async fn refresh_custom_providers() -> Result<()> {\n let registry = get_registry().await;\n registry.write().unwrap().remove_custom_providers();\n\n if let Err(e) = load_custom_providers_into_registry(&mut registry.write().unwrap()) {\n tracing::warn!(\"Failed to refresh custom providers: {}\", e);\n return Err(e);\n }\n\n tracing::info!(\"Custom providers refreshed\");\n Ok(())\n}\n\nasync fn get_from_registry(name: &str) -> Result<ProviderEntry> {\n // 将各种 Provider 名称映射到 Aster 支持的 Provider\n let mapped_name = map_provider_alias(name);\n\n let guard = get_registry().await.read().unwrap();\n guard\n .entries\n .get(mapped_name)\n .ok_or_else(|| anyhow::anyhow!(\"Unknown provider: {} (mapped to: {})\", name, mapped_name))\n .cloned()\n}\n\n/// 将各种 Provider 名称映射到 Aster 支持的 Provider\n///\n/// Aster 原生支持的 Provider:\n/// - openai, anthropic, google, azure, bedrock, ollama, gcpvertexai\n/// - openrouter, litellm, databricks, codex, xai, venice, tetrate\n/// - snowflake, sagemaker_tgi, githubcopilot, gemini_cli, cursor_agent, claude_code\n///\n/// 其他 Provider 会映射到兼容的 Provider\nfn map_provider_alias(name: &str) -> &str {\n match name.to_lowercase().as_str() {\n // ========== OpenAI 兼容格式 ==========\n // 国内 AI 服务\n \"deepseek\" | \"deep_seek\" | \"deep-seek\" => \"openai\",\n \"qwen\" | \"tongyi\" | \"dashscope\" | \"aliyun\" => \"openai\",\n \"zhipu\" | \"glm\" | \"chatglm\" => \"openai\",\n \"baichuan\" => \"openai\",\n \"moonshot\" | \"kimi\" => \"openai\",\n \"minimax\" => \"openai\",\n \"yi\" | \"01ai\" | \"lingyiwanwu\" => \"openai\",\n \"stepfun\" | \"step\" => \"openai\",\n \"bailian\" | \"百炼\" => \"openai\",\n \"doubao\" | \"豆包\" => \"openai\",\n \"spark\" | \"讯飞\" | \"xunfei\" => \"openai\",\n \"hunyuan\" | \"混元\" => \"openai\",\n \"ernie\" | \"文心\" | \"wenxin\" => \"openai\",\n\n // 国际 AI 服务(OpenAI 兼容)\n \"groq\" => \"openai\",\n \"together\" | \"togetherai\" => \"openai\",\n \"fireworks\" | \"fireworksai\" => \"openai\",\n \"perplexity\" => \"openai\",\n \"anyscale\" => \"openai\",\n \"lepton\" | \"leptonai\" => \"openai\",\n \"novita\" | \"novitaai\" => \"openai\",\n \"siliconflow\" => \"openai\",\n \"mistral\" => \"openai\",\n \"cohere\" => \"openai\",\n\n // API 聚合服务\n \"oneapi\" | \"one-api\" | \"one_api\" => \"openai\",\n \"newapi\" | \"new-api\" | \"new_api\" => \"openai\",\n \"vercel\" | \"vercel_ai\" | \"vercel-ai\" => \"openai\",\n\n // 自定义/通用 OpenAI 兼容\n \"custom\" | \"custom_openai\" | \"openai_compatible\" => \"openai\",\n\n // ========== Anthropic 兼容格式 ==========\n \"claude\" => \"anthropic\",\n \"anthropic_compatible\" | \"anthropic-compatible\" => \"anthropic\",\n\n // ========== Google/Gemini 格式 ==========\n \"gemini\" | \"gemini_api_key\" => \"google\",\n \"antigravity\" => \"google\",\n\n // ========== 其他已支持的 Provider(保持原名) ==========\n \"azure\" | \"azure_openai\" | \"azure-openai\" => \"azure\",\n \"vertex\" | \"vertexai\" | \"vertex_ai\" => \"gcpvertexai\",\n \"aws_bedrock\" | \"aws-bedrock\" => \"bedrock\",\n \"kiro\" => \"bedrock\", // Kiro 使用 CodeWhisperer API\n\n // 默认返回原名称(让 Aster 原生处理)\n _ => name,\n }\n}\n\npub async fn create(name: &str, model: ModelConfig) -> Result<Arc<dyn Provider>> {\n let config = crate::config::Config::global();\n\n if let Ok(lead_model_name) = config.get_param::<String>(\"ASTER_LEAD_MODEL\") {\n tracing::info!(\"Creating lead/worker provider from environment variables\");\n return create_lead_worker_from_env(name, &model, &lead_model_name).await;\n }\n\n let constructor = get_from_registry(name).await?.constructor.clone();\n constructor(model).await\n}\n\npub async fn create_with_default_model(name: impl AsRef<str>) -> Result<Arc<dyn Provider>> {\n get_from_registry(name.as_ref())\n .await?\n .create_with_default_model()\n .await\n}\n\npub async fn create_with_named_model(\n provider_name: &str,\n model_name: &str,\n) -> Result<Arc<dyn Provider>> {\n let config = ModelConfig::new(model_name)?;\n create(provider_name, config).await\n}\n\nasync fn create_lead_worker_from_env(\n default_provider_name: &str,\n default_model: &ModelConfig,\n lead_model_name: &str,\n) -> Result<Arc<dyn Provider>> {\n let config = crate::config::Config::global();\n\n let lead_provider_name = config\n .get_param::<String>(\"ASTER_LEAD_PROVIDER\")\n .unwrap_or_else(|_| default_provider_name.to_string());\n\n let lead_turns = config\n .get_param::<usize>(\"ASTER_LEAD_TURNS\")\n .unwrap_or(DEFAULT_LEAD_TURNS);\n let failure_threshold = config\n .get_param::<usize>(\"ASTER_LEAD_FAILURE_THRESHOLD\")\n .unwrap_or(DEFAULT_FAILURE_THRESHOLD);\n let fallback_turns = config\n .get_param::<usize>(\"ASTER_LEAD_FALLBACK_TURNS\")\n .unwrap_or(DEFAULT_FALLBACK_TURNS);\n\n let lead_model_config = ModelConfig::new_with_context_env(\n lead_model_name.to_string(),\n Some(\"ASTER_LEAD_CONTEXT_LIMIT\"),\n )?;\n\n let worker_model_config = create_worker_model_config(default_model)?;\n\n let registry = get_registry().await;\n\n let lead_constructor = {\n let guard = registry.read().unwrap();\n guard\n .entries\n .get(&lead_provider_name)\n .ok_or_else(|| anyhow::anyhow!(\"Unknown provider: {}\", lead_provider_name))?\n .constructor\n .clone()\n };\n\n let worker_constructor = {\n let guard = registry.read().unwrap();\n guard\n .entries\n .get(default_provider_name)\n .ok_or_else(|| anyhow::anyhow!(\"Unknown provider: {}\", default_provider_name))?\n .constructor\n .clone()\n };\n\n let lead_provider = lead_constructor(lead_model_config).await?;\n let worker_provider = worker_constructor(worker_model_config).await?;\n\n Ok(Arc::new(LeadWorkerProvider::new_with_settings(\n lead_provider,\n worker_provider,\n lead_turns,\n failure_threshold,\n fallback_turns,\n )))\n}\n\nfn create_worker_model_config(default_model: &ModelConfig) -> Result<ModelConfig> {\n let mut worker_config = ModelConfig::new_or_fail(&default_model.model_name)\n .with_context_limit(default_model.context_limit)\n .with_temperature(default_model.temperature)\n .with_max_tokens(default_model.max_tokens)\n .with_toolshim(default_model.toolshim)\n .with_toolshim_model(default_model.toolshim_model.clone());\n\n let global_config = crate::config::Config::global();\n\n if let Ok(limit) = global_config.get_param::<usize>(\"ASTER_WORKER_CONTEXT_LIMIT\") {\n worker_config = worker_config.with_context_limit(Some(limit));\n } else if let Ok(limit) = global_config.get_param::<usize>(\"ASTER_CONTEXT_LIMIT\") {\n worker_config = worker_config.with_context_limit(Some(limit));\n }\n\n Ok(worker_config)\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test_case::test_case(None, None, None, DEFAULT_LEAD_TURNS, DEFAULT_FAILURE_THRESHOLD, DEFAULT_FALLBACK_TURNS ; \"defaults\")]\n #[test_case::test_case(Some(\"7\"), Some(\"4\"), Some(\"3\"), 7, 4, 3 ; \"custom\")]\n #[tokio::test]\n async fn test_create_lead_worker_provider(\n lead_turns: Option<&str>,\n failure_threshold: Option<&str>,\n fallback_turns: Option<&str>,\n expected_turns: usize,\n expected_failure: usize,\n expected_fallback: usize,\n ) {\n let _guard = env_lock::lock_env([\n (\"ASTER_LEAD_MODEL\", Some(\"gpt-4o\")),\n (\"ASTER_LEAD_PROVIDER\", None),\n (\"ASTER_LEAD_TURNS\", lead_turns),\n (\"ASTER_LEAD_FAILURE_THRESHOLD\", failure_threshold),\n (\"ASTER_LEAD_FALLBACK_TURNS\", fallback_turns),\n (\"OPENAI_API_KEY\", Some(\"fake-openai-no-keyring\")),\n ]);\n\n let provider = create(\"openai\", ModelConfig::new_or_fail(\"gpt-4o-mini\"))\n .await\n .unwrap();\n let lw = provider.as_lead_worker().unwrap();\n let (lead, worker) = lw.get_model_info();\n assert_eq!(lead, \"gpt-4o\");\n assert_eq!(worker, \"gpt-4o-mini\");\n assert_eq!(\n lw.get_settings(),\n (expected_turns, expected_failure, expected_fallback)\n );\n }\n\n #[tokio::test]\n async fn test_create_regular_provider_without_lead_config() {\n let _guard = env_lock::lock_env([\n (\"ASTER_LEAD_MODEL\", None),\n (\"ASTER_LEAD_PROVIDER\", None),\n (\"ASTER_LEAD_TURNS\", None),\n (\"ASTER_LEAD_FAILURE_THRESHOLD\", None),\n (\"ASTER_LEAD_FALLBACK_TURNS\", None),\n (\"OPENAI_API_KEY\", Some(\"fake-openai-no-keyring\")),\n ]);\n\n let provider = create(\"openai\", ModelConfig::new_or_fail(\"gpt-4o-mini\"))\n .await\n .unwrap();\n assert!(provider.as_lead_worker().is_none());\n assert_eq!(provider.get_model_config().model_name, \"gpt-4o-mini\");\n }\n\n #[test_case::test_case(None, None, 16_000 ; \"no overrides uses default\")]\n #[test_case::test_case(Some(\"32000\"), None, 32_000 ; \"worker limit overrides default\")]\n #[test_case::test_case(Some(\"32000\"), Some(\"64000\"), 32_000 ; \"worker limit takes priority over global\")]\n fn test_worker_model_context_limit(\n worker_limit: Option<&str>,\n global_limit: Option<&str>,\n expected_limit: usize,\n ) {\n let _guard = env_lock::lock_env([\n (\"ASTER_WORKER_CONTEXT_LIMIT\", worker_limit),\n (\"ASTER_CONTEXT_LIMIT\", global_limit),\n ]);\n\n let default_model =\n ModelConfig::new_or_fail(\"gpt-3.5-turbo\").with_context_limit(Some(16_000));\n\n let result = create_worker_model_config(&default_model).unwrap();\n assert_eq!(result.context_limit, Some(expected_limit));\n }\n\n #[tokio::test]\n async fn test_openai_compatible_providers_config_keys() {\n let providers_list = providers().await;\n let cases = vec![\n (\"openai\", \"OPENAI_API_KEY\"),\n (\"groq\", \"GROQ_API_KEY\"),\n (\"mistral\", \"MISTRAL_API_KEY\"),\n (\"custom_deepseek\", \"DEEPSEEK_API_KEY\"),\n ];\n for (name, expected_key) in cases {\n if let Some((meta, _)) = providers_list.iter().find(|(m, _)| m.name == name) {\n assert!(\n !meta.config_keys.is_empty(),\n \"{name} provider should have config keys\"\n );\n assert_eq!(\n meta.config_keys[0].name, expected_key,\n \"First config key for {name} should be {expected_key}, got {}\",\n meta.config_keys[0].name\n );\n assert!(\n meta.config_keys[0].required,\n \"{expected_key} should be required\"\n );\n assert!(\n meta.config_keys[0].secret,\n \"{expected_key} should be secret\"\n );\n } else {\n // Provider not registered; skip test for this provider\n continue;\n }\n }\n }\n}\n"}, "b37eba10e7486f8ea437cff3ce0bd5019cd33664": {"t": 1785757079.694753, "n": 15420, "h": "797734d42094ef0f", "c": "use std::sync::{Arc, RwLock};\n\nuse super::{\n anthropic::AnthropicProvider,\n azure::AzureProvider,\n base::{Provider, ProviderMetadata},\n bedrock::BedrockProvider,\n claude_code::ClaudeCodeProvider,\n codex::CodexProvider,\n cursor_agent::CursorAgentProvider,\n databricks::DatabricksProvider,\n gcpvertexai::GcpVertexAIProvider,\n gemini_cli::GeminiCliProvider,\n githubcopilot::GithubCopilotProvider,\n google::GoogleProvider,\n lead_worker::LeadWorkerProvider,\n litellm::LiteLLMProvider,\n ollama::OllamaProvider,\n openai::OpenAiProvider,\n openrouter::OpenRouterProvider,\n provider_registry::ProviderRegistry,\n sagemaker_tgi::SageMakerTgiProvider,\n snowflake::SnowflakeProvider,\n tetrate::TetrateProvider,\n venice::VeniceProvider,\n xai::XaiProvider,\n};\nuse crate::model::ModelConfig;\nuse crate::providers::base::ProviderType;\nuse crate::{\n config::declarative_providers::register_declarative_providers,\n providers::provider_registry::ProviderEntry,\n};\nuse anyhow::Result;\nuse tokio::sync::OnceCell;\n\nconst DEFAULT_LEAD_TURNS: usize = 3;\nconst DEFAULT_FAILURE_THRESHOLD: usize = 2;\nconst DEFAULT_FALLBACK_TURNS: usize = 2;\n\nstatic REGISTRY: OnceCell<RwLock<ProviderRegistry>> = OnceCell::const_new();\n\nasync fn init_registry() -> RwLock<ProviderRegistry> {\n let mut registry = ProviderRegistry::new().with_providers(|registry| {\n registry\n .register::<AnthropicProvider, _>(|m| Box::pin(AnthropicProvider::from_env(m)), true);\n registry.register::<AzureProvider, _>(|m| Box::pin(AzureProvider::from_env(m)), false);\n registry.register::<BedrockProvider, _>(|m| Box::pin(BedrockProvider::from_env(m)), false);\n registry\n .register::<ClaudeCodeProvider, _>(|m| Box::pin(ClaudeCodeProvider::from_env(m)), true);\n registry.register::<CodexProvider, _>(|m| Box::pin(CodexProvider::from_env(m)), true);\n registry.register::<CursorAgentProvider, _>(\n |m| Box::pin(CursorAgentProvider::from_env(m)),\n false,\n );\n registry\n .register::<DatabricksProvider, _>(|m| Box::pin(DatabricksProvider::from_env(m)), true);\n registry.register::<GcpVertexAIProvider, _>(\n |m| Box::pin(GcpVertexAIProvider::from_env(m)),\n false,\n );\n registry\n .register::<GeminiCliProvider, _>(|m| Box::pin(GeminiCliProvider::from_env(m)), false);\n registry.register::<GithubCopilotProvider, _>(\n |m| Box::pin(GithubCopilotProvider::from_env(m)),\n false,\n );\n registry.register::<GoogleProvider, _>(|m| Box::pin(GoogleProvider::from_env(m)), true);\n registry.register::<LiteLLMProvider, _>(|m| Box::pin(LiteLLMProvider::from_env(m)), false);\n registry.register::<OllamaProvider, _>(|m| Box::pin(OllamaProvider::from_env(m)), true);\n registry.register::<OpenAiProvider, _>(|m| Box::pin(OpenAiProvider::from_env(m)), true);\n registry\n .register::<OpenRouterProvider, _>(|m| Box::pin(OpenRouterProvider::from_env(m)), true);\n registry.register::<SageMakerTgiProvider, _>(\n |m| Box::pin(SageMakerTgiProvider::from_env(m)),\n false,\n );\n registry\n .register::<SnowflakeProvider, _>(|m| Box::pin(SnowflakeProvider::from_env(m)), false);\n registry.register::<TetrateProvider, _>(|m| Box::pin(TetrateProvider::from_env(m)), true);\n registry.register::<VeniceProvider, _>(|m| Box::pin(VeniceProvider::from_env(m)), false);\n registry.register::<XaiProvider, _>(|m| Box::pin(XaiProvider::from_env(m)), false);\n });\n if let Err(e) = load_custom_providers_into_registry(&mut registry) {\n tracing::warn!(\"Failed to load custom providers: {}\", e);\n }\n RwLock::new(registry)\n}\n\nfn load_custom_providers_into_registry(registry: &mut ProviderRegistry) -> Result<()> {\n register_declarative_providers(registry)\n}\n\nasync fn get_registry() -> &'static RwLock<ProviderRegistry> {\n REGISTRY.get_or_init(init_registry).await\n}\n\npub async fn providers() -> Vec<(ProviderMetadata, ProviderType)> {\n get_registry()\n .await\n .read()\n .unwrap()\n .all_metadata_with_types()\n}\n\npub async fn refresh_custom_providers() -> Result<()> {\n let registry = get_registry().await;\n registry.write().unwrap().remove_custom_providers();\n\n if let Err(e) = load_custom_providers_into_registry(&mut registry.write().unwrap()) {\n tracing::warn!(\"Failed to refresh custom providers: {}\", e);\n return Err(e);\n }\n\n tracing::info!(\"Custom providers refreshed\");\n Ok(())\n}\n\nasync fn get_from_registry(name: &str) -> Result<ProviderEntry> {\n // 将各种 Provider 名称映射到 Aster 支持的 Provider\n let mapped_name = map_provider_alias(name);\n \n let guard = get_registry().await.read().unwrap();\n guard\n .entries\n .get(mapped_name)\n .ok_or_else(|| anyhow::anyhow!(\"Unknown provider: {} (mapped to: {})\", name, mapped_name))\n .cloned()\n}\n\n/// 将各种 Provider 名称映射到 Aster 支持的 Provider\n/// \n/// Aster 原生支持的 Provider:\n/// - openai, anthropic, google, azure, bedrock, ollama, gcpvertexai\n/// - openrouter, litellm, databricks, codex, xai, venice, tetrate\n/// - snowflake, sagemaker_tgi, githubcopilot, gemini_cli, cursor_agent, claude_code\n/// \n/// 其他 Provider 会映射到兼容的 Provider\nfn map_provider_alias(name: &str) -> &str {\n match name.to_lowercase().as_str() {\n // ========== OpenAI 兼容格式 ==========\n // 国内 AI 服务\n \"deepseek\" | \"deep_seek\" | \"deep-seek\" => \"openai\",\n \"qwen\" | \"tongyi\" | \"dashscope\" | \"aliyun\" => \"openai\",\n \"zhipu\" | \"glm\" | \"chatglm\" => \"openai\",\n \"baichuan\" => \"openai\",\n \"moonshot\" | \"kimi\" => \"openai\",\n \"minimax\" => \"openai\",\n \"yi\" | \"01ai\" | \"lingyiwanwu\" => \"openai\",\n \"stepfun\" | \"step\" => \"openai\",\n \"bailian\" | \"百炼\" => \"openai\",\n \"doubao\" | \"豆包\" => \"openai\",\n \"spark\" | \"讯飞\" | \"xunfei\" => \"openai\",\n \"hunyuan\" | \"混元\" => \"openai\",\n \"ernie\" | \"文心\" | \"wenxin\" => \"openai\",\n \n // 国际 AI 服务(OpenAI 兼容)\n \"groq\" => \"openai\",\n \"together\" | \"togetherai\" => \"openai\",\n \"fireworks\" | \"fireworksai\" => \"openai\",\n \"perplexity\" => \"openai\",\n \"anyscale\" => \"openai\",\n \"lepton\" | \"leptonai\" => \"openai\",\n \"novita\" | \"novitaai\" => \"openai\",\n \"siliconflow\" => \"openai\",\n \"mistral\" => \"openai\",\n \"cohere\" => \"openai\",\n \n // API 聚合服务\n \"oneapi\" | \"one-api\" | \"one_api\" => \"openai\",\n \"newapi\" | \"new-api\" | \"new_api\" => \"openai\",\n \"vercel\" | \"vercel_ai\" | \"vercel-ai\" => \"openai\",\n \n // 自定义/通用 OpenAI 兼容\n \"custom\" | \"custom_openai\" | \"openai_compatible\" => \"openai\",\n \n // ========== Anthropic 兼容格式 ==========\n \"claude\" => \"anthropic\",\n \"anthropic_compatible\" | \"anthropic-compatible\" => \"anthropic\",\n \n // ========== Google/Gemini 格式 ==========\n \"gemini\" | \"gemini_api_key\" => \"google\",\n \"antigravity\" => \"google\",\n \n // ========== 其他已支持的 Provider(保持原名) ==========\n \"azure\" | \"azure_openai\" | \"azure-openai\" => \"azure\",\n \"vertex\" | \"vertexai\" | \"vertex_ai\" => \"gcpvertexai\",\n \"aws_bedrock\" | \"aws-bedrock\" => \"bedrock\",\n \"kiro\" => \"bedrock\", // Kiro 使用 CodeWhisperer API\n \n // 默认返回原名称(让 Aster 原生处理)\n _ => name,\n }\n}\n\npub async fn create(name: &str, model: ModelConfig) -> Result<Arc<dyn Provider>> {\n let config = crate::config::Config::global();\n\n if let Ok(lead_model_name) = config.get_param::<String>(\"ASTER_LEAD_MODEL\") {\n tracing::info!(\"Creating lead/worker provider from environment variables\");\n return create_lead_worker_from_env(name, &model, &lead_model_name).await;\n }\n\n let constructor = get_from_registry(name).await?.constructor.clone();\n constructor(model).await\n}\n\npub async fn create_with_default_model(name: impl AsRef<str>) -> Result<Arc<dyn Provider>> {\n get_from_registry(name.as_ref())\n .await?\n .create_with_default_model()\n .await\n}\n\npub async fn create_with_named_model(\n provider_name: &str,\n model_name: &str,\n) -> Result<Arc<dyn Provider>> {\n let config = ModelConfig::new(model_name)?;\n create(provider_name, config).await\n}\n\nasync fn create_lead_worker_from_env(\n default_provider_name: &str,\n default_model: &ModelConfig,\n lead_model_name: &str,\n) -> Result<Arc<dyn Provider>> {\n let config = crate::config::Config::global();\n\n let lead_provider_name = config\n .get_param::<String>(\"ASTER_LEAD_PROVIDER\")\n .unwrap_or_else(|_| default_provider_name.to_string());\n\n let lead_turns = config\n .get_param::<usize>(\"ASTER_LEAD_TURNS\")\n .unwrap_or(DEFAULT_LEAD_TURNS);\n let failure_threshold = config\n .get_param::<usize>(\"ASTER_LEAD_FAILURE_THRESHOLD\")\n .unwrap_or(DEFAULT_FAILURE_THRESHOLD);\n let fallback_turns = config\n .get_param::<usize>(\"ASTER_LEAD_FALLBACK_TURNS\")\n .unwrap_or(DEFAULT_FALLBACK_TURNS);\n\n let lead_model_config = ModelConfig::new_with_context_env(\n lead_model_name.to_string(),\n Some(\"ASTER_LEAD_CONTEXT_LIMIT\"),\n )?;\n\n let worker_model_config = create_worker_model_config(default_model)?;\n\n let registry = get_registry().await;\n\n let lead_constructor = {\n let guard = registry.read().unwrap();\n guard\n .entries\n .get(&lead_provider_name)\n .ok_or_else(|| anyhow::anyhow!(\"Unknown provider: {}\", lead_provider_name))?\n .constructor\n .clone()\n };\n\n let worker_constructor = {\n let guard = registry.read().unwrap();\n guard\n .entries\n .get(default_provider_name)\n .ok_or_else(|| anyhow::anyhow!(\"Unknown provider: {}\", default_provider_name))?\n .constructor\n .clone()\n };\n\n let lead_provider = lead_constructor(lead_model_config).await?;\n let worker_provider = worker_constructor(worker_model_config).await?;\n\n Ok(Arc::new(LeadWorkerProvider::new_with_settings(\n lead_provider,\n worker_provider,\n lead_turns,\n failure_threshold,\n fallback_turns,\n )))\n}\n\nfn create_worker_model_config(default_model: &ModelConfig) -> Result<ModelConfig> {\n let mut worker_config = ModelConfig::new_or_fail(&default_model.model_name)\n .with_context_limit(default_model.context_limit)\n .with_temperature(default_model.temperature)\n .with_max_tokens(default_model.max_tokens)\n .with_toolshim(default_model.toolshim)\n .with_toolshim_model(default_model.toolshim_model.clone());\n\n let global_config = crate::config::Config::global();\n\n if let Ok(limit) = global_config.get_param::<usize>(\"ASTER_WORKER_CONTEXT_LIMIT\") {\n worker_config = worker_config.with_context_limit(Some(limit));\n } else if let Ok(limit) = global_config.get_param::<usize>(\"ASTER_CONTEXT_LIMIT\") {\n worker_config = worker_config.with_context_limit(Some(limit));\n }\n\n Ok(worker_config)\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test_case::test_case(None, None, None, DEFAULT_LEAD_TURNS, DEFAULT_FAILURE_THRESHOLD, DEFAULT_FALLBACK_TURNS ; \"defaults\")]\n #[test_case::test_case(Some(\"7\"), Some(\"4\"), Some(\"3\"), 7, 4, 3 ; \"custom\")]\n #[tokio::test]\n async fn test_create_lead_worker_provider(\n lead_turns: Option<&str>,\n failure_threshold: Option<&str>,\n fallback_turns: Option<&str>,\n expected_turns: usize,\n expected_failure: usize,\n expected_fallback: usize,\n ) {\n let _guard = env_lock::lock_env([\n (\"ASTER_LEAD_MODEL\", Some(\"gpt-4o\")),\n (\"ASTER_LEAD_PROVIDER\", None),\n (\"ASTER_LEAD_TURNS\", lead_turns),\n (\"ASTER_LEAD_FAILURE_THRESHOLD\", failure_threshold),\n (\"ASTER_LEAD_FALLBACK_TURNS\", fallback_turns),\n (\"OPENAI_API_KEY\", Some(\"fake-openai-no-keyring\")),\n ]);\n\n let provider = create(\"openai\", ModelConfig::new_or_fail(\"gpt-4o-mini\"))\n .await\n .unwrap();\n let lw = provider.as_lead_worker().unwrap();\n let (lead, worker) = lw.get_model_info();\n assert_eq!(lead, \"gpt-4o\");\n assert_eq!(worker, \"gpt-4o-mini\");\n assert_eq!(\n lw.get_settings(),\n (expected_turns, expected_failure, expected_fallback)\n );\n }\n\n #[tokio::test]\n async fn test_create_regular_provider_without_lead_config() {\n let _guard = env_lock::lock_env([\n (\"ASTER_LEAD_MODEL\", None),\n (\"ASTER_LEAD_PROVIDER\", None),\n (\"ASTER_LEAD_TURNS\", None),\n (\"ASTER_LEAD_FAILURE_THRESHOLD\", None),\n (\"ASTER_LEAD_FALLBACK_TURNS\", None),\n (\"OPENAI_API_KEY\", Some(\"fake-openai-no-keyring\")),\n ]);\n\n let provider = create(\"openai\", ModelConfig::new_or_fail(\"gpt-4o-mini\"))\n .await\n .unwrap();\n assert!(provider.as_lead_worker().is_none());\n assert_eq!(provider.get_model_config().model_name, \"gpt-4o-mini\");\n }\n\n #[test_case::test_case(None, None, 16_000 ; \"no overrides uses default\")]\n #[test_case::test_case(Some(\"32000\"), None, 32_000 ; \"worker limit overrides default\")]\n #[test_case::test_case(Some(\"32000\"), Some(\"64000\"), 32_000 ; \"worker limit takes priority over global\")]\n fn test_worker_model_context_limit(\n worker_limit: Option<&str>,\n global_limit: Option<&str>,\n expected_limit: usize,\n ) {\n let _guard = env_lock::lock_env([\n (\"ASTER_WORKER_CONTEXT_LIMIT\", worker_limit),\n (\"ASTER_CONTEXT_LIMIT\", global_limit),\n ]);\n\n let default_model =\n ModelConfig::new_or_fail(\"gpt-3.5-turbo\").with_context_limit(Some(16_000));\n\n let result = create_worker_model_config(&default_model).unwrap();\n assert_eq!(result.context_limit, Some(expected_limit));\n }\n\n #[tokio::test]\n async fn test_openai_compatible_providers_config_keys() {\n let providers_list = providers().await;\n let cases = vec![\n (\"openai\", \"OPENAI_API_KEY\"),\n (\"groq\", \"GROQ_API_KEY\"),\n (\"mistral\", \"MISTRAL_API_KEY\"),\n (\"custom_deepseek\", \"DEEPSEEK_API_KEY\"),\n ];\n for (name, expected_key) in cases {\n if let Some((meta, _)) = providers_list.iter().find(|(m, _)| m.name == name) {\n assert!(\n !meta.config_keys.is_empty(),\n \"{name} provider should have config keys\"\n );\n assert_eq!(\n meta.config_keys[0].name, expected_key,\n \"First config key for {name} should be {expected_key}, got {}\",\n meta.config_keys[0].name\n );\n assert!(\n meta.config_keys[0].required,\n \"{expected_key} should be required\"\n );\n assert!(\n meta.config_keys[0].secret,\n \"{expected_key} should be secret\"\n );\n } else {\n // Provider not registered; skip test for this provider\n continue;\n }\n }\n }\n}\n"}, "ab0459aef77ecc82ae662a9dccb4a2082333dfde": {"t": 1785757081.388486, "n": 12841, "h": "bddd9245b3c164a0", "c": "use std::sync::{Arc, RwLock};\n\nuse super::{\n anthropic::AnthropicProvider,\n azure::AzureProvider,\n base::{Provider, ProviderMetadata},\n bedrock::BedrockProvider,\n claude_code::ClaudeCodeProvider,\n codex::CodexProvider,\n cursor_agent::CursorAgentProvider,\n databricks::DatabricksProvider,\n gcpvertexai::GcpVertexAIProvider,\n gemini_cli::GeminiCliProvider,\n githubcopilot::GithubCopilotProvider,\n google::GoogleProvider,\n lead_worker::LeadWorkerProvider,\n litellm::LiteLLMProvider,\n ollama::OllamaProvider,\n openai::OpenAiProvider,\n openrouter::OpenRouterProvider,\n provider_registry::ProviderRegistry,\n sagemaker_tgi::SageMakerTgiProvider,\n snowflake::SnowflakeProvider,\n tetrate::TetrateProvider,\n venice::VeniceProvider,\n xai::XaiProvider,\n};\nuse crate::model::ModelConfig;\nuse crate::providers::base::ProviderType;\nuse crate::{\n config::declarative_providers::register_declarative_providers,\n providers::provider_registry::ProviderEntry,\n};\nuse anyhow::Result;\nuse tokio::sync::OnceCell;\n\nconst DEFAULT_LEAD_TURNS: usize = 3;\nconst DEFAULT_FAILURE_THRESHOLD: usize = 2;\nconst DEFAULT_FALLBACK_TURNS: usize = 2;\n\nstatic REGISTRY: OnceCell<RwLock<ProviderRegistry>> = OnceCell::const_new();\n\nasync fn init_registry() -> RwLock<ProviderRegistry> {\n let mut registry = ProviderRegistry::new().with_providers(|registry| {\n registry\n .register::<AnthropicProvider, _>(|m| Box::pin(AnthropicProvider::from_env(m)), true);\n registry.register::<AzureProvider, _>(|m| Box::pin(AzureProvider::from_env(m)), false);\n registry.register::<BedrockProvider, _>(|m| Box::pin(BedrockProvider::from_env(m)), false);\n registry\n .register::<ClaudeCodeProvider, _>(|m| Box::pin(ClaudeCodeProvider::from_env(m)), true);\n registry.register::<CodexProvider, _>(|m| Box::pin(CodexProvider::from_env(m)), true);\n registry.register::<CursorAgentProvider, _>(\n |m| Box::pin(CursorAgentProvider::from_env(m)),\n false,\n );\n registry\n .register::<DatabricksProvider, _>(|m| Box::pin(DatabricksProvider::from_env(m)), true);\n registry.register::<GcpVertexAIProvider, _>(\n |m| Box::pin(GcpVertexAIProvider::from_env(m)),\n false,\n );\n registry\n .register::<GeminiCliProvider, _>(|m| Box::pin(GeminiCliProvider::from_env(m)), false);\n registry.register::<GithubCopilotProvider, _>(\n |m| Box::pin(GithubCopilotProvider::from_env(m)),\n false,\n );\n registry.register::<GoogleProvider, _>(|m| Box::pin(GoogleProvider::from_env(m)), true);\n registry.register::<LiteLLMProvider, _>(|m| Box::pin(LiteLLMProvider::from_env(m)), false);\n registry.register::<OllamaProvider, _>(|m| Box::pin(OllamaProvider::from_env(m)), true);\n registry.register::<OpenAiProvider, _>(|m| Box::pin(OpenAiProvider::from_env(m)), true);\n registry\n .register::<OpenRouterProvider, _>(|m| Box::pin(OpenRouterProvider::from_env(m)), true);\n registry.register::<SageMakerTgiProvider, _>(\n |m| Box::pin(SageMakerTgiProvider::from_env(m)),\n false,\n );\n registry\n .register::<SnowflakeProvider, _>(|m| Box::pin(SnowflakeProvider::from_env(m)), false);\n registry.register::<TetrateProvider, _>(|m| Box::pin(TetrateProvider::from_env(m)), true);\n registry.register::<VeniceProvider, _>(|m| Box::pin(VeniceProvider::from_env(m)), false);\n registry.register::<XaiProvider, _>(|m| Box::pin(XaiProvider::from_env(m)), false);\n });\n if let Err(e) = load_custom_providers_into_registry(&mut registry) {\n tracing::warn!(\"Failed to load custom providers: {}\", e);\n }\n RwLock::new(registry)\n}\n\nfn load_custom_providers_into_registry(registry: &mut ProviderRegistry) -> Result<()> {\n register_declarative_providers(registry)\n}\n\nasync fn get_registry() -> &'static RwLock<ProviderRegistry> {\n REGISTRY.get_or_init(init_registry).await\n}\n\npub async fn providers() -> Vec<(ProviderMetadata, ProviderType)> {\n get_registry()\n .await\n .read()\n .unwrap()\n .all_metadata_with_types()\n}\n\npub async fn refresh_custom_providers() -> Result<()> {\n let registry = get_registry().await;\n registry.write().unwrap().remove_custom_providers();\n\n if let Err(e) = load_custom_providers_into_registry(&mut registry.write().unwrap()) {\n tracing::warn!(\"Failed to refresh custom providers: {}\", e);\n return Err(e);\n }\n\n tracing::info!(\"Custom providers refreshed\");\n Ok(())\n}\n\nasync fn get_from_registry(name: &str) -> Result<ProviderEntry> {\n let guard = get_registry().await.read().unwrap();\n guard\n .entries\n .get(name)\n .ok_or_else(|| anyhow::anyhow!(\"Unknown provider: {}\", name))\n .cloned()\n}\n\npub async fn create(name: &str, model: ModelConfig) -> Result<Arc<dyn Provider>> {\n let config = crate::config::Config::global();\n\n if let Ok(lead_model_name) = config.get_param::<String>(\"ASTER_LEAD_MODEL\") {\n tracing::info!(\"Creating lead/worker provider from environment variables\");\n return create_lead_worker_from_env(name, &model, &lead_model_name).await;\n }\n\n let constructor = get_from_registry(name).await?.constructor.clone();\n constructor(model).await\n}\n\npub async fn create_with_default_model(name: impl AsRef<str>) -> Result<Arc<dyn Provider>> {\n get_from_registry(name.as_ref())\n .await?\n .create_with_default_model()\n .await\n}\n\npub async fn create_with_named_model(\n provider_name: &str,\n model_name: &str,\n) -> Result<Arc<dyn Provider>> {\n let config = ModelConfig::new(model_name)?;\n create(provider_name, config).await\n}\n\nasync fn create_lead_worker_from_env(\n default_provider_name: &str,\n default_model: &ModelConfig,\n lead_model_name: &str,\n) -> Result<Arc<dyn Provider>> {\n let config = crate::config::Config::global();\n\n let lead_provider_name = config\n .get_param::<String>(\"ASTER_LEAD_PROVIDER\")\n .unwrap_or_else(|_| default_provider_name.to_string());\n\n let lead_turns = config\n .get_param::<usize>(\"ASTER_LEAD_TURNS\")\n .unwrap_or(DEFAULT_LEAD_TURNS);\n let failure_threshold = config\n .get_param::<usize>(\"ASTER_LEAD_FAILURE_THRESHOLD\")\n .unwrap_or(DEFAULT_FAILURE_THRESHOLD);\n let fallback_turns = config\n .get_param::<usize>(\"ASTER_LEAD_FALLBACK_TURNS\")\n .unwrap_or(DEFAULT_FALLBACK_TURNS);\n\n let lead_model_config = ModelConfig::new_with_context_env(\n lead_model_name.to_string(),\n Some(\"ASTER_LEAD_CONTEXT_LIMIT\"),\n )?;\n\n let worker_model_config = create_worker_model_config(default_model)?;\n\n let registry = get_registry().await;\n\n let lead_constructor = {\n let guard = registry.read().unwrap();\n guard\n .entries\n .get(&lead_provider_name)\n .ok_or_else(|| anyhow::anyhow!(\"Unknown provider: {}\", lead_provider_name))?\n .constructor\n .clone()\n };\n\n let worker_constructor = {\n let guard = registry.read().unwrap();\n guard\n .entries\n .get(default_provider_name)\n .ok_or_else(|| anyhow::anyhow!(\"Unknown provider: {}\", default_provider_name))?\n .constructor\n .clone()\n };\n\n let lead_provider = lead_constructor(lead_model_config).await?;\n let worker_provider = worker_constructor(worker_model_config).await?;\n\n Ok(Arc::new(LeadWorkerProvider::new_with_settings(\n lead_provider,\n worker_provider,\n lead_turns,\n failure_threshold,\n fallback_turns,\n )))\n}\n\nfn create_worker_model_config(default_model: &ModelConfig) -> Result<ModelConfig> {\n let mut worker_config = ModelConfig::new_or_fail(&default_model.model_name)\n .with_context_limit(default_model.context_limit)\n .with_temperature(default_model.temperature)\n .with_max_tokens(default_model.max_tokens)\n .with_toolshim(default_model.toolshim)\n .with_toolshim_model(default_model.toolshim_model.clone());\n\n let global_config = crate::config::Config::global();\n\n if let Ok(limit) = global_config.get_param::<usize>(\"ASTER_WORKER_CONTEXT_LIMIT\") {\n worker_config = worker_config.with_context_limit(Some(limit));\n } else if let Ok(limit) = global_config.get_param::<usize>(\"ASTER_CONTEXT_LIMIT\") {\n worker_config = worker_config.with_context_limit(Some(limit));\n }\n\n Ok(worker_config)\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test_case::test_case(None, None, None, DEFAULT_LEAD_TURNS, DEFAULT_FAILURE_THRESHOLD, DEFAULT_FALLBACK_TURNS ; \"defaults\")]\n #[test_case::test_case(Some(\"7\"), Some(\"4\"), Some(\"3\"), 7, 4, 3 ; \"custom\")]\n #[tokio::test]\n async fn test_create_lead_worker_provider(\n lead_turns: Option<&str>,\n failure_threshold: Option<&str>,\n fallback_turns: Option<&str>,\n expected_turns: usize,\n expected_failure: usize,\n expected_fallback: usize,\n ) {\n let _guard = env_lock::lock_env([\n (\"ASTER_LEAD_MODEL\", Some(\"gpt-4o\")),\n (\"ASTER_LEAD_PROVIDER\", None),\n (\"ASTER_LEAD_TURNS\", lead_turns),\n (\"ASTER_LEAD_FAILURE_THRESHOLD\", failure_threshold),\n (\"ASTER_LEAD_FALLBACK_TURNS\", fallback_turns),\n (\"OPENAI_API_KEY\", Some(\"fake-openai-no-keyring\")),\n ]);\n\n let provider = create(\"openai\", ModelConfig::new_or_fail(\"gpt-4o-mini\"))\n .await\n .unwrap();\n let lw = provider.as_lead_worker().unwrap();\n let (lead, worker) = lw.get_model_info();\n assert_eq!(lead, \"gpt-4o\");\n assert_eq!(worker, \"gpt-4o-mini\");\n assert_eq!(\n lw.get_settings(),\n (expected_turns, expected_failure, expected_fallback)\n );\n }\n\n #[tokio::test]\n async fn test_create_regular_provider_without_lead_config() {\n let _guard = env_lock::lock_env([\n (\"ASTER_LEAD_MODEL\", None),\n (\"ASTER_LEAD_PROVIDER\", None),\n (\"ASTER_LEAD_TURNS\", None),\n (\"ASTER_LEAD_FAILURE_THRESHOLD\", None),\n (\"ASTER_LEAD_FALLBACK_TURNS\", None),\n (\"OPENAI_API_KEY\", Some(\"fake-openai-no-keyring\")),\n ]);\n\n let provider = create(\"openai\", ModelConfig::new_or_fail(\"gpt-4o-mini\"))\n .await\n .unwrap();\n assert!(provider.as_lead_worker().is_none());\n assert_eq!(provider.get_model_config().model_name, \"gpt-4o-mini\");\n }\n\n #[test_case::test_case(None, None, 16_000 ; \"no overrides uses default\")]\n #[test_case::test_case(Some(\"32000\"), None, 32_000 ; \"worker limit overrides default\")]\n #[test_case::test_case(Some(\"32000\"), Some(\"64000\"), 32_000 ; \"worker limit takes priority over global\")]\n fn test_worker_model_context_limit(\n worker_limit: Option<&str>,\n global_limit: Option<&str>,\n expected_limit: usize,\n ) {\n let _guard = env_lock::lock_env([\n (\"ASTER_WORKER_CONTEXT_LIMIT\", worker_limit),\n (\"ASTER_CONTEXT_LIMIT\", global_limit),\n ]);\n\n let default_model =\n ModelConfig::new_or_fail(\"gpt-3.5-turbo\").with_context_limit(Some(16_000));\n\n let result = create_worker_model_config(&default_model).unwrap();\n assert_eq!(result.context_limit, Some(expected_limit));\n }\n\n #[tokio::test]\n async fn test_openai_compatible_providers_config_keys() {\n let providers_list = providers().await;\n let cases = vec![\n (\"openai\", \"OPENAI_API_KEY\"),\n (\"groq\", \"GROQ_API_KEY\"),\n (\"mistral\", \"MISTRAL_API_KEY\"),\n (\"custom_deepseek\", \"DEEPSEEK_API_KEY\"),\n ];\n for (name, expected_key) in cases {\n if let Some((meta, _)) = providers_list.iter().find(|(m, _)| m.name == name) {\n assert!(\n !meta.config_keys.is_empty(),\n \"{name} provider should have config keys\"\n );\n assert_eq!(\n meta.config_keys[0].name, expected_key,\n \"First config key for {name} should be {expected_key}, got {}\",\n meta.config_keys[0].name\n );\n assert!(\n meta.config_keys[0].required,\n \"{expected_key} should be required\"\n );\n assert!(\n meta.config_keys[0].secret,\n \"{expected_key} should be secret\"\n );\n } else {\n // Provider not registered; skip test for this provider\n continue;\n }\n }\n }\n}\n"}, "090928fbccaa8d272e922255606921e8219662bd": {"t": 1785757083.2715197, "n": 12841, "h": "c50a2fafbc49dae4", "c": "use std::sync::{Arc, RwLock};\n\nuse super::{\n anthropic::AnthropicProvider,\n azure::AzureProvider,\n base::{Provider, ProviderMetadata},\n bedrock::BedrockProvider,\n claude_code::ClaudeCodeProvider,\n codex::CodexProvider,\n cursor_agent::CursorAgentProvider,\n databricks::DatabricksProvider,\n gcpvertexai::GcpVertexAIProvider,\n gemini_cli::GeminiCliProvider,\n githubcopilot::GithubCopilotProvider,\n google::GoogleProvider,\n lead_worker::LeadWorkerProvider,\n litellm::LiteLLMProvider,\n ollama::OllamaProvider,\n openai::OpenAiProvider,\n openrouter::OpenRouterProvider,\n provider_registry::ProviderRegistry,\n sagemaker_tgi::SageMakerTgiProvider,\n snowflake::SnowflakeProvider,\n tetrate::TetrateProvider,\n venice::VeniceProvider,\n xai::XaiProvider,\n};\nuse crate::model::ModelConfig;\nuse crate::providers::base::ProviderType;\nuse crate::{\n config::declarative_providers::register_declarative_providers,\n providers::provider_registry::ProviderEntry,\n};\nuse anyhow::Result;\nuse tokio::sync::OnceCell;\n\nconst DEFAULT_LEAD_TURNS: usize = 3;\nconst DEFAULT_FAILURE_THRESHOLD: usize = 2;\nconst DEFAULT_FALLBACK_TURNS: usize = 2;\n\nstatic REGISTRY: OnceCell<RwLock<ProviderRegistry>> = OnceCell::const_new();\n\nasync fn init_registry() -> RwLock<ProviderRegistry> {\n let mut registry = ProviderRegistry::new().with_providers(|registry| {\n registry\n .register::<AnthropicProvider, _>(|m| Box::pin(AnthropicProvider::from_env(m)), true);\n registry.register::<AzureProvider, _>(|m| Box::pin(AzureProvider::from_env(m)), false);\n registry.register::<BedrockProvider, _>(|m| Box::pin(BedrockProvider::from_env(m)), false);\n registry\n .register::<ClaudeCodeProvider, _>(|m| Box::pin(ClaudeCodeProvider::from_env(m)), true);\n registry.register::<CodexProvider, _>(|m| Box::pin(CodexProvider::from_env(m)), true);\n registry.register::<CursorAgentProvider, _>(\n |m| Box::pin(CursorAgentProvider::from_env(m)),\n false,\n );\n registry\n .register::<DatabricksProvider, _>(|m| Box::pin(DatabricksProvider::from_env(m)), true);\n registry.register::<GcpVertexAIProvider, _>(\n |m| Box::pin(GcpVertexAIProvider::from_env(m)),\n false,\n );\n registry\n .register::<GeminiCliProvider, _>(|m| Box::pin(GeminiCliProvider::from_env(m)), false);\n registry.register::<GithubCopilotProvider, _>(\n |m| Box::pin(GithubCopilotProvider::from_env(m)),\n false,\n );\n registry.register::<GoogleProvider, _>(|m| Box::pin(GoogleProvider::from_env(m)), true);\n registry.register::<LiteLLMProvider, _>(|m| Box::pin(LiteLLMProvider::from_env(m)), false);\n registry.register::<OllamaProvider, _>(|m| Box::pin(OllamaProvider::from_env(m)), true);\n registry.register::<OpenAiProvider, _>(|m| Box::pin(OpenAiProvider::from_env(m)), true);\n registry\n .register::<OpenRouterProvider, _>(|m| Box::pin(OpenRouterProvider::from_env(m)), true);\n registry.register::<SageMakerTgiProvider, _>(\n |m| Box::pin(SageMakerTgiProvider::from_env(m)),\n false,\n );\n registry\n .register::<SnowflakeProvider, _>(|m| Box::pin(SnowflakeProvider::from_env(m)), false);\n registry.register::<TetrateProvider, _>(|m| Box::pin(TetrateProvider::from_env(m)), true);\n registry.register::<VeniceProvider, _>(|m| Box::pin(VeniceProvider::from_env(m)), false);\n registry.register::<XaiProvider, _>(|m| Box::pin(XaiProvider::from_env(m)), false);\n });\n if let Err(e) = load_custom_providers_into_registry(&mut registry) {\n tracing::warn!(\"Failed to load custom providers: {}\", e);\n }\n RwLock::new(registry)\n}\n\nfn load_custom_providers_into_registry(registry: &mut ProviderRegistry) -> Result<()> {\n register_declarative_providers(registry)\n}\n\nasync fn get_registry() -> &'static RwLock<ProviderRegistry> {\n REGISTRY.get_or_init(init_registry).await\n}\n\npub async fn providers() -> Vec<(ProviderMetadata, ProviderType)> {\n get_registry()\n .await\n .read()\n .unwrap()\n .all_metadata_with_types()\n}\n\npub async fn refresh_custom_providers() -> Result<()> {\n let registry = get_registry().await;\n registry.write().unwrap().remove_custom_providers();\n\n if let Err(e) = load_custom_providers_into_registry(&mut registry.write().unwrap()) {\n tracing::warn!(\"Failed to refresh custom providers: {}\", e);\n return Err(e);\n }\n\n tracing::info!(\"Custom providers refreshed\");\n Ok(())\n}\n\nasync fn get_from_registry(name: &str) -> Result<ProviderEntry> {\n let guard = get_registry().await.read().unwrap();\n guard\n .entries\n .get(name)\n .ok_or_else(|| anyhow::anyhow!(\"Unknown provider: {}\", name))\n .cloned()\n}\n\npub async fn create(name: &str, model: ModelConfig) -> Result<Arc<dyn Provider>> {\n let config = crate::config::Config::global();\n\n if let Ok(lead_model_name) = config.get_param::<String>(\"GOOSE_LEAD_MODEL\") {\n tracing::info!(\"Creating lead/worker provider from environment variables\");\n return create_lead_worker_from_env(name, &model, &lead_model_name).await;\n }\n\n let constructor = get_from_registry(name).await?.constructor.clone();\n constructor(model).await\n}\n\npub async fn create_with_default_model(name: impl AsRef<str>) -> Result<Arc<dyn Provider>> {\n get_from_registry(name.as_ref())\n .await?\n .create_with_default_model()\n .await\n}\n\npub async fn create_with_named_model(\n provider_name: &str,\n model_name: &str,\n) -> Result<Arc<dyn Provider>> {\n let config = ModelConfig::new(model_name)?;\n create(provider_name, config).await\n}\n\nasync fn create_lead_worker_from_env(\n default_provider_name: &str,\n default_model: &ModelConfig,\n lead_model_name: &str,\n) -> Result<Arc<dyn Provider>> {\n let config = crate::config::Config::global();\n\n let lead_provider_name = config\n .get_param::<String>(\"GOOSE_LEAD_PROVIDER\")\n .unwrap_or_else(|_| default_provider_name.to_string());\n\n let lead_turns = config\n .get_param::<usize>(\"GOOSE_LEAD_TURNS\")\n .unwrap_or(DEFAULT_LEAD_TURNS);\n let failure_threshold = config\n .get_param::<usize>(\"GOOSE_LEAD_FAILURE_THRESHOLD\")\n .unwrap_or(DEFAULT_FAILURE_THRESHOLD);\n let fallback_turns = config\n .get_param::<usize>(\"GOOSE_LEAD_FALLBACK_TURNS\")\n .unwrap_or(DEFAULT_FALLBACK_TURNS);\n\n let lead_model_config = ModelConfig::new_with_context_env(\n lead_model_name.to_string(),\n Some(\"GOOSE_LEAD_CONTEXT_LIMIT\"),\n )?;\n\n let worker_model_config = create_worker_model_config(default_model)?;\n\n let registry = get_registry().await;\n\n let lead_constructor = {\n let guard = registry.read().unwrap();\n guard\n .entries\n .get(&lead_provider_name)\n .ok_or_else(|| anyhow::anyhow!(\"Unknown provider: {}\", lead_provider_name))?\n .constructor\n .clone()\n };\n\n let worker_constructor = {\n let guard = registry.read().unwrap();\n guard\n .entries\n .get(default_provider_name)\n .ok_or_else(|| anyhow::anyhow!(\"Unknown provider: {}\", default_provider_name))?\n .constructor\n .clone()\n };\n\n let lead_provider = lead_constructor(lead_model_config).await?;\n let worker_provider = worker_constructor(worker_model_config).await?;\n\n Ok(Arc::new(LeadWorkerProvider::new_with_settings(\n lead_provider,\n worker_provider,\n lead_turns,\n failure_threshold,\n fallback_turns,\n )))\n}\n\nfn create_worker_model_config(default_model: &ModelConfig) -> Result<ModelConfig> {\n let mut worker_config = ModelConfig::new_or_fail(&default_model.model_name)\n .with_context_limit(default_model.context_limit)\n .with_temperature(default_model.temperature)\n .with_max_tokens(default_model.max_tokens)\n .with_toolshim(default_model.toolshim)\n .with_toolshim_model(default_model.toolshim_model.clone());\n\n let global_config = crate::config::Config::global();\n\n if let Ok(limit) = global_config.get_param::<usize>(\"GOOSE_WORKER_CONTEXT_LIMIT\") {\n worker_config = worker_config.with_context_limit(Some(limit));\n } else if let Ok(limit) = global_config.get_param::<usize>(\"GOOSE_CONTEXT_LIMIT\") {\n worker_config = worker_config.with_context_limit(Some(limit));\n }\n\n Ok(worker_config)\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test_case::test_case(None, None, None, DEFAULT_LEAD_TURNS, DEFAULT_FAILURE_THRESHOLD, DEFAULT_FALLBACK_TURNS ; \"defaults\")]\n #[test_case::test_case(Some(\"7\"), Some(\"4\"), Some(\"3\"), 7, 4, 3 ; \"custom\")]\n #[tokio::test]\n async fn test_create_lead_worker_provider(\n lead_turns: Option<&str>,\n failure_threshold: Option<&str>,\n fallback_turns: Option<&str>,\n expected_turns: usize,\n expected_failure: usize,\n expected_fallback: usize,\n ) {\n let _guard = env_lock::lock_env([\n (\"GOOSE_LEAD_MODEL\", Some(\"gpt-4o\")),\n (\"GOOSE_LEAD_PROVIDER\", None),\n (\"GOOSE_LEAD_TURNS\", lead_turns),\n (\"GOOSE_LEAD_FAILURE_THRESHOLD\", failure_threshold),\n (\"GOOSE_LEAD_FALLBACK_TURNS\", fallback_turns),\n (\"OPENAI_API_KEY\", Some(\"fake-openai-no-keyring\")),\n ]);\n\n let provider = create(\"openai\", ModelConfig::new_or_fail(\"gpt-4o-mini\"))\n .await\n .unwrap();\n let lw = provider.as_lead_worker().unwrap();\n let (lead, worker) = lw.get_model_info();\n assert_eq!(lead, \"gpt-4o\");\n assert_eq!(worker, \"gpt-4o-mini\");\n assert_eq!(\n lw.get_settings(),\n (expected_turns, expected_failure, expected_fallback)\n );\n }\n\n #[tokio::test]\n async fn test_create_regular_provider_without_lead_config() {\n let _guard = env_lock::lock_env([\n (\"GOOSE_LEAD_MODEL\", None),\n (\"GOOSE_LEAD_PROVIDER\", None),\n (\"GOOSE_LEAD_TURNS\", None),\n (\"GOOSE_LEAD_FAILURE_THRESHOLD\", None),\n (\"GOOSE_LEAD_FALLBACK_TURNS\", None),\n (\"OPENAI_API_KEY\", Some(\"fake-openai-no-keyring\")),\n ]);\n\n let provider = create(\"openai\", ModelConfig::new_or_fail(\"gpt-4o-mini\"))\n .await\n .unwrap();\n assert!(provider.as_lead_worker().is_none());\n assert_eq!(provider.get_model_config().model_name, \"gpt-4o-mini\");\n }\n\n #[test_case::test_case(None, None, 16_000 ; \"no overrides uses default\")]\n #[test_case::test_case(Some(\"32000\"), None, 32_000 ; \"worker limit overrides default\")]\n #[test_case::test_case(Some(\"32000\"), Some(\"64000\"), 32_000 ; \"worker limit takes priority over global\")]\n fn test_worker_model_context_limit(\n worker_limit: Option<&str>,\n global_limit: Option<&str>,\n expected_limit: usize,\n ) {\n let _guard = env_lock::lock_env([\n (\"GOOSE_WORKER_CONTEXT_LIMIT\", worker_limit),\n (\"GOOSE_CONTEXT_LIMIT\", global_limit),\n ]);\n\n let default_model =\n ModelConfig::new_or_fail(\"gpt-3.5-turbo\").with_context_limit(Some(16_000));\n\n let result = create_worker_model_config(&default_model).unwrap();\n assert_eq!(result.context_limit, Some(expected_limit));\n }\n\n #[tokio::test]\n async fn test_openai_compatible_providers_config_keys() {\n let providers_list = providers().await;\n let cases = vec![\n (\"openai\", \"OPENAI_API_KEY\"),\n (\"groq\", \"GROQ_API_KEY\"),\n (\"mistral\", \"MISTRAL_API_KEY\"),\n (\"custom_deepseek\", \"DEEPSEEK_API_KEY\"),\n ];\n for (name, expected_key) in cases {\n if let Some((meta, _)) = providers_list.iter().find(|(m, _)| m.name == name) {\n assert!(\n !meta.config_keys.is_empty(),\n \"{name} provider should have config keys\"\n );\n assert_eq!(\n meta.config_keys[0].name, expected_key,\n \"First config key for {name} should be {expected_key}, got {}\",\n meta.config_keys[0].name\n );\n assert!(\n meta.config_keys[0].required,\n \"{expected_key} should be required\"\n );\n assert!(\n meta.config_keys[0].secret,\n \"{expected_key} should be secret\"\n );\n } else {\n // Provider not registered; skip test for this provider\n continue;\n }\n }\n }\n}\n"}}}} |