diff --git a/pycodeloop/cli/flow.py b/pycodeloop/cli/flow.py index 6b16d02..665c194 100644 --- a/pycodeloop/cli/flow.py +++ b/pycodeloop/cli/flow.py @@ -24,7 +24,7 @@ from pycodeloop.core.codeloop import CodeLoop from pycodeloop.core.config import Config from pycodeloop.mcp import MCPServer, MCPServerRegistry, load_mcp_tools -from pycodeloop.providers import get_provider +from pycodeloop.providers import BUNDLED_TEMPLATES, get_provider from pycodeloop.settings import Settings from pycodeloop.store.json_store import default_store from pycodeloop.tools import DEFAULT_TOOLS @@ -105,7 +105,9 @@ def resolve_provider( """Build a `Provider` from CLI-style args, returning it alongside the resolved provider name — shared by the interactive CLI and `serve`.""" provider_name = provider_name or Settings.PROVIDER - is_json_config = provider_name.endswith(".json") + is_json_config = ( + provider_name.endswith(".json") or provider_name in BUNDLED_TEMPLATES + ) if is_json_config: provider_kwargs: dict = {} diff --git a/pycodeloop/providers/__init__.py b/pycodeloop/providers/__init__.py index 527a47e..49aa5a1 100644 --- a/pycodeloop/providers/__init__.py +++ b/pycodeloop/providers/__init__.py @@ -2,13 +2,17 @@ import importlib import os +from pathlib import Path from pycodeloop.constants import ENV_API_KEY from pycodeloop.providers.generic import GenericProvider PROVIDERS = {"generic": GenericProvider} -__all__ = ["GenericProvider", "get_provider", "PROVIDERS"] +TEMPLATES_DIR = Path(__file__).parent / "templates" +BUNDLED_TEMPLATES = {p.stem for p in TEMPLATES_DIR.glob("*.json")} + +__all__ = ["GenericProvider", "get_provider", "PROVIDERS", "BUNDLED_TEMPLATES"] def get_provider(name: str, **kwargs): @@ -17,20 +21,18 @@ def get_provider(name: str, **kwargs): `name` is one of: - a path to a JSON config file (see `pycodeloop.providers.generic`), the standard way to point at any HTTP LLM API; + - the bare name of a template shipped in `providers/templates/` + (e.g. `"anthropic"`, `"ollama"`) — resolved from the installed + package, so it works from any directory without a path; - `"generic"`, paired with `url=`/`model=` kwargs for an ad-hoc config with no file; - `'module.path:ClassName'` for a custom `Provider` subclass. """ if name.endswith(".json"): - provider = GenericProvider.from_json(name) - - if kwargs.get("model"): - provider.model = kwargs["model"] - - if kwargs.get("api_key"): - provider.api_key = kwargs["api_key"] + return _from_json_path(name, kwargs) - return provider + if name in BUNDLED_TEMPLATES: + return _from_json_path(str(TEMPLATES_DIR / f"{name}.json"), kwargs) if ":" in name: module_path, class_name = name.split(":", 1) @@ -44,6 +46,7 @@ def get_provider(name: str, **kwargs): except KeyError: raise ValueError( f"Unknown provider '{name}'. Available: {list(PROVIDERS)}, " + f"a bundled template ({', '.join(sorted(BUNDLED_TEMPLATES))}), " "a path to a JSON config file, or 'module.path:ClassName' " "for a custom Provider." ) from None @@ -51,6 +54,18 @@ def get_provider(name: str, **kwargs): return provider_cls(**_with_env_api_key(kwargs)) +def _from_json_path(path: str, kwargs: dict) -> GenericProvider: + provider = GenericProvider.from_json(path) + + if kwargs.get("model"): + provider.model = kwargs["model"] + + if kwargs.get("api_key"): + provider.api_key = kwargs["api_key"] + + return provider + + def _with_env_api_key(kwargs: dict) -> dict: if kwargs.get("api_key"): return kwargs