Add llama.cpp and vLLM local providers

Add llama.cpp / llama-server support: - Register llama.cpp chat and embedding providers through hosted_vllm defaults on host.docker.internal:8080. - Add onboarding metadata, a bundled icon, no-key provider metadata, model discovery coverage, setup docs, and tests. Add vLLM support: - Register vLLM chat and embedding providers through hosted_vllm defaults on host.docker.internal:8000. - Add onboarding metadata, a bundled icon, no-key provider metadata, model discovery coverage, setup docs, and tests. - Strip empty tools arrays from the Responses-to-chat fallback path so strict OpenAI-compatible servers accept local vLLM calls.

Alessandro committed Jun 15, 2026 at 05:37 UTC 9bcef39028a8d5579cfa9752c1584eb8d39cd4ca
18 files changed +450 -6
conf/model_providers.yaml
+30
@@ -86,6 +86,15 @@ chat:
86 kwargs:
87 api_base: "http://host.docker.internal:1234/v1"
88 api_key: "lm-studio"
89 + llama_cpp:
90 + name: llama.cpp
91 + litellm_provider: hosted_vllm
92 + models_list:
93 + endpoint_url: "/v1/models"
94 + default_base: "http://host.docker.internal:8080"
95 + kwargs:
96 + api_base: "http://host.docker.internal:8080/v1"
97 + api_key: "llama-cpp"
98 mistral:
99 name: Mistral AI
100 litellm_provider: mistral
@@ -167,6 +176,15 @@ chat:
176 api_base: https://api.venice.ai/api/v1
177 venice_parameters:
178 include_venice_system_prompt: false
179 + vllm:
180 + name: vLLM
181 + litellm_provider: hosted_vllm
182 + models_list:
183 + endpoint_url: "/v1/models"
184 + default_base: "http://host.docker.internal:8000"
185 + kwargs:
186 + api_base: "http://host.docker.internal:8000/v1"
187 + api_key: "vllm"
188 xai:
189 name: xAI
190 litellm_provider: xai
@@ -203,6 +221,12 @@ embedding:
221 kwargs:
222 api_base: "http://host.docker.internal:1234/v1"
223 api_key: "lm-studio"
224 + llama_cpp:
225 + name: llama.cpp
226 + litellm_provider: hosted_vllm
227 + kwargs:
228 + api_base: "http://host.docker.internal:8080/v1"
229 + api_key: "llama-cpp"
230 mistral:
231 name: Mistral AI
232 litellm_provider: mistral
@@ -250,6 +274,12 @@ embedding:
274 endpoint_url: "https://api.venice.ai/api/v1/models"
275 kwargs:
276 api_base: https://api.venice.ai/api/v1
277 + vllm:
278 + name: vLLM
279 + litellm_provider: hosted_vllm
280 + kwargs:
281 + api_base: "http://host.docker.internal:8000/v1"
282 + api_key: "vllm"
283 other:
284 name: Other OpenAI compatible
285 litellm_provider: openai
docs/setup/installation.md
+67
@@ -439,6 +439,8 @@ Use the naming format required by your selected provider:
439 | OpenRouter | Provider prefix mostly required | `anthropic/claude-sonnet-4-5` |
440 | Ollama | Model name only | `gpt-oss:20b` |
441 | oMLX | API-visible model name from `/v1/models` | `Qwen3-0.6B-4bit` |
442 +| llama.cpp | API-visible model name from `/v1/models` or `--alias` | `local-gguf` |
443 +| vLLM | Hugging Face model ID or served model alias | `Qwen/Qwen2.5-1.5B-Instruct` |
444
445 > [!TIP]
446 > If you see "Invalid model ID," verify the provider and naming format on the provider website, or search the web for "<name-of-ai-model> model naming".
@@ -504,6 +506,71 @@ omlx serve --model-dir ~/.omlx/models --paged-ssd-cache-dir ~/.omlx/cache
506
507 ---
508
509 +## Installing and Using llama.cpp (GGUF Local Models)
510 +
511 +llama.cpp provides `llama-server`, a lightweight OpenAI-compatible HTTP server for GGUF models. Agent Zero talks to it through the same `/v1` API used by OpenAI-compatible clients.
512 +
513 +### macOS llama.cpp Installation
514 +
515 +**Using Homebrew:**
516 +
517 +```bash
518 +brew install llama.cpp
519 +```
520 +
521 +Start a server with a downloaded GGUF model:
522 +
523 +```bash
524 +llama-server -m ~/models/model.gguf --port 8080 --alias local-gguf
525 +```
526 +
527 +By default, Agent Zero expects llama.cpp at `http://host.docker.internal:8080/v1`. The model name can be the model path returned by `/v1/models`, but using `--alias` gives you a short stable name such as `local-gguf`.
528 +
529 +### Configuring llama.cpp in Agent Zero
530 +
531 +1. Start `llama-server` and confirm `http://localhost:8080/v1/models` returns your model.
532 +2. In Agent Zero Settings, choose **llama.cpp** as the Chat model, Utility model, or Embedding model provider.
533 +3. Use the model ID shown by `/v1/models`, or the alias you passed with `--alias`.
534 +4. Override the API base URL only if you started `llama-server` on another host or port.
535 +5. Click `Save` to confirm your settings.
536 +
537 +> [!NOTE]
538 +> If Agent Zero runs in Docker and cannot reach a host-side `llama-server`, start the server on an address Docker can reach, for example `--host 0.0.0.0`, and keep the port firewalled to trusted clients.
539 +
540 +---
541 +
542 +## Installing and Using vLLM (Local OpenAI-Compatible Serving)
543 +
544 +vLLM is a high-throughput local inference server with an OpenAI-compatible API. It is most common on Linux GPU hosts, and can also run on Apple Silicon through the vLLM Apple Silicon path or vLLM-Metal.
545 +
546 +For Apple Silicon Macs, install and activate vLLM-Metal:
547 +
548 +```bash
549 +curl -fsSL https://raw.githubusercontent.com/vllm-project/vllm-metal/main/install.sh | bash
550 +source ~/.venv-vllm-metal/bin/activate
551 +```
552 +
553 +Start a basic OpenAI-compatible server:
554 +
555 +```bash
556 +vllm serve Qwen/Qwen2.5-1.5B-Instruct --host 0.0.0.0 --port 8000
557 +```
558 +
559 +By default, Agent Zero expects vLLM at `http://host.docker.internal:8000/v1`, matching vLLM's default HTTP port. If another local provider already uses port 8000, start vLLM on another port and update Agent Zero's API base, for example `http://host.docker.internal:8001/v1`.
560 +
561 +### Configuring vLLM in Agent Zero
562 +
563 +1. Start vLLM and confirm `http://localhost:8000/v1/models` returns the served model.
564 +2. In Agent Zero Settings, choose **vLLM** as the Chat model, Utility model, or Embedding model provider.
565 +3. Use the model ID returned by vLLM's model list endpoint.
566 +4. If you started vLLM with `--api-key`, enter the same key in the advanced provider settings or environment.
567 +5. Click `Save` to confirm your settings.
568 +
569 +> [!NOTE]
570 +> vLLM serves one model at a time by default. Use a generation model for Chat and Utility slots, and a separate embedding-capable vLLM server if you want vLLM embeddings.
571 +
572 +---
573 +
574 ## Installing and Using Ollama (Local Models)
575
576 Ollama is a powerful tool that allows you to run various large language models locally.
helpers/litellm_transport.py
+3
@@ -459,6 +459,7 @@ class ChatCompletionsTransport:
459 chat_kwargs = dict(kwargs)
460 _drop_internal_transport_kwargs(chat_kwargs)
461 if not _has_tools(chat_kwargs.get("tools")):
462 + chat_kwargs.pop("tools", None)
463 chat_kwargs.pop("tool_choice", None)
464 chat_kwargs.pop("parallel_tool_calls", None)
465 if _is_openai_prompt_cache_provider(model, chat_kwargs):
@@ -1538,6 +1539,8 @@ def _is_responses_not_supported_error(exc: Exception) -> bool:
1539 "unsupportedparamserror",
1540 "does not support parameters",
1541 "no 'tools' defined while 'tool_choice' is specified",
1542 + "tools` must not be an empty array",
1543 + "tools must not be an empty array",
1544 )
1545 )
1546
helpers/litellm_transport.py.dox.md new
+45
@@ -0,0 +1,45 @@
1 +# litellm_transport.py DOX
2 +
3 +## Purpose
4 +
5 +- Own Agent Zero's LiteLLM transport adapter for Chat Completions and Responses API calls.
6 +- Normalize Agent Zero model-call kwargs into provider-safe LiteLLM requests.
7 +- Preserve canonical response metadata for history, provider-state continuation, and fallback decisions.
8 +
9 +## Ownership
10 +
11 +- `litellm_transport.py` owns the runtime implementation.
12 +- `litellm_transport.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
13 +- Classes:
14 +- `TransportMode`
15 +- `TransportRecovery`
16 +- `TransportPolicy`
17 +- `LiteLLMTransport`
18 +- `ChatCompletionsTransport`
19 +- `ResponsesTransport`
20 +- `ResponsesEventParser`
21 +- Top-level functions include transport cache reset, request normalization, parsing, prompt-cache preparation, and response/error classifiers.
22 +
23 +## Runtime Contracts
24 +
25 +- Keep provider selection and provider-specific defaults outside this helper; callers pass a resolved LiteLLM model name and kwargs.
26 +- Strip Agent Zero internal kwargs before sending requests to LiteLLM.
27 +- Do not send orphan tool controls when no tools are present; strict OpenAI-compatible servers can reject empty `tools` arrays.
28 +- Prefer Responses API when configured, but fallback to Chat Completions when the provider does not support Responses.
29 +- Preserve provider-state metadata when Responses API calls succeed, and fall back to local replay when provider state is unsupported.
30 +- Keep prompt-cache markers only for providers that accept them.
31 +
32 +## Work Guidance
33 +
34 +- Add provider-agnostic request cleanup here when multiple OpenAI-compatible providers can benefit.
35 +- Treat fallback behavior as a shared transport contract, not a provider registry.
36 +- Keep tool conversion symmetric between Chat Completions and Responses requests.
37 +
38 +## Verification
39 +
40 +- Run `pytest tests/test_stream_tool_early_stop.py tests/test_responses_architecture.py -q` after changing transport normalization or fallback behavior.
41 +- Run local-provider smoke checks when changing OpenAI-compatible request cleanup.
42 +
43 +## Child DOX Index
44 +
45 +No child DOX files.
helpers/llm_result.py.dox.md new
+39
@@ -0,0 +1,39 @@
1 +# llm_result.py DOX
2 +
3 +## Purpose
4 +
5 +- Own canonical LLM result metadata shared by model transports, history, and tool-result processing.
6 +- Preserve Responses API output items, provider response IDs, reasoning text, usage, and capability metadata in a serializable form.
7 +
8 +## Ownership
9 +
10 +- `llm_result.py` owns the runtime implementation.
11 +- `llm_result.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
12 +- Classes:
13 +- `ResponseItem`
14 +- `ResponseFunctionCall`
15 +- `LLMResult`
16 +- Top-level functions include metadata conversion, function-call output item construction, object normalization, output-text extraction, reasoning extraction, and function-call argument parsing.
17 +
18 +## Runtime Contracts
19 +
20 +- `LLMResult.metadata()` stores data under `RESPONSE_METADATA_KEY` so history can round-trip provider state.
21 +- `from_response(...)` must preserve provider `response_id`, `previous_response_id`, raw output items, usage, and capability metadata.
22 +- `from_chat(...)` must produce an equivalent chat-completions result with `mode="chat_completions"` and `state="off"`.
23 +- Function-call output items must preserve `call_id` and optional acknowledged safety checks.
24 +- Argument parsing must tolerate JSON strings, dictionaries, and malformed values without throwing.
25 +
26 +## Work Guidance
27 +
28 +- Keep metadata backward-compatible with existing serialized chat history.
29 +- Treat unknown response item types as preserved built-in items unless they are local function calls, message text, or reasoning.
30 +- Avoid provider-specific assumptions in result parsing.
31 +
32 +## Verification
33 +
34 +- Run `pytest tests/test_responses_architecture.py -q` after changing result metadata behavior.
35 +- Run focused history/tool-processing tests when changing function-call serialization.
36 +
37 +## Child DOX Index
38 +
39 +No child DOX files.
helpers/tunnel_origins.py.dox.md new
+37
@@ -0,0 +1,37 @@
1 +# tunnel_origins.py DOX
2 +
3 +## Purpose
4 +
5 +- Own origin normalization for Remote Control tunnel URLs and CSRF/WebSocket same-origin checks.
6 +- Provide a small helper boundary between tunnel discovery and security enforcement.
7 +
8 +## Ownership
9 +
10 +- `tunnel_origins.py` owns the runtime implementation.
11 +- `tunnel_origins.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
12 +- Top-level functions:
13 +- `origin_from_url(value)`
14 +- `origin_key(value)`
15 +- `get_active_tunnel_origins()`
16 +
17 +## Runtime Contracts
18 +
19 +- Normalize URL and Origin header values to `scheme://host[:port]`, omitting default ports.
20 +- Return comparable origin keys with default ports restored for same-origin checks.
21 +- Treat invalid, missing, or malformed origins as `None`.
22 +- Discover active tunnel origins from `TunnelManager` and the Docker tunnel API without raising if either source is unavailable.
23 +- Keep tunnel service lookups short-timeout and local-only.
24 +
25 +## Work Guidance
26 +
27 +- Keep parsing based on `urllib.parse` rather than hand-rolled string checks.
28 +- Preserve defensive exception handling because tunnel services are optional and may not be running.
29 +- Coordinate security-sensitive changes with CSRF and WebSocket tests.
30 +
31 +## Verification
32 +
33 +- Run `pytest tests/test_csrf_tunnel_origins.py tests/test_ws_csrf.py -q` after changing tunnel origin behavior.
34 +
35 +## Child DOX Index
36 +
37 +No child DOX files.
plugins/_browser/helpers/connector_runtime.py
+1 -1
@@ -58,7 +58,7 @@ HOST_BROWSER_PROFILE_MODE_KEY = getattr(
58 "host_browser_profile_mode",
59 )
60 get_browser_config = browser_config.get_browser_config
61 -_LOCAL_PROVIDERS = {"ollama", "lm_studio", "omlx"}
61 +_LOCAL_PROVIDERS = {"ollama", "lm_studio", "llama_cpp", "omlx", "vllm"}
62 _LOCAL_HOSTS = {"localhost", "127.0.0.1", "::1", "host.docker.internal"}
63 _SENSITIVE_ACTIONS = {"content", "detail", "evaluate", "screenshot", "screenshot_file"}
64 _KEY_ALIASES = {
plugins/_model_config/api/model_search.py
+8 -2
@@ -26,6 +26,12 @@ _NON_CHAT_EXCLUDE = frozenset({
26 "omni-moderation",
27 "vision-preview",
28 })
29 +_LOCAL_PLACEHOLDER_KEYS = {
30 + "lm_studio": {"lm-studio"},
31 + "llama_cpp": {"llama-cpp"},
32 + "omlx": {"omlx"},
33 + "vllm": {"vllm"},
34 +}
35
36
37 class ModelSearch(ApiHandler):
@@ -179,8 +185,8 @@ class ModelSearch(ApiHandler):
185 elif provider == "azure":
186 if has_key:
187 headers["api-key"] = api_key
182 - elif provider not in ("ollama", "lm_studio", "omlx"):
183 - if has_key:
188 + elif provider != "ollama":
189 + if has_key and api_key not in _LOCAL_PLACEHOLDER_KEYS.get(provider, set()):
190 headers["Authorization"] = f"Bearer {api_key}"
191
192 extra = (cfg or {}).get("kwargs", {}).get("extra_headers", {})
plugins/_model_config/extensions/python/banners/_20_missing_api_key.py
+1 -1
@@ -6,7 +6,7 @@ from plugins._model_config.helpers import model_config
6 class MissingApiKeyCheck(Extension):
7 """Check if API keys are configured for selected model providers."""
8
9 - LOCAL_PROVIDERS = {"ollama", "lm_studio", "omlx"}
9 + LOCAL_PROVIDERS = {"ollama", "lm_studio", "llama_cpp", "omlx", "vllm"}
10 CONFIGURE_MODEL_SETTINGS_LINK = (
11 """<div class="onboarding-banner-btn-container" style="margin-top: 12px;">"""
12 """<button class="btn btn-ok" onclick="window.openModal('/plugins/_onboarding/webui/onboarding.html');return false;">"""
plugins/_model_config/helpers/model_config.py
+1 -1
@@ -32,7 +32,7 @@ IMPLICIT_PRESET_SLOT_DEFAULTS = {
32 "kwargs": {},
33 },
34 }
35 -LOCAL_PROVIDERS = {"ollama", "lm_studio", "omlx"}
35 +LOCAL_PROVIDERS = {"ollama", "lm_studio", "llama_cpp", "omlx", "vllm"}
36 LOCAL_EMBEDDING = {"huggingface"}
37 _PROVIDER_METADATA_CACHE: dict | None = None
38
plugins/_model_config/provider_metadata.yaml
+8
@@ -1,10 +1,14 @@
1 chat:
2 lm_studio:
3 api_key_mode: none
4 + llama_cpp:
5 + api_key_mode: none
6 ollama:
7 api_key_mode: none
8 omlx:
9 api_key_mode: none
10 + vllm:
11 + api_key_mode: none
12 other:
13 api_key_mode: optional
14
@@ -13,9 +17,13 @@ embedding:
17 api_key_mode: none
18 lm_studio:
19 api_key_mode: none
20 + llama_cpp:
21 + api_key_mode: none
22 ollama:
23 api_key_mode: none
24 omlx:
25 api_key_mode: none
26 + vllm:
27 + api_key_mode: none
28 other:
29 api_key_mode: optional
plugins/_onboarding/webui/assets/provider-logos/llama-cpp.svg new
+17
@@ -0,0 +1,17 @@
1 +<svg xmlns="http://www.w3.org/2000/svg" width="160" height="160" viewBox="0 0 160 160" role="img" aria-labelledby="llama-cpp-title">
2 + <title id="llama-cpp-title">llama.cpp</title>
3 + <defs>
4 + <linearGradient id="llama-cpp-bg" x1="0" y1="0" x2="1" y2="1">
5 + <stop offset="0" stop-color="#242424"/>
6 + <stop offset="1" stop-color="#111111"/>
7 + </linearGradient>
8 + </defs>
9 + <rect x="10" y="10" width="140" height="140" rx="28" fill="url(#llama-cpp-bg)"/>
10 + <path d="M47 111V65c0-18 14-32 32-32h9c18 0 32 14 32 32v46" fill="none" stroke="#f1e4c8" stroke-width="11" stroke-linecap="round" stroke-linejoin="round"/>
11 + <path d="M58 42 47 24M103 42l12-18" fill="none" stroke="#f1e4c8" stroke-width="10" stroke-linecap="round"/>
12 + <path d="M58 72h50" stroke="#79c7b4" stroke-width="9" stroke-linecap="round"/>
13 + <circle cx="67" cy="88" r="5" fill="#f1e4c8"/>
14 + <circle cx="96" cy="88" r="5" fill="#f1e4c8"/>
15 + <path d="M72 106h22" stroke="#f1e4c8" stroke-width="8" stroke-linecap="round"/>
16 + <text x="80" y="133" text-anchor="middle" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="23" font-weight="800" fill="#79c7b4">.cpp</text>
17 +</svg>
plugins/_onboarding/webui/assets/provider-logos/vllm.svg new
+18
@@ -0,0 +1,18 @@
1 +<svg xmlns="http://www.w3.org/2000/svg" width="160" height="160" viewBox="0 0 160 160" role="img" aria-labelledby="vllm-title">
2 + <title id="vllm-title">vLLM</title>
3 + <defs>
4 + <linearGradient id="vllm-bg" x1="0" y1="0" x2="1" y2="1">
5 + <stop offset="0" stop-color="#12303a"/>
6 + <stop offset="1" stop-color="#101820"/>
7 + </linearGradient>
8 + <linearGradient id="vllm-mark" x1="0" y1="0" x2="1" y2="1">
9 + <stop offset="0" stop-color="#8ce9dc"/>
10 + <stop offset="1" stop-color="#4da3ff"/>
11 + </linearGradient>
12 + </defs>
13 + <rect x="10" y="10" width="140" height="140" rx="28" fill="url(#vllm-bg)"/>
14 + <path d="M37 43 65 116 93 43" fill="none" stroke="url(#vllm-mark)" stroke-width="15" stroke-linecap="round" stroke-linejoin="round"/>
15 + <path d="M99 68v48M120 68v48" fill="none" stroke="#efffff" stroke-width="12" stroke-linecap="round"/>
16 + <path d="M99 92h21" stroke="#efffff" stroke-width="12" stroke-linecap="round"/>
17 + <text x="80" y="135" text-anchor="middle" font-family="Inter, ui-sans-serif, system-ui, sans-serif" font-size="21" font-weight="900" fill="#8ce9dc">vLLM</text>
18 +</svg>
plugins/_onboarding/webui/onboarding-providers.js
+19 -1
@@ -25,7 +25,7 @@ export const MORE_CLOUD_PROVIDER_IDS = [
25 "other",
26 ];
27
28 -export const LOCAL_PROVIDER_IDS = ["ollama", "lm_studio", "omlx", "other"];
28 +export const LOCAL_PROVIDER_IDS = ["ollama", "lm_studio", "omlx", "llama_cpp", "vllm", "other"];
29
30 export const ONBOARDING_PROVIDER_OVERRIDES = {
31 a0_venice: {
@@ -126,6 +126,15 @@ export const ONBOARDING_PROVIDER_OVERRIDES = {
126 model_list_autoload: true,
127 short_description: "Run local models through LM Studio.",
128 },
129 + llama_cpp: {
130 + logo: "/plugins/_onboarding/webui/assets/provider-logos/llama-cpp.svg",
131 + setup_url: "https://github.com/ggml-org/llama.cpp",
132 + docs_url: "https://github.com/ggml-org/llama.cpp/blob/master/tools/server/README.md",
133 + default_api_base: "http://host.docker.internal:8080/v1",
134 + api_key_mode: "none",
135 + model_list_autoload: true,
136 + short_description: "Run GGUF models with llama-server.",
137 + },
138 mistral: {
139 logo: "https://mistral.ai/favicon.ico",
140 setup_url: "https://console.mistral.ai/",
@@ -181,6 +190,15 @@ export const ONBOARDING_PROVIDER_OVERRIDES = {
190 model_list_autoload: true,
191 short_description: "Apple Silicon local inference with MLX.",
192 },
193 + vllm: {
194 + logo: "/plugins/_onboarding/webui/assets/provider-logos/vllm.svg",
195 + setup_url: "https://docs.vllm.ai/",
196 + docs_url: "https://docs.vllm.ai/en/stable/serving/online_serving/",
197 + default_api_base: "http://host.docker.internal:8000/v1",
198 + api_key_mode: "none",
199 + model_list_autoload: true,
200 + short_description: "High-throughput local OpenAI-compatible serving.",
201 + },
202 openai: {
203 logo: "https://openai.com/favicon.ico",
204 setup_url: "https://platform.openai.com/",
tests/test_model_config_api_keys.py
+61
@@ -274,10 +274,14 @@ def test_provider_key_modes_for_local_and_ollama_cloud():
274
275 assert model_config.provider_requires_api_key("ollama") is False
276 assert model_config.provider_requires_api_key("lm_studio") is False
277 + assert model_config.provider_requires_api_key("llama_cpp") is False
278 assert model_config.provider_requires_api_key("omlx") is False
279 + assert model_config.provider_requires_api_key("vllm") is False
280 assert model_config.provider_requires_api_key("other") is False
281 assert model_config.provider_requires_api_key("ollama_cloud") is True
282 + assert "llama_cpp" in missing_key_banner.MissingApiKeyCheck.LOCAL_PROVIDERS
283 assert "omlx" in missing_key_banner.MissingApiKeyCheck.LOCAL_PROVIDERS
284 + assert "vllm" in missing_key_banner.MissingApiKeyCheck.LOCAL_PROVIDERS
285
286
287 def test_local_provider_defaults_are_docker_friendly():
@@ -293,6 +297,15 @@ def test_local_provider_defaults_are_docker_friendly():
297 assert provider_config["chat"]["lm_studio"]["models_list"]["default_base"] == (
298 "http://host.docker.internal:1234"
299 )
300 + assert provider_config["chat"]["llama_cpp"]["litellm_provider"] == "hosted_vllm"
301 + assert provider_config["chat"]["llama_cpp"]["kwargs"]["api_base"] == (
302 + "http://host.docker.internal:8080/v1"
303 + )
304 + assert provider_config["chat"]["llama_cpp"]["kwargs"]["api_key"] == "llama-cpp"
305 + assert provider_config["chat"]["llama_cpp"]["models_list"]["default_base"] == (
306 + "http://host.docker.internal:8080"
307 + )
308 + assert provider_config["chat"]["llama_cpp"]["models_list"]["endpoint_url"] == "/v1/models"
309 assert provider_config["chat"]["ollama"]["kwargs"]["api_base"] == (
310 "http://host.docker.internal:11434"
311 )
@@ -308,10 +321,24 @@ def test_local_provider_defaults_are_docker_friendly():
321 "http://host.docker.internal:8000"
322 )
323 assert provider_config["chat"]["omlx"]["models_list"]["endpoint_url"] == "/v1/models"
324 + assert provider_config["chat"]["vllm"]["litellm_provider"] == "hosted_vllm"
325 + assert provider_config["chat"]["vllm"]["kwargs"]["api_base"] == (
326 + "http://host.docker.internal:8000/v1"
327 + )
328 + assert provider_config["chat"]["vllm"]["kwargs"]["api_key"] == "vllm"
329 + assert provider_config["chat"]["vllm"]["models_list"]["default_base"] == (
330 + "http://host.docker.internal:8000"
331 + )
332 + assert provider_config["chat"]["vllm"]["models_list"]["endpoint_url"] == "/v1/models"
333 assert provider_config["embedding"]["lm_studio"]["kwargs"]["api_base"] == (
334 "http://host.docker.internal:1234/v1"
335 )
336 assert provider_config["embedding"]["lm_studio"]["kwargs"]["api_key"] == "lm-studio"
337 + assert provider_config["embedding"]["llama_cpp"]["litellm_provider"] == "hosted_vllm"
338 + assert provider_config["embedding"]["llama_cpp"]["kwargs"]["api_base"] == (
339 + "http://host.docker.internal:8080/v1"
340 + )
341 + assert provider_config["embedding"]["llama_cpp"]["kwargs"]["api_key"] == "llama-cpp"
342 assert provider_config["embedding"]["ollama"]["kwargs"]["api_base"] == (
343 "http://host.docker.internal:11434"
344 )
@@ -320,6 +347,11 @@ def test_local_provider_defaults_are_docker_friendly():
347 "http://host.docker.internal:8000/v1"
348 )
349 assert provider_config["embedding"]["omlx"]["kwargs"]["api_key"] == "omlx"
350 + assert provider_config["embedding"]["vllm"]["litellm_provider"] == "hosted_vllm"
351 + assert provider_config["embedding"]["vllm"]["kwargs"]["api_base"] == (
352 + "http://host.docker.internal:8000/v1"
353 + )
354 + assert provider_config["embedding"]["vllm"]["kwargs"]["api_key"] == "vllm"
355
356
357 def test_local_provider_runtime_defaults_and_overrides(monkeypatch):
@@ -344,6 +376,16 @@ def test_local_provider_runtime_defaults_and_overrides(monkeypatch):
376 assert custom_lm_embedding.kwargs["api_base"] == "http://127.0.0.1:1234/v1"
377 assert custom_lm_embedding.kwargs["api_key"] == "real-local-key"
378
379 + llama_cpp_chat = models.get_chat_model("llama_cpp", "local-chat-model")
380 + assert llama_cpp_chat.model_name == "hosted_vllm/local-chat-model"
381 + assert llama_cpp_chat.kwargs["api_base"] == "http://host.docker.internal:8080/v1"
382 + assert llama_cpp_chat.kwargs["api_key"] == "llama-cpp"
383 +
384 + llama_cpp_embedding = models.get_embedding_model("llama_cpp", "local-embedding-model")
385 + assert llama_cpp_embedding.model_name == "hosted_vllm/local-embedding-model"
386 + assert llama_cpp_embedding.kwargs["api_base"] == "http://host.docker.internal:8080/v1"
387 + assert llama_cpp_embedding.kwargs["api_key"] == "llama-cpp"
388 +
389 ollama_embedding = models.get_embedding_model("ollama", "nomic-embed-text")
390 assert ollama_embedding.model_name == "ollama/nomic-embed-text"
391 assert ollama_embedding.kwargs["api_base"] == "http://host.docker.internal:11434"
@@ -368,6 +410,25 @@ def test_local_provider_runtime_defaults_and_overrides(monkeypatch):
410 assert custom_omlx_chat.kwargs["api_base"] == "http://127.0.0.1:8000/v1"
411 assert custom_omlx_chat.kwargs["api_key"] == "real-local-key"
412
413 + vllm_chat = models.get_chat_model("vllm", "local-chat-model")
414 + assert vllm_chat.model_name == "hosted_vllm/local-chat-model"
415 + assert vllm_chat.kwargs["api_base"] == "http://host.docker.internal:8000/v1"
416 + assert vllm_chat.kwargs["api_key"] == "vllm"
417 +
418 + vllm_embedding = models.get_embedding_model("vllm", "local-embedding-model")
419 + assert vllm_embedding.model_name == "hosted_vllm/local-embedding-model"
420 + assert vllm_embedding.kwargs["api_base"] == "http://host.docker.internal:8000/v1"
421 + assert vllm_embedding.kwargs["api_key"] == "vllm"
422 +
423 + custom_vllm_chat = models.get_chat_model(
424 + "vllm",
425 + "local-chat-model",
426 + api_base="http://127.0.0.1:8001/v1",
427 + api_key="real-local-key",
428 + )
429 + assert custom_vllm_chat.kwargs["api_base"] == "http://127.0.0.1:8001/v1"
430 + assert custom_vllm_chat.kwargs["api_key"] == "real-local-key"
431 +
432
433 def test_docker_compose_maps_host_docker_internal_for_local_models():
434 import yaml
tests/test_model_search.py
+10
@@ -72,6 +72,16 @@ def test_model_search_omits_auth_header_for_omlx_placeholder_key():
72 assert handler._build_headers("omlx", "omlx", {}) == {}
73
74
75 +def test_model_search_omits_auth_header_for_local_placeholder_keys():
76 + handler = _handler()
77 +
78 + assert handler._build_headers("llama_cpp", "llama-cpp", {}) == {}
79 + assert handler._build_headers("vllm", "vllm", {}) == {}
80 + assert handler._build_headers("llama_cpp", "real-local-key", {}) == {
81 + "Authorization": "Bearer real-local-key"
82 + }
83 +
84 +
85 def test_model_search_filters_non_chat_models():
86 handler = _handler()
87
tests/test_onboarding_static.py
+14
@@ -65,16 +65,24 @@ def test_onboarding_provider_grid_names_are_present_in_metadata():
65 assert 'docs_url: "https://docs.venice.ai/guides/getting-started/generating-api-key"' in provider_ui
66 assert 'docs_url: "https://docs.tokenfactory.nebius.com/api-reference/introduction"' in provider_ui
67 assert 'docs_url: "https://lmstudio.ai/docs/developer/core/authentication"' in provider_ui
68 + assert 'logo: "/plugins/_onboarding/webui/assets/provider-logos/llama-cpp.svg"' in provider_ui
69 + assert 'docs_url: "https://github.com/ggml-org/llama.cpp/blob/master/tools/server/README.md"' in provider_ui
70 + assert 'default_api_base: "http://host.docker.internal:8080/v1"' in provider_ui
71 assert 'logo: "/plugins/_onboarding/webui/assets/provider-logos/omlx.svg"' in provider_ui
72 assert 'docs_url: "https://github.com/jundot/omlx#readme"' in provider_ui
73 assert 'default_api_base: "http://host.docker.internal:8000/v1"' in provider_ui
74 + assert 'logo: "/plugins/_onboarding/webui/assets/provider-logos/vllm.svg"' in provider_ui
75 + assert 'docs_url: "https://docs.vllm.ai/en/stable/serving/online_serving/"' in provider_ui
76 assert 'docs_url: ""' in provider_ui
77 assert "api_key_mode: none" in model_metadata
78 assert "api_key_mode: optional" in model_metadata
79 assert "Ollama Cloud" in provider_yaml
80 assert "https://ollama.com/v1" in provider_yaml
81 + assert "llama.cpp" in provider_yaml
82 + assert "http://host.docker.internal:8080/v1" in provider_yaml
83 assert "oMLX" in provider_yaml
84 assert "http://host.docker.internal:8000/v1" in provider_yaml
85 + assert "vLLM" in provider_yaml
86 assert "Nebius Token Factory" in provider_yaml
87 assert "https://api.tokenfactory.nebius.com/v1" in provider_yaml
88 assert not (PROJECT_ROOT / "plugins/_model_config/conf/model_providers.yaml").exists()
@@ -92,7 +100,9 @@ def test_onboarding_provider_grid_names_are_present_in_metadata():
100 "Z.AI",
101 "Mistral AI",
102 "Azure OpenAI",
103 + "llama.cpp",
104 "oMLX",
105 + "vLLM",
106 ]:
107 assert name in provider_yaml + provider_ui
108
@@ -122,12 +132,16 @@ def test_onboarding_provider_grid_names_are_present_in_metadata():
132 "sambanova.png",
133 "cometapi.ico",
134 "github-copilot.svg",
135 + "llama-cpp.svg",
136 "zai-logo.svg",
137 "omlx.svg",
138 + "vllm.svg",
139 ]:
140 assert logo in provider_ui
141
142 + assert (PROJECT_ROOT / "plugins/_onboarding/webui/assets/provider-logos/llama-cpp.svg").exists()
143 assert (PROJECT_ROOT / "plugins/_onboarding/webui/assets/provider-logos/omlx.svg").exists()
144 + assert (PROJECT_ROOT / "plugins/_onboarding/webui/assets/provider-logos/vllm.svg").exists()
145
146
147 def test_nebius_provider_config_uses_openai_compatible_token_factory_endpoint():
tests/test_stream_tool_early_stop.py
+71
@@ -643,6 +643,77 @@ def test_responses_request_normalizes_reasoning_and_orphan_tool_choice():
643 assert "reasoning" not in request
644
645
646 +def test_chat_completions_kwargs_omit_empty_tools():
647 + kwargs = litellm_transport.ChatCompletionsTransport.prepare_kwargs(
648 + {
649 + "tools": [],
650 + "tool_choice": "auto",
651 + "parallel_tool_calls": True,
652 + "max_tokens": 8,
653 + }
654 + )
655 +
656 + assert kwargs == {"max_tokens": 8}
657 +
658 + kwargs = litellm_transport.ChatCompletionsTransport.prepare_kwargs(
659 + {
660 + "tools": [
661 + {
662 + "type": "function",
663 + "function": {
664 + "name": "lookup",
665 + "parameters": {"type": "object"},
666 + },
667 + }
668 + ],
669 + "tool_choice": "auto",
670 + }
671 + )
672 +
673 + assert kwargs["tools"][0]["function"]["name"] == "lookup"
674 + assert kwargs["tool_choice"] == "auto"
675 +
676 +
677 +def test_complete_falls_back_to_chat_when_responses_shim_sends_empty_tools(
678 + monkeypatch,
679 +):
680 + calls: list[str] = []
681 +
682 + def fake_responses(*args, **kwargs):
683 + calls.append("responses")
684 + raise RuntimeError(
685 + "Value error, `tools` must not be an empty array. "
686 + "Either provide at least one tool or omit the field entirely."
687 + )
688 +
689 + def fake_completion(*args, **kwargs):
690 + calls.append("chat")
691 + assert kwargs["drop_params"] is True
692 + assert "tools" not in kwargs
693 + assert "tool_choice" not in kwargs
694 + assert "parallel_tool_calls" not in kwargs
695 + return {"choices": [{"message": {"content": "ok"}}]}
696 +
697 + monkeypatch.setattr(litellm_transport, "responses", fake_responses)
698 + monkeypatch.setattr(litellm_transport, "completion", fake_completion)
699 +
700 + transport = litellm_transport.LiteLLMTransport(
701 + model="hosted_vllm/qwen",
702 + messages=[{"role": "user", "content": "hi"}],
703 + kwargs={
704 + "tools": [],
705 + "tool_choice": "auto",
706 + "parallel_tool_calls": True,
707 + "max_tokens": 8,
708 + },
709 + )
710 +
711 + parsed = transport.complete()
712 +
713 + assert parsed["response_delta"] == "ok"
714 + assert calls == ["responses", "chat"]
715 +
716 +
717 def test_responses_request_adds_openai_prompt_cache_key_for_static_prefix():
718 request = litellm_transport.ResponsesTransport.from_chat(
719 [