fix(memory): handle Ollama version errors during model pull (#760)

* fix(memory): handle Ollama version errors during model pull

- Add error handling for streaming response errors in cmd_pull_model
- Add version compatibility checking before model pull
- Add min_version metadata to known embedding models
- Enhanced check-status with supports_new_models flag
- Enhanced get-recommended-models with compatibility info

Fixes silent failures when Ollama version is too old for newer
embedding models like qwen3-embedding:8b.

Fixes #758

* fix: address code review feedback

- Add defensive None handling in parse_version()
- Sort model keys by length for more specific matching
- Add compatibility note when Ollama version is unknown

---------

Co-authored-by: Andy <119136210+AndyMik90@users.noreply.github.com>
This commit is contained in:
Brett Bonner
2026-01-07 02:03:19 -08:00
committed by GitHub
parent 96b7eb4a3e
commit 01decaeb26
+119 -6
View File
@@ -16,6 +16,7 @@ Output:
import argparse import argparse
import json import json
import re
import sys import sys
import urllib.error import urllib.error
import urllib.request import urllib.request
@@ -23,6 +24,10 @@ from typing import Any
DEFAULT_OLLAMA_URL = "http://localhost:11434" DEFAULT_OLLAMA_URL = "http://localhost:11434"
# Minimum Ollama version required for newer embedding models (qwen3-embedding, etc.)
# These models were added in Ollama 0.10.0
MIN_OLLAMA_VERSION_FOR_NEW_MODELS = "0.10.0"
# Known embedding models and their dimensions # Known embedding models and their dimensions
# This list helps identify embedding models from the model name # This list helps identify embedding models from the model name
KNOWN_EMBEDDING_MODELS = { KNOWN_EMBEDDING_MODELS = {
@@ -31,10 +36,26 @@ KNOWN_EMBEDDING_MODELS = {
"dim": 768, "dim": 768,
"description": "Google EmbeddingGemma (lightweight)", "description": "Google EmbeddingGemma (lightweight)",
}, },
"qwen3-embedding": {"dim": 1024, "description": "Qwen3 Embedding (0.6B)"}, "qwen3-embedding": {
"qwen3-embedding:0.6b": {"dim": 1024, "description": "Qwen3 Embedding 0.6B"}, "dim": 1024,
"qwen3-embedding:4b": {"dim": 2560, "description": "Qwen3 Embedding 4B"}, "description": "Qwen3 Embedding (0.6B)",
"qwen3-embedding:8b": {"dim": 4096, "description": "Qwen3 Embedding 8B"}, "min_version": "0.10.0",
},
"qwen3-embedding:0.6b": {
"dim": 1024,
"description": "Qwen3 Embedding 0.6B",
"min_version": "0.10.0",
},
"qwen3-embedding:4b": {
"dim": 2560,
"description": "Qwen3 Embedding 4B",
"min_version": "0.10.0",
},
"qwen3-embedding:8b": {
"dim": 4096,
"description": "Qwen3 Embedding 8B",
"min_version": "0.10.0",
},
"bge-base-en": {"dim": 768, "description": "BAAI General Embedding - Base"}, "bge-base-en": {"dim": 768, "description": "BAAI General Embedding - Base"},
"bge-large-en": {"dim": 1024, "description": "BAAI General Embedding - Large"}, "bge-large-en": {"dim": 1024, "description": "BAAI General Embedding - Large"},
"bge-small-en": {"dim": 384, "description": "BAAI General Embedding - Small"}, "bge-small-en": {"dim": 384, "description": "BAAI General Embedding - Small"},
@@ -63,6 +84,7 @@ RECOMMENDED_EMBEDDING_MODELS = [
"size_estimate": "3.1 GB", "size_estimate": "3.1 GB",
"dim": 2560, "dim": 2560,
"badge": "recommended", "badge": "recommended",
"min_ollama_version": "0.10.0",
}, },
{ {
"name": "qwen3-embedding:8b", "name": "qwen3-embedding:8b",
@@ -70,6 +92,7 @@ RECOMMENDED_EMBEDDING_MODELS = [
"size_estimate": "6.0 GB", "size_estimate": "6.0 GB",
"dim": 4096, "dim": 4096,
"badge": "quality", "badge": "quality",
"min_ollama_version": "0.10.0",
}, },
{ {
"name": "qwen3-embedding:0.6b", "name": "qwen3-embedding:0.6b",
@@ -77,6 +100,7 @@ RECOMMENDED_EMBEDDING_MODELS = [
"size_estimate": "494 MB", "size_estimate": "494 MB",
"dim": 1024, "dim": 1024,
"badge": "fast", "badge": "fast",
"min_ollama_version": "0.10.0",
}, },
{ {
"name": "embeddinggemma", "name": "embeddinggemma",
@@ -112,6 +136,22 @@ EMBEDDING_PATTERNS = [
] ]
def parse_version(version_str: str | None) -> tuple[int, ...]:
"""Parse a version string like '0.10.0' into a tuple for comparison."""
if not version_str or not isinstance(version_str, str):
return (0, 0, 0)
# Extract just the numeric parts (handles versions like "0.10.0-rc1")
match = re.match(r"(\d+)\.(\d+)\.(\d+)", version_str)
if match:
return tuple(int(x) for x in match.groups())
return (0, 0, 0)
def version_gte(version: str | None, min_version: str | None) -> bool:
"""Check if version >= min_version."""
return parse_version(version) >= parse_version(min_version)
def output_json(success: bool, data: Any = None, error: str | None = None) -> None: def output_json(success: bool, data: Any = None, error: str | None = None) -> None:
"""Output JSON result to stdout and exit.""" """Output JSON result to stdout and exit."""
result = {"success": success} result = {"success": success}
@@ -145,6 +185,14 @@ def fetch_ollama_api(base_url: str, endpoint: str, timeout: int = 5) -> dict | N
return None return None
def get_ollama_version(base_url: str) -> str | None:
"""Get the Ollama server version."""
result = fetch_ollama_api(base_url, "api/version")
if result:
return result.get("version")
return None
def is_embedding_model(model_name: str) -> bool: def is_embedding_model(model_name: str) -> bool:
"""Check if a model name suggests it's an embedding model.""" """Check if a model name suggests it's an embedding model."""
name_lower = model_name.lower() name_lower = model_name.lower()
@@ -192,6 +240,19 @@ def get_embedding_description(model_name: str) -> str:
return "Embedding model" return "Embedding model"
def get_model_min_version(model_name: str) -> str | None:
"""Get the minimum Ollama version required for a model."""
name_lower = model_name.lower()
# Sort keys by length descending to match more specific names first
# e.g., "qwen3-embedding:8b" before "qwen3-embedding"
for known_model in sorted(KNOWN_EMBEDDING_MODELS.keys(), key=len, reverse=True):
if known_model in name_lower:
return KNOWN_EMBEDDING_MODELS[known_model].get("min_version")
return None
def cmd_check_status(args) -> None: def cmd_check_status(args) -> None:
"""Check if Ollama is running and accessible.""" """Check if Ollama is running and accessible."""
base_url = args.base_url or DEFAULT_OLLAMA_URL base_url = args.base_url or DEFAULT_OLLAMA_URL
@@ -200,12 +261,18 @@ def cmd_check_status(args) -> None:
result = fetch_ollama_api(base_url, "api/version") result = fetch_ollama_api(base_url, "api/version")
if result: if result:
version = result.get("version", "unknown")
output_json( output_json(
True, True,
data={ data={
"running": True, "running": True,
"url": base_url, "url": base_url,
"version": result.get("version", "unknown"), "version": version,
"supports_new_models": version_gte(
version, MIN_OLLAMA_VERSION_FOR_NEW_MODELS
)
if version != "unknown"
else None,
}, },
) )
else: else:
@@ -319,6 +386,9 @@ def cmd_get_recommended_models(args) -> None:
"""Get recommended embedding models with install status.""" """Get recommended embedding models with install status."""
base_url = args.base_url or DEFAULT_OLLAMA_URL base_url = args.base_url or DEFAULT_OLLAMA_URL
# Get Ollama version for compatibility checking
ollama_version = get_ollama_version(base_url)
# Get currently installed models # Get currently installed models
result = fetch_ollama_api(base_url, "api/tags") result = fetch_ollama_api(base_url, "api/tags")
installed_names = set() installed_names = set()
@@ -330,17 +400,30 @@ def cmd_get_recommended_models(args) -> None:
installed_names.add(name) installed_names.add(name)
installed_names.add(base_name) installed_names.add(base_name)
# Build recommended list with install status # Build recommended list with install status and compatibility
recommended = [] recommended = []
for model in RECOMMENDED_EMBEDDING_MODELS: for model in RECOMMENDED_EMBEDDING_MODELS:
name = model["name"] name = model["name"]
base_name = name.split(":")[0] if ":" in name else name base_name = name.split(":")[0] if ":" in name else name
is_installed = name in installed_names or base_name in installed_names is_installed = name in installed_names or base_name in installed_names
# Check version compatibility
min_version = model.get("min_ollama_version")
is_compatible = True
compatibility_note = None
if min_version and ollama_version:
is_compatible = version_gte(ollama_version, min_version)
if not is_compatible:
compatibility_note = f"Requires Ollama {min_version}+"
elif min_version and not ollama_version:
compatibility_note = "Version compatibility could not be verified"
recommended.append( recommended.append(
{ {
**model, **model,
"installed": is_installed, "installed": is_installed,
"compatible": is_compatible,
"compatibility_note": compatibility_note,
} }
) )
@@ -350,6 +433,7 @@ def cmd_get_recommended_models(args) -> None:
"recommended": recommended, "recommended": recommended,
"count": len(recommended), "count": len(recommended),
"url": base_url, "url": base_url,
"ollama_version": ollama_version,
}, },
) )
@@ -363,6 +447,19 @@ def cmd_pull_model(args) -> None:
output_error("Model name is required") output_error("Model name is required")
return return
# Check Ollama version compatibility before attempting pull
ollama_version = get_ollama_version(base_url)
min_version = get_model_min_version(model_name)
if min_version and ollama_version:
if not version_gte(ollama_version, min_version):
output_error(
f"Model '{model_name}' requires Ollama {min_version} or newer. "
f"Your version is {ollama_version}. "
f"Please upgrade Ollama: https://ollama.com/download"
)
return
try: try:
url = f"{base_url.rstrip('/')}/api/pull" url = f"{base_url.rstrip('/')}/api/pull"
data = json.dumps({"name": model_name}).encode("utf-8") data = json.dumps({"name": model_name}).encode("utf-8")
@@ -376,6 +473,22 @@ def cmd_pull_model(args) -> None:
try: try:
progress = json.loads(line.decode("utf-8")) progress = json.loads(line.decode("utf-8"))
# Check for error in the streaming response
# This handles cases like "requires newer version of Ollama"
if "error" in progress:
error_msg = progress["error"]
# Clean up the error message (remove extra whitespace/newlines)
error_msg = " ".join(error_msg.split())
# Check if it's a version-related error
if "newer version" in error_msg.lower():
error_msg = (
f"Model '{model_name}' requires a newer version of Ollama. "
f"Your version: {ollama_version or 'unknown'}. "
f"Please upgrade: https://ollama.com/download"
)
output_error(error_msg)
return
# Emit progress as NDJSON to stderr for main process to parse # Emit progress as NDJSON to stderr for main process to parse
if "completed" in progress and "total" in progress: if "completed" in progress and "total" in progress:
print( print(