Skip to content

OllamaModelBackend.is_model_available() is case-sensitive, causing false negatives against locally installed models #1690

Description

@lamoghaighoroje

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

  1. 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).
  2. 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)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions