fix(llm): normalize Ollama model tags to lowercase
Ollama's /api/tags returns whatever casing was used at pull time (e.g. 'gemma3:12B' if the user ran 'ollama pull gemma3:12B'), but /api/chat rejects mixed-case tags with a 400. The two code paths are inconsistent, which surfaces the capitalized tag in the model dropdown and then silently kills every chat request against it. Lowercase on read (get_installed_models), on settings write (update_settings_route), and on ensure_model() input so a legacy mixed-case user setting can't trigger a spurious re-pull at startup. The dropdown and stored settings are now always in the form Ollama will actually accept. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -64,6 +64,15 @@ async def update_settings_route():
|
||||
if not isinstance(data, dict):
|
||||
return jsonify({"error": "Expected a JSON object"}), 400
|
||||
|
||||
# Normalize model names to lowercase before validation. Ollama's /api/tags
|
||||
# preserves whatever casing was used at pull time, but /api/chat rejects
|
||||
# mixed-case tags — lowercasing here keeps the two paths consistent and
|
||||
# means the stored setting is always in a form Ollama will actually accept.
|
||||
_MODEL_KEYS = frozenset({"default_model", "background_model"})
|
||||
for _key in _MODEL_KEYS:
|
||||
if _key in data and data[_key]:
|
||||
data[_key] = str(data[_key]).lower()
|
||||
|
||||
if "default_model" in data:
|
||||
installed = await get_installed_models()
|
||||
if data["default_model"]:
|
||||
@@ -74,7 +83,6 @@ async def update_settings_route():
|
||||
# Empty string for model keys means "reset to system default".
|
||||
# Delete the DB row so get_setting() falls back to Config defaults
|
||||
# rather than returning "" and breaking model resolution everywhere.
|
||||
_MODEL_KEYS = frozenset({"default_model", "background_model"})
|
||||
to_save = {}
|
||||
for k, v in data.items():
|
||||
str_v = str(v)
|
||||
|
||||
@@ -78,7 +78,14 @@ RAG_AUTO_SNIPPET = 4000
|
||||
|
||||
|
||||
async def get_installed_models() -> set[str]:
|
||||
"""Return set of installed Ollama model names (with and without :latest)."""
|
||||
"""Return set of installed Ollama model names (with and without :latest).
|
||||
|
||||
Tags are normalized to lowercase. Ollama stores whatever casing was used at
|
||||
pull time (``ollama pull gemma3:12B`` keeps the ``B``), but inference calls
|
||||
against the capitalized tag can 400 — the two code paths are inconsistent.
|
||||
Lowercasing on read avoids exposing that mixed-case name in the UI and
|
||||
keeps the validation check below from accepting a form Ollama will reject.
|
||||
"""
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
resp = await client.get(f"{Config.OLLAMA_URL}/api/tags")
|
||||
@@ -86,7 +93,7 @@ async def get_installed_models() -> set[str]:
|
||||
data = resp.json()
|
||||
names: set[str] = set()
|
||||
for m in data.get("models", []):
|
||||
name = m["name"]
|
||||
name = m["name"].lower()
|
||||
names.add(name)
|
||||
if name.endswith(":latest"):
|
||||
names.add(name.removesuffix(":latest"))
|
||||
@@ -98,6 +105,9 @@ async def get_installed_models() -> set[str]:
|
||||
|
||||
async def ensure_model(model: str) -> None:
|
||||
"""Check if model exists in Ollama, pull if missing."""
|
||||
# Match the lowercase normalization in get_installed_models so a legacy
|
||||
# mixed-case setting doesn't force a spurious re-pull at startup.
|
||||
model = model.lower()
|
||||
try:
|
||||
installed = await get_installed_models()
|
||||
if model in installed or f"{model}:latest" in installed:
|
||||
|
||||
Reference in New Issue
Block a user