mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-07-09 08:55:09 +00:00
Compare commits
3 Commits
main
...
lorenze/im
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e72b2039f9 | ||
|
|
8307d4ba14 | ||
|
|
c4ff2c4ebd |
@@ -144,6 +144,18 @@ In this section, you'll find detailed examples that help you select, configure,
|
||||
)
|
||||
```
|
||||
|
||||
**Custom OpenAI-Compatible Endpoint:**
|
||||
```python Code
|
||||
from crewai import LLM
|
||||
|
||||
llm = LLM(
|
||||
model="anthropic/claude-sonnet-4-6",
|
||||
custom_openai=True,
|
||||
base_url="https://your-gateway.example.com/v1",
|
||||
api_key="your-gateway-api-key",
|
||||
)
|
||||
```
|
||||
|
||||
**Advanced Configuration:**
|
||||
```python Code
|
||||
from crewai import LLM
|
||||
|
||||
@@ -240,14 +240,15 @@ from crewai import LLM
|
||||
|
||||
# After (OpenAI-compatible mode, no LiteLLM needed):
|
||||
llm = LLM(
|
||||
model="openai/llama3",
|
||||
model="llama3",
|
||||
custom_openai=True,
|
||||
base_url="http://localhost:11434/v1",
|
||||
api_key="ollama" # Ollama doesn't require a real API key
|
||||
)
|
||||
```
|
||||
|
||||
<Tip>
|
||||
Many local inference servers (Ollama, vLLM, LM Studio, llama.cpp) expose an OpenAI-compatible API. You can use the `openai/` prefix with a custom `base_url` to connect to any of them natively.
|
||||
Many local inference servers (Ollama, vLLM, LM Studio, llama.cpp) expose an OpenAI-compatible API. You can use `custom_openai=True` with a custom `base_url` to connect to any of them natively while keeping the model ID your gateway expects.
|
||||
</Tip>
|
||||
|
||||
### Step 4: Update your YAML configs
|
||||
|
||||
@@ -404,9 +404,20 @@ class LLM(BaseLLM):
|
||||
if not model or not isinstance(model, str):
|
||||
raise ValueError("Model must be a non-empty string")
|
||||
|
||||
custom_openai = bool(kwargs.pop("custom_openai", False))
|
||||
custom_openai_route = custom_openai
|
||||
explicit_provider = kwargs.get("provider")
|
||||
|
||||
if explicit_provider:
|
||||
if custom_openai:
|
||||
if not cls._has_custom_openai_base_url(kwargs):
|
||||
raise ValueError(
|
||||
"custom_openai=True requires base_url, api_base, "
|
||||
"OPENAI_BASE_URL, or OPENAI_API_BASE"
|
||||
)
|
||||
provider = "openai"
|
||||
use_native = True
|
||||
model_string = model
|
||||
elif explicit_provider:
|
||||
provider = explicit_provider
|
||||
use_native = True
|
||||
model_string = model
|
||||
@@ -435,9 +446,17 @@ class LLM(BaseLLM):
|
||||
|
||||
canonical_provider = provider_mapping.get(prefix.lower())
|
||||
|
||||
if canonical_provider and cls._validate_model_in_constants(
|
||||
model_part, canonical_provider
|
||||
):
|
||||
valid_native_model = bool(
|
||||
canonical_provider
|
||||
and cls._validate_model_in_constants(model_part, canonical_provider)
|
||||
)
|
||||
custom_openai_route = bool(
|
||||
canonical_provider == "openai"
|
||||
and not valid_native_model
|
||||
and cls._has_custom_openai_base_url(kwargs)
|
||||
)
|
||||
|
||||
if canonical_provider and (valid_native_model or custom_openai_route):
|
||||
provider = canonical_provider
|
||||
use_native = True
|
||||
model_string = model_part
|
||||
@@ -455,6 +474,8 @@ class LLM(BaseLLM):
|
||||
try:
|
||||
# Remove 'provider' from kwargs if it exists to avoid duplicate keyword argument
|
||||
kwargs_copy = {k: v for k, v in kwargs.items() if k != "provider"}
|
||||
if custom_openai_route:
|
||||
kwargs_copy["custom_openai"] = True
|
||||
return cast(
|
||||
Self,
|
||||
native_class(model=model_string, provider=provider, **kwargs_copy),
|
||||
@@ -590,6 +611,15 @@ class LLM(BaseLLM):
|
||||
|
||||
return cls._matches_provider_pattern(model, provider)
|
||||
|
||||
@staticmethod
|
||||
def _has_custom_openai_base_url(kwargs: dict[str, Any]) -> bool:
|
||||
return bool(
|
||||
kwargs.get("base_url")
|
||||
or kwargs.get("api_base")
|
||||
or os.getenv("OPENAI_BASE_URL")
|
||||
or os.getenv("OPENAI_API_BASE")
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _infer_provider_from_model(cls, model: str) -> str:
|
||||
"""Infer the provider from the model name.
|
||||
|
||||
@@ -232,6 +232,7 @@ class OpenAICompletion(BaseLLM):
|
||||
auto_chain: bool = False
|
||||
auto_chain_reasoning: bool = False
|
||||
api_base: str | None = None
|
||||
custom_openai: bool = False
|
||||
is_o1_model: bool = False
|
||||
is_gpt4_model: bool = False
|
||||
|
||||
@@ -355,6 +356,9 @@ class OpenAICompletion(BaseLLM):
|
||||
config["seed"] = self.seed
|
||||
if self.reasoning_effort is not None:
|
||||
config["reasoning_effort"] = self.reasoning_effort
|
||||
if self.custom_openai:
|
||||
config["model"] = self.model
|
||||
config["custom_openai"] = True
|
||||
return config
|
||||
|
||||
def _get_client_params(self) -> dict[str, Any]:
|
||||
@@ -372,6 +376,7 @@ class OpenAICompletion(BaseLLM):
|
||||
"base_url": self.base_url
|
||||
or self.api_base
|
||||
or os.getenv("OPENAI_BASE_URL")
|
||||
or os.getenv("OPENAI_API_BASE")
|
||||
or None,
|
||||
"timeout": self.timeout,
|
||||
"max_retries": self.max_retries,
|
||||
|
||||
@@ -30,10 +30,84 @@ def test_openai_completion_is_used_when_no_provider_prefix():
|
||||
llm = LLM(model="gpt-4o")
|
||||
|
||||
from crewai.llms.providers.openai.completion import OpenAICompletion
|
||||
assert isinstance(llm, OpenAICompletion)
|
||||
assert llm.__class__.__name__ == "OpenAICompletion"
|
||||
assert llm.provider == "openai"
|
||||
assert llm.model == "gpt-4o"
|
||||
|
||||
|
||||
def test_custom_openai_flag_uses_native_openai_without_provider_prefix():
|
||||
"""Custom OpenAI-compatible endpoints can serve arbitrary model ids."""
|
||||
with patch.dict(os.environ, {"OPENAI_API_KEY": "test-key"}, clear=False):
|
||||
llm = LLM(
|
||||
model="anthropic/claude-sonnet-4-6",
|
||||
custom_openai=True,
|
||||
base_url="https://asimov.example/v1",
|
||||
is_litellm=False,
|
||||
)
|
||||
|
||||
assert llm.__class__.__name__ == "OpenAICompletion"
|
||||
assert llm.is_litellm is False
|
||||
assert llm.provider == "openai"
|
||||
assert llm.model == "anthropic/claude-sonnet-4-6"
|
||||
assert llm.base_url == "https://asimov.example/v1"
|
||||
assert llm.custom_openai is True
|
||||
assert "custom_openai" not in llm.additional_params
|
||||
|
||||
config = llm.to_config_dict()
|
||||
assert config["model"] == "anthropic/claude-sonnet-4-6"
|
||||
assert config["custom_openai"] is True
|
||||
assert config["base_url"] == "https://asimov.example/v1"
|
||||
|
||||
|
||||
def test_custom_openai_flag_requires_custom_base_url():
|
||||
"""Avoid routing arbitrary custom model ids to api.openai.com by mistake."""
|
||||
with patch.dict(os.environ, {"OPENAI_API_KEY": "test-key"}, clear=True):
|
||||
with pytest.raises(ValueError, match="custom_openai=True requires"):
|
||||
LLM(
|
||||
model="anthropic/claude-sonnet-4-6",
|
||||
custom_openai=True,
|
||||
is_litellm=False,
|
||||
)
|
||||
|
||||
|
||||
def test_openai_prefixed_custom_endpoint_uses_native_sdk_for_nested_model_id():
|
||||
"""Custom OpenAI-compatible endpoints may serve non-OpenAI model ids."""
|
||||
with patch.dict(os.environ, {"OPENAI_API_KEY": "test-key"}, clear=False):
|
||||
llm = LLM(
|
||||
model="openai/anthropic/claude-sonnet-4-6",
|
||||
base_url="https://asimov.example/v1",
|
||||
is_litellm=False,
|
||||
)
|
||||
|
||||
assert llm.__class__.__name__ == "OpenAICompletion"
|
||||
assert llm.is_litellm is False
|
||||
assert llm.provider == "openai"
|
||||
assert llm.model == "anthropic/claude-sonnet-4-6"
|
||||
assert llm.custom_openai is True
|
||||
assert llm.base_url == "https://asimov.example/v1"
|
||||
|
||||
def test_openai_prefixed_custom_endpoint_uses_legacy_api_base_env_var():
|
||||
"""Legacy OPENAI_API_BASE still marks the endpoint as OpenAI-compatible."""
|
||||
with patch.dict(
|
||||
os.environ,
|
||||
{
|
||||
"OPENAI_API_KEY": "test-key",
|
||||
"OPENAI_API_BASE": "https://asimov.example/v1",
|
||||
},
|
||||
clear=False,
|
||||
):
|
||||
os.environ.pop("OPENAI_BASE_URL", None)
|
||||
llm = LLM(
|
||||
model="openai/anthropic/claude-sonnet-4-6",
|
||||
is_litellm=False,
|
||||
)
|
||||
|
||||
assert isinstance(llm, OpenAICompletion)
|
||||
assert llm.is_litellm is False
|
||||
assert llm.provider == "openai"
|
||||
assert llm.model == "anthropic/claude-sonnet-4-6"
|
||||
assert llm.custom_openai is True
|
||||
|
||||
@pytest.mark.vcr()
|
||||
def test_openai_is_default_provider_without_explicit_llm_set_on_agent():
|
||||
"""
|
||||
@@ -60,14 +134,13 @@ def test_openai_is_default_provider_without_explicit_llm_set_on_agent():
|
||||
|
||||
|
||||
|
||||
def test_openai_completion_module_is_imported():
|
||||
def test_openai_completion_module_is_imported(monkeypatch):
|
||||
"""
|
||||
Test that the completion module is properly imported when using OpenAI provider
|
||||
"""
|
||||
module_name = "crewai.llms.providers.openai.completion"
|
||||
|
||||
if module_name in sys.modules:
|
||||
del sys.modules[module_name]
|
||||
monkeypatch.delitem(sys.modules, module_name, raising=False)
|
||||
|
||||
LLM(model="gpt-4o")
|
||||
|
||||
@@ -421,12 +494,25 @@ def test_openai_get_client_params_with_env_var():
|
||||
client_params = llm._get_client_params()
|
||||
assert client_params["base_url"] == "https://env.openai.com/v1"
|
||||
|
||||
def test_openai_get_client_params_with_legacy_api_base_env_var():
|
||||
"""
|
||||
Test that _get_client_params uses OPENAI_API_BASE when OPENAI_BASE_URL is absent.
|
||||
"""
|
||||
with patch.dict(os.environ, {
|
||||
"OPENAI_API_BASE": "https://legacy-env.openai.com/v1",
|
||||
}, clear=False):
|
||||
os.environ.pop("OPENAI_BASE_URL", None)
|
||||
llm = OpenAICompletion(model="gpt-4o")
|
||||
client_params = llm._get_client_params()
|
||||
assert client_params["base_url"] == "https://legacy-env.openai.com/v1"
|
||||
|
||||
def test_openai_get_client_params_priority_order():
|
||||
"""
|
||||
Test the priority order: base_url > api_base > OPENAI_BASE_URL env var
|
||||
Test the priority order: base_url > api_base > OPENAI_BASE_URL > OPENAI_API_BASE
|
||||
"""
|
||||
with patch.dict(os.environ, {
|
||||
"OPENAI_BASE_URL": "https://env.openai.com/v1",
|
||||
"OPENAI_API_BASE": "https://legacy-env.openai.com/v1",
|
||||
}):
|
||||
llm1 = OpenAICompletion(
|
||||
model="gpt-4o",
|
||||
|
||||
Reference in New Issue
Block a user