diff --git a/README.md b/README.md index cc2c0a5f..12aad625 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,7 @@ MCP CLI supports all providers and models from CHUK-LLM, including cutting-edge | **Perplexity** 🌐 | Sonar models | Real-time web search with citations | | **IBM watsonx** 🏢 | Granite, Llama models | Enterprise compliance | | **Mistral AI** 🇪🇺 | Mistral Large, Medium | European, efficient models | +| **OrcaRouter** 🐋 | openai/gpt-4o-mini, orcarouter/auto | OpenAI-compatible routing gateway — 150+ models behind one API key | ### Robust Tool System (Powered by CHUK Tool Processor v0.22+) - **Automatic Discovery**: Server-provided tools are automatically detected and catalogued @@ -208,6 +209,7 @@ Comprehensive documentation is available in the `docs/` directory: - Azure: `AZURE_OPENAI_API_KEY` and `AZURE_OPENAI_ENDPOINT` (for enterprise GPT-5) - Google: `GEMINI_API_KEY` (for Gemini models) - Groq: `GROQ_API_KEY` (for fast Llama models) + - OrcaRouter: `ORCAROUTER_API_KEY` (keys start with `sk-orca-`; 150+ models behind one endpoint) - Custom providers: Provider-specific configuration - **MCP Servers**: Server configuration file (default: `server_config.json`) @@ -502,6 +504,7 @@ mcp-cli --server sqlite --provider anthropic --model claude-4-5-opus /provider openai # Switch to OpenAI (requires API key) /provider anthropic # Switch to Anthropic (requires API key) /provider openai gpt-5 # Switch to OpenAI GPT-5 +/provider orcarouter # Switch to OrcaRouter (requires ORCAROUTER_API_KEY) # Custom Provider Management /provider custom # List custom providers diff --git a/src/mcp_cli/auth/provider_tokens.py b/src/mcp_cli/auth/provider_tokens.py index 96e2a771..8a1eb7e4 100644 --- a/src/mcp_cli/auth/provider_tokens.py +++ b/src/mcp_cli/auth/provider_tokens.py @@ -28,6 +28,7 @@ "mistral": "MISTRAL_API_KEY", "perplexity": "PERPLEXITY_API_KEY", "openrouter": "OPENROUTER_API_KEY", + "orcarouter": "ORCAROUTER_API_KEY", "togetherai": "TOGETHER_API_KEY", "deepseek": "DEEPSEEK_API_KEY", "azure_openai": "AZURE_OPENAI_API_KEY", diff --git a/src/mcp_cli/model_management/gateway_providers.py b/src/mcp_cli/model_management/gateway_providers.py new file mode 100644 index 00000000..4905a533 --- /dev/null +++ b/src/mcp_cli/model_management/gateway_providers.py @@ -0,0 +1,55 @@ +"""Built-in gateway providers registered into chuk_llm at startup. + +chuk-llm ships named providers (openai, anthropic, openrouter, ...) in its +bundled YAML configuration. Aggregator gateways such as OrcaRouter are +registered here so mcp-cli users can select them by name and by their own +API key without waiting for a chuk-llm release to add them. + +Registration is idempotent: if a chuk-llm version already ships the provider +natively, we leave its definition untouched. +""" + +from __future__ import annotations + +import logging +from typing import Any + +logger = logging.getLogger(__name__) + +#: OrcaRouter OpenAI-compatible gateway. +ORCAROUTER_BASE_URL = "https://api.orcarouter.ai/v1" +ORCAROUTER_API_KEY_ENV = "ORCAROUTER_API_KEY" +ORCAROUTER_DEFAULT_MODEL = "openai/gpt-4o-mini" + +#: Named gateway providers to register into the chuk_llm config manager. +#: The default client class (OpenAI-compatible) is used, mirroring how the +#: bundled chuk_llm.yaml wires openrouter / deepseek / moonshot. +GATEWAY_PROVIDERS: dict[str, dict[str, Any]] = { + "orcarouter": { + "api_key_env": ORCAROUTER_API_KEY_ENV, + "api_base": ORCAROUTER_BASE_URL, + "default_model": ORCAROUTER_DEFAULT_MODEL, + "models": ["*"], + }, +} + + +def register_gateway_providers(config: Any) -> None: + """Register built-in gateway providers into a chuk_llm config manager. + + Args: + config: A chuk_llm configuration manager (as returned by + ``chuk_llm.configuration.get_config()``). ``None`` is a no-op. + """ + if config is None: + return + + try: + existing = set(config.get_all_providers()) + for name, definition in GATEWAY_PROVIDERS.items(): + if name in existing: + continue + config.register_provider(name=name, **definition) + logger.info("Registered built-in gateway provider: %s", name) + except Exception as e: # pragma: no cover - defensive + logger.warning("Failed to register built-in gateway providers: %s", e) diff --git a/src/mcp_cli/model_management/model_manager.py b/src/mcp_cli/model_management/model_manager.py index 56b5daa2..92cbfa08 100644 --- a/src/mcp_cli/model_management/model_manager.py +++ b/src/mcp_cli/model_management/model_manager.py @@ -60,6 +60,14 @@ def _initialize_chuk_llm(self) -> None: from chuk_llm.configuration import get_config self._chuk_config = get_config() + + # Register built-in gateway providers (e.g. OrcaRouter) so users + # can select them by name out of the box. + from mcp_cli.model_management.gateway_providers import ( + register_gateway_providers, + ) + + register_gateway_providers(self._chuk_config) logger.debug("Loaded chuk_llm configuration") # Use configured default provider (from defaults.py) diff --git a/tests/model_management/test_gateway_providers.py b/tests/model_management/test_gateway_providers.py new file mode 100644 index 00000000..68d2d1e6 --- /dev/null +++ b/tests/model_management/test_gateway_providers.py @@ -0,0 +1,67 @@ +"""Tests for built-in gateway provider registration.""" + +from __future__ import annotations + +from unittest.mock import MagicMock + +from mcp_cli.auth.provider_tokens import PROVIDER_ENV_VAR_MAP +from mcp_cli.model_management.gateway_providers import ( + GATEWAY_PROVIDERS, + ORCAROUTER_API_KEY_ENV, + ORCAROUTER_BASE_URL, + ORCAROUTER_DEFAULT_MODEL, + register_gateway_providers, +) + + +class TestRegisterGatewayProviders: + """Tests for register_gateway_providers().""" + + def test_registers_orcarouter_with_openai_compatible_wiring(self) -> None: + """OrcaRouter is registered as a named OpenAI-compatible provider.""" + mock_config = MagicMock() + mock_config.get_all_providers.return_value = ["openai", "anthropic"] + + register_gateway_providers(mock_config) + + mock_config.register_provider.assert_called_once_with( + name="orcarouter", + api_key_env=ORCAROUTER_API_KEY_ENV, + api_base=ORCAROUTER_BASE_URL, + default_model=ORCAROUTER_DEFAULT_MODEL, + models=["*"], + ) + + def test_skips_provider_already_registered(self) -> None: + """If chuk_llm already ships orcarouter, leave it untouched.""" + mock_config = MagicMock() + mock_config.get_all_providers.return_value = ["openai", "orcarouter"] + + register_gateway_providers(mock_config) + + mock_config.register_provider.assert_not_called() + + def test_none_config_is_noop(self) -> None: + """A missing config manager should not raise.""" + register_gateway_providers(None) + + def test_registration_failure_is_swallowed(self) -> None: + """A failing registration should not crash startup.""" + mock_config = MagicMock() + mock_config.get_all_providers.side_effect = RuntimeError("boom") + + register_gateway_providers(mock_config) + + def test_gateway_provider_defaults_are_sane(self) -> None: + """The OrcaRouter definition is a named OpenAI-compatible gateway.""" + orcarouter = GATEWAY_PROVIDERS["orcarouter"] + assert orcarouter["api_base"] == "https://api.orcarouter.ai/v1" + assert orcarouter["api_key_env"] == "ORCAROUTER_API_KEY" + assert orcarouter["models"] == ["*"] + + +class TestProviderEnvVarMap: + """Tests for the provider token env var mapping.""" + + def test_orcarouter_maps_to_orcarouter_api_key(self) -> None: + assert PROVIDER_ENV_VAR_MAP["orcarouter"] == "ORCAROUTER_API_KEY"