"""Build an :class:`LLMProvider` from a ``"name:model"`` spec string. Spec format: ``":"`` — e.g. ``"anthropic:claude-haiku-4-5"``, ``"openai:gpt-4o-mini"``, ``"gemini:gemini-2.0-flash"``, ``"ollama:qwen3.5:cloud"``, ``"fake:smoke"``. Only the FIRST ``:`` separates the provider name from the model id (model ids may themselves contain ``:``). Real providers are imported LAZILY (only when their spec is requested), so importing this module — or building a ``FakeProvider`` — never requires the heavy provider SDKs (openai / anthropic / google-genai / httpx) to be installed. This keeps the offline test suite dependency-free. """ from __future__ import annotations import importlib from proteus.providers.base import LLMProvider from proteus.providers.fake import FakeProvider # Provider name -> (module path, class name). Imported lazily in make_provider. _REGISTRY: dict[str, tuple[str, str]] = { "openai": ("proteus.providers.openai", "OpenAIProvider"), "anthropic": ("proteus.providers.anthropic_provider", "AnthropicProvider"), "gemini": ("proteus.providers.gemini", "GeminiProvider"), "ollama": ("proteus.providers.ollama_cloud", "OllamaCloudProvider"), "ollama_local": ("proteus.providers.ollama_local", "OllamaLocalProvider"), } # The canned action a CLI ``fake:`` provider replays each turn (offline smoke). _FAKE_DEFAULT_RESPONSE = "ACTION: stay" def available_providers() -> list[str]: """Return the sorted list of provider names accepted by :func:`make_provider`.""" return sorted([*_REGISTRY, "fake"]) def make_provider(spec: str) -> LLMProvider: """Build a provider from a ``":"`` spec. Args: spec: ``":"``. ``fake:`` yields an offline :class:`FakeProvider`. Real providers are imported lazily. Returns: A concrete :class:`LLMProvider`. Raises: ValueError: If the spec has no ``:`` separator, or names an unknown provider. """ name, sep, model = spec.partition(":") if not sep: raise ValueError( f"Provider spec must be ':', got {spec!r}. " f"Available providers: {', '.join(available_providers())}." ) if name == "fake": return FakeProvider( responses=[_FAKE_DEFAULT_RESPONSE], model_name=model or "fake", ) if name not in _REGISTRY: raise ValueError( f"Unknown provider {name!r}. " f"Available providers: {', '.join(available_providers())}." ) module_path, cls_name = _REGISTRY[name] provider_cls = getattr(importlib.import_module(module_path), cls_name) return provider_cls(model=model)