# NousResearch/hermes-agent · MiniMax OAuth URL staleness fixes and hardening

## Open now

- 1 issue, 1 PR open on GitHub.
- #19337: MiniMax OAuth returns stale verification_uri pointing to deleted /oauth-authorize page on www.minimax.io
- PR #19461: fix(auth): rewrite stale MiniMax OAuth approval URL

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.

## TL;DR

- 7 member(s): 1 open issue(s), 0 closed issue(s), 1 open PR(s), 1 merged PR(s), 4 PR(s) closed without merging.
- A fix was merged to main in PR #24165. Verify it addresses your case.
- **Recommended:** read PR #24165 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `unresolved`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.29`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Open related issues

- #19337: MiniMax OAuth returns stale verification_uri pointing to deleted /oauth-authorize page on www.minimax.io

## Prior & competing PRs

- **#19466: fix(cli): rewrite stale MiniMax OAuth verification_uri and surface setup failures**. closed without merge · fix tier `suggested`
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#19461`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#19550: fix(auth): rewrite stale MiniMax OAuth verification_uri host**. closed without merge · fix tier `suggested`
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#19466`, `#19336`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#24165: fix(minimax): harden OAuth dashboard and runtime**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `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:
  - `hermes_cli/auth.py`

    ```diff
    @@ -4046,6 +4046,8 @@ def get_auth_status(provider_id: Optional[str] = None) -> Dict[str, Any]:
             return get_qwen_auth_status()
         if target == "google-gemini-cli":
             return get_gemini_oauth_auth_status()
    +    if target == "minimax-oauth":
    +        return get_minimax_oauth_auth_status()
         if target == "copilot-acp":
             return get_external_process_provider_status(target)
         # API-key providers
    ```

    ```diff
    @@ -4757,6 +4759,20 @@ def _minimax_request_user_code(
         return payload
     
     
    +def _minimax_expired_in_looks_like_unix_ms(expired_in: int, *, now_ms: int) -> bool:
    +    """True if ``expired_in`` is plausibly a unix-ms absolute time (vs TTL seconds)."""
    +    return int(expired_in) > (now_ms // 2)
    +
    +
    +def _minimax_resolve_token_expiry_unix(expired_in: int, *, now: datetime) -> float:
    +    """Return access-token expiry as unix seconds (MiniMax uses ms epoch or TTL seconds)."""
    +    raw = int(expired_in)
    +    now_ms = int(now.timestamp() * 1000)
    +    if _minimax_expired_in_looks_like_unix_ms(raw, now_ms=now_ms):
    +        return raw / 1000.0
    +    return now.timestamp() + max(1, raw)
    +
    +
     def _minimax_poll_token(
         client: httpx.Client, *, portal_base_url: str, client_id: str,
         user_code: str, code_verifier: str, expired_in: int, interval_ms: Optional[int],
    ```

    ```diff
    @@ -4765,12 +4781,11 @@ def _minimax_poll_token(
         # Defensive parsing: if it's small enough to be a duration, treat as seconds.
         import time as _time
         now_ms = int(_time.time() * 1000)
    -    if expired_in > now_ms // 2:
    -        # Looks like a unix-ms timestamp.
    -        deadline = expired_in / 1000.0
    +    raw = int(expired_in)
    +    if _minimax_expired_in_looks_like_unix_ms(raw, now_ms=now_ms):
    +        deadline = raw / 1000.0
         else:
    -        # Treat as duration in seconds from now.
    -        deadline = _time.time() + max(1, expired_in)
    +        deadline = _time.time() + max(1, raw)
         interval = max(2.0, (interval_ms or 2000) / 1000.0)
     
         while _time.time() < deadline:
    ```

    ```diff
    @@ -4884,8 +4899,10 @@ def _minimax_oauth_login(
             )
     
         now = datetime.now(timezone.utc)
    -    expires_in_s = int(token_data["expired_in"])
    -    expires_at = now.timestamp() + expires_in_s
    +    expires_at_unix = _minimax_resolve_token_expiry_unix(
    +        int(token_data["expired_in"]), now=now,
    +    )
    +    expires_in_s = max(0, int(expires_at_unix - now.timestamp()))
     
         auth_state = {
             "provider": "minimax-oauth",
    ```

    ```diff
    @@ -4899,7 +4916,7 @@ def _minimax_oauth_login(
             "refresh_token": [redacted:secret-assignment]"refresh_token"],
             "resource_url": token_data.get("resource_url"),
             "obtained_at": now.isoformat(),
    -        "expires_at": datetime.fromtimestamp(expires_at, tz=timezone.utc).isoformat(),
    +        "expires_at": datetime.fromtimestamp(expires_at_unix, tz=timezone.utc).isoformat(),
             "expires_in": expires_in_s,
         }
     
    ```

    ```diff
    @@ -4960,14 +4977,16 @@ def _refresh_minimax_oauth_state(
                 relogin_required=True,
             )
         now_dt = datetime.now(timezone.utc)
    -    expires_in_s = int(payload["expired_in"])
    +    expires_at_unix = _minimax_resolve_token_expiry_unix(
    +        int(payload["expired_in"]), now=now_dt,
    +    )
    +    expires_in_s = max(0, int(expires_at_unix - now_dt.timestamp()))
         new_state = dict(state)
         new_state.update({
             "access_token": [redacted:secret-assignment]"access_token"],
             "refresh_token": payload.get("refresh_token", state["refresh_token"]),
             "obtained_at": now_dt.isoformat(),
    -        "expires_at": datetime.fromtimestamp(now_dt.timestamp() + expires_in_s,
    -                                             tz=timezone.utc).isoformat(),
    +        "expires_at": datetime.fromtimestamp(expires_at_unix, tz=timezone.utc).isoformat(),
             "expires_in": expires_in_s,
         })
         _minimax_save_auth_state(new_state)
    ```
  - `hermes_cli/auth_commands.py`

    ```diff
    @@ -375,10 +375,12 @@ def auth_add_command(args) -> None:
             return
     
         if provider == "minimax-oauth":
    -        from hermes_cli.auth import resolve_minimax_oauth_runtime_credentials
    -        creds = resolve_minimax_oauth_runtime_credentials()
    +        creds = auth_mod._minimax_oauth_login(
    +            open_browser=not getattr(args, "no_browser", False),
    +            timeout_seconds=getattr(args, "timeout", None) or 15.0,
    +        )
             label = (getattr(args, "label", None) or "").strip() or label_from_token(
    -            creds["api_key"],
    +            creds["access_token"],
                 _oauth_default_label(provider, len(pool.entries()) + 1),
             )
             entry = PooledCredential(
    ```

    ```diff
    @@ -388,8 +390,9 @@ def auth_add_command(args) -> None:
                 auth_type=AUTH_TYPE_OAUTH,
                 priority=0,
                 source=f"{SOURCE_MANUAL}:minimax_oauth",
    -            access_token=[redacted:secret-assignment]"api_key"],
    -            base_url=creds.get("base_url"),
    +            access_token=[redacted:secret-assignment]"access_token"],
    +            refresh_token=creds.get("refresh_token"),
    +            base_url=creds.get("inference_base_url"),
             )
             pool.add_entry(entry)
             print(f'Added {provider} OAuth credential #{len(pool.entries())}: "{entry.label}"')
    ```
  - `hermes_cli/runtime_provider.py`

    ```diff
    @@ -205,6 +205,14 @@ def _resolve_runtime_from_pool_entry(
         elif provider == "google-gemini-cli":
             api_mode = "chat_completions"
             base_url = base_url or "cloudcode-pa://google"
    +    elif provider == "minimax-oauth":
    +        # MiniMax OAuth tokens are valid only against the Anthropic Messages
    +        # compatible endpoint. Do not honor stale model.api_mode values from a
    +        # prior OpenAI-compatible provider, or the client will hit
    +        # /chat/completions under /anthropic and receive a bare nginx 404.
    +        api_mode = "anthropic_messages"
    +        pconfig = PROVIDER_REGISTRY.get(provider)
    +        base_url = base_url or (pconfig.inference_base_url if pconfig else "")
         elif provider == "anthropic":
             api_mode = "anthropic_messages"
             cfg_provider = str(model_cfg.get("provider") or "").strip().lower()
    ```
  - `hermes_cli/web_server.py`

    ```diff
    @@ -2053,6 +2053,7 @@ def _minimax_poller(session_id: str) -> None:
         """
         from hermes_cli.auth import (
             _minimax_poll_token,
    +        _minimax_resolve_token_expiry_unix,
             _minimax_save_auth_state,
             MINIMAX_OAUTH_GLOBAL_INFERENCE,
             MINIMAX_OAUTH_SCOPE,
    ```

    ```diff
    @@ -2090,8 +2091,10 @@ def _minimax_poller(session_id: str) -> None:
             # dashboard path; cn-region operators can still use the CLI
             # flow which supports `--region cn`.
             now = datetime.now(timezone.utc)
    -        expires_in_s = int(token_data["expired_in"])
    -        expires_at_ts = now.timestamp() + expires_in_s
    +        expires_at_ts = _minimax_resolve_token_expiry_unix(
    +            int(token_data["expired_in"]), now=now,
    +        )
    +        expires_in_s = max(0, int(expires_at_ts - now.timestamp()))
             auth_state = {
                 "provider": "minimax-oauth",
                 "region": sess.get("region", "global"),
    ```
  - `tests/hermes_cli/test_auth_commands.py`

    ```diff
    @@ -170,6 +170,50 @@ class _Args:
         assert singleton["inference_base_url"] == "https://inference.example.com/v1"
     
     
    +def test_auth_add_minimax_oauth_starts_login_and_persists_pool_entry(tmp_path, monkeypatch):
    +    monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes"))
    +    _write_auth_store(tmp_path, {"version": 1, "providers": {}})
    +    token = _jwt_with_email("[redacted:email]")
    +    monkeypatch.setattr(
    +        "hermes_cli.auth._minimax_oauth_login",
    +        lambda **kwargs: {
    +            "provider": "minimax-oauth",
    +            "region": "global",
    +            "portal_base_url": "https://api.minimax.io",
    +            "inference_base_url": "https://api.minimax.io/anthropic",
    +            "client_id": "client-id",
    +            "scope": "group_id profile model.completion",
    +            "token_type": "[redacted:secret-assignment]",
    +            "access_token": [redacted:secret-assignment]
    +            "refresh_token": "[redacted:secret-assignment]",
    +            "resource_url": None,
    +            "obtained_at": "2026-05-11T10:00:00+00:00",
    +            "expires_at": "2026-05-14T10:00:00+00:00",
    +            "expires_in": 259200,
    +        },
    +    )
    +
    +    from hermes_cli.auth_commands import auth_add_command
    +
    +    class _Args:
    +        provider = "minimax-oauth"
    +        auth_type = "oauth"
    +        api_key = None
    +        label = None
    +        no_browser = True
    +        timeout = None
    +
    +    auth_add_command(_Args())
    +
    +    payload = json.loads((tmp_path / "hermes" / "auth.json").read_text())
    +    entries = payload["credential_pool"]["minimax-oauth"]
    +    entry = next(item for item in entries if item["source"] == "manual:minimax_oauth")
    +    assert entry["label"] == "[redacted:email]"
    +    assert entry["access_token"] == token
    +    assert entry["refresh_token"] == "refresh-token"
    +    assert entry["base_url"] == "https://api.minimax.io/anthropic"
    +
    +
     def test_auth_add_nous_oauth_honors_custom_label(tmp_path, monkeypatch):
         """`hermes auth add nous --type oauth --label <name>` must preserve the
         custom label end-to-end — it was silently dropped in the first cut of the
    ```
  - `tests/hermes_cli/test_runtime_provider_resolution.py`

    ```diff
    @@ -2285,3 +2285,39 @@ def test_minimax_oauth_runtime_uses_inference_base_url(monkeypatch):
         resolved = rp.resolve_runtime_provider(requested="minimax-oauth")
     
         assert MINIMAX_OAUTH_CN_INFERENCE.rstrip("/") in resolved["base_url"]
    +
    +
    +def test_minimax_oauth_pool_forces_anthropic_messages_despite_stale_config(monkeypatch):
    +    """A pooled MiniMax OAuth token must not inherit stale chat_completions config."""
    +
    +    class _Entry:
    +        access_token = "[redacted:secret-assignment]"
    +        source = "manual:minimax_oauth"
    +        base_url = "https://api.minimax.io/anthropic"
    +
    +    class _Pool:
    +        def has_credentials(self):
    +            return True
    +
    +        def select(self):
    +            return _Entry()
    +
    +    monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "minimax-oauth")
    +    monkeypatch.setattr(
    +        rp,
    +        "_get_model_config",
    +        lambda: {
    +            "provider": "minimax-oauth",
    +            "default": "MiniMax-M2.7",
    +            "api_mode": "chat_completions",
    +        },
    +    )
    +    monkeypatch.setattr(rp, "load_pool", lambda provider: _Pool())
    +    monkeypatch.setattr(rp, "_resolve_named_custom_runtime", lambda **k: None)
    +    monkeypatch.setattr(rp, "_resolve_explicit_runtime", lambda **k: None)
    +
    +    resolved = rp.resolve_runtime_provider(requested="minimax-oauth")
    +
    +    assert resolved["provider"] == "minimax-oauth"
    +    assert resolved["api_mode"] == "anthropic_messages"
    +    assert resolved["base_url"] == "https://api.minimax.io/anthropic"
    ```
  - `tests/hermes_cli/test_web_oauth_dispatch.py`

    ```diff
    @@ -19,6 +19,8 @@
     
     These tests pin the corrected behavior.
     """
    +import time
    +from datetime import datetime, timezone
     from unittest.mock import patch
     
     import pytest
    ```

    ```diff
    @@ -67,6 +69,53 @@ def test_minimax_login_does_not_launch_anthropic_flow():
         assert body["expires_in"] == 600
     
     
    +def test_minimax_dashboard_poller_accepts_absolute_ms_expired_in():
    +    """Dashboard MiniMax completion must accept unix-ms token expiry values."""
    +    from hermes_cli import web_server as ws
    +
    +    now = datetime.now(timezone.utc)
    +    abs_ms = int((now.timestamp() + 1800) * 1000)
    +    session_id = "minimax-absolute-ms-test"
    +    ws._oauth_sessions[session_id] = {
    +        "session_id": session_id,
    +        "provider": "minimax-oauth",
    +        "flow": "device_code",
    +        "created_at": time.time(),
    +        "status": "pending",
    +        "error_message": None,
    +        "portal_base_url": "https://api.minimax.io",
    +        "client_id": "client-id",
    +        "user_code": "ABCD-1234",
    +        "code_verifier": "verifier",
    +        "interval_ms": 2000,
    +        "expired_in_raw": abs_ms,
    +        "region": "global",
    +    }
    +    captured_state = {}
    +
    +    try:
    +        with patch(
    +            "hermes_cli.auth._minimax_poll_token",
    +            return_value={
    +                "status": "success",
    +                "access_token": "[redacted:secret-assignment]",
    +                "refresh_token": "[redacted:secret-assignment]",
    +                "expired_in": abs_ms,
    +                "token_type": "[redacted:secret-assignment]",
    +            },
    +        ), patch(
    +            "hermes_cli.auth._minimax_save_auth_state",
    +            side_effect=lambda state: captured_state.update(state),
    +        ):
    +            ws._minimax_poller(session_id)
    +    finally:
    +        ws._oauth_sessions.pop(session_id, None)
    +
    +    assert captured_state["access_token"] == "access"
    +    assert 1790 <= captured_state["expires_in"] <= 1810
    +    assert datetime.fromisoformat(captured_state["expires_at"]).year < 9999
    +
    +
     def test_anthropic_pkce_branch_still_works():
         """Sanity: the dispatcher tightening doesn't break the legitimate Anthropic PKCE path."""
         fake_anthropic_response = {
    ```
  - `tests/test_minimax_oauth.py`

    ```diff
    @@ -32,9 +32,11 @@
         _minimax_pkce_pair,
         _minimax_request_user_code,
         _minimax_poll_token,
    +    _minimax_resolve_token_expiry_unix,
         _refresh_minimax_oauth_state,
         resolve_minimax_oauth_runtime_credentials,
         get_minimax_oauth_auth_status,
    +    get_auth_status,
         get_provider_auth_state,
     )
     
    ```

    ```diff
    @@ -67,6 +69,23 @@ def _past_iso(seconds_ago: int = 3600) -> str:
         return datetime.fromtimestamp(ts, tz=timezone.utc).isoformat()
     
     
    +# ---------------------------------------------------------------------------
    +# 0. test_resolve_token_expiry_unix_ttl_vs_absolute_ms
    +# ---------------------------------------------------------------------------
    +
    +def test_resolve_token_expiry_unix_ttl_seconds():
    +    now = datetime(2025, 6, 1, 12, 0, 0, tzinfo=timezone.utc)
    +    got = _minimax_resolve_token_expiry_unix(3600, now=now)
    +    assert abs(got - (now.timestamp() + 3600)) < 0.01
    +
    +
    +def test_resolve_token_expiry_unix_absolute_ms():
    +    now = datetime(2025, 6, 1, 12, 0, 0, tzinfo=timezone.utc)
    +    abs_ms = int((now.timestamp() + 7200) * 1000)
    +    got = _minimax_resolve_token_expiry_unix(abs_ms, now=now)
    +    assert abs(got - (now.timestamp() + 7200)) < 0.01
    +
    +
     # ---------------------------------------------------------------------------
     # 1. test_pkce_pair_produces_valid_s256
     # ---------------------------------------------------------------------------
    ```

    ```diff
    @@ -362,6 +381,46 @@ def test_refresh_updates_access_token():
         assert result["expires_in"] == 7200
     
     
    +def test_refresh_updates_access_token_absolute_ms_expired_in():
    +    """Refresh payload may use unix-ms absolute ``expired_in`` (same as device-code)."""
    +    now0 = datetime.now(timezone.utc)
    +    abs_ms = int((now0.timestamp() + 1800) * 1000)
    +
    +    state = {
    +        "access_token": "[redacted:secret-assignment]",
    +        "refresh_token": "[redacted:secret-assignment]",
    +        "portal_base_url": MINIMAX_OAUTH_GLOBAL_BASE,
    +        "client_id": MINIMAX_OAUTH_CLIENT_ID,
    +        "inference_base_url": MINIMAX_OAUTH_GLOBAL_INFERENCE,
    +        "expires_at": _future_iso(MINIMAX_OAUTH_REFRESH_SKEW_SECONDS - 1),
    +    }
    +
    +    new_token_body = {
    +        "status": "success",
    +        "access_token": "[redacted:secret-assignment]",
    +        "refresh_token": "[redacted:secret-assignment]",
    +        "expired_in": abs_ms,
    +    }
    +
    +    mock_resp = _make_httpx_response(200, new_token_body)
    +
    +    with patch("httpx.Client") as mock_client_class:
    +        mock_client_instance = MagicMock()
    +        mock_client_instance.__enter__ = MagicMock(return_value=mock_client_instance)
    +        mock_client_instance.__exit__ = MagicMock(return_value=False)
    +        mock_client_instance.post.return_value = mock_resp
    +        mock_client_class.return_value = mock_client_instance
    +
    +        with patch("hermes_cli.auth._minimax_save_auth_state"):
    +            result = _refresh_minimax_oauth_state(state)
    +
    +    assert result["access_token"] == "new-access"
    +    assert 1790 <= result["expires_in"] <= 1810
    +    exp = datetime.fromisoformat(result["expires_at"].replace("Z", "+00:00"))
    +    skew = exp.timestamp() - datetime.now(timezone.utc).timestamp()
    +    assert 1790 <= skew <= 1810
    +
    +
     # ---------------------------------------------------------------------------
     # 10. test_refresh_reuse_triggers_relogin_required
     # ---------------------------------------------------------------------------
    ```

    ```diff
    @@ -464,3 +523,18 @@ def test_get_minimax_oauth_auth_status_logged_in():
     
         assert status["logged_in"] is True
         assert status["region"] == "global"
    +
    +
    +def test_generic_auth_status_dispatches_minimax_oauth():
    +    state = {
    +        "access_token": "tok",
    +        "expires_at": _future_iso(3600),
    +        "region": "global",
    +    }
    +
    +    with patch("hermes_cli.auth.get_provider_auth_state", return_value=state):
    +        status = get_auth_status("minimax-oauth")
    +
    +    assert status["logged_in"] is True
    +    assert status["provider"] == "minimax-oauth"
    +    assert status["region"] == "global"
    ```
- **#19461: fix(auth): rewrite stale MiniMax OAuth approval URL**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`
  - files: `hermes_cli/auth.py`, `tests/test_minimax_oauth.py`
- **#20374: fix(minimax-oauth): correct authorize URL domain and handle ms timestamps**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#24165`
- **#124236: fix(minimax-oauth): rewrite stale verification_uri host (#19337)**. closed without merge
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `89a9ef3853c5`

## Fix status

- Merged to main in PR #24165 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-05-12`.
- Subsystem: `hermes_cli`
