"""Tests for Ollama Cloud provider integration.""" import os import pytest from unittest.mock import patch, MagicMock from hermes_cli.auth import PROVIDER_REGISTRY, resolve_provider, resolve_api_key_provider_credentials from hermes_cli.models import _PROVIDER_MODELS, _PROVIDER_LABELS, _PROVIDER_ALIASES, normalize_provider from hermes_cli.model_normalize import normalize_model_for_provider from agent.model_metadata import _URL_TO_PROVIDER, _PROVIDER_PREFIXES from agent.models_dev import PROVIDER_TO_MODELS_DEV, list_agentic_models # ── Provider Registry ── class TestOllamaCloudProviderRegistry: def test_ollama_cloud_in_registry(self): assert "ollama-cloud" in PROVIDER_REGISTRY def test_ollama_cloud_config(self): pconfig = PROVIDER_REGISTRY["ollama-cloud"] assert pconfig.id == "ollama-cloud" assert pconfig.name == "Ollama Cloud" assert pconfig.auth_type == "api_key" assert pconfig.inference_base_url == "https://ollama.com/v1" def test_ollama_cloud_env_vars(self): pconfig = PROVIDER_REGISTRY["ollama-cloud"] assert pconfig.api_key_env_vars == ("OLLAMA_API_KEY",) assert pconfig.base_url_env_var == "OLLAMA_BASE_URL" def test_ollama_cloud_base_url(self): assert "ollama.com" in PROVIDER_REGISTRY["ollama-cloud"].inference_base_url # ── Provider Aliases ── PROVIDER_ENV_VARS = ( "OPENROUTER_API_KEY", "OPENAI_API_KEY", "ANTHROPIC_API_KEY", "GOOGLE_API_KEY", "GEMINI_API_KEY", "OLLAMA_API_KEY", "GLM_API_KEY", "ZAI_API_KEY", "KIMI_API_KEY", "MINIMAX_API_KEY", "DEEPSEEK_API_KEY", ) @pytest.fixture(autouse=True) def _clean_provider_env(monkeypatch): for var in PROVIDER_ENV_VARS: monkeypatch.delenv(var, raising=False) class TestOllamaCloudAliases: def test_explicit_ollama_cloud(self): assert resolve_provider("ollama-cloud") == "ollama-cloud" def test_alias_ollama_underscore(self): """ollama_cloud (underscore) is the unambiguous cloud alias.""" assert resolve_provider("ollama_cloud") == "ollama-cloud" def test_bare_ollama_stays_local(self): """Bare 'ollama' alias routes to 'custom' (local) — not cloud.""" assert resolve_provider("ollama") == "custom" def test_models_py_aliases(self): assert _PROVIDER_ALIASES.get("ollama_cloud") == "ollama-cloud" # bare "ollama" stays local assert _PROVIDER_ALIASES.get("ollama") == "custom" def test_normalize_provider(self): assert normalize_provider("ollama-cloud") == "ollama-cloud" # ── Auto-detection ── class TestOllamaCloudAutoDetection: def test_auto_detects_ollama_api_key(self, monkeypatch): monkeypatch.setenv("OLLAMA_API_KEY", "test-ollama-key") assert resolve_provider("auto") == "ollama-cloud" # ── Credential Resolution ── class TestOllamaCloudCredentials: def test_resolve_with_ollama_api_key(self, monkeypatch): monkeypatch.setenv("OLLAMA_API_KEY", "ollama-secret") creds = resolve_api_key_provider_credentials("ollama-cloud") assert creds["provider"] == "ollama-cloud" assert creds["api_key"] == "ollama-secret" assert creds["base_url"] == "https://ollama.com/v1" def test_resolve_with_custom_base_url(self, monkeypatch): monkeypatch.setenv("OLLAMA_API_KEY", "key") monkeypatch.setenv("OLLAMA_BASE_URL", "https://custom.ollama/v1") creds = resolve_api_key_provider_credentials("ollama-cloud") assert creds["base_url"] == "https://custom.ollama/v1" def test_runtime_ollama_cloud(self, monkeypatch): monkeypatch.setenv("OLLAMA_API_KEY", "ollama-key") from hermes_cli.runtime_provider import resolve_runtime_provider result = resolve_runtime_provider(requested="ollama-cloud") assert result["provider"] == "ollama-cloud" assert result["api_mode"] == "chat_completions" assert result["api_key"] == "ollama-key" assert result["base_url"] == "https://ollama.com/v1" # ── Model Catalog (dynamic — no static list) ── class TestOllamaCloudModelCatalog: def test_no_static_model_list(self): """Ollama Cloud models are fetched dynamically — no static list to maintain.""" assert "ollama-cloud" not in _PROVIDER_MODELS def test_provider_label(self): assert "ollama-cloud" in _PROVIDER_LABELS assert _PROVIDER_LABELS["ollama-cloud"] == "Ollama Cloud" def test_provider_model_ids_returns_dynamic_models(self, tmp_path, monkeypatch): """provider_model_ids('ollama-cloud') should call fetch_ollama_cloud_models().""" from hermes_cli.models import provider_model_ids monkeypatch.setenv("HERMES_HOME", str(tmp_path)) monkeypatch.setenv("OLLAMA_API_KEY", "test-key") mock_mdev = { "ollama-cloud": { "models": { "qwen3.5:397b": {"tool_call": True}, "glm-5": {"tool_call": True}, } } } with patch("hermes_cli.models.fetch_api_models", return_value=["qwen3.5:397b"]), \ patch("agent.models_dev.fetch_models_dev", return_value=mock_mdev): result = provider_model_ids("ollama-cloud", force_refresh=True) assert len(result) > 0 assert "qwen3.5:397b" in result # ── Model Picker (list_authenticated_providers) ── class TestOllamaCloudModelPicker: def test_ollama_cloud_shows_model_count(self, tmp_path, monkeypatch): """Ollama Cloud should show non-zero model count in provider picker.""" from hermes_cli.model_switch import list_authenticated_providers monkeypatch.setenv("HERMES_HOME", str(tmp_path)) monkeypatch.setenv("OLLAMA_API_KEY", "test-key") mock_mdev = { "ollama-cloud": { "models": { "qwen3.5:397b": {"tool_call": True}, "glm-5": {"tool_call": True}, } } } with patch("hermes_cli.models.fetch_api_models", return_value=["qwen3.5:397b"]), \ patch("agent.models_dev.fetch_models_dev", return_value=mock_mdev): providers = list_authenticated_providers(current_provider="ollama-cloud") ollama = next((p for p in providers if p["slug"] == "ollama-cloud"), None) assert ollama is not None, "ollama-cloud should appear when OLLAMA_API_KEY is set" assert ollama["total_models"] > 0, "ollama-cloud should show non-zero model count" def test_ollama_cloud_not_shown_without_creds(self, monkeypatch): """Ollama Cloud should not appear without credentials.""" from hermes_cli.model_switch import list_authenticated_providers monkeypatch.delenv("OLLAMA_API_KEY", raising=False) providers = list_authenticated_providers(current_provider="openrouter") ollama = next((p for p in providers if p["slug"] == "ollama-cloud"), None) assert ollama is None, "ollama-cloud should not appear without OLLAMA_API_KEY" # ── Merged Model Discovery ── class TestOllamaCloudMergedDiscovery: def test_merges_live_and_models_dev(self, tmp_path, monkeypatch): """Live API models appear first, models.dev additions fill gaps.""" from hermes_cli.models import fetch_ollama_cloud_models monkeypatch.setenv("HERMES_HOME", str(tmp_path)) monkeypatch.setenv("OLLAMA_API_KEY", "test-key") mock_mdev = { "ollama-cloud": { "models": { "glm-5": {"tool_call": True}, "kimi-k2.5": {"tool_call": True}, "nemotron-3-super": {"tool_call": True}, } } } with patch("hermes_cli.models.fetch_api_models", return_value=["qwen3.5:397b", "glm-5"]), \ patch("agent.models_dev.fetch_models_dev", return_value=mock_mdev): result = fetch_ollama_cloud_models(force_refresh=True) # Live models first, then models.dev additions (deduped) assert result[0] == "qwen3.5:397b" # from live API assert result[1] == "glm-5" # from live API (also in models.dev) assert "kimi-k2.5" in result # from models.dev only assert "nemotron-3-super" in result # from models.dev only assert result.count("glm-5") == 1 # no duplicates def test_falls_back_to_models_dev_without_api_key(self, tmp_path, monkeypatch): """Without API key, only models.dev results are returned.""" from hermes_cli.models import fetch_ollama_cloud_models monkeypatch.setenv("HERMES_HOME", str(tmp_path)) monkeypatch.delenv("OLLAMA_API_KEY", raising=False) mock_mdev = { "ollama-cloud": { "models": { "glm-5": {"tool_call": True}, } } } with patch("agent.models_dev.fetch_models_dev", return_value=mock_mdev): result = fetch_ollama_cloud_models(force_refresh=True) assert result == ["glm-5"] def test_uses_disk_cache(self, tmp_path, monkeypatch): """Second call returns cached results without hitting APIs.""" from hermes_cli.models import fetch_ollama_cloud_models monkeypatch.setenv("HERMES_HOME", str(tmp_path)) monkeypatch.setenv("OLLAMA_API_KEY", "test-key") with patch("hermes_cli.models.fetch_api_models", return_value=["model-a"]) as mock_api, \ patch("agent.models_dev.fetch_models_dev", return_value={}): first = fetch_ollama_cloud_models(force_refresh=True) assert first == ["model-a"] assert mock_api.call_count == 1 # Second call — should use disk cache, not call API second = fetch_ollama_cloud_models() assert second == ["model-a"] assert mock_api.call_count == 1 # no extra API call def test_force_refresh_bypasses_cache(self, tmp_path, monkeypatch): """force_refresh=True always hits the API even with fresh cache.""" from hermes_cli.models import fetch_ollama_cloud_models monkeypatch.setenv("HERMES_HOME", str(tmp_path)) monkeypatch.setenv("OLLAMA_API_KEY", "test-key") with patch("hermes_cli.models.fetch_api_models", return_value=["model-a"]) as mock_api, \ patch("agent.models_dev.fetch_models_dev", return_value={}): fetch_ollama_cloud_models(force_refresh=True) fetch_ollama_cloud_models(force_refresh=True) assert mock_api.call_count == 2 def test_stale_cache_used_on_total_failure(self, tmp_path, monkeypatch): """If both API and models.dev fail, stale cache is returned.""" from hermes_cli.models import fetch_ollama_cloud_models, _save_ollama_cloud_cache monkeypatch.setenv("HERMES_HOME", str(tmp_path)) monkeypatch.setenv("OLLAMA_API_KEY", "test-key") # Pre-populate a stale cache _save_ollama_cloud_cache(["stale-model"]) # Make the cache appear stale by backdating it import json cache_path = tmp_path / "ollama_cloud_models_cache.json" with open(cache_path) as f: data = json.load(f) data["cached_at"] = 0 # epoch = very stale with open(cache_path, "w") as f: json.dump(data, f) with patch("hermes_cli.models.fetch_api_models", return_value=None), \ patch("agent.models_dev.fetch_models_dev", return_value={}): result = fetch_ollama_cloud_models(force_refresh=True) assert result == ["stale-model"] def test_empty_on_total_failure_no_cache(self, tmp_path, monkeypatch): """Returns empty list when everything fails and no cache exists.""" from hermes_cli.models import fetch_ollama_cloud_models monkeypatch.setenv("HERMES_HOME", str(tmp_path)) monkeypatch.delenv("OLLAMA_API_KEY", raising=False) with patch("agent.models_dev.fetch_models_dev", return_value={}): result = fetch_ollama_cloud_models(force_refresh=True) assert result == [] # ── Model Normalization ── class TestOllamaCloudModelNormalization: def test_passthrough_bare_name(self): """Ollama Cloud is a passthrough provider — model names used as-is.""" assert normalize_model_for_provider("qwen3.5:397b", "ollama-cloud") == "qwen3.5:397b" def test_passthrough_with_tag(self): assert normalize_model_for_provider("cogito-2.1:671b", "ollama-cloud") == "cogito-2.1:671b" def test_passthrough_no_tag(self): assert normalize_model_for_provider("glm-5", "ollama-cloud") == "glm-5" # ── URL-to-Provider Mapping ── class TestOllamaCloudUrlMapping: def test_url_to_provider(self): assert _URL_TO_PROVIDER.get("ollama.com") == "ollama-cloud" def test_provider_prefix_canonical(self): assert "ollama-cloud" in _PROVIDER_PREFIXES def test_provider_prefix_alias(self): assert "ollama" in _PROVIDER_PREFIXES # ── models.dev Integration ── class TestOllamaCloudModelsDev: def test_ollama_cloud_mapped(self): assert PROVIDER_TO_MODELS_DEV.get("ollama-cloud") == "ollama-cloud" def test_list_agentic_models_with_mock_data(self): """list_agentic_models filters correctly from mock models.dev data.""" mock_data = { "ollama-cloud": { "models": { "qwen3.5:397b": {"tool_call": True}, "glm-5": {"tool_call": True}, "nemotron-3-nano:30b": {"tool_call": True}, "some-embedding:latest": {"tool_call": False}, } } } with patch("agent.models_dev.fetch_models_dev", return_value=mock_data): result = list_agentic_models("ollama-cloud") assert "qwen3.5:397b" in result assert "glm-5" in result assert "nemotron-3-nano:30b" in result assert "some-embedding:latest" not in result # no tool_call # ── Agent Init (no SyntaxError) ── class TestOllamaCloudAgentInit: def test_agent_imports_without_error(self): """Verify run_agent.py has no SyntaxError.""" import importlib import run_agent importlib.reload(run_agent) def test_ollama_cloud_agent_uses_chat_completions(self, monkeypatch): """Ollama Cloud falls through to chat_completions — no special elif needed.""" monkeypatch.setenv("OLLAMA_API_KEY", "test-key") with patch("run_agent.OpenAI") as mock_openai: mock_openai.return_value = MagicMock() from run_agent import AIAgent agent = AIAgent( model="qwen3.5:397b", provider="ollama-cloud", api_key="test-key", base_url="https://ollama.com/v1", ) assert agent.api_mode == "chat_completions" assert agent.provider == "ollama-cloud" # ── providers.py New System ── class TestOllamaCloudProvidersNew: def test_overlay_exists(self): from hermes_cli.providers import HERMES_OVERLAYS assert "ollama-cloud" in HERMES_OVERLAYS overlay = HERMES_OVERLAYS["ollama-cloud"] assert overlay.transport == "openai_chat" assert overlay.base_url_env_var == "OLLAMA_BASE_URL" def test_alias_resolves(self): from hermes_cli.providers import normalize_provider as np assert np("ollama") == "custom" # bare "ollama" = local assert np("ollama-cloud") == "ollama-cloud" def test_label_override(self): from hermes_cli.providers import _LABEL_OVERRIDES assert _LABEL_OVERRIDES.get("ollama-cloud") == "Ollama Cloud" def test_get_label(self): from hermes_cli.providers import get_label assert get_label("ollama-cloud") == "Ollama Cloud" def test_get_provider(self): from hermes_cli.providers import get_provider pdef = get_provider("ollama-cloud") assert pdef is not None assert pdef.id == "ollama-cloud" assert pdef.transport == "openai_chat" # ── Auxiliary Model ── class TestOllamaCloudAuxiliary: def test_aux_model_defined(self): from agent.auxiliary_client import _API_KEY_PROVIDER_AUX_MODELS assert "ollama-cloud" in _API_KEY_PROVIDER_AUX_MODELS assert _API_KEY_PROVIDER_AUX_MODELS["ollama-cloud"] == "nemotron-3-nano:30b"