Files

1 line
120 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"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"}}}}