-
Notifications
You must be signed in to change notification settings - Fork 8.4k
feat: add llmman as an OpenAI-compatible provider #7217
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -341,6 +341,7 @@ def _ensure_litellm() -> bool: | |
| "deepseek", | ||
| "ollama", | ||
| "ollama_chat", | ||
| "llmman", | ||
| "hosted_vllm", | ||
| "cerebras", | ||
| "dashscope", | ||
|
|
@@ -446,6 +447,7 @@ def __new__(cls, model: str, is_litellm: bool = False, **kwargs: Any) -> LLM: | |
| "deepseek": "deepseek", | ||
| "ollama": "ollama", | ||
| "ollama_chat": "ollama_chat", | ||
| "llmman": "llmman", | ||
| "hosted_vllm": "hosted_vllm", | ||
| "cerebras": "cerebras", | ||
| "dashscope": "dashscope", | ||
|
|
@@ -561,8 +563,8 @@ def _matches_provider_pattern(cls, model: str, provider: str) -> bool: | |
| if provider == "deepseek": | ||
| return model_lower.startswith("deepseek") | ||
|
|
||
| if provider == "ollama" or provider == "ollama_chat": | ||
| # Ollama accepts any local model name | ||
| if provider in ("ollama", "ollama_chat", "llmman"): | ||
| # Local servers accept any model name they can resolve | ||
|
Comment on lines
+566
to
+567
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- repository knowledge files ---'
find /tmp/coderabbit-repo-knowledge/crewaiinc-crewai-a921f2d1 -type f -name '*.md' -print
printf '%s\n' '--- relevant source ---'
sed -n '320,365p;430,475p;545,590p;690,725p' lib/crewai/src/crewai/llm.py
printf '%s\n' '--- direct definitions and usages ---'
rg -n --glob '*.py' 'OpenAICompatibleCompletion|llmman|model_part|class LLM|def __init__' lib/crewai/src/crewai/llm.py lib/crewai/src/crewai 2>/dev/null | head -200Repository: crewAIInc/crewAI Length of output: 26232 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- library review conventions ---'
cat /tmp/coderabbit-repo-knowledge/crewaiinc-crewai-a921f2d1/conventions/lib.md 2>/dev/null || true
printf '%s\n' '--- LLM model parsing and construction ---'
sed -n '390,490p' lib/crewai/src/crewai/llm.py
sed -n '690,730p' lib/crewai/src/crewai/llm.py
printf '%s\n' '--- OpenAI-compatible provider implementation ---'
sed -n '1,270p' lib/crewai/src/crewai/llms/providers/openai_compatible/completion.py
printf '%s\n' '--- relevant tests ---'
rg -n -C 4 'llmman|OpenAICompatibleCompletion|model.*empty|empty.*model' lib/crewai/tests lib/crewai/src/crewai 2>/dev/null | head -240Repository: crewAIInc/crewAI Length of output: 36657 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- model validation helper ---'
sed -n '490,575p' lib/crewai/src/crewai/llm.py
printf '%s\n' '--- OpenAI completion and base model declarations ---'
rg -n -C 8 'class OpenAICompletion|class BaseLLM|model:|model_validator|field_validator' \
lib/crewai/src/crewai/llms/providers/openai/completion.py \
lib/crewai/src/crewai/llms/base_llm.pyRepository: crewAIInc/crewAI Length of output: 50372 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- BaseLLM fields and validators ---'
sed -n '65,180p' lib/crewai/src/crewai/llms/base_llm.py
printf '%s\n' '--- OpenAICompletion fields and validators ---'
sed -n '186,315p' lib/crewai/src/crewai/llms/providers/openai/completion.pyRepository: crewAIInc/crewAI Length of output: 10107 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- complete constant-validation path ---'
sed -n '570,630p' lib/crewai/src/crewai/llm.pyRepository: crewAIInc/crewAI Length of output: 2320 Reject an empty
🤖 Prompt for AI Agents |
||
| return True | ||
|
|
||
| if provider == "hosted_vllm": | ||
|
|
@@ -704,6 +706,7 @@ def _get_native_provider(cls, provider: str) -> type | None: | |
| "deepseek", | ||
| "ollama", | ||
| "ollama_chat", | ||
| "llmman", | ||
| "hosted_vllm", | ||
| "cerebras", | ||
| "dashscope", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -70,6 +70,13 @@ class ProviderConfig: | |
| api_key_required=False, | ||
| default_api_key="ollama", | ||
| ), | ||
| "llmman": ProviderConfig( | ||
| base_url="http://localhost:17434/v1", | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: # Inspect the provider configuration, changed URL resolution, and the parent
# request construction without executing repository code.
printf '%s\n' '--- repository conventions ---'
find /tmp/coderabbit-repo-knowledge/crewaiinc-crewai-a921f2d1 -maxdepth 2 -type f -name '*.md' -print
printf '%s\n' '--- provider file ---'
sed -n '1,285p' lib/crewai/src/crewai/llms/providers/openai_compatible/completion.py
printf '%s\n' '--- OpenAI completion definitions and request path references ---'
rg -n -S 'class OpenAICompletion|OpenAI\\(|AsyncOpenAI|base_url|api_key|chat\\.completions|responses\\.create' lib/crewai/src/crewai/llms/providers/openai lib/crewai/src/crewai/llms/providers/openai_compatibleRepository: crewAIInc/crewAI Length of output: 11181 🏁 Script executed: #!/bin/bash
set -o pipefail
printf '%s\n' '--- applicable repository guidance ---'
cat /tmp/coderabbit-repo-knowledge/crewaiinc-crewai-a921f2d1/conventions/repo-wide.md 2>/dev/null || true
cat /tmp/coderabbit-repo-knowledge/crewaiinc-crewai-a921f2d1/conventions/lib.md 2>/dev/null || true
printf '%s\n' '--- parent completion file ---'
fd -t f -i 'completion.py' lib/crewai/src/crewai/llms/providers/openai
printf '%s\n' '--- parent symbols and request references ---'
rg -n -F -e 'class OpenAICompletion' -e 'OpenAI(' -e 'AsyncOpenAI' -e 'base_url' -e 'api_key' -e 'chat.completions' -e 'responses.create' lib/crewai/src/crewai/llms/providers/openai
printf '%s\n' '--- parent completion outline ---'
ast-grep outline lib/crewai/src/crewai/llms/providers/openai/completion.pyRepository: crewAIInc/crewAI Length of output: 7313 🏁 Script executed: #!/bin/bash
printf '%s\n' '--- LLMMAN references ---'
rg -n -i --glob '!*.pyc' --glob '!node_modules/**' --glob '!dist/**' \
'LLMMAN_HOST|LLMMAN_API_KEY|llmman' \
README.md docs lib/crewai/src lib/crewai/tests 2>/dev/null
printf '%s\n' '--- parent client construction ---'
sed -n '300,322p' lib/crewai/src/crewai/llms/providers/openai/completion.py
sed -n '390,422p' lib/crewai/src/crewai/llms/providers/openai/completion.py
sed -n '1918,1945p' lib/crewai/src/crewai/llms/providers/openai/completion.pyRepository: crewAIInc/crewAI Length of output: 7540 Sensitive Data Exposure (CWE-319): Cleartext Transmission of Sensitive Information Reachability: Internal · Exploitability: Moderate Require a transport policy for non-loopback The provider accepts an HTTP 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||
| api_key_env="LLMMAN_API_KEY", | ||
| base_url_env="LLMMAN_HOST", | ||
| api_key_required=False, | ||
| default_api_key="llmman", | ||
| ), | ||
| "hosted_vllm": ProviderConfig( | ||
| base_url="http://localhost:8000/v1", | ||
| api_key_env="VLLM_API_KEY", | ||
|
|
@@ -92,11 +99,11 @@ class ProviderConfig: | |
| } | ||
|
|
||
|
|
||
| def _normalize_ollama_base_url(base_url: str) -> str: | ||
| """Normalize Ollama base URL to ensure it ends with /v1. | ||
| def _normalize_local_base_url(base_url: str) -> str: | ||
| """Ensure a local server base URL ends with /v1. | ||
|
|
||
| Ollama uses OLLAMA_HOST which may not include the /v1 suffix, | ||
| but the OpenAI-compatible endpoint requires it. | ||
| Hosts configured via OLLAMA_HOST or LLMMAN_HOST are bare host:port values, | ||
| but the OpenAI-compatible endpoint requires the /v1 suffix. | ||
|
|
||
| Args: | ||
| base_url: The base URL, potentially without /v1 suffix. | ||
|
|
@@ -122,6 +129,7 @@ class OpenAICompatibleCompletion(OpenAICompletion): | |
| - deepseek: DeepSeek (https://deepseek.com) | ||
| - ollama: Ollama local server (https://ollama.ai) | ||
| - ollama_chat: Alias for ollama | ||
| - llmman: llmman local server (https://github.com/llmmanorg/llmman) | ||
| - hosted_vllm: vLLM server (https://github.com/vllm-project/vllm) | ||
| - cerebras: Cerebras (https://cerebras.ai) | ||
| - dashscope: Alibaba Dashscope/Qwen (https://dashscope.aliyun.com) | ||
|
|
@@ -222,8 +230,8 @@ def _resolve_base_url( | |
| else: | ||
| resolved = config.base_url | ||
|
|
||
| if provider in ("ollama", "ollama_chat"): | ||
| resolved = _normalize_ollama_base_url(resolved) | ||
| if provider in ("ollama", "ollama_chat", "llmman"): | ||
| resolved = _normalize_local_base_url(resolved) | ||
|
|
||
| return resolved | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -10,7 +10,7 @@ | |||||||||||||||||||||||
| OPENAI_COMPATIBLE_PROVIDERS, | ||||||||||||||||||||||||
| OpenAICompatibleCompletion, | ||||||||||||||||||||||||
| ProviderConfig, | ||||||||||||||||||||||||
| _normalize_ollama_base_url, | ||||||||||||||||||||||||
| _normalize_local_base_url, | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
@@ -73,6 +73,24 @@ def test_ollama_chat_is_alias(self): | |||||||||||||||||||||||
| assert ollama.base_url == ollama_chat.base_url | ||||||||||||||||||||||||
| assert ollama.api_key_required == ollama_chat.api_key_required | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def test_llmman_config(self): | ||||||||||||||||||||||||
| """Test llmman provider configuration.""" | ||||||||||||||||||||||||
| config = OPENAI_COMPATIBLE_PROVIDERS["llmman"] | ||||||||||||||||||||||||
| assert config.base_url == "http://localhost:17434/v1" | ||||||||||||||||||||||||
| assert config.api_key_env == "LLMMAN_API_KEY" | ||||||||||||||||||||||||
| assert config.base_url_env == "LLMMAN_HOST" | ||||||||||||||||||||||||
| assert config.api_key_required is False | ||||||||||||||||||||||||
| assert config.default_api_key == "llmman" | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def test_llmman_base_url_does_not_collide(self): | ||||||||||||||||||||||||
| """llmman must not reuse another provider's default endpoint.""" | ||||||||||||||||||||||||
| others = [ | ||||||||||||||||||||||||
| cfg.base_url | ||||||||||||||||||||||||
| for name, cfg in OPENAI_COMPATIBLE_PROVIDERS.items() | ||||||||||||||||||||||||
| if name != "llmman" | ||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||
| assert OPENAI_COMPATIBLE_PROVIDERS["llmman"].base_url not in others | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def test_hosted_vllm_config(self): | ||||||||||||||||||||||||
| """Test hosted_vllm provider configuration.""" | ||||||||||||||||||||||||
| config = OPENAI_COMPATIBLE_PROVIDERS["hosted_vllm"] | ||||||||||||||||||||||||
|
|
@@ -96,24 +114,24 @@ def test_dashscope_config(self): | |||||||||||||||||||||||
| assert config.api_key_required is True | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| class TestNormalizeOllamaBaseUrl: | ||||||||||||||||||||||||
| """Tests for _normalize_ollama_base_url helper.""" | ||||||||||||||||||||||||
| class TestNormalizeLocalBaseUrl: | ||||||||||||||||||||||||
| """Tests for _normalize_local_base_url helper.""" | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def test_adds_v1_suffix(self): | ||||||||||||||||||||||||
| """Test that /v1 is added when missing.""" | ||||||||||||||||||||||||
| assert _normalize_ollama_base_url("http://localhost:11434") == "http://localhost:11434/v1" | ||||||||||||||||||||||||
| assert _normalize_local_base_url("http://localhost:11434") == "http://localhost:11434/v1" | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def test_preserves_existing_v1(self): | ||||||||||||||||||||||||
| """Test that existing /v1 is preserved.""" | ||||||||||||||||||||||||
| assert _normalize_ollama_base_url("http://localhost:11434/v1") == "http://localhost:11434/v1" | ||||||||||||||||||||||||
| assert _normalize_local_base_url("http://localhost:11434/v1") == "http://localhost:11434/v1" | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def test_strips_trailing_slash(self): | ||||||||||||||||||||||||
| """Test that trailing slash is handled.""" | ||||||||||||||||||||||||
| assert _normalize_ollama_base_url("http://localhost:11434/") == "http://localhost:11434/v1" | ||||||||||||||||||||||||
| assert _normalize_local_base_url("http://localhost:11434/") == "http://localhost:11434/v1" | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def test_handles_v1_with_trailing_slash(self): | ||||||||||||||||||||||||
| """Test /v1/ is normalized.""" | ||||||||||||||||||||||||
| assert _normalize_ollama_base_url("http://localhost:11434/v1/") == "http://localhost:11434/v1" | ||||||||||||||||||||||||
| assert _normalize_local_base_url("http://localhost:11434/v1/") == "http://localhost:11434/v1" | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| class TestOpenAICompatibleCompletion: | ||||||||||||||||||||||||
|
|
@@ -197,6 +215,12 @@ def test_ollama_base_url_normalized(self): | |||||||||||||||||||||||
| completion = OpenAICompatibleCompletion(model="llama3", provider="ollama") | ||||||||||||||||||||||||
| assert completion.base_url == "http://custom-ollama:11434/v1" | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def test_llmman_base_url_normalized(self): | ||||||||||||||||||||||||
| """Test llmman base URL is normalized to include /v1.""" | ||||||||||||||||||||||||
| with patch.dict(os.environ, {"LLMMAN_HOST": "http://custom-llmman:17434"}): | ||||||||||||||||||||||||
| completion = OpenAICompatibleCompletion(model="qwen3.8", provider="llmman") | ||||||||||||||||||||||||
| assert completion.base_url == "http://custom-llmman:17434/v1" | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def test_openrouter_headers(self): | ||||||||||||||||||||||||
| """Test OpenRouter has HTTP-Referer header.""" | ||||||||||||||||||||||||
| with patch.dict(os.environ, {"OPENROUTER_API_KEY": "test-key"}): | ||||||||||||||||||||||||
|
|
@@ -242,6 +266,21 @@ def test_llm_creates_openai_compatible_for_ollama(self): | |||||||||||||||||||||||
| assert llm.provider == "ollama" | ||||||||||||||||||||||||
| assert llm.model == "llama3" | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def test_llm_creates_openai_compatible_for_llmman(self): | ||||||||||||||||||||||||
| """Test LLM factory creates OpenAICompatibleCompletion for llmman.""" | ||||||||||||||||||||||||
| llm = LLM(model="llmman/qwen3.8") | ||||||||||||||||||||||||
| assert isinstance(llm, OpenAICompatibleCompletion) | ||||||||||||||||||||||||
| assert llm.provider == "llmman" | ||||||||||||||||||||||||
| assert llm.model == "qwen3.8" | ||||||||||||||||||||||||
| assert llm.base_url == "http://localhost:17434/v1" | ||||||||||||||||||||||||
|
Comment on lines
+271
to
+275
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Make the default URL assertion independent of This test reads the process environment but asserts the default URL. A developer or CI job that defines Proposed test isolation- llm = LLM(model="llmman/qwen3.8")
- assert isinstance(llm, OpenAICompatibleCompletion)
- assert llm.provider == "llmman"
- assert llm.model == "qwen3.8"
- assert llm.base_url == "http://localhost:17434/v1"
+ with patch.dict(os.environ, {"LLMMAN_HOST": ""}):
+ llm = LLM(model="llmman/qwen3.8")
+ assert isinstance(llm, OpenAICompatibleCompletion)
+ assert llm.provider == "llmman"
+ assert llm.model == "qwen3.8"
+ assert llm.base_url == "http://localhost:17434/v1"📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def test_llm_creates_openai_compatible_for_explicit_llmman(self): | ||||||||||||||||||||||||
| """Test LLM factory routes an explicit llmman provider without a prefix.""" | ||||||||||||||||||||||||
| llm = LLM(model="qwen3.8", provider="llmman") | ||||||||||||||||||||||||
| assert isinstance(llm, OpenAICompatibleCompletion) | ||||||||||||||||||||||||
| assert llm.provider == "llmman" | ||||||||||||||||||||||||
| assert llm.model == "qwen3.8" | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def test_llm_creates_openai_compatible_for_openrouter(self): | ||||||||||||||||||||||||
| """Test LLM factory creates OpenAICompatibleCompletion for OpenRouter.""" | ||||||||||||||||||||||||
| with patch.dict(os.environ, {"OPENROUTER_API_KEY": "test-key"}): | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: crewAIInc/crewAI
Length of output: 29021
🏁 Script executed:
Repository: crewAIInc/crewAI
Length of output: 33502
🏁 Script executed:
Repository: crewAIInc/crewAI
Length of output: 13237
Write
LLMMAN_HOSTfor llmman. The CLI writesAPI_BASEunchanged to.env, butOpenAICompatibleCompletionresolves the llmman endpoint only fromLLMMAN_HOST. Therefore, changing the generatedAPI_BASEdoes not change the runtime endpoint. UseLLMMAN_HOSTor add an explicit translation.🤖 Prompt for AI Agents