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:
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user