{"repo":"NousResearch/hermes-agent","public_id":"5781","display_label":"MiniMax auxiliary client base URL routing (/anthropic vs /v1)","summary":"Most members concern MiniMax auxiliary/compression clients using an /anthropic base URL where an OpenAI-compatible /v1 endpoint is expected, with proposed URL normalization or routing changes. One member is a broader bundle of provider/model resolution changes that also includes the MiniMax URL fix, so it is less tightly focused.","subsystem":"agent","counts":{"members":4,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":2},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":5983,"merged_at":"2026-04-08T05:23:28+00:00"},"resolution":{"state":"unresolved","by":null},"freshness":{"state":"stale","last_verified_at":"2026-04-08T05:23:28+00:00","is_stale":true},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.5},"fix_edges":[{"issue_number":2708,"pr_number":5983,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-04-08T05:23:28+00:00"},{"issue_number":5781,"pr_number":5983,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-04-08T05:23:28+00:00"},{"issue_number":5910,"pr_number":5983,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-04-08T05:23:28+00:00"}],"members":[{"kind":"issue","number":5781,"title":"[Bug]: MiniMax auxiliary/compression clients route to wrong endpoint (/anthropic instead of /v1)","url":null,"state":"closed"},{"kind":"pr","number":5786,"title":"fix(auxiliary): normalize /anthropic base URLs to /v1 for OpenAI-compatible clients","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#5983","number":5983}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":5789,"title":"fix: route MiniMax auxiliary/compression clients to /v1 endpoint","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#5786","number":5786},{"kind":"pr","value":"#5781","number":5781},{"kind":"pr","value":"#5983","number":5983}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":5983,"title":"fix: provider/model resolution — salvage 4 community PRs + MiniMax aux URL fix","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["agent/auxiliary_client.py","agent/model_metadata.py","hermes_cli/model_switch.py","run_agent.py","tests/agent/test_auxiliary_client.py","tests/agent/test_minimax_auxiliary_url.py","tests/test_ollama_num_ctx.py"],"fix_hunks":[{"file":"agent/auxiliary_client.py","hunks":["@@ -59,6 +59,41 @@\n \n logger = logging.getLogger(__name__)\n \n+_PROVIDER_ALIASES = {\n+    \"google\": \"gemini\",\n+    \"google-gemini\": \"gemini\",\n+    \"google-ai-studio\": \"gemini\",\n+    \"glm\": \"zai\",\n+    \"z-ai\": \"zai\",\n+    \"z.ai\": \"zai\",\n+    \"zhipu\": \"zai\",\n+    \"kimi\": \"kimi-coding\",\n+    \"moonshot\": \"kimi-coding\",\n+    \"minimax-china\": \"minimax-cn\",\n+    \"minimax_cn\": \"minimax-cn\",\n+    \"claude\": \"anthropic\",\n+    \"claude-code\": \"anthropic\",\n+}\n+\n+\n+def _normalize_aux_provider(provider: Optional[str], *, for_vision: bool = False) -> str:\n+    normalized = (provider or \"auto\").strip().lower()\n+    if normalized.startswith(\"custom:\"):\n+        suffix = normalized.split(\":\", 1)[1].strip()\n+        if not suffix:\n+            return \"custom\"\n+        normalized = suffix if not for_vision else \"custom\"\n+    if normalized == \"codex\":\n+        return \"openai-codex\"\n+    if normalized == \"main\":\n+        # Resolve to the user's actual main provider so named custom providers\n+        # and non-aggregator providers (DeepSeek, Alibaba, etc.) work correctly.\n+        main_prov = _read_main_provider()\n+        if main_prov and main_prov not in (\"auto\", \"main\", \"\"):\n+            return main_prov\n+        return \"custom\"\n+    return _PROVIDER_ALIASES.get(normalized, normalized)\n+\n # Default auxiliary models for direct API-key providers (cheap/fast for side tasks)\n _API_KEY_PROVIDER_AUX_MODELS: [redacted:secret-assignment] str] = {\n     \"gemini\": \"gemini-3-flash-preview\",","@@ -106,6 +141,23 @@\n _CODEX_AUX_BASE_URL = \"https://chatgpt.com/backend-api/codex\"\n \n \n+def _to_openai_base_url(base_url: str) -> str:\n+    \"\"\"Normalize an Anthropic-style base URL to OpenAI-compatible format.\n+\n+    Some providers (MiniMax, MiniMax-CN) expose an ``/anthropic`` endpoint for\n+    the Anthropic Messages API and a separate ``/v1`` endpoint for OpenAI chat\n+    completions.  The auxiliary client uses the OpenAI SDK, so it must hit the\n+    ``/v1`` surface.  Passing the raw ``inference_base_url`` causes requests to\n+    land on ``/anthropic/chat/completions`` — a 404.\n+    \"\"\"\n+    url = str(base_url or \"\").strip().rstrip(\"/\")\n+    if url.endswith(\"/anthropic\"):\n+        rewritten = url[: -len(\"/anthropic\")] + \"/v1\"\n+        logger.debug(\"Auxiliary client: rewrote base URL %s → %s\", url, rewritten)\n+        return rewritten\n+    return url\n+\n+\n def _select_pool_entry(provider: str) -> Tuple[bool, Optional[Any]]:\n     \"\"\"Return (pool_exists_for_provider, selected_entry).\"\"\"\n     try:","@@ -635,7 +687,9 @@ def _resolve_api_key_provider() -> Tuple[Optional[OpenAI], Optional[str]]:\n             if not api_key:\n                 continue\n \n-            base_url = _pool_runtime_base_url(entry, pconfig.inference_base_url) or pconfig.inference_base_url\n+            base_url = _to_openai_base_url(\n+                _pool_runtime_base_url(entry, pconfig.inference_base_url) or pconfig.inference_base_url\n+            )\n             model = _API_KEY_PROVIDER_AUX_MODELS.get(provider_id, \"default\")\n             logger.debug(\"Auxiliary text client: %s (%s) via pool\", pconfig.name, model)\n             extra = {}","@@ -652,7 +706,9 @@ def _resolve_api_key_provider() -> Tuple[Optional[OpenAI], Optional[str]]:\n         if not api_key:\n             continue\n \n-        base_url = str(creds.get(\"base_url\", \"\")).strip().rstrip(\"/\") or pconfig.inference_base_url\n+        base_url = _to_openai_base_url(\n+            str(creds.get(\"base_url\", \"\")).strip().rstrip(\"/\") or pconfig.inference_base_url\n+        )\n         model = _API_KEY_PROVIDER_AUX_MODELS.get(provider_id, \"default\")\n         logger.debug(\"Auxiliary text client: %s (%s)\", pconfig.name, model)\n         extra = {}","@@ -778,7 +834,7 @@ def _read_main_provider() -> str:\n         if isinstance(model_cfg, dict):\n             provider = model_cfg.get(\"provider\", \"\")\n             if isinstance(provider, str) and provider.strip():\n-                return provider.strip().lower()\n+                return _normalize_aux_provider(provider)\n     except Exception:\n         pass\n     return \"\"","@@ -1140,17 +1196,7 @@ def resolve_provider_client(\n         (client, resolved_model) or (None, None) if auth is unavailable.\n     \"\"\"\n     # Normalise aliases\n-    provider = (provider or \"auto\").strip().lower()\n-    if provider == \"codex\":\n-        provider = \"openai-codex\"\n-    if provider == \"main\":\n-        # Resolve to the user's actual main provider so named custom providers\n-        # and non-aggregator providers (DeepSeek, Alibaba, etc.) work correctly.\n-        main_prov = _read_main_provider()\n-        if main_prov and main_prov not in (\"auto\", \"main\", \"\"):\n-            provider = main_prov\n-        else:\n-            provider = \"custom\"\n+    provider = _normalize_aux_provider(provider)\n \n     # ── Auto: try all providers in priority order ────────────────────\n     if provider == \"auto\":","@@ -1300,7 +1346,9 @@ def resolve_provider_client(\n                          provider, \", \".join(tried_sources))\n             return None, None\n \n-        base_url = str(creds.get(\"base_url\", \"\")).strip().rstrip(\"/\") or pconfig.inference_base_url\n+        base_url = _to_openai_base_url(\n+            str(creds.get(\"base_url\", \"\")).strip().rstrip(\"/\") or pconfig.inference_base_url\n+        )\n \n         default_model = _API_KEY_PROVIDER_AUX_MODELS.get(provider, \"\")\n         final_model = model or default_model","@@ -1384,17 +1432,7 @@ def get_async_text_auxiliary_client(task: str = \"\"):\n \n \n def _normalize_vision_provider(provider: Optional[str]) -> str:\n-    provider = (provider or \"auto\").strip().lower()\n-    if provider == \"codex\":\n-        return \"openai-codex\"\n-    if provider == \"main\":\n-        # Resolve to actual main provider — named custom providers and\n-        # non-aggregator providers need to pass through as their real name.\n-        main_prov = _read_main_provider()\n-        if main_prov and main_prov not in (\"auto\", \"main\", \"\"):\n-            return main_prov\n-        return \"custom\"\n-    return provider\n+    return _normalize_aux_provider(provider, for_vision=True)\n \n \n def _resolve_strict_vision_backend(provider: str) -> Tuple[Optional[Any], Optional[str]]:"]},{"file":"agent/model_metadata.py","hunks":["@@ -611,6 +611,59 @@ def _model_id_matches(candidate_id: str, lookup_model: str) -> bool:\n     return False\n \n \n+def query_ollama_num_ctx(model: str, base_url: str) -> Optional[int]:\n+    \"\"\"Query an Ollama server for the model's context length.\n+\n+    Returns the model's maximum context from GGUF metadata via ``/api/show``,\n+    or the explicit ``num_ctx`` from the Modelfile if set.  Returns None if\n+    the server is unreachable or not Ollama.\n+\n+    This is the value that should be passed as ``num_ctx`` in Ollama chat\n+    requests to override the default 2048.\n+    \"\"\"\n+    import httpx\n+\n+    bare_model = _strip_provider_prefix(model)\n+    server_url = base_url.rstrip(\"/\")\n+    if server_url.endswith(\"/v1\"):\n+        server_url = server_url[:-3]\n+\n+    try:\n+        server_type = detect_local_server_type(base_url)\n+    except Exception:\n+        return None\n+    if server_type != \"ollama\":\n+        return None\n+\n+    try:\n+        with httpx.Client(timeout=3.0) as client:\n+            resp = client.post(f\"{server_url}/api/show\", json={\"name\": bare_model})\n+            if resp.status_code != 200:\n+                return None\n+            data = resp.json()\n+\n+            # Prefer explicit num_ctx from Modelfile parameters (user override)\n+            params = data.get(\"parameters\", \"\")\n+            if \"num_ctx\" in params:\n+                for line in params.split(\"\\n\"):\n+                    if \"num_ctx\" in line:\n+                        parts = line.strip().split()\n+                        if len(parts) >= 2:\n+                            try:\n+                                return int(parts[-1])\n+                            except ValueError:\n+                                pass\n+\n+            # Fall back to GGUF model_info context_length (training max)\n+            model_info = data.get(\"model_info\", {})\n+            for key, value in model_info.items():\n+                if \"context_length\" in key and isinstance(value, (int, float)):\n+                    return int(value)\n+    except Exception:\n+        pass\n+    return None\n+\n+\n def _query_local_context_length(model: str, base_url: str) -> Optional[int]:\n     \"\"\"Query a local server for the model's context length.\"\"\"\n     import httpx"]},{"file":"hermes_cli/model_switch.py","hunks":["@@ -791,12 +791,12 @@ def list_authenticated_providers(\n         if overlay.auth_type in (\"oauth_device_code\", \"oauth_external\", \"external_process\"):\n             # These use auth stores, not env vars — check for auth.json entries\n             try:\n-                from hermes_cli.auth import _read_auth_store\n-                store = _read_auth_store()\n-                if store and pid in store:\n+                from hermes_cli.auth import _load_auth_store\n+                store = _load_auth_store()\n+                if store and (pid in store.get(\"providers\", {}) or pid in store.get(\"credential_pool\", {})):\n                     has_creds = True\n-            except Exception:\n-                pass\n+            except Exception as exc:\n+                logger.debug(\"Auth store check failed for %s: %s\", pid, exc)\n         if not has_creds:\n             continue\n "]},{"file":"run_agent.py","hunks":["@@ -85,6 +85,7 @@\n     estimate_tokens_rough, estimate_messages_tokens_rough, estimate_request_tokens_rough,\n     get_next_probe_tier, parse_context_limit_from_error,\n     save_context_length, is_local_endpoint,\n+    query_ollama_num_ctx,\n )\n from agent.context_compressor import ContextCompressor\n from agent.subdirectory_hints import SubdirectoryHintTracker","@@ -1216,6 +1217,33 @@ def __init__(\n         self.session_cost_status = \"unknown\"\n         self.session_cost_source = \"none\"\n         \n+        # ── Ollama num_ctx injection ──\n+        # Ollama defaults to 2048 context regardless of the model's capabilities.\n+        # When running against an Ollama server, detect the model's max context\n+        # and pass num_ctx on every chat request so the full window is used.\n+        # User override: set model.ollama_num_ctx in config.yaml to cap VRAM use.\n+        self._ollama_num_ctx: int | None = None\n+        _ollama_num_ctx_override = None\n+        if isinstance(_model_cfg, dict):\n+            _ollama_num_ctx_override = _model_cfg.get(\"ollama_num_ctx\")\n+        if _ollama_num_ctx_override is not None:\n+            try:\n+                self._ollama_num_ctx = int(_ollama_num_ctx_override)\n+            except (TypeError, ValueError):\n+                logger.debug(\"Invalid ollama_num_ctx config value: %r\", _ollama_num_ctx_override)\n+        if self._ollama_num_ctx is None and self.base_url and is_local_endpoint(self.base_url):\n+            try:\n+                _detected = query_ollama_num_ctx(self.model, self.base_url)\n+                if _detected and _detected > 0:\n+                    self._ollama_num_ctx = _detected\n+            except Exception as exc:\n+                logger.debug(\"Ollama num_ctx detection failed: %s\", exc)\n+        if self._ollama_num_ctx and not self.quiet_mode:\n+            logger.info(\n+                \"Ollama num_ctx: will request %d tokens (model max from /api/show)\",\n+                self._ollama_num_ctx,\n+            )\n+\n         if not self.quiet_mode:\n             if compression_enabled:\n                 print(f\"📊 Context limit: {self.context_compressor.context_length:,} tokens (compress at {int(compression_threshold*100)}% = {self.context_compressor.threshold_tokens:,})\")","@@ -5456,6 +5484,15 @@ def _build_api_kwargs(self, api_messages: list) -> dict:\n         if _is_nous:\n             extra_body[\"tags\"] = [\"product=hermes-agent\"]\n \n+        # Ollama num_ctx: override the 2048 default so the model actually\n+        # uses the context window it was trained for.  Passed via the OpenAI\n+        # SDK's extra_body → options.num_ctx, which Ollama's OpenAI-compat\n+        # endpoint forwards to the runner as --ctx-size.\n+        if self._ollama_num_ctx:\n+            options = extra_body.get(\"options\", {})\n+            options[\"num_ctx\"] = self._ollama_num_ctx\n+            extra_body[\"options\"] = options\n+\n         if extra_body:\n             api_kwargs[\"extra_body\"] = extra_body\n "]},{"file":"tests/agent/test_auxiliary_client.py","hunks":["@@ -471,6 +471,23 @@ def test_explicit_zai(self, monkeypatch):\n             client, model = resolve_provider_client(\"zai\")\n             assert client is not None\n \n+    def test_explicit_google_alias_uses_gemini_credentials(self):\n+        \"\"\"provider='google' should route through the gemini API-key provider.\"\"\"\n+        with (\n+            patch(\"hermes_cli.auth.resolve_api_key_provider_credentials\", return_value={\n+                \"api_key\": \"[redacted:secret-assignment]\",\n+                \"base_url\": \"https://generativelanguage.googleapis.com/v1beta/openai\",\n+            }),\n+            patch(\"agent.auxiliary_client.OpenAI\") as mock_openai,\n+        ):\n+            mock_openai.return_value = MagicMock()\n+            client, model = resolve_provider_client(\"google\", model=\"gemini-3.1-pro-preview\")\n+\n+        assert client is not None\n+        assert model == \"gemini-3.1-pro-preview\"\n+        assert mock_openai.call_args.kwargs[\"api_key\"] == \"gemini-key\"\n+        assert mock_openai.call_args.kwargs[\"base_url\"] == \"https://generativelanguage.googleapis.com/v1beta/openai\"\n+\n     def test_explicit_unknown_returns_none(self, monkeypatch):\n         \"\"\"Unknown provider should return None.\"\"\"\n         client, model = resolve_provider_client(\"nonexistent-provider\")","@@ -822,6 +839,31 @@ def test_vision_uses_nous_when_available(self, monkeypatch):\n         assert model == \"google/gemini-3-flash-preview\"\n         assert client is not None\n \n+    def test_vision_config_google_provider_uses_gemini_credentials(self, monkeypatch):\n+        config = {\n+            \"auxiliary\": {\n+                \"vision\": {\n+                    \"provider\": \"google\",\n+                    \"model\": \"gemini-3.1-pro-preview\",\n+                }\n+            }\n+        }\n+        monkeypatch.setattr(\"hermes_cli.config.load_config\", lambda: config)\n+        with (\n+            patch(\"hermes_cli.auth.resolve_api_key_provider_credentials\", return_value={\n+                \"api_key\": \"[redacted:secret-assignment]\",\n+                \"base_url\": \"https://generativelanguage.googleapis.com/v1beta/openai\",\n+            }),\n+            patch(\"agent.auxiliary_client.OpenAI\") as mock_openai,\n+        ):\n+            resolved_provider, client, model = resolve_vision_provider_client()\n+\n+        assert resolved_provider == \"gemini\"\n+        assert client is not None\n+        assert model == \"gemini-3.1-pro-preview\"\n+        assert mock_openai.call_args.kwargs[\"api_key\"] == \"gemini-key\"\n+        assert mock_openai.call_args.kwargs[\"base_url\"] == \"https://generativelanguage.googleapis.com/v1beta/openai\"\n+\n     def test_vision_forced_main_uses_custom_endpoint(self, monkeypatch):\n         \"\"\"When explicitly forced to 'main', vision CAN use custom endpoint.\"\"\"\n         config = {"]},{"file":"tests/agent/test_minimax_auxiliary_url.py","hunks":["@@ -0,0 +1,42 @@\n+\"\"\"Tests for MiniMax auxiliary client URL normalization.\n+\n+MiniMax and MiniMax-CN set inference_base_url to the /anthropic path.\n+The auxiliary client uses the OpenAI SDK, which needs /v1 instead.\n+\"\"\"\n+\n+import sys\n+import os\n+\n+sys.path.insert(0, os.path.join(os.path.dirname(__file__), \"..\", \"..\"))\n+\n+from agent.auxiliary_client import _to_openai_base_url\n+\n+\n+class TestToOpenaiBaseUrl:\n+    def test_minimax_global_anthropic_suffix_replaced(self):\n+        assert _to_openai_base_url(\"https://api.minimax.io/anthropic\") == \"https://api.minimax.io/v1\"\n+\n+    def test_minimax_cn_anthropic_suffix_replaced(self):\n+        assert _to_openai_base_url(\"https://api.minimaxi.com/anthropic\") == \"https://api.minimaxi.com/v1\"\n+\n+    def test_trailing_slash_stripped_before_replace(self):\n+        assert _to_openai_base_url(\"https://api.minimax.io/anthropic/\") == \"https://api.minimax.io/v1\"\n+\n+    def test_v1_url_unchanged(self):\n+        assert _to_openai_base_url(\"https://api.openai.com/v1\") == \"https://api.openai.com/v1\"\n+\n+    def test_openrouter_url_unchanged(self):\n+        assert _to_openai_base_url(\"https://openrouter.ai/api/v1\") == \"https://openrouter.ai/api/v1\"\n+\n+    def test_anthropic_domain_unchanged(self):\n+        \"\"\"api.anthropic.com doesn't end with /anthropic — should be untouched.\"\"\"\n+        assert _to_openai_base_url(\"https://api.anthropic.com\") == \"https://api.anthropic.com\"\n+\n+    def test_anthropic_in_subpath_unchanged(self):\n+        assert _to_openai_base_url(\"https://example.com/anthropic/extra\") == \"https://example.com/anthropic/extra\"\n+\n+    def test_empty_string(self):\n+        assert _to_openai_base_url(\"\") == \"\"\n+\n+    def test_none(self):\n+        assert _to_openai_base_url(None) == \"\""]},{"file":"tests/test_ollama_num_ctx.py","hunks":["@@ -0,0 +1,135 @@\n+\"\"\"Tests for Ollama num_ctx context length detection and injection.\n+\n+Covers:\n+  agent/model_metadata.py — query_ollama_num_ctx()\n+  run_agent.py — _ollama_num_ctx detection + extra_body injection\n+\"\"\"\n+\n+from unittest.mock import patch, MagicMock\n+\n+import pytest\n+\n+from agent.model_metadata import query_ollama_num_ctx\n+\n+\n+# ═══════════════════════════════════════════════════════════════════════\n+# Level 1: query_ollama_num_ctx — Ollama API interaction\n+# ═══════════════════════════════════════════════════════════════════════\n+\n+\n+def _mock_httpx_client(show_response_data, status_code=200):\n+    \"\"\"Create a mock httpx.Client context manager that returns given /api/show data.\"\"\"\n+    mock_resp = MagicMock(status_code=status_code)\n+    mock_resp.json.return_value = show_response_data\n+    mock_client = MagicMock()\n+    mock_client.post.return_value = mock_resp\n+    mock_ctx = MagicMock()\n+    mock_ctx.__enter__ = MagicMock(return_value=mock_client)\n+    mock_ctx.__exit__ = MagicMock(return_value=False)\n+    return mock_ctx, mock_client\n+\n+\n+class TestQueryOllamaNumCtx:\n+    \"\"\"Test the Ollama /api/show context length query.\"\"\"\n+\n+    def test_returns_context_from_model_info(self):\n+        \"\"\"Should extract context_length from GGUF model_info metadata.\"\"\"\n+        show_data = {\n+            \"model_info\": {\"llama.context_length\": 131072},\n+            \"parameters\": \"\",\n+        }\n+        mock_ctx, _ = _mock_httpx_client(show_data)\n+\n+        with patch(\"agent.model_metadata.detect_local_server_type\", return_value=\"ollama\"):\n+            # httpx is imported inside the function — patch the module import\n+            import httpx\n+            with patch.object(httpx, \"Client\", return_value=mock_ctx):\n+                result = query_ollama_num_ctx(\"llama3.1:8b\", \"http://localhost:11434/v1\")\n+\n+        assert result == 131072\n+\n+    def test_prefers_explicit_num_ctx_from_modelfile(self):\n+        \"\"\"If the Modelfile sets num_ctx explicitly, that should take priority.\"\"\"\n+        show_data = {\n+            \"model_info\": {\"llama.context_length\": 131072},\n+            \"parameters\": \"num_ctx 32768\\ntemperature 0.7\",\n+        }\n+        mock_ctx, _ = _mock_httpx_client(show_data)\n+\n+        with patch(\"agent.model_metadata.detect_local_server_type\", return_value=\"ollama\"):\n+            import httpx\n+            with patch.object(httpx, \"Client\", return_value=mock_ctx):\n+                result = query_ollama_num_ctx(\"custom-model\", \"http://localhost:11434\")\n+\n+        assert result == 32768\n+\n+    def test_returns_none_for_non_ollama_server(self):\n+        \"\"\"Should return None if the server is not Ollama.\"\"\"\n+        with patch(\"agent.model_metadata.detect_local_server_type\", return_value=\"lm-studio\"):\n+            result = query_ollama_num_ctx(\"model\", \"http://localhost:1234\")\n+        assert result is None\n+\n+    def test_returns_none_on_connection_error(self):\n+        \"\"\"Should return None if the server is unreachable.\"\"\"\n+        with patch(\"agent.model_metadata.detect_local_server_type\", side_effect=Exception(\"timeout\")):\n+            result = query_ollama_num_ctx(\"model\", \"http://localhost:11434\")\n+        assert result is None\n+\n+    def test_returns_none_on_404(self):\n+        \"\"\"Should return None if the model is not found.\"\"\"\n+        mock_ctx, _ = _mock_httpx_client({}, status_code=404)\n+\n+        with patch(\"agent.model_metadata.detect_local_server_type\", return_value=\"ollama\"):\n+            import httpx\n+            with patch.object(httpx, \"Client\", return_value=mock_ctx):\n+                result = query_ollama_num_ctx(\"nonexistent\", \"http://localhost:11434\")\n+\n+        assert result is None\n+\n+    def test_strips_provider_prefix(self):\n+        \"\"\"Should strip 'local:' prefix from model name before querying.\"\"\"\n+        show_data = {\n+            \"model_info\": {\"qwen2.context_length\": 32768},\n+            \"parameters\": \"\",\n+        }\n+        mock_ctx, mock_client = _mock_httpx_client(show_data)\n+\n+        with patch(\"agent.model_metadata.detect_local_server_type\", return_value=\"ollama\"):\n+            import httpx\n+            with patch.object(httpx, \"Client\", return_value=mock_ctx):\n+                result = query_ollama_num_ctx(\"local:qwen2.5:7b\", \"http://localhost:11434/v1\")\n+\n+        # Verify the post was called with stripped name (no \"local:\" prefix)\n+        call_args = mock_client.post.call_args\n+        assert call_args[1][\"json\"][\"name\"] == \"qwen2.5:7b\" or call_args[0][1] is not None\n+        assert result == 32768\n+\n+    def test_handles_qwen2_architecture_key(self):\n+        \"\"\"Different model architectures use different key prefixes in model_info.\"\"\"\n+        show_data = {\n+            \"model_info\": {\"qwen2.context_length\": 65536},\n+            \"parameters\": \"\",\n+        }\n+        mock_ctx, _ = _mock_httpx_client(show_data)\n+\n+        with patch(\"agent.model_metadata.detect_local_server_type\", return_value=\"ollama\"):\n+            import httpx\n+            with patch.object(httpx, \"Client\", return_value=mock_ctx):\n+                result = query_ollama_num_ctx(\"qwen2.5:32b\", \"http://localhost:11434\")\n+\n+        assert result == 65536\n+\n+    def test_returns_none_when_model_info_empty(self):\n+        \"\"\"Should return None if model_info has no context_length key.\"\"\"\n+        show_data = {\n+            \"model_info\": {\"llama.embedding_length\": 4096},\n+            \"parameters\": \"\",\n+        }\n+        mock_ctx, _ = _mock_httpx_client(show_data)\n+\n+        with patch(\"agent.model_metadata.detect_local_server_type\", return_value=\"ollama\"):\n+            import httpx\n+            with patch.object(httpx, \"Client\", return_value=mock_ctx):\n+                result = query_ollama_num_ctx(\"model\", \"http://localhost:11434\")\n+\n+        assert result is None\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}