From e4949e9d353dd20a3a95e94c2a0a4b84b613120a Mon Sep 17 00:00:00 2001 From: fortesoftware Date: Tue, 11 Aug 2026 16:25:26 -0500 Subject: [PATCH] fix(ollama): report model capabilities via runtime API lookup Ollama models were missing capabilities (supports_vision, supports_function_calling) and context window data in LiteLLM's /model/info endpoint. The runtime /api/show lookup was never reached because api_base and api_key were not threaded through the enrichment pipeline, and max_output_tokens was incorrectly set to the context length. Changes: 1. Thread api_base and api_key through the model info enrichment passes (proxy_server.py). The duplicated 3-pass lookup in _enrich_model_info_with_litellm_data and _get_proxy_model_info is consolidated into get_litellm_model_info, which passes api_base and api_key from litellm_params on every attempt so the Ollama provider can reach /api/show. 2. Snapshot built-in cost map keys at class definition time in _is_static_ollama_model (common_utils.py). The Router registers every deployment into litellm.model_cost at startup via register_model(persist_across_reloads=False). These registrations are not tracked in _runtime_registered_model_cost, so the previous check treated all configured Ollama models as static, skipping the runtime lookup. Snapshotting at class definition time ensures dynamically-registered entries don't pollute the static check. 3. Check the Ollama capabilities list for function calling detection (common_utils.py). _supports_function_calling previously only checked the template string for "tools", missing models like qwen3-coder and deepseek-r1 that have "tools" in their capabilities list but not in the template. Now checks capabilities first, falling back to the template heuristic. 4. Add supports_vision detection from Ollama capabilities (common_utils.py). New _supports_vision method checks if "vision" is in the Ollama capabilities list. 5. Set max_output_tokens to None in get_runtime_model_info (common_utils.py). The Ollama /api/show endpoint only provides context_length (the total context window), not max_output_tokens. Setting it to the context length caused clients to send max_tokens values exceeding the model's actual output limit, which Ollama rejected. 6. Cache the /api/show lookup with lru_cache (common_utils.py). Passing api_key to litellm.get_model_info bypasses its LRU cache, so the network call is extracted into _cached_ollama_show keyed on (model, api_base) only, matching _cached_get_model_info and _cached_get_model_group_info elsewhere in the codebase. 7. Add missing capability fields to all 29 ollama/ cost map entries (model_prices_and_context_window.json). All entries were missing supports_vision; 12 were also missing supports_function_calling. None of these models support vision, and the 12 without function calling are older models (llama2, llama3, orca-mini, vicuna, codellama, codegemma). --- litellm/llms/ollama/common_utils.py | 63 ++++++---- litellm/proxy/proxy_server.py | 91 ++++++--------- model_prices_and_context_window.json | 99 +++++++++++----- tests/proxy_unit_tests/test_proxy_server.py | 2 +- .../llms/ollama/test_ollama_model_info.py | 108 +++++++++++++++++- 5 files changed, 256 insertions(+), 107 deletions(-) diff --git a/litellm/llms/ollama/common_utils.py b/litellm/llms/ollama/common_utils.py index ed4bab22a84..a5d5281091d 100644 --- a/litellm/llms/ollama/common_utils.py +++ b/litellm/llms/ollama/common_utils.py @@ -1,11 +1,31 @@ +from functools import lru_cache from typing import Any, Final import httpx +from litellm import model_cost as _model_cost from litellm import verbose_logger +from litellm.constants import DEFAULT_MAX_LRU_CACHE_SIZE from litellm.llms.base_llm.chat.transformation import BaseLLMException +@lru_cache(maxsize=DEFAULT_MAX_LRU_CACHE_SIZE) +def _cached_ollama_show(model: str, api_base: str, headers: tuple[tuple[str, str], ...] = ()) -> dict[str, Any] | None: + from litellm import module_level_client + + try: + response: Final = module_level_client.post( + url=f"{api_base}/api/show", + json={"name": model}, + headers=dict(headers), + ) + response.raise_for_status() + return response.json() + except Exception: + verbose_logger.debug("OllamaError: Could not get model info.") + return None + + class OllamaError(BaseLLMException): def __init__(self, status_code: int, message: str, headers: dict | httpx.Headers): super().__init__(status_code=status_code, message=message, headers=headers) @@ -52,6 +72,8 @@ class OllamaModelInfo(BaseLLMModelInfo): Returns the union of all model names. """ + _builtin_model_cost_keys: Final = frozenset(key.lower() for key in _model_cost) + @staticmethod def get_api_key(api_key=None) -> str | None: """Get API key from environment variables or litellm configuration""" @@ -141,8 +163,8 @@ class OllamaModelInfo(BaseLLMModelInfo): @staticmethod def _is_static_ollama_model(model: str) -> bool: - from litellm import model_cost - + # Snapshot at class definition time so Router-registered keys + # (added via register_model at startup) don't pollute the check stripped_model: Final = OllamaModelInfo._strip_ollama_model_prefix(model) potential_model_names: Final = { model, @@ -150,14 +172,21 @@ class OllamaModelInfo(BaseLLMModelInfo): "ollama/" + stripped_model, "ollama_chat/" + stripped_model, } - model_cost_keys: Final = {key.lower() for key in model_cost} - return any(name.lower() in model_cost_keys for name in potential_model_names) + return any(name.lower() in OllamaModelInfo._builtin_model_cost_keys for name in potential_model_names) @staticmethod def _supports_function_calling(ollama_model_info: dict) -> bool: + capabilities: Final = ollama_model_info.get("capabilities", []) + if isinstance(capabilities, list) and "tools" in capabilities: + return True _template: Final[str] = str(ollama_model_info.get("template", "") or "") return "tools" in _template.lower() + @staticmethod + def _supports_vision(ollama_model_info: dict) -> bool: + capabilities: Final = ollama_model_info.get("capabilities", []) + return isinstance(capabilities, list) and "vision" in capabilities + @staticmethod def _get_max_tokens(ollama_model_info: dict) -> int | None: _model_info: Final[dict] = ollama_model_info.get("model_info", {}) @@ -173,23 +202,15 @@ class OllamaModelInfo(BaseLLMModelInfo): api_base: str | None = None, api_key: str | None = None, ) -> dict[str, Any]: - from litellm import module_level_client - model = self._strip_ollama_model_prefix(model) passed_api_base: Final = api_base api_base = self.get_server_api_base(api_base) - api_key = self.get_api_key(api_key) if passed_api_base is None or api_key else None - headers: Final = {"Authorization": f"Bearer {api_key}"} if api_key else {} + resolved_api_key: Final = self.get_api_key(api_key) if passed_api_base is None or api_key else None + headers: Final = {"Authorization": f"Bearer {resolved_api_key}"} if resolved_api_key else {} - try: - response: Final = module_level_client.post( - url=f"{api_base}/api/show", - json={"name": model}, - headers=headers, - ) - response.raise_for_status() - except Exception: - verbose_logger.debug("OllamaError: Could not get model info.") + ollama_model_info = _cached_ollama_show(model, api_base, tuple(sorted(headers.items()))) + + if ollama_model_info is None: return { "key": model, "litellm_provider": "ollama", @@ -201,19 +222,19 @@ class OllamaModelInfo(BaseLLMModelInfo): "max_output_tokens": None, } - model_info: Final = response.json() - max_tokens: Final = self._get_max_tokens(model_info) + max_tokens: Final = self._get_max_tokens(ollama_model_info) return { "key": model, "litellm_provider": "ollama", "mode": "chat", - "supports_function_calling": self._supports_function_calling(model_info), + "supports_function_calling": self._supports_function_calling(ollama_model_info), + "supports_vision": self._supports_vision(ollama_model_info), "input_cost_per_token": 0.0, "output_cost_per_token": 0.0, "max_tokens": max_tokens, "max_input_tokens": max_tokens, - "max_output_tokens": max_tokens, + "max_output_tokens": None, } def get_model_info( diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 9ee62f94647..092b59ed7c2 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -8443,18 +8443,42 @@ def select_data_generator( ) -def get_litellm_model_info(model: dict = {}): - model_info: Final = model.get("model_info", {}) - model_to_lookup = model.get("litellm_params", {}).get("model", None) - try: - if "azure" in model_to_lookup or model_info.get("base_model"): - model_to_lookup = model_info.get("base_model", None) - litellm_model_info: Final = litellm.get_model_info(model_to_lookup) - return litellm_model_info - except Exception: - # this should not block returning on /model/info - # if litellm does not have info on the model it should return {} - return {} +def get_litellm_model_info(model: dict) -> dict: + litellm_params: Final = model.get("litellm_params", {}) + config_model_info: Final = model.get("model_info", {}) + api_base: Final = litellm_params.get("api_base") + api_key: Final = litellm_params.get("api_key") + model_to_lookup: Final = litellm_params.get("model") + base_model: Final = config_model_info.get("base_model") + + candidates: Final = tuple(m for m in (base_model, model_to_lookup) if m) + + for candidate in candidates: + try: + result = litellm.get_model_info( + model=candidate, + api_base=api_base, + api_key=api_key, + ) + if result: + return result + except Exception: + continue + + if model_to_lookup: + split_model: Final = model_to_lookup.split("/") + if len(split_model) > 1: + try: + return litellm.get_model_info( + model=split_model[-1], + custom_llm_provider=split_model[0], + api_base=api_base, + api_key=api_key, + ) + except Exception: + pass + + return {} def on_backoff(details): @@ -12467,28 +12491,6 @@ def _enrich_model_info_with_litellm_data( # input_cost_per_token, output_cost_per_token, max_tokens litellm_model_info = get_litellm_model_info(model=model) - # 2nd pass on the model, try seeing if we can find model in litellm model_cost map - if litellm_model_info == {}: - # use litellm_param model_name to get model_info - litellm_params = model.get("litellm_params", {}) - litellm_model = litellm_params.get("model", None) - try: - litellm_model_info = litellm.get_model_info(model=litellm_model) - except Exception: - litellm_model_info = {} - # 3rd pass on the model, try seeing if we can find model but without the "/" in model cost map - if litellm_model_info == {}: - # use litellm_param model_name to get model_info - litellm_params = model.get("litellm_params", {}) - litellm_model = litellm_params.get("model", None) - if litellm_model: - split_model: Final = litellm_model.split("/") - if len(split_model) > 0: - litellm_model = split_model[-1] - try: - litellm_model_info = litellm.get_model_info(model=litellm_model, custom_llm_provider=split_model[0]) - except Exception: - litellm_model_info = {} for k, v in litellm_model_info.items(): if k not in model_info: model_info[k] = v @@ -13890,27 +13892,6 @@ def _get_proxy_model_info(model: dict) -> dict: # input_cost_per_token, output_cost_per_token, max_tokens litellm_model_info = get_litellm_model_info(model=model) - # 2nd pass on the model, try seeing if we can find model in litellm model_cost map - if litellm_model_info == {}: - # use litellm_param model_name to get model_info - litellm_params = model.get("litellm_params", {}) - litellm_model = litellm_params.get("model", None) - try: - litellm_model_info = litellm.get_model_info(model=litellm_model) - except Exception: - litellm_model_info = {} - # 3rd pass on the model, try seeing if we can find model but without the "/" in model cost map - if litellm_model_info == {}: - # use litellm_param model_name to get model_info - litellm_params = model.get("litellm_params", {}) - litellm_model = litellm_params.get("model", None) - split_model: Final = litellm_model.split("/") - if len(split_model) > 0: - litellm_model = split_model[-1] - try: - litellm_model_info = litellm.get_model_info(model=litellm_model, custom_llm_provider=split_model[0]) - except Exception: - litellm_model_info = {} for k, v in litellm_model_info.items(): if k not in model_info: model_info[k] = v diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 91c10d13e8e..429d706eb0e 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -33060,7 +33060,8 @@ "max_tokens": 8192, "mode": "chat", "output_cost_per_token": 0.0, - "supports_function_calling": false + "supports_function_calling": false, + "supports_vision": false }, "ollama/codegemma": { "input_cost_per_token": 0.0, @@ -33069,7 +33070,9 @@ "max_output_tokens": 8192, "max_tokens": 8192, "mode": "completion", - "output_cost_per_token": 0.0 + "output_cost_per_token": 0.0, + "supports_vision": false, + "supports_function_calling": false }, "ollama/codellama": { "input_cost_per_token": 0.0, @@ -33078,7 +33081,9 @@ "max_output_tokens": 4096, "max_tokens": 4096, "mode": "completion", - "output_cost_per_token": 0.0 + "output_cost_per_token": 0.0, + "supports_vision": false, + "supports_function_calling": false }, "ollama/deepseek-coder-v2-base": { "input_cost_per_token": 0.0, @@ -33088,7 +33093,8 @@ "max_tokens": 8192, "mode": "completion", "output_cost_per_token": 0.0, - "supports_function_calling": true + "supports_function_calling": true, + "supports_vision": false }, "ollama/deepseek-coder-v2-instruct": { "input_cost_per_token": 0.0, @@ -33098,7 +33104,8 @@ "max_tokens": 8192, "mode": "chat", "output_cost_per_token": 0.0, - "supports_function_calling": true + "supports_function_calling": true, + "supports_vision": false }, "ollama/deepseek-coder-v2-lite-base": { "input_cost_per_token": 0.0, @@ -33108,7 +33115,8 @@ "max_tokens": 8192, "mode": "completion", "output_cost_per_token": 0.0, - "supports_function_calling": true + "supports_function_calling": true, + "supports_vision": false }, "ollama/deepseek-coder-v2-lite-instruct": { "input_cost_per_token": 0.0, @@ -33118,7 +33126,8 @@ "max_tokens": 8192, "mode": "chat", "output_cost_per_token": 0.0, - "supports_function_calling": true + "supports_function_calling": true, + "supports_vision": false }, "ollama/deepseek-v3.1:671b-cloud": { "input_cost_per_token": 0.0, @@ -33128,7 +33137,8 @@ "max_tokens": 163840, "mode": "chat", "output_cost_per_token": 0.0, - "supports_function_calling": true + "supports_function_calling": true, + "supports_vision": false }, "ollama/gpt-oss:120b-cloud": { "input_cost_per_token": 0.0, @@ -33138,7 +33148,8 @@ "max_tokens": 131072, "mode": "chat", "output_cost_per_token": 0.0, - "supports_function_calling": true + "supports_function_calling": true, + "supports_vision": false }, "ollama/gpt-oss:20b-cloud": { "input_cost_per_token": 0.0, @@ -33148,7 +33159,8 @@ "max_tokens": 131072, "mode": "chat", "output_cost_per_token": 0.0, - "supports_function_calling": true + "supports_function_calling": true, + "supports_vision": false }, "ollama/internlm2_5-20b-chat": { "input_cost_per_token": 0.0, @@ -33158,7 +33170,8 @@ "max_tokens": 8192, "mode": "chat", "output_cost_per_token": 0.0, - "supports_function_calling": true + "supports_function_calling": true, + "supports_vision": false }, "ollama/llama2": { "input_cost_per_token": 0.0, @@ -33167,7 +33180,9 @@ "max_output_tokens": 4096, "max_tokens": 4096, "mode": "chat", - "output_cost_per_token": 0.0 + "output_cost_per_token": 0.0, + "supports_vision": false, + "supports_function_calling": false }, "ollama/llama2-uncensored": { "input_cost_per_token": 0.0, @@ -33176,7 +33191,9 @@ "max_output_tokens": 4096, "max_tokens": 4096, "mode": "completion", - "output_cost_per_token": 0.0 + "output_cost_per_token": 0.0, + "supports_vision": false, + "supports_function_calling": false }, "ollama/llama2:13b": { "input_cost_per_token": 0.0, @@ -33185,7 +33202,9 @@ "max_output_tokens": 4096, "max_tokens": 4096, "mode": "chat", - "output_cost_per_token": 0.0 + "output_cost_per_token": 0.0, + "supports_vision": false, + "supports_function_calling": false }, "ollama/llama2:70b": { "input_cost_per_token": 0.0, @@ -33194,7 +33213,9 @@ "max_output_tokens": 4096, "max_tokens": 4096, "mode": "chat", - "output_cost_per_token": 0.0 + "output_cost_per_token": 0.0, + "supports_vision": false, + "supports_function_calling": false }, "ollama/llama2:7b": { "input_cost_per_token": 0.0, @@ -33203,7 +33224,9 @@ "max_output_tokens": 4096, "max_tokens": 4096, "mode": "chat", - "output_cost_per_token": 0.0 + "output_cost_per_token": 0.0, + "supports_vision": false, + "supports_function_calling": false }, "ollama/llama3": { "input_cost_per_token": 0.0, @@ -33212,7 +33235,9 @@ "max_output_tokens": 8192, "max_tokens": 8192, "mode": "chat", - "output_cost_per_token": 0.0 + "output_cost_per_token": 0.0, + "supports_vision": false, + "supports_function_calling": false }, "ollama/llama3.1": { "input_cost_per_token": 0.0, @@ -33222,7 +33247,8 @@ "max_tokens": 8192, "mode": "chat", "output_cost_per_token": 0.0, - "supports_function_calling": true + "supports_function_calling": true, + "supports_vision": false }, "ollama/llama3:70b": { "input_cost_per_token": 0.0, @@ -33231,7 +33257,9 @@ "max_output_tokens": 8192, "max_tokens": 8192, "mode": "chat", - "output_cost_per_token": 0.0 + "output_cost_per_token": 0.0, + "supports_vision": false, + "supports_function_calling": false }, "ollama/llama3:8b": { "input_cost_per_token": 0.0, @@ -33240,7 +33268,9 @@ "max_output_tokens": 8192, "max_tokens": 8192, "mode": "chat", - "output_cost_per_token": 0.0 + "output_cost_per_token": 0.0, + "supports_vision": false, + "supports_function_calling": false }, "ollama/mistral": { "input_cost_per_token": 0.0, @@ -33250,7 +33280,8 @@ "max_tokens": 8192, "mode": "completion", "output_cost_per_token": 0.0, - "supports_function_calling": true + "supports_function_calling": true, + "supports_vision": false }, "ollama/mistral-7B-Instruct-v0.1": { "input_cost_per_token": 0.0, @@ -33260,7 +33291,8 @@ "max_tokens": 8192, "mode": "chat", "output_cost_per_token": 0.0, - "supports_function_calling": true + "supports_function_calling": true, + "supports_vision": false }, "ollama/mistral-7B-Instruct-v0.2": { "input_cost_per_token": 0.0, @@ -33270,7 +33302,8 @@ "max_tokens": 32768, "mode": "chat", "output_cost_per_token": 0.0, - "supports_function_calling": true + "supports_function_calling": true, + "supports_vision": false }, "ollama/mistral-large-instruct-2407": { "input_cost_per_token": 0.0, @@ -33280,7 +33313,8 @@ "max_tokens": 8192, "mode": "chat", "output_cost_per_token": 0.0, - "supports_function_calling": true + "supports_function_calling": true, + "supports_vision": false }, "ollama/mixtral-8x22B-Instruct-v0.1": { "input_cost_per_token": 0.0, @@ -33290,7 +33324,8 @@ "max_tokens": 65536, "mode": "chat", "output_cost_per_token": 0.0, - "supports_function_calling": true + "supports_function_calling": true, + "supports_vision": false }, "ollama/mixtral-8x7B-Instruct-v0.1": { "input_cost_per_token": 0.0, @@ -33300,7 +33335,8 @@ "max_tokens": 32768, "mode": "chat", "output_cost_per_token": 0.0, - "supports_function_calling": true + "supports_function_calling": true, + "supports_vision": false }, "ollama/orca-mini": { "input_cost_per_token": 0.0, @@ -33309,7 +33345,9 @@ "max_output_tokens": 4096, "max_tokens": 4096, "mode": "completion", - "output_cost_per_token": 0.0 + "output_cost_per_token": 0.0, + "supports_vision": false, + "supports_function_calling": false }, "ollama/qwen3-coder:480b-cloud": { "input_cost_per_token": 0.0, @@ -33319,7 +33357,8 @@ "max_tokens": 262144, "mode": "chat", "output_cost_per_token": 0.0, - "supports_function_calling": true + "supports_function_calling": true, + "supports_vision": false }, "ollama/vicuna": { "input_cost_per_token": 0.0, @@ -33328,7 +33367,9 @@ "max_output_tokens": 2048, "max_tokens": 2048, "mode": "completion", - "output_cost_per_token": 0.0 + "output_cost_per_token": 0.0, + "supports_vision": false, + "supports_function_calling": false }, "omni-moderation-2024-09-26": { "input_cost_per_token": 0.0, diff --git a/tests/proxy_unit_tests/test_proxy_server.py b/tests/proxy_unit_tests/test_proxy_server.py index 04bc80bf0d6..512f3d71f99 100644 --- a/tests/proxy_unit_tests/test_proxy_server.py +++ b/tests/proxy_unit_tests/test_proxy_server.py @@ -3116,4 +3116,4 @@ def test_get_litellm_model_info(data): new=get_info_mock, ): get_litellm_model_info(model=model) - get_info_mock.assert_called_once_with(data["expected"]) + get_info_mock.assert_called_once_with(model=data["expected"], api_base=None, api_key=None) diff --git a/tests/test_litellm/llms/ollama/test_ollama_model_info.py b/tests/test_litellm/llms/ollama/test_ollama_model_info.py index 8d46151ecce..10ac7f1cac1 100644 --- a/tests/test_litellm/llms/ollama/test_ollama_model_info.py +++ b/tests/test_litellm/llms/ollama/test_ollama_model_info.py @@ -24,7 +24,15 @@ if "httpx" not in sys.modules: import httpx import litellm -from litellm.llms.ollama.common_utils import OllamaModelInfo +from litellm.llms.ollama.common_utils import OllamaModelInfo, _cached_ollama_show + + +@pytest.fixture(autouse=True) +def _clear_ollama_show_cache(): + """Clear the lru_cache on _cached_ollama_show before each test.""" + _cached_ollama_show.cache_clear() + yield + _cached_ollama_show.cache_clear() class DummyResponse: @@ -517,6 +525,7 @@ class TestOllamaGetModelInfo: ) assert captured_json[0]["name"] == "my-custom-model" + _cached_ollama_show.cache_clear() config.get_model_info( "ollama_chat/my-custom-model", api_base="http://localhost:11434" ) @@ -580,6 +589,103 @@ class TestOllamaGetModelInfo: assert model_info["litellm_provider"] == "ollama" +class TestOllamaModelInfoCapabilities: + """Tests for capability detection from Ollama /api/show response.""" + + def test_get_runtime_model_info_reports_vision_capability(self, monkeypatch): + """supports_vision should be True when capabilities includes 'vision'.""" + from litellm.llms.ollama.completion.transformation import OllamaConfig + + def mock_post(url, json, headers=None): + return DummyResponse( + { + "template": "{{ .System }} tools {{ .Prompt }}", + "capabilities": ["completion", "tools", "vision"], + "model_info": {"llama.context_length": 131072}, + }, + status_code=200, + ) + + monkeypatch.setattr("litellm.module_level_client.post", mock_post) + + config = OllamaConfig() + result = config.get_model_info("my-vision-model", api_base="http://localhost:11434") + + assert result["supports_vision"] is True + assert result["supports_function_calling"] is True + assert result["max_input_tokens"] == 131072 + + def test_get_runtime_model_info_no_vision_capability(self, monkeypatch): + """supports_vision should be False when capabilities lacks 'vision'.""" + from litellm.llms.ollama.completion.transformation import OllamaConfig + + def mock_post(url, json, headers=None): + return DummyResponse( + { + "template": "{{ .System }} {{ .Prompt }}", + "capabilities": ["completion"], + "model_info": {"llama.context_length": 8192}, + }, + status_code=200, + ) + + monkeypatch.setattr("litellm.module_level_client.post", mock_post) + + config = OllamaConfig() + result = config.get_model_info("my-text-model", api_base="http://localhost:11434") + + assert result["supports_vision"] is False + + def test_get_runtime_model_info_no_capabilities_field(self, monkeypatch): + """supports_vision should be False when capabilities field is absent.""" + from litellm.llms.ollama.completion.transformation import OllamaConfig + + def mock_post(url, json, headers=None): + return DummyResponse( + { + "template": "{{ .System }} {{ .Prompt }}", + "model_info": {"llama.context_length": 8192}, + }, + status_code=200, + ) + + monkeypatch.setattr("litellm.module_level_client.post", mock_post) + + config = OllamaConfig() + result = config.get_model_info("my-text-model", api_base="http://localhost:11434") + + assert result["supports_vision"] is False + + def test_litellm_get_model_info_threads_api_base_to_ollama(self, monkeypatch): + """litellm.get_model_info should pass api_base through to the Ollama provider hook.""" + captured_urls = [] + + def mock_post(url, json, headers=None): + captured_urls.append(url) + return DummyResponse( + { + "template": "{{ .System }} tools {{ .Prompt }}", + "capabilities": ["completion", "tools", "vision"], + "model_info": {"llama.context_length": 32768}, + }, + status_code=200, + ) + + litellm.get_model_info.cache_clear() + monkeypatch.setattr("litellm.module_level_client.post", mock_post) + try: + model_info = litellm.get_model_info( + "ollama_chat/llama3.2-vision:11b", + api_base="http://remote-ollama:11434", + ) + finally: + litellm.get_model_info.cache_clear() + + assert captured_urls[0] == "http://remote-ollama:11434/api/show" + assert model_info["supports_vision"] is True + assert model_info["supports_function_calling"] is True + assert model_info["max_input_tokens"] == 32768 + class TestOllamaAuthHeaders: """Tests for Ollama authentication header handling in completion calls."""