{"repo":"NousResearch/hermes-agent","public_id":"19337","display_label":"MiniMax OAuth URL staleness fixes and hardening","summary":"These issues and PRs address a stale or incorrect verification_uri returned by MiniMax OAuth, pointing to a deleted or wrong page, along with related fixes for URL rewriting, timestamp normalization, and setup error surfacing.","subsystem":"hermes_cli","counts":{"members":7,"open_issues":1,"open_issues_distinct":1,"open_prs":1,"closed_issues":0,"merged_prs":1,"attempt_prs":5},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":24165,"merged_at":"2026-05-12T05:15:17+00:00"},"resolution":{"state":"unresolved","by":null},"freshness":{"state":"verified","last_verified_at":"2026-05-12T05:15:17+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.2857142857142857},"fix_edges":[{"issue_number":19337,"pr_number":24165,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-05-12T05:15:17+00:00"},{"issue_number":21779,"pr_number":24165,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-05-12T05:15:17+00:00"}],"members":[{"kind":"issue","number":19337,"title":"MiniMax OAuth returns stale verification_uri pointing to deleted /oauth-authorize page on www.minimax.io","url":null,"state":"open"},{"kind":"pr","number":19461,"title":"fix(auth): rewrite stale MiniMax OAuth approval URL","url":null,"state":"open","close_reason":{"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":19466,"title":"fix(cli): rewrite stale MiniMax OAuth verification_uri and surface setup failures","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#19461","number":19461}],"category":"superseded_by_pr","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":19550,"title":"fix(auth): rewrite stale MiniMax OAuth verification_uri host","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#19466","number":19466},{"kind":"pr","value":"#19336","number":19336}],"category":"superseded_by_pr","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":20374,"title":"fix(minimax-oauth): correct authorize URL domain and handle ms timestamps","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#24165","number":24165}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":24165,"title":"fix(minimax): harden OAuth dashboard and runtime","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["hermes_cli/auth.py","hermes_cli/auth_commands.py","hermes_cli/runtime_provider.py","hermes_cli/web_server.py","tests/hermes_cli/test_auth_commands.py","tests/hermes_cli/test_runtime_provider_resolution.py","tests/hermes_cli/test_web_oauth_dispatch.py","tests/test_minimax_oauth.py"],"fix_hunks":[{"file":"hermes_cli/auth.py","hunks":["@@ -4046,6 +4046,8 @@ def get_auth_status(provider_id: Optional[str] = None) -> Dict[str, Any]:\n         return get_qwen_auth_status()\n     if target == \"google-gemini-cli\":\n         return get_gemini_oauth_auth_status()\n+    if target == \"minimax-oauth\":\n+        return get_minimax_oauth_auth_status()\n     if target == \"copilot-acp\":\n         return get_external_process_provider_status(target)\n     # API-key providers","@@ -4757,6 +4759,20 @@ def _minimax_request_user_code(\n     return payload\n \n \n+def _minimax_expired_in_looks_like_unix_ms(expired_in: int, *, now_ms: int) -> bool:\n+    \"\"\"True if ``expired_in`` is plausibly a unix-ms absolute time (vs TTL seconds).\"\"\"\n+    return int(expired_in) > (now_ms // 2)\n+\n+\n+def _minimax_resolve_token_expiry_unix(expired_in: int, *, now: datetime) -> float:\n+    \"\"\"Return access-token expiry as unix seconds (MiniMax uses ms epoch or TTL seconds).\"\"\"\n+    raw = int(expired_in)\n+    now_ms = int(now.timestamp() * 1000)\n+    if _minimax_expired_in_looks_like_unix_ms(raw, now_ms=now_ms):\n+        return raw / 1000.0\n+    return now.timestamp() + max(1, raw)\n+\n+\n def _minimax_poll_token(\n     client: httpx.Client, *, portal_base_url: str, client_id: str,\n     user_code: str, code_verifier: str, expired_in: int, interval_ms: Optional[int],","@@ -4765,12 +4781,11 @@ def _minimax_poll_token(\n     # Defensive parsing: if it's small enough to be a duration, treat as seconds.\n     import time as _time\n     now_ms = int(_time.time() * 1000)\n-    if expired_in > now_ms // 2:\n-        # Looks like a unix-ms timestamp.\n-        deadline = expired_in / 1000.0\n+    raw = int(expired_in)\n+    if _minimax_expired_in_looks_like_unix_ms(raw, now_ms=now_ms):\n+        deadline = raw / 1000.0\n     else:\n-        # Treat as duration in seconds from now.\n-        deadline = _time.time() + max(1, expired_in)\n+        deadline = _time.time() + max(1, raw)\n     interval = max(2.0, (interval_ms or 2000) / 1000.0)\n \n     while _time.time() < deadline:","@@ -4884,8 +4899,10 @@ def _minimax_oauth_login(\n         )\n \n     now = datetime.now(timezone.utc)\n-    expires_in_s = int(token_data[\"expired_in\"])\n-    expires_at = now.timestamp() + expires_in_s\n+    expires_at_unix = _minimax_resolve_token_expiry_unix(\n+        int(token_data[\"expired_in\"]), now=now,\n+    )\n+    expires_in_s = max(0, int(expires_at_unix - now.timestamp()))\n \n     auth_state = {\n         \"provider\": \"minimax-oauth\",","@@ -4899,7 +4916,7 @@ def _minimax_oauth_login(\n         \"refresh_token\": [redacted:secret-assignment]\"refresh_token\"],\n         \"resource_url\": token_data.get(\"resource_url\"),\n         \"obtained_at\": now.isoformat(),\n-        \"expires_at\": datetime.fromtimestamp(expires_at, tz=timezone.utc).isoformat(),\n+        \"expires_at\": datetime.fromtimestamp(expires_at_unix, tz=timezone.utc).isoformat(),\n         \"expires_in\": expires_in_s,\n     }\n ","@@ -4960,14 +4977,16 @@ def _refresh_minimax_oauth_state(\n             relogin_required=True,\n         )\n     now_dt = datetime.now(timezone.utc)\n-    expires_in_s = int(payload[\"expired_in\"])\n+    expires_at_unix = _minimax_resolve_token_expiry_unix(\n+        int(payload[\"expired_in\"]), now=now_dt,\n+    )\n+    expires_in_s = max(0, int(expires_at_unix - now_dt.timestamp()))\n     new_state = dict(state)\n     new_state.update({\n         \"access_token\": [redacted:secret-assignment]\"access_token\"],\n         \"refresh_token\": payload.get(\"refresh_token\", state[\"refresh_token\"]),\n         \"obtained_at\": now_dt.isoformat(),\n-        \"expires_at\": datetime.fromtimestamp(now_dt.timestamp() + expires_in_s,\n-                                             tz=timezone.utc).isoformat(),\n+        \"expires_at\": datetime.fromtimestamp(expires_at_unix, tz=timezone.utc).isoformat(),\n         \"expires_in\": expires_in_s,\n     })\n     _minimax_save_auth_state(new_state)"]},{"file":"hermes_cli/auth_commands.py","hunks":["@@ -375,10 +375,12 @@ def auth_add_command(args) -> None:\n         return\n \n     if provider == \"minimax-oauth\":\n-        from hermes_cli.auth import resolve_minimax_oauth_runtime_credentials\n-        creds = resolve_minimax_oauth_runtime_credentials()\n+        creds = auth_mod._minimax_oauth_login(\n+            open_browser=not getattr(args, \"no_browser\", False),\n+            timeout_seconds=getattr(args, \"timeout\", None) or 15.0,\n+        )\n         label = (getattr(args, \"label\", None) or \"\").strip() or label_from_token(\n-            creds[\"api_key\"],\n+            creds[\"access_token\"],\n             _oauth_default_label(provider, len(pool.entries()) + 1),\n         )\n         entry = PooledCredential(","@@ -388,8 +390,9 @@ def auth_add_command(args) -> None:\n             auth_type=AUTH_TYPE_OAUTH,\n             priority=0,\n             source=f\"{SOURCE_MANUAL}:minimax_oauth\",\n-            access_token=[redacted:secret-assignment]\"api_key\"],\n-            base_url=creds.get(\"base_url\"),\n+            access_token=[redacted:secret-assignment]\"access_token\"],\n+            refresh_token=creds.get(\"refresh_token\"),\n+            base_url=creds.get(\"inference_base_url\"),\n         )\n         pool.add_entry(entry)\n         print(f'Added {provider} OAuth credential #{len(pool.entries())}: \"{entry.label}\"')"]},{"file":"hermes_cli/runtime_provider.py","hunks":["@@ -205,6 +205,14 @@ def _resolve_runtime_from_pool_entry(\n     elif provider == \"google-gemini-cli\":\n         api_mode = \"chat_completions\"\n         base_url = base_url or \"cloudcode-pa://google\"\n+    elif provider == \"minimax-oauth\":\n+        # MiniMax OAuth tokens are valid only against the Anthropic Messages\n+        # compatible endpoint. Do not honor stale model.api_mode values from a\n+        # prior OpenAI-compatible provider, or the client will hit\n+        # /chat/completions under /anthropic and receive a bare nginx 404.\n+        api_mode = \"anthropic_messages\"\n+        pconfig = PROVIDER_REGISTRY.get(provider)\n+        base_url = base_url or (pconfig.inference_base_url if pconfig else \"\")\n     elif provider == \"anthropic\":\n         api_mode = \"anthropic_messages\"\n         cfg_provider = str(model_cfg.get(\"provider\") or \"\").strip().lower()"]},{"file":"hermes_cli/web_server.py","hunks":["@@ -2053,6 +2053,7 @@ def _minimax_poller(session_id: str) -> None:\n     \"\"\"\n     from hermes_cli.auth import (\n         _minimax_poll_token,\n+        _minimax_resolve_token_expiry_unix,\n         _minimax_save_auth_state,\n         MINIMAX_OAUTH_GLOBAL_INFERENCE,\n         MINIMAX_OAUTH_SCOPE,","@@ -2090,8 +2091,10 @@ def _minimax_poller(session_id: str) -> None:\n         # dashboard path; cn-region operators can still use the CLI\n         # flow which supports `--region cn`.\n         now = datetime.now(timezone.utc)\n-        expires_in_s = int(token_data[\"expired_in\"])\n-        expires_at_ts = now.timestamp() + expires_in_s\n+        expires_at_ts = _minimax_resolve_token_expiry_unix(\n+            int(token_data[\"expired_in\"]), now=now,\n+        )\n+        expires_in_s = max(0, int(expires_at_ts - now.timestamp()))\n         auth_state = {\n             \"provider\": \"minimax-oauth\",\n             \"region\": sess.get(\"region\", \"global\"),"]},{"file":"tests/hermes_cli/test_auth_commands.py","hunks":["@@ -170,6 +170,50 @@ class _Args:\n     assert singleton[\"inference_base_url\"] == \"https://inference.example.com/v1\"\n \n \n+def test_auth_add_minimax_oauth_starts_login_and_persists_pool_entry(tmp_path, monkeypatch):\n+    monkeypatch.setenv(\"HERMES_HOME\", str(tmp_path / \"hermes\"))\n+    _write_auth_store(tmp_path, {\"version\": 1, \"providers\": {}})\n+    token = _jwt_with_email(\"[redacted:email]\")\n+    monkeypatch.setattr(\n+        \"hermes_cli.auth._minimax_oauth_login\",\n+        lambda **kwargs: {\n+            \"provider\": \"minimax-oauth\",\n+            \"region\": \"global\",\n+            \"portal_base_url\": \"https://api.minimax.io\",\n+            \"inference_base_url\": \"https://api.minimax.io/anthropic\",\n+            \"client_id\": \"client-id\",\n+            \"scope\": \"group_id profile model.completion\",\n+            \"token_type\": \"[redacted:secret-assignment]\",\n+            \"access_token\": [redacted:secret-assignment]\n+            \"refresh_token\": \"[redacted:secret-assignment]\",\n+            \"resource_url\": None,\n+            \"obtained_at\": \"2026-05-11T10:00:00+00:00\",\n+            \"expires_at\": \"2026-05-14T10:00:00+00:00\",\n+            \"expires_in\": 259200,\n+        },\n+    )\n+\n+    from hermes_cli.auth_commands import auth_add_command\n+\n+    class _Args:\n+        provider = \"minimax-oauth\"\n+        auth_type = \"oauth\"\n+        api_key = None\n+        label = None\n+        no_browser = True\n+        timeout = None\n+\n+    auth_add_command(_Args())\n+\n+    payload = json.loads((tmp_path / \"hermes\" / \"auth.json\").read_text())\n+    entries = payload[\"credential_pool\"][\"minimax-oauth\"]\n+    entry = next(item for item in entries if item[\"source\"] == \"manual:minimax_oauth\")\n+    assert entry[\"label\"] == \"[redacted:email]\"\n+    assert entry[\"access_token\"] == token\n+    assert entry[\"refresh_token\"] == \"refresh-token\"\n+    assert entry[\"base_url\"] == \"https://api.minimax.io/anthropic\"\n+\n+\n def test_auth_add_nous_oauth_honors_custom_label(tmp_path, monkeypatch):\n     \"\"\"`hermes auth add nous --type oauth --label <name>` must preserve the\n     custom label end-to-end — it was silently dropped in the first cut of the"]},{"file":"tests/hermes_cli/test_runtime_provider_resolution.py","hunks":["@@ -2285,3 +2285,39 @@ def test_minimax_oauth_runtime_uses_inference_base_url(monkeypatch):\n     resolved = rp.resolve_runtime_provider(requested=\"minimax-oauth\")\n \n     assert MINIMAX_OAUTH_CN_INFERENCE.rstrip(\"/\") in resolved[\"base_url\"]\n+\n+\n+def test_minimax_oauth_pool_forces_anthropic_messages_despite_stale_config(monkeypatch):\n+    \"\"\"A pooled MiniMax OAuth token must not inherit stale chat_completions config.\"\"\"\n+\n+    class _Entry:\n+        access_token = \"[redacted:secret-assignment]\"\n+        source = \"manual:minimax_oauth\"\n+        base_url = \"https://api.minimax.io/anthropic\"\n+\n+    class _Pool:\n+        def has_credentials(self):\n+            return True\n+\n+        def select(self):\n+            return _Entry()\n+\n+    monkeypatch.setattr(rp, \"resolve_provider\", lambda *a, **k: \"minimax-oauth\")\n+    monkeypatch.setattr(\n+        rp,\n+        \"_get_model_config\",\n+        lambda: {\n+            \"provider\": \"minimax-oauth\",\n+            \"default\": \"MiniMax-M2.7\",\n+            \"api_mode\": \"chat_completions\",\n+        },\n+    )\n+    monkeypatch.setattr(rp, \"load_pool\", lambda provider: _Pool())\n+    monkeypatch.setattr(rp, \"_resolve_named_custom_runtime\", lambda **k: None)\n+    monkeypatch.setattr(rp, \"_resolve_explicit_runtime\", lambda **k: None)\n+\n+    resolved = rp.resolve_runtime_provider(requested=\"minimax-oauth\")\n+\n+    assert resolved[\"provider\"] == \"minimax-oauth\"\n+    assert resolved[\"api_mode\"] == \"anthropic_messages\"\n+    assert resolved[\"base_url\"] == \"https://api.minimax.io/anthropic\""]},{"file":"tests/hermes_cli/test_web_oauth_dispatch.py","hunks":["@@ -19,6 +19,8 @@\n \n These tests pin the corrected behavior.\n \"\"\"\n+import time\n+from datetime import datetime, timezone\n from unittest.mock import patch\n \n import pytest","@@ -67,6 +69,53 @@ def test_minimax_login_does_not_launch_anthropic_flow():\n     assert body[\"expires_in\"] == 600\n \n \n+def test_minimax_dashboard_poller_accepts_absolute_ms_expired_in():\n+    \"\"\"Dashboard MiniMax completion must accept unix-ms token expiry values.\"\"\"\n+    from hermes_cli import web_server as ws\n+\n+    now = datetime.now(timezone.utc)\n+    abs_ms = int((now.timestamp() + 1800) * 1000)\n+    session_id = \"minimax-absolute-ms-test\"\n+    ws._oauth_sessions[session_id] = {\n+        \"session_id\": session_id,\n+        \"provider\": \"minimax-oauth\",\n+        \"flow\": \"device_code\",\n+        \"created_at\": time.time(),\n+        \"status\": \"pending\",\n+        \"error_message\": None,\n+        \"portal_base_url\": \"https://api.minimax.io\",\n+        \"client_id\": \"client-id\",\n+        \"user_code\": \"ABCD-1234\",\n+        \"code_verifier\": \"verifier\",\n+        \"interval_ms\": 2000,\n+        \"expired_in_raw\": abs_ms,\n+        \"region\": \"global\",\n+    }\n+    captured_state = {}\n+\n+    try:\n+        with patch(\n+            \"hermes_cli.auth._minimax_poll_token\",\n+            return_value={\n+                \"status\": \"success\",\n+                \"access_token\": \"[redacted:secret-assignment]\",\n+                \"refresh_token\": \"[redacted:secret-assignment]\",\n+                \"expired_in\": abs_ms,\n+                \"token_type\": \"[redacted:secret-assignment]\",\n+            },\n+        ), patch(\n+            \"hermes_cli.auth._minimax_save_auth_state\",\n+            side_effect=lambda state: captured_state.update(state),\n+        ):\n+            ws._minimax_poller(session_id)\n+    finally:\n+        ws._oauth_sessions.pop(session_id, None)\n+\n+    assert captured_state[\"access_token\"] == \"access\"\n+    assert 1790 <= captured_state[\"expires_in\"] <= 1810\n+    assert datetime.fromisoformat(captured_state[\"expires_at\"]).year < 9999\n+\n+\n def test_anthropic_pkce_branch_still_works():\n     \"\"\"Sanity: the dispatcher tightening doesn't break the legitimate Anthropic PKCE path.\"\"\"\n     fake_anthropic_response = {"]},{"file":"tests/test_minimax_oauth.py","hunks":["@@ -32,9 +32,11 @@\n     _minimax_pkce_pair,\n     _minimax_request_user_code,\n     _minimax_poll_token,\n+    _minimax_resolve_token_expiry_unix,\n     _refresh_minimax_oauth_state,\n     resolve_minimax_oauth_runtime_credentials,\n     get_minimax_oauth_auth_status,\n+    get_auth_status,\n     get_provider_auth_state,\n )\n ","@@ -67,6 +69,23 @@ def _past_iso(seconds_ago: int = 3600) -> str:\n     return datetime.fromtimestamp(ts, tz=timezone.utc).isoformat()\n \n \n+# ---------------------------------------------------------------------------\n+# 0. test_resolve_token_expiry_unix_ttl_vs_absolute_ms\n+# ---------------------------------------------------------------------------\n+\n+def test_resolve_token_expiry_unix_ttl_seconds():\n+    now = datetime(2025, 6, 1, 12, 0, 0, tzinfo=timezone.utc)\n+    got = _minimax_resolve_token_expiry_unix(3600, now=now)\n+    assert abs(got - (now.timestamp() + 3600)) < 0.01\n+\n+\n+def test_resolve_token_expiry_unix_absolute_ms():\n+    now = datetime(2025, 6, 1, 12, 0, 0, tzinfo=timezone.utc)\n+    abs_ms = int((now.timestamp() + 7200) * 1000)\n+    got = _minimax_resolve_token_expiry_unix(abs_ms, now=now)\n+    assert abs(got - (now.timestamp() + 7200)) < 0.01\n+\n+\n # ---------------------------------------------------------------------------\n # 1. test_pkce_pair_produces_valid_s256\n # ---------------------------------------------------------------------------","@@ -362,6 +381,46 @@ def test_refresh_updates_access_token():\n     assert result[\"expires_in\"] == 7200\n \n \n+def test_refresh_updates_access_token_absolute_ms_expired_in():\n+    \"\"\"Refresh payload may use unix-ms absolute ``expired_in`` (same as device-code).\"\"\"\n+    now0 = datetime.now(timezone.utc)\n+    abs_ms = int((now0.timestamp() + 1800) * 1000)\n+\n+    state = {\n+        \"access_token\": \"[redacted:secret-assignment]\",\n+        \"refresh_token\": \"[redacted:secret-assignment]\",\n+        \"portal_base_url\": MINIMAX_OAUTH_GLOBAL_BASE,\n+        \"client_id\": MINIMAX_OAUTH_CLIENT_ID,\n+        \"inference_base_url\": MINIMAX_OAUTH_GLOBAL_INFERENCE,\n+        \"expires_at\": _future_iso(MINIMAX_OAUTH_REFRESH_SKEW_SECONDS - 1),\n+    }\n+\n+    new_token_body = {\n+        \"status\": \"success\",\n+        \"access_token\": \"[redacted:secret-assignment]\",\n+        \"refresh_token\": \"[redacted:secret-assignment]\",\n+        \"expired_in\": abs_ms,\n+    }\n+\n+    mock_resp = _make_httpx_response(200, new_token_body)\n+\n+    with patch(\"httpx.Client\") as mock_client_class:\n+        mock_client_instance = MagicMock()\n+        mock_client_instance.__enter__ = MagicMock(return_value=mock_client_instance)\n+        mock_client_instance.__exit__ = MagicMock(return_value=False)\n+        mock_client_instance.post.return_value = mock_resp\n+        mock_client_class.return_value = mock_client_instance\n+\n+        with patch(\"hermes_cli.auth._minimax_save_auth_state\"):\n+            result = _refresh_minimax_oauth_state(state)\n+\n+    assert result[\"access_token\"] == \"new-access\"\n+    assert 1790 <= result[\"expires_in\"] <= 1810\n+    exp = datetime.fromisoformat(result[\"expires_at\"].replace(\"Z\", \"+00:00\"))\n+    skew = exp.timestamp() - datetime.now(timezone.utc).timestamp()\n+    assert 1790 <= skew <= 1810\n+\n+\n # ---------------------------------------------------------------------------\n # 10. test_refresh_reuse_triggers_relogin_required\n # ---------------------------------------------------------------------------","@@ -464,3 +523,18 @@ def test_get_minimax_oauth_auth_status_logged_in():\n \n     assert status[\"logged_in\"] is True\n     assert status[\"region\"] == \"global\"\n+\n+\n+def test_generic_auth_status_dispatches_minimax_oauth():\n+    state = {\n+        \"access_token\": \"tok\",\n+        \"expires_at\": _future_iso(3600),\n+        \"region\": \"global\",\n+    }\n+\n+    with patch(\"hermes_cli.auth.get_provider_auth_state\", return_value=state):\n+        status = get_auth_status(\"minimax-oauth\")\n+\n+    assert status[\"logged_in\"] is True\n+    assert status[\"provider\"] == \"minimax-oauth\"\n+    assert status[\"region\"] == \"global\"\n"]}]},{"kind":"pr","number":124236,"title":"fix(minimax-oauth): rewrite stale verification_uri host (#19337)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"89a9ef3853c5","number":0}],"category":"uncategorized","implication":"needs_llm"},"merged":false}],"trust":{"default_tier":"verified","includes_suggested":false}}