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:
2026-03-14 18:04:40 -04:00
2 changed files with 133 additions and 15 deletions

View File

@@ -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]:

View File

@@ -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"