Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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`)

Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/mcp_cli/auth/provider_tokens.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
55 changes: 55 additions & 0 deletions src/mcp_cli/model_management/gateway_providers.py
Original file line number Diff line number Diff line change
@@ -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)
8 changes: 8 additions & 0 deletions src/mcp_cli/model_management/model_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
67 changes: 67 additions & 0 deletions tests/model_management/test_gateway_providers.py
Original file line number Diff line number Diff line change
@@ -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"