Description
OllamaModelBackend.is_model_available() (mellea/backends/ollama.py) compares model names case-sensitively via Python's str.startswith(), but Ollama itself treats model tags case-insensitively everywhere else (CLI ollama show, the HTTP /api/show endpoint, ollama pull, ollama cp). This causes is_model_available() to return False for a model that is genuinely installed and resolvable, whenever the locally-registered tag's casing differs from the casing used in the check — which can easily diverge because Ollama silently canonicalizes/normalizes a model's tag casing on pull, independent of what the user types.
Steps to Reproduce
- Have Ollama register a model under a tag with mixed case, e.g.
granite4.1:3B (confirmed via ollama list, ollama pull granite4.1:3b, and ollama cp granite4.1:3B granite4.1:3b all normalizing to/keeping the same 3B entry — Ollama does not create a distinct lowercase entry no matter how the tag is typed).
- Call
is_model_available("granite4.1:3b") (lowercase, matching how a config elsewhere in your own stack might store it) against that same install.
from mellea.backends.ollama import OllamaModelBackend
from ollama import Client
b = OllamaModelBackend.__new__(OllamaModelBackend)
b._client = Client()
print(b.is_model_available("granite4.1:3b")) # False
print(b.is_model_available("granite4.1:3B")) # True
Meanwhile, Ollama's own tooling resolves the lowercase form fine:
$ ollama show granite4.1:3b # exits 0, prints model details
And the HTTP API agrees:
$ curl -s http://localhost:11434/api/show -d '{"model": "granite4.1:3b"}'
# returns a normal 200 response with model details
So ollama show / /api/show resolve case-insensitively server-side, but is_model_available()'s client-side comparison against the list returned by /api/tags does not.
Root Cause
mellea/backends/ollama.py, is_model_available():
def is_model_available(self, model_name):
...
try:
models = self._client.list()
for model in models["models"]:
if model.model.startswith(model_name):
return True
return False
except Exception as e:
print(f"An error occurred: {e}")
return False
models["models"][i].model is whatever casing Ollama's /api/tags returns for the locally registered tag (its canonical casing, which the caller does not control). Comparing that against a caller-supplied model_name with str.startswith() is case-sensitive, so any casing mismatch between the two — even though both refer to the exact same model as far as Ollama itself is concerned — produces a False.
This also affects _pull_ollama_model(), since it short-circuits on is_model_available(self._model_id) before attempting a pull, and can surface downstream as "Model '<name>' not found. Please download it first." even when the model is already installed under a differently-cased tag.
Confirmed still present in v0.8.0 and on main
Checked directly against the v0.8.0 tag and the current main branch — the method is unchanged (only a self._raise_if_closed() guard was added ahead of the comparison logic; the case-sensitive startswith() check itself is identical).
Proposed Fix
Case-fold both sides before comparing, matching how Ollama's own CLI/HTTP API already resolve tags:
for model in models["models"]:
if model.model.lower().startswith(model_name.lower()):
return True
Environment
mellea version: confirmed present in 0.7.0, 0.8.0, and current main
- Ollama: local install, model registered as
granite4.1:3B
- macOS (M5 Max)
Description
OllamaModelBackend.is_model_available()(mellea/backends/ollama.py) compares model names case-sensitively via Python'sstr.startswith(), but Ollama itself treats model tags case-insensitively everywhere else (CLIollama show, the HTTP/api/showendpoint,ollama pull,ollama cp). This causesis_model_available()to returnFalsefor a model that is genuinely installed and resolvable, whenever the locally-registered tag's casing differs from the casing used in the check — which can easily diverge because Ollama silently canonicalizes/normalizes a model's tag casing on pull, independent of what the user types.Steps to Reproduce
granite4.1:3B(confirmed viaollama list,ollama pull granite4.1:3b, andollama cp granite4.1:3B granite4.1:3ball normalizing to/keeping the same3Bentry — Ollama does not create a distinct lowercase entry no matter how the tag is typed).is_model_available("granite4.1:3b")(lowercase, matching how a config elsewhere in your own stack might store it) against that same install.Meanwhile, Ollama's own tooling resolves the lowercase form fine:
$ ollama show granite4.1:3b # exits 0, prints model detailsAnd the HTTP API agrees:
So
ollama show//api/showresolve case-insensitively server-side, butis_model_available()'s client-side comparison against the list returned by/api/tagsdoes not.Root Cause
mellea/backends/ollama.py,is_model_available():models["models"][i].modelis whatever casing Ollama's/api/tagsreturns for the locally registered tag (its canonical casing, which the caller does not control). Comparing that against a caller-suppliedmodel_namewithstr.startswith()is case-sensitive, so any casing mismatch between the two — even though both refer to the exact same model as far as Ollama itself is concerned — produces aFalse.This also affects
_pull_ollama_model(), since it short-circuits onis_model_available(self._model_id)before attempting a pull, and can surface downstream as"Model '<name>' not found. Please download it first."even when the model is already installed under a differently-cased tag.Confirmed still present in v0.8.0 and on
mainChecked directly against the
v0.8.0tag and the currentmainbranch — the method is unchanged (only aself._raise_if_closed()guard was added ahead of the comparison logic; the case-sensitivestartswith()check itself is identical).Proposed Fix
Case-fold both sides before comparing, matching how Ollama's own CLI/HTTP API already resolve tags:
Environment
melleaversion: confirmed present in 0.7.0, 0.8.0, and currentmaingranite4.1:3B