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
7 changes: 7 additions & 0 deletions lib/crewai/src/crewai/llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,7 @@
"hosted_vllm",
"cerebras",
"dashscope",
"orcarouter",
]


Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
),
}


Expand Down Expand Up @@ -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
Expand Down
67 changes: 67 additions & 0 deletions lib/crewai/tests/llms/openai_compatible/test_openai_compatible.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down Expand Up @@ -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"}):
Expand Down