Created
April 1, 2026 13:52
-
-
Save dgomesbr/34354c511ca3de7d6d7387c86f5d8cfe to your computer and use it in GitHub Desktop.
refactor: extract shared provider_registry module — eliminates 4-file duplication
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| diff --git a/bot/core.py b/bot/core.py | |
| index d842ac3..468a4d3 100644 | |
| --- a/bot/core.py | |
| +++ b/bot/core.py | |
| @@ -30,88 +30,14 @@ from openai import AsyncOpenAI, APIError | |
| # --------------------------------------------------------------------------- | |
| # LLM provider presets (Multi-Provider support) | |
| # --------------------------------------------------------------------------- | |
| -# Each provider maps to (base_url, default_model, api_key_env_var). | |
| -# Users set LLM_PROVIDER=<key> for one-step configuration; | |
| -# LLM_BASE_URL and OMICSCLAW_MODEL can still override. | |
| -# | |
| -# Inspired by EvoScientist's Multi-Provider architecture, adapted for | |
| -# OmicsClaw's lightweight AsyncOpenAI-based design. All providers are | |
| -# accessed through the OpenAI-compatible API protocol. | |
| - | |
| -PROVIDER_PRESETS: dict[str, tuple[str, str, str]] = { | |
| - # --- Tier 1: Primary providers --- | |
| - "deepseek": ("https://api.deepseek.com", "deepseek-chat", "DEEPSEEK_API_KEY"), | |
| - "openai": ("", "gpt-4o", "OPENAI_API_KEY"), | |
| - "anthropic": ("https://api.anthropic.com/v1/", "claude-sonnet-4-5-20250514", "ANTHROPIC_API_KEY"), | |
| - "gemini": ("https://generativelanguage.googleapis.com/v1beta/openai/", "gemini-2.5-flash", "GOOGLE_API_KEY"), | |
| - "nvidia": ("https://integrate.api.nvidia.com/v1", "deepseek-ai/deepseek-r1", "NVIDIA_API_KEY"), | |
| - | |
| - # --- Tier 2: Third-party aggregators --- | |
| - "siliconflow": ("https://api.siliconflow.cn/v1", "deepseek-ai/DeepSeek-V3", "SILICONFLOW_API_KEY"), | |
| - "openrouter": ("https://openrouter.ai/api/v1", "deepseek/deepseek-chat-v3-0324", "OPENROUTER_API_KEY"), | |
| - "volcengine": ("https://ark.cn-beijing.volces.com/api/v3", "doubao-1.5-pro-256k", "VOLCENGINE_API_KEY"), | |
| - "dashscope": ("https://dashscope.aliyuncs.com/compatible-mode/v1", "qwen-max", "DASHSCOPE_API_KEY"), | |
| - "zhipu": ("https://open.bigmodel.cn/api/paas/v4", "glm-4-flash", "ZHIPU_API_KEY"), | |
| - | |
| - # --- Tier 3: Local & custom --- | |
| - "ollama": ("http://localhost:11434/v1", "qwen2.5:7b", ""), | |
| - "custom": ("", "", ""), | |
| - | |
| - # --- Legacy alias (backward compat — same as gemini) --- | |
| -} | |
| - | |
| -# Ordered list for auto-detection: when LLM_PROVIDER is not set, we pick the | |
| -# first provider whose API key env var is present in the environment. | |
| -_PROVIDER_DETECT_ORDER = [ | |
| - "deepseek", "openai", "anthropic", "gemini", "nvidia", | |
| - "siliconflow", "openrouter", "volcengine", "dashscope", "zhipu", | |
| -] | |
| - | |
| - | |
| -def resolve_provider( | |
| - provider: str = "", | |
| - base_url: str = "", | |
| - model: str = "", | |
| - api_key: str = "", | |
| -) -> tuple[str | None, str, str]: | |
| - """Return (base_url_or_None, model, resolved_api_key) after applying provider defaults. | |
| - | |
| - Priority: explicit env vars > provider preset > auto-detect > hardcoded fallback. | |
| +# Centralised in omicsclaw.core.provider_registry — imported here so that | |
| +# every consumer (bot, interactive, routing, agents) shares one definition. | |
| - When *provider* is empty and *api_key* is empty, we scan provider-specific | |
| - environment variables (DEEPSEEK_API_KEY, OPENAI_API_KEY, ANTHROPIC_API_KEY, …) | |
| - to auto-detect the provider. | |
| - """ | |
| - provider_key = provider.lower().strip() if provider else "" | |
| - | |
| - # Auto-detect provider from available API keys | |
| - if not provider_key and not api_key: | |
| - for p in _PROVIDER_DETECT_ORDER: | |
| - env_var = PROVIDER_PRESETS[p][2] | |
| - if env_var and os.environ.get(env_var): | |
| - provider_key = p | |
| - api_key = os.environ[env_var] | |
| - break | |
| - | |
| - # Look up preset | |
| - preset = PROVIDER_PRESETS.get(provider_key, ("", "", "")) | |
| - preset_url, preset_model, preset_key_env = preset | |
| - | |
| - # Allow per-provider base_url override via env var (e.g. ANTHROPIC_BASE_URL) | |
| - env_base_url = "" | |
| - if provider_key: | |
| - env_base_url = os.environ.get(f"{provider_key.upper()}_BASE_URL", "") | |
| - | |
| - resolved_url = base_url or env_base_url or preset_url or None | |
| - resolved_model = model or preset_model or "deepseek-chat" | |
| - | |
| - # Resolve API key: explicit > per-provider env > LLM_API_KEY fallback | |
| - if not api_key and preset_key_env: | |
| - api_key = os.environ.get(preset_key_env, "") | |
| - if not api_key: | |
| - api_key = os.environ.get("LLM_API_KEY", os.environ.get("OPENAI_API_KEY", "")) | |
| - | |
| - return resolved_url, resolved_model, api_key | |
| +from omicsclaw.core.provider_registry import ( # noqa: E402 | |
| + PROVIDER_PRESETS, | |
| + PROVIDER_DETECT_ORDER as _PROVIDER_DETECT_ORDER, | |
| + resolve_provider, | |
| +) | |
| # --------------------------------------------------------------------------- | |
| diff --git a/bot/onboard.py b/bot/onboard.py | |
| index b33efb5..fc97d47 100644 | |
| --- a/bot/onboard.py | |
| +++ b/bot/onboard.py | |
| @@ -18,6 +18,12 @@ console = Console() | |
| _PROJECT_ROOT = Path(__file__).resolve().parent.parent | |
| _ENV_PATH = _PROJECT_ROOT / ".env" | |
| +# Import provider list from the single source of truth | |
| +import sys | |
| +if str(_PROJECT_ROOT) not in sys.path: | |
| + sys.path.insert(0, str(_PROJECT_ROOT)) | |
| +from omicsclaw.core.provider_registry import PROVIDER_CHOICES | |
| + | |
| def load_env() -> dict: | |
| env_vars = {} | |
| if _ENV_PATH.exists(): | |
| @@ -63,7 +69,7 @@ def run_onboard(): | |
| env_vars = load_env() | |
| # ── 1. LLM Provider ── | |
| - providers = ["deepseek", "openai", "anthropic", "gemini", "siliconflow", "zhipu", "dashscope", "volcengine", "openrouter", "ollama", "custom"] | |
| + providers = list(PROVIDER_CHOICES) | |
| current_provider = env_vars.get("LLM_PROVIDER", "deepseek") | |
| provider = questionary.select( | |
| diff --git a/omicsclaw/core/provider_registry.py b/omicsclaw/core/provider_registry.py | |
| new file mode 100644 | |
| index 0000000..e8778cf | |
| --- /dev/null | |
| +++ b/omicsclaw/core/provider_registry.py | |
| @@ -0,0 +1,88 @@ | |
| +"""Centralised LLM provider registry. | |
| + | |
| +Single source of truth for provider presets, auto-detection order, and | |
| +credential resolution. Every module that needs provider config imports | |
| +from here — no more copy-pasting PROVIDER_PRESETS into fallback blocks. | |
| + | |
| +Consumers: | |
| + bot/core.py → init(), resolve_provider() | |
| + omicsclaw/routing/ → _resolve_llm_config() | |
| + omicsclaw/agents/ → _get_llm() | |
| + bot/onboard.py → provider list for wizard | |
| +""" | |
| +from __future__ import annotations | |
| + | |
| +import os | |
| + | |
| +# Each provider maps to (base_url, default_model, api_key_env_var). | |
| +PROVIDER_PRESETS: dict[str, tuple[str, str, str]] = { | |
| + # --- Tier 1: Primary providers --- | |
| + "deepseek": ("https://api.deepseek.com", "deepseek-chat", "DEEPSEEK_API_KEY"), | |
| + "openai": ("", "gpt-4o", "OPENAI_API_KEY"), | |
| + "anthropic": ("https://api.anthropic.com/v1/", "claude-sonnet-4-5-20250514", "ANTHROPIC_API_KEY"), | |
| + "gemini": ("https://generativelanguage.googleapis.com/v1beta/openai/", "gemini-2.5-flash", "GOOGLE_API_KEY"), | |
| + "nvidia": ("https://integrate.api.nvidia.com/v1", "deepseek-ai/deepseek-r1", "NVIDIA_API_KEY"), | |
| + # --- Tier 2: Third-party aggregators --- | |
| + "siliconflow": ("https://api.siliconflow.cn/v1", "deepseek-ai/DeepSeek-V3", "SILICONFLOW_API_KEY"), | |
| + "openrouter": ("https://openrouter.ai/api/v1", "deepseek/deepseek-chat-v3-0324", "OPENROUTER_API_KEY"), | |
| + "volcengine": ("https://ark.cn-beijing.volces.com/api/v3", "doubao-1.5-pro-256k", "VOLCENGINE_API_KEY"), | |
| + "dashscope": ("https://dashscope.aliyuncs.com/compatible-mode/v1", "qwen-max", "DASHSCOPE_API_KEY"), | |
| + "zhipu": ("https://open.bigmodel.cn/api/paas/v4", "glm-4-flash", "ZHIPU_API_KEY"), | |
| + # --- Tier 3: Local & custom --- | |
| + "ollama": ("http://localhost:11434/v1", "qwen2.5:7b", ""), | |
| + "custom": ("", "", ""), | |
| +} | |
| + | |
| +# Ordered list for auto-detection: first provider whose API key env var is | |
| +# present wins. | |
| +PROVIDER_DETECT_ORDER: list[str] = [ | |
| + "deepseek", "openai", "anthropic", "gemini", "nvidia", | |
| + "siliconflow", "openrouter", "volcengine", "dashscope", "zhipu", | |
| +] | |
| + | |
| +# Provider names offered in the onboard wizard (display order). | |
| +PROVIDER_CHOICES: list[str] = [ | |
| + "deepseek", "openai", "anthropic", "gemini", | |
| + "siliconflow", "zhipu", "dashscope", "volcengine", | |
| + "openrouter", "ollama", "custom", | |
| +] | |
| + | |
| + | |
| +def resolve_provider( | |
| + provider: str = "", | |
| + base_url: str = "", | |
| + model: str = "", | |
| + api_key: str = "", | |
| +) -> tuple[str | None, str, str]: | |
| + """Return *(base_url | None, model, api_key)* after applying provider defaults. | |
| + | |
| + Priority: explicit args → provider preset → auto-detect → fallback. | |
| + """ | |
| + provider_key = provider.lower().strip() if provider else "" | |
| + | |
| + # Auto-detect from available API keys | |
| + if not provider_key and not api_key: | |
| + for p in PROVIDER_DETECT_ORDER: | |
| + env_var = PROVIDER_PRESETS[p][2] | |
| + if env_var and os.environ.get(env_var): | |
| + provider_key = p | |
| + api_key = os.environ[env_var] | |
| + break | |
| + | |
| + preset = PROVIDER_PRESETS.get(provider_key, ("", "", "")) | |
| + preset_url, preset_model, preset_key_env = preset | |
| + | |
| + # Per-provider base_url override (e.g. ANTHROPIC_BASE_URL) | |
| + env_base_url = "" | |
| + if provider_key: | |
| + env_base_url = os.environ.get(f"{provider_key.upper()}_BASE_URL", "") | |
| + | |
| + resolved_url = base_url or env_base_url or preset_url or None | |
| + resolved_model = model or preset_model or "deepseek-chat" | |
| + | |
| + if not api_key and preset_key_env: | |
| + api_key = os.environ.get(preset_key_env, "") | |
| + if not api_key: | |
| + api_key = os.environ.get("LLM_API_KEY", os.environ.get("OPENAI_API_KEY", "")) | |
| + | |
| + return resolved_url, resolved_model, api_key | |
| diff --git a/omicsclaw/routing/llm_router.py b/omicsclaw/routing/llm_router.py | |
| index b10713f..59731e7 100644 | |
| --- a/omicsclaw/routing/llm_router.py | |
| +++ b/omicsclaw/routing/llm_router.py | |
| @@ -18,35 +18,12 @@ except ImportError: | |
| logger = logging.getLogger(__name__) | |
| # --------------------------------------------------------------------------- | |
| -# Provider resolution — reuses bot.core when available, else falls back | |
| +# Provider resolution — single source of truth in provider_registry | |
| # --------------------------------------------------------------------------- | |
| - | |
| -# Import shared provider presets from bot.core (avoids hard-coding provider | |
| -# URLs in multiple places). Graceful fallback for standalone usage. | |
| -try: | |
| - from bot.core import PROVIDER_PRESETS, _PROVIDER_DETECT_ORDER | |
| - _HAS_CORE = True | |
| -except ImportError: | |
| - _HAS_CORE = False | |
| - # Minimal fallback matching the most common providers | |
| - PROVIDER_PRESETS = { | |
| - "deepseek": ("https://api.deepseek.com", "deepseek-chat", "DEEPSEEK_API_KEY"), | |
| - "openai": ("https://api.openai.com/v1", "gpt-4o", "OPENAI_API_KEY"), | |
| - "gemini": ("https://generativelanguage.googleapis.com/v1beta/openai/", "gemini-2.5-flash", "GOOGLE_API_KEY"), | |
| - "anthropic": ("https://api.anthropic.com/v1/", "claude-sonnet-4-5-20250514", "ANTHROPIC_API_KEY"), | |
| - "nvidia": ("https://integrate.api.nvidia.com/v1", "deepseek-ai/deepseek-r1", "NVIDIA_API_KEY"), | |
| - "siliconflow":("https://api.siliconflow.cn/v1", "deepseek-ai/DeepSeek-V3", "SILICONFLOW_API_KEY"), | |
| - "openrouter": ("https://openrouter.ai/api/v1", "deepseek/deepseek-chat-v3-0324", "OPENROUTER_API_KEY"), | |
| - "volcengine": ("https://ark.cn-beijing.volces.com/api/v3", "doubao-1.5-pro-256k", "VOLCENGINE_API_KEY"), | |
| - "dashscope": ("https://dashscope.aliyuncs.com/compatible-mode/v1", "qwen-max", "DASHSCOPE_API_KEY"), | |
| - "zhipu": ("https://open.bigmodel.cn/api/paas/v4", "glm-4-flash", "ZHIPU_API_KEY"), | |
| - "ollama": ("http://localhost:11434/v1", "qwen2.5:7b", ""), | |
| - "custom": ("", "", ""), | |
| - } | |
| - _PROVIDER_DETECT_ORDER = [ | |
| - "deepseek", "openai", "anthropic", "gemini", "nvidia", | |
| - "siliconflow", "openrouter", "volcengine", "dashscope", "zhipu", | |
| - ] | |
| +from omicsclaw.core.provider_registry import ( | |
| + PROVIDER_PRESETS, | |
| + PROVIDER_DETECT_ORDER as _PROVIDER_DETECT_ORDER, | |
| +) | |
| def _resolve_llm_config() -> Tuple[str, str, str]: |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment