forked from Rockachopa/Timmy-time-dashboard
Merge pull request '[loop-cycle-3] fix: model introspection prefix-match collision (#77)' (#91) from fix/model-introspection-prefix-match into main
This commit is contained in:
@@ -55,26 +55,45 @@ def get_system_info() -> dict[str, Any]:
|
||||
|
||||
|
||||
def _get_ollama_model() -> str:
|
||||
"""Query Ollama API to get the current model."""
|
||||
"""Query Ollama API to get the actual running model.
|
||||
|
||||
Strategy:
|
||||
1. /api/ps — models currently loaded in memory (most accurate)
|
||||
2. /api/tags — all installed models (fallback)
|
||||
Both use exact name match to avoid prefix collisions
|
||||
(e.g. 'qwen3:30b' vs 'qwen3.5:latest').
|
||||
"""
|
||||
from config import settings
|
||||
|
||||
configured = settings.ollama_model
|
||||
|
||||
try:
|
||||
# First try to get tags to see available models
|
||||
# First: check actually loaded models via /api/ps
|
||||
response = httpx.get(f"{settings.ollama_url}/api/ps", timeout=5)
|
||||
if response.status_code == 200:
|
||||
running = response.json().get("models", [])
|
||||
for model in running:
|
||||
name = model.get("name", "")
|
||||
if name == configured or name == f"{configured}:latest":
|
||||
return name
|
||||
# Configured model not loaded — return first running model
|
||||
# so Timmy reports what's *actually* serving his requests
|
||||
if running:
|
||||
return running[0].get("name", configured)
|
||||
|
||||
# Second: check installed models via /api/tags (exact match)
|
||||
response = httpx.get(f"{settings.ollama_url}/api/tags", timeout=5)
|
||||
if response.status_code == 200:
|
||||
models = response.json().get("models", [])
|
||||
# Check if configured model is available
|
||||
for model in models:
|
||||
if model.get("name", "").startswith(settings.ollama_model.split(":")[0]):
|
||||
return settings.ollama_model
|
||||
|
||||
# Fallback: return configured model
|
||||
return settings.ollama_model
|
||||
installed = response.json().get("models", [])
|
||||
for model in installed:
|
||||
name = model.get("name", "")
|
||||
if name == configured or name == f"{configured}:latest":
|
||||
return configured
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Fallback to configured model
|
||||
return settings.ollama_model
|
||||
return configured
|
||||
|
||||
|
||||
def check_ollama_health() -> dict[str, Any]:
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
"""Tests for system introspection tools."""
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import httpx
|
||||
|
||||
|
||||
def test_get_system_info_returns_dict():
|
||||
"""System info should return a dictionary."""
|
||||
@@ -15,15 +19,17 @@ def test_get_system_info_returns_dict():
|
||||
|
||||
|
||||
def test_get_system_info_contains_model():
|
||||
"""System info should include model name."""
|
||||
from config import settings
|
||||
"""System info should include a model name (may differ from config if
|
||||
the actual running model is different — see issue #77)."""
|
||||
from timmy.tools_intro import get_system_info
|
||||
|
||||
info = get_system_info()
|
||||
|
||||
assert "model" in info
|
||||
# Model should come from settings
|
||||
assert info["model"] == settings.ollama_model
|
||||
# Model should be a non-empty string — exact value depends on what
|
||||
# Ollama has loaded (verified by TestGetOllamaModelExactMatch tests)
|
||||
assert isinstance(info["model"], str)
|
||||
assert len(info["model"]) > 0
|
||||
|
||||
|
||||
def test_get_system_info_contains_repo_root():
|
||||
@@ -59,3 +65,96 @@ def test_get_memory_status_returns_dict():
|
||||
assert isinstance(status, dict)
|
||||
assert "tier1_hot_memory" in status
|
||||
assert "tier2_vault" in status
|
||||
|
||||
|
||||
# --- _get_ollama_model exact-match tests (issue #77) ---
|
||||
|
||||
|
||||
def _mock_response(json_data, status_code=200):
|
||||
"""Create a mock httpx response."""
|
||||
resp = MagicMock(spec=httpx.Response)
|
||||
resp.status_code = status_code
|
||||
resp.json.return_value = json_data
|
||||
return resp
|
||||
|
||||
|
||||
class TestGetOllamaModelExactMatch:
|
||||
"""Ensure _get_ollama_model uses exact match, not prefix match."""
|
||||
|
||||
@patch("timmy.tools_intro.httpx.get")
|
||||
def test_exact_match_from_ps(self, mock_get):
|
||||
"""Should return exact model from /api/ps."""
|
||||
from timmy.tools_intro import _get_ollama_model
|
||||
|
||||
ps_resp = _mock_response({"models": [{"name": "qwen3:30b"}]})
|
||||
mock_get.return_value = ps_resp
|
||||
|
||||
with patch("config.settings") as mock_settings:
|
||||
mock_settings.ollama_model = "qwen3:30b"
|
||||
mock_settings.ollama_url = "http://localhost:11434"
|
||||
result = _get_ollama_model()
|
||||
|
||||
assert result == "qwen3:30b"
|
||||
|
||||
@patch("timmy.tools_intro.httpx.get")
|
||||
def test_prefix_collision_returns_correct_model(self, mock_get):
|
||||
"""qwen3:30b configured — must NOT match qwen3.5:latest (prefix bug)."""
|
||||
from timmy.tools_intro import _get_ollama_model
|
||||
|
||||
# /api/ps has both models loaded; configured is qwen3:30b
|
||||
ps_resp = _mock_response({"models": [{"name": "qwen3.5:latest"}, {"name": "qwen3:30b"}]})
|
||||
mock_get.return_value = ps_resp
|
||||
|
||||
with patch("config.settings") as mock_settings:
|
||||
mock_settings.ollama_model = "qwen3:30b"
|
||||
mock_settings.ollama_url = "http://localhost:11434"
|
||||
result = _get_ollama_model()
|
||||
|
||||
assert result == "qwen3:30b", f"Got '{result}' — prefix collision bug!"
|
||||
|
||||
@patch("timmy.tools_intro.httpx.get")
|
||||
def test_configured_model_not_running_returns_actual(self, mock_get):
|
||||
"""If configured model isn't loaded, report what IS running."""
|
||||
from timmy.tools_intro import _get_ollama_model
|
||||
|
||||
ps_resp = _mock_response({"models": [{"name": "qwen3.5:latest"}]})
|
||||
mock_get.return_value = ps_resp
|
||||
|
||||
with patch("config.settings") as mock_settings:
|
||||
mock_settings.ollama_model = "qwen3:30b"
|
||||
mock_settings.ollama_url = "http://localhost:11434"
|
||||
result = _get_ollama_model()
|
||||
|
||||
# Should report actual running model, not configured one
|
||||
assert result == "qwen3.5:latest"
|
||||
|
||||
@patch("timmy.tools_intro.httpx.get")
|
||||
def test_latest_suffix_match(self, mock_get):
|
||||
"""'qwen3:30b' config should match 'qwen3:30b:latest' from API."""
|
||||
from timmy.tools_intro import _get_ollama_model
|
||||
|
||||
ps_resp = _mock_response({"models": []})
|
||||
tags_resp = _mock_response({"models": [{"name": "qwen3:30b:latest"}]})
|
||||
mock_get.side_effect = [ps_resp, tags_resp]
|
||||
|
||||
with patch("config.settings") as mock_settings:
|
||||
mock_settings.ollama_model = "qwen3:30b"
|
||||
mock_settings.ollama_url = "http://localhost:11434"
|
||||
result = _get_ollama_model()
|
||||
|
||||
# Falls back to configured since no exact match
|
||||
assert result == "qwen3:30b"
|
||||
|
||||
@patch("timmy.tools_intro.httpx.get")
|
||||
def test_ollama_down_returns_configured(self, mock_get):
|
||||
"""If Ollama is unreachable, return configured model."""
|
||||
from timmy.tools_intro import _get_ollama_model
|
||||
|
||||
mock_get.side_effect = httpx.ConnectError("connection refused")
|
||||
|
||||
with patch("config.settings") as mock_settings:
|
||||
mock_settings.ollama_model = "qwen3:30b"
|
||||
mock_settings.ollama_url = "http://localhost:11434"
|
||||
result = _get_ollama_model()
|
||||
|
||||
assert result == "qwen3:30b"
|
||||
|
||||
Reference in New Issue
Block a user