diff --git a/lib/crewai/src/crewai/llm.py b/lib/crewai/src/crewai/llm.py index e452dc394d..75bdbe36e7 100644 --- a/lib/crewai/src/crewai/llm.py +++ b/lib/crewai/src/crewai/llm.py @@ -299,6 +299,7 @@ "hosted_vllm", "cerebras", "dashscope", + "orcarouter", ] @@ -388,6 +389,7 @@ def __new__(cls, model: str, is_litellm: bool = False, **kwargs: Any) -> LLM: "hosted_vllm": "hosted_vllm", "cerebras": "cerebras", "dashscope": "dashscope", + "orcarouter": "orcarouter", } canonical_provider = provider_mapping.get(prefix.lower()) @@ -507,6 +509,10 @@ def _matches_provider_pattern(cls, model: str, provider: str) -> bool: # OpenRouter uses org/model format but accepts anything return True + if provider == "orcarouter": + # OrcaRouter uses org/model format (e.g. openai/gpt-5) but accepts anything + return True + return False @classmethod @@ -615,6 +621,7 @@ def _get_native_provider(cls, provider: str) -> type | None: "hosted_vllm", "cerebras", "dashscope", + "orcarouter", } if provider in openai_compatible_providers: from crewai.llms.providers.openai_compatible.completion import ( diff --git a/lib/crewai/src/crewai/llms/providers/openai_compatible/completion.py b/lib/crewai/src/crewai/llms/providers/openai_compatible/completion.py index da4cfd03db..1c6a8e87eb 100644 --- a/lib/crewai/src/crewai/llms/providers/openai_compatible/completion.py +++ b/lib/crewai/src/crewai/llms/providers/openai_compatible/completion.py @@ -89,6 +89,16 @@ class ProviderConfig: base_url_env="DASHSCOPE_BASE_URL", api_key_required=True, ), + "orcarouter": ProviderConfig( + base_url="https://api.orcarouter.ai/v1", + api_key_env="ORCAROUTER_API_KEY", + base_url_env="ORCAROUTER_API_BASE_URL", + default_headers={ + "HTTP-Referer": "https://crewai.com", + "X-Title": "crewai", + }, + api_key_required=True, + ), } @@ -125,6 +135,7 @@ class OpenAICompatibleCompletion(OpenAICompletion): - hosted_vllm: vLLM server (https://github.com/vllm-project/vllm) - cerebras: Cerebras (https://cerebras.ai) - dashscope: Alibaba Dashscope/Qwen (https://dashscope.aliyun.com) + - orcarouter: OrcaRouter (https://www.orcarouter.ai) Example: # Using provider prefix diff --git a/lib/crewai/tests/llms/openai_compatible/test_openai_compatible.py b/lib/crewai/tests/llms/openai_compatible/test_openai_compatible.py index fd59702994..3381a085d1 100644 --- a/lib/crewai/tests/llms/openai_compatible/test_openai_compatible.py +++ b/lib/crewai/tests/llms/openai_compatible/test_openai_compatible.py @@ -95,6 +95,16 @@ def test_dashscope_config(self): assert config.api_key_env == "DASHSCOPE_API_KEY" assert config.api_key_required is True + def test_orcarouter_config(self): + """Test OrcaRouter provider configuration.""" + config = OPENAI_COMPATIBLE_PROVIDERS["orcarouter"] + assert config.base_url == "https://api.orcarouter.ai/v1" + assert config.api_key_env == "ORCAROUTER_API_KEY" + assert config.base_url_env == "ORCAROUTER_API_BASE_URL" + assert config.api_key_required is True + assert "HTTP-Referer" in config.default_headers + assert "X-Title" in config.default_headers + class TestNormalizeOllamaBaseUrl: """Tests for _normalize_ollama_base_url helper.""" @@ -272,6 +282,63 @@ def test_llm_creates_openai_compatible_for_dashscope(self): assert isinstance(llm, OpenAICompatibleCompletion) assert llm.provider == "dashscope" + def test_llm_creates_openai_compatible_for_orcarouter(self): + """Test LLM factory creates OpenAICompatibleCompletion for OrcaRouter.""" + with patch.dict(os.environ, {"ORCAROUTER_API_KEY": "test-key"}): + llm = LLM(model="orcarouter/openai/gpt-5") + assert isinstance(llm, OpenAICompatibleCompletion) + assert llm.provider == "orcarouter" + # Model should include the full path after provider prefix + assert llm.model == "openai/gpt-5" + assert llm.base_url == "https://api.orcarouter.ai/v1" + + def test_llm_creates_openai_compatible_for_orcarouter_auto(self): + """Test LLM factory creates OpenAICompatibleCompletion for orcarouter/auto router.""" + with patch.dict(os.environ, {"ORCAROUTER_API_KEY": "test-key"}): + llm = LLM(model="orcarouter/auto") + assert isinstance(llm, OpenAICompatibleCompletion) + assert llm.provider == "orcarouter" + assert llm.model == "auto" + + def test_llm_creates_openai_compatible_for_orcarouter_with_explicit_provider(self): + """Test LLM factory routes to OrcaRouter when `provider="orcarouter"` is + passed explicitly, even if the model string has no `orcarouter/` prefix. + + Covers the explicit-provider branch in addition to the prefix-based + routing branch verified by `test_llm_creates_openai_compatible_for_orcarouter`. + """ + with patch.dict(os.environ, {"ORCAROUTER_API_KEY": "test-key"}): + llm = LLM(model="openai/gpt-5", provider="orcarouter") + assert isinstance(llm, OpenAICompatibleCompletion) + assert llm.provider == "orcarouter" + # Model passes through untouched — no prefix to strip. + assert llm.model == "openai/gpt-5" + assert llm.base_url == "https://api.orcarouter.ai/v1" + + def test_orcarouter_attribution_headers(self): + """Test OrcaRouter sends HTTP-Referer and X-Title attribution headers.""" + with patch.dict(os.environ, {"ORCAROUTER_API_KEY": "test-key"}): + completion = OpenAICompatibleCompletion( + model="openai/gpt-5", provider="orcarouter" + ) + assert completion.default_headers is not None + assert completion.default_headers.get("HTTP-Referer") == "https://crewai.com" + assert completion.default_headers.get("X-Title") == "crewai" + + def test_orcarouter_base_url_env_override(self): + """Test ORCAROUTER_API_BASE_URL env var overrides default base URL.""" + with patch.dict( + os.environ, + { + "ORCAROUTER_API_KEY": "test-key", + "ORCAROUTER_API_BASE_URL": "https://staging.orcarouter.ai/v1", + }, + ): + completion = OpenAICompatibleCompletion( + model="openai/gpt-5", provider="orcarouter" + ) + assert completion.base_url == "https://staging.orcarouter.ai/v1" + def test_llm_with_explicit_provider(self): """Test LLM with explicit provider parameter.""" with patch.dict(os.environ, {"DEEPSEEK_API_KEY": "test-key"}):