# NousResearch/hermes-agent · MiniMax auth broken by Anthropic token refresh logic

## Open now

- Nothing open on GitHub.

These issues and PRs address a bug where Anthropic's token refresh mechanism overrides authentication for third-party providers using the anthropic_messages format, particularly MiniMax, causing 401 errors. Fixes focus on gating Anthropic token injection to native Anthropic endpoints only.

## TL;DR

- 8 member(s): 0 open issue(s), 2 closed issue(s), 0 open PR(s), 2 merged PR(s), 4 PR(s) closed without merging.
- A fix was merged to main in PR #2103. Verify it addresses your case.
- **Recommended:** read PR #2103 (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: `partially_resolved` · deciding PR(s): `pr-2103`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
- Members outside that structural core may belong to a different underlying problem.
- Pairwise link density `0.23`: 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.

## Closed / duplicate issues

- #2094: \[Bug\]: Error code: 401 - Authorization field missing. Minimax configuration.
- #2374: MiniMax (/anthropic) auth gets overridden by Anthropic token refresh, causing 401

## Prior & competing PRs

- **#2103: fix: resolve MiniMax 401 auth error by defaulting to anthropic_messages**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `hermes_cli/auth.py`, `hermes_cli/runtime_provider.py`, `tests/test_api_key_providers.py`, `tests/test_runtime_provider_resolution.py`
  - fix hunks:
  - `hermes_cli/auth.py`

    ```diff
    @@ -145,7 +145,7 @@ class ProviderConfig:
             id="minimax",
             name="MiniMax",
             auth_type="api_key",
    -        inference_base_url="https://api.minimax.io/v1",
    +        inference_base_url="https://api.minimax.io/anthropic",
             api_key_env_vars=("MINIMAX_API_KEY",),
             base_url_env_var="MINIMAX_BASE_URL",
         ),
    ```

    ```diff
    @@ -168,7 +168,7 @@ class ProviderConfig:
             id="minimax-cn",
             name="MiniMax (China)",
             auth_type="api_key",
    -        inference_base_url="https://api.minimaxi.com/v1",
    +        inference_base_url="https://api.minimaxi.com/anthropic",
             api_key_env_vars=("MINIMAX_CN_API_KEY",),
             base_url_env_var="MINIMAX_CN_BASE_URL",
         ),
    ```
  - `hermes_cli/runtime_provider.py`

    ```diff
    @@ -387,6 +387,12 @@ def resolve_runtime_provider(
                 # (e.g. https://api.minimax.io/anthropic, https://dashscope.../anthropic)
                 elif base_url.rstrip("/").endswith("/anthropic"):
                     api_mode = "anthropic_messages"
    +            # MiniMax providers always use Anthropic Messages API.
    +            # Auto-correct stale /v1 URLs (from old .env or config) to /anthropic.
    +            elif provider in ("minimax", "minimax-cn"):
    +                api_mode = "anthropic_messages"
    +                if base_url.rstrip("/").endswith("/v1"):
    +                    base_url = base_url.rstrip("/")[:-3] + "/anthropic"
             return {
                 "provider": provider,
                 "api_mode": api_mode,
    ```
  - `tests/test_api_key_providers.py`

    ```diff
    @@ -92,8 +92,8 @@ def test_base_urls(self):
             assert PROVIDER_REGISTRY["copilot-acp"].inference_base_url == "acp://copilot"
             assert PROVIDER_REGISTRY["zai"].inference_base_url == "https://api.z.ai/api/paas/v4"
             assert PROVIDER_REGISTRY["kimi-coding"].inference_base_url == "https://api.moonshot.ai/v1"
    -        assert PROVIDER_REGISTRY["minimax"].inference_base_url == "https://api.minimax.io/v1"
    -        assert PROVIDER_REGISTRY["minimax-cn"].inference_base_url == "https://api.minimaxi.com/v1"
    +        assert PROVIDER_REGISTRY["minimax"].inference_base_url == "https://api.minimax.io/anthropic"
    +        assert PROVIDER_REGISTRY["minimax-cn"].inference_base_url == "https://api.minimaxi.com/anthropic"
             assert PROVIDER_REGISTRY["ai-gateway"].inference_base_url == "https://ai-gateway.vercel.sh/v1"
             assert PROVIDER_REGISTRY["kilocode"].inference_base_url == "https://api.kilo.ai/api/gateway"
     
    ```

    ```diff
    @@ -399,14 +399,14 @@ def test_resolve_minimax_with_key(self, monkeypatch):
             creds = resolve_api_key_provider_credentials("minimax")
             assert creds["provider"] == "minimax"
             assert creds["api_key"] == "mm-secret-key"
    -        assert creds["base_url"] == "https://api.minimax.io/v1"
    +        assert creds["base_url"] == "https://api.minimax.io/anthropic"
     
         def test_resolve_minimax_cn_with_key(self, monkeypatch):
             monkeypatch.setenv("MINIMAX_CN_API_KEY", "mmcn-secret-key")
             creds = resolve_api_key_provider_credentials("minimax-cn")
             assert creds["provider"] == "minimax-cn"
             assert creds["api_key"] == "mmcn-secret-key"
    -        assert creds["base_url"] == "https://api.minimaxi.com/v1"
    +        assert creds["base_url"] == "https://api.minimaxi.com/anthropic"
     
         def test_resolve_ai_gateway_with_key(self, monkeypatch):
             monkeypatch.setenv("AI_GATEWAY_API_KEY", "gw-secret-key")
    ```
  - `tests/test_runtime_provider_resolution.py`

    ```diff
    @@ -479,8 +479,8 @@ def test_api_key_provider_explicit_api_mode_config(monkeypatch):
         assert resolved["api_mode"] == "anthropic_messages"
     
     
    -def test_api_key_provider_default_url_stays_chat_completions(monkeypatch):
    -    """API-key providers with default /v1 URL should stay on chat_completions."""
    +def test_minimax_default_url_uses_anthropic_messages(monkeypatch):
    +    """MiniMax with default /anthropic URL should auto-detect anthropic_messages mode."""
         monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "minimax")
         monkeypatch.setattr(rp, "_get_model_config", lambda: {})
         monkeypatch.setenv("MINIMAX_API_KEY", "test-minimax-key")
    ```

    ```diff
    @@ -488,9 +488,50 @@ def test_api_key_provider_default_url_stays_chat_completions(monkeypatch):
     
         resolved = rp.resolve_runtime_provider(requested="minimax")
     
    +    assert resolved["provider"] == "minimax"
    +    assert resolved["api_mode"] == "anthropic_messages"
    +    assert resolved["base_url"] == "https://api.minimax.io/anthropic"
    +
    +
    +def test_minimax_stale_v1_url_auto_corrected(monkeypatch):
    +    """MiniMax with stale /v1 base URL should be auto-corrected to /anthropic."""
    +    monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "minimax")
    +    monkeypatch.setattr(rp, "_get_model_config", lambda: {})
    +    monkeypatch.setenv("MINIMAX_API_KEY", "test-minimax-key")
    +    monkeypatch.setenv("MINIMAX_BASE_URL", "https://api.minimax.io/v1")
    +
    +    resolved = rp.resolve_runtime_provider(requested="minimax")
    +
    +    assert resolved["provider"] == "minimax"
    +    assert resolved["api_mode"] == "anthropic_messages"
    +    assert resolved["base_url"] == "https://api.minimax.io/anthropic"
    +
    +
    +def test_minimax_cn_stale_v1_url_auto_corrected(monkeypatch):
    +    """MiniMax-CN with stale /v1 base URL should be auto-corrected to /anthropic."""
    +    monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "minimax-cn")
    +    monkeypatch.setattr(rp, "_get_model_config", lambda: {})
    +    monkeypatch.setenv("MINIMAX_CN_API_KEY", "test-minimax-cn-key")
    +    monkeypatch.setenv("MINIMAX_CN_BASE_URL", "https://api.minimaxi.com/v1")
    +
    +    resolved = rp.resolve_runtime_provider(requested="minimax-cn")
    +
    +    assert resolved["provider"] == "minimax-cn"
    +    assert resolved["api_mode"] == "anthropic_messages"
    +    assert resolved["base_url"] == "https://api.minimaxi.com/anthropic"
    +
    +
    +def test_minimax_explicit_api_mode_respected(monkeypatch):
    +    """Explicit api_mode config should override MiniMax auto-detection."""
    +    monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "minimax")
    +    monkeypatch.setattr(rp, "_get_model_config", lambda: {"api_mode": "chat_completions"})
    +    monkeypatch.setenv("MINIMAX_API_KEY", "test-minimax-key")
    +    monkeypatch.delenv("MINIMAX_BASE_URL", raising=False)
    +
    +    resolved = rp.resolve_runtime_provider(requested="minimax")
    +
         assert resolved["provider"] == "minimax"
         assert resolved["api_mode"] == "chat_completions"
    -    assert resolved["base_url"] == "https://api.minimax.io/v1"
     
     
     def test_named_custom_provider_anthropic_api_mode(monkeypatch):
    ```
- **#2377: fix(auth): gate Anthropic token refresh to native Anthropic endpoints only**. closed without merge · fix tier `suggested`
  - maintainer direction: category `uncategorized`; implication `needs_llm`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#2388: fix(provider): prevent Anthropic fallback from inheriting non-Anthropic base_url + fix(update): reset on stash conflict**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `agent/auxiliary_client.py`, `hermes_cli/main.py`, `hermes_cli/runtime_provider.py`, `tests/hermes_cli/test_update_autostash.py`
  - fix hunks:
  - `agent/auxiliary_client.py`

    ```diff
    @@ -654,16 +654,20 @@ def _try_anthropic() -> Tuple[Optional[Any], Optional[str]]:
         if not token:
             return None, None
     
    -    # Allow base URL override from config.yaml model.base_url
    +    # Allow base URL override from config.yaml model.base_url, but only
    +    # when the configured provider is anthropic — otherwise a non-Anthropic
    +    # base_url (e.g. Codex endpoint) would leak into Anthropic requests.
         base_url = _ANTHROPIC_DEFAULT_BASE_URL
         try:
             from hermes_cli.config import load_config
             cfg = load_config()
             model_cfg = cfg.get("model")
             if isinstance(model_cfg, dict):
    -            cfg_base_url = (model_cfg.get("base_url") or "").strip().rstrip("/")
    -            if cfg_base_url:
    -                base_url = cfg_base_url
    +            cfg_provider = str(model_cfg.get("provider") or "").strip().lower()
    +            if cfg_provider == "anthropic":
    +                cfg_base_url = (model_cfg.get("base_url") or "").strip().rstrip("/")
    +                if cfg_base_url:
    +                    base_url = cfg_base_url
         except Exception:
             pass
     
    ```
  - `hermes_cli/main.py`

    ```diff
    @@ -2559,12 +2559,29 @@ def _restore_stashed_changes(
             capture_output=True,
             text=True,
         )
    -    if restore.returncode != 0:
    +
    +    # Check for unmerged (conflicted) files — can happen even when returncode is 0
    +    unmerged = subprocess.run(
    +        git_cmd + ["diff", "--name-only", "--diff-filter=U"],
    +        cwd=cwd,
    +        capture_output=True,
    +        text=True,
    +    )
    +    has_conflicts = bool(unmerged.stdout.strip())
    +
    +    if restore.returncode != 0 or has_conflicts:
    +        # Reset the working tree so Hermes is runnable with the updated code
    +        subprocess.run(
    +            git_cmd + ["reset", "--hard", "HEAD"],
    +            cwd=cwd,
    +            capture_output=True,
    +        )
             print("✗ Update pulled new code, but restoring local changes failed.")
             if restore.stdout.strip():
                 print(restore.stdout.strip())
             if restore.stderr.strip():
                 print(restore.stderr.strip())
    +        print("The working tree has been reset to a clean state.")
             print("Your changes are still preserved in git stash.")
             print(f"Resolve manually with: git stash apply {stash_ref}")
             sys.exit(1)
    ```
  - `hermes_cli/runtime_provider.py`

    ```diff
    @@ -359,9 +359,14 @@ def resolve_runtime_provider(
                     "No Anthropic credentials found. Set ANTHROPIC_TOKEN or ANTHROPIC_API_KEY, "
                     "run 'claude setup-token', or authenticate with 'claude /login'."
                 )
    -        # Allow base URL override from config.yaml model.base_url
    +        # Allow base URL override from config.yaml model.base_url, but only
    +        # when the configured provider is anthropic — otherwise a non-Anthropic
    +        # base_url (e.g. Codex endpoint) would leak into Anthropic requests.
             model_cfg = _get_model_config()
    -        cfg_base_url = (model_cfg.get("base_url") or "").strip().rstrip("/")
    +        cfg_provider = str(model_cfg.get("provider") or "").strip().lower()
    +        cfg_base_url = ""
    +        if cfg_provider == "anthropic":
    +            cfg_base_url = (model_cfg.get("base_url") or "").strip().rstrip("/")
             base_url = cfg_base_url or "https://api.anthropic.com"
             return {
                 "provider": "anthropic",
    ```
  - `tests/hermes_cli/test_update_autostash.py`

    ```diff
    @@ -68,6 +68,8 @@ def fake_run(cmd, **kwargs):
             calls.append((cmd, kwargs))
             if cmd[1:3] == ["stash", "apply"]:
                 return SimpleNamespace(stdout="applied\n", stderr="", returncode=0)
    +        if cmd[1:3] == ["diff", "--name-only"]:
    +            return SimpleNamespace(stdout="", stderr="", returncode=0)
             if cmd[1:3] == ["stash", "list"]:
                 return SimpleNamespace(stdout="stash@{1} abc123\n", stderr="", returncode=0)
             if cmd[1:3] == ["stash", "drop"]:
    ```

    ```diff
    @@ -81,8 +83,9 @@ def fake_run(cmd, **kwargs):
     
         assert restored is True
         assert calls[0][0] == ["git", "stash", "apply", "abc123"]
    -    assert calls[1][0] == ["git", "stash", "list", "--format=%gd %H"]
    -    assert calls[2][0] == ["git", "stash", "drop", "stash@{1}"]
    +    assert calls[1][0] == ["git", "diff", "--name-only", "--diff-filter=U"]
    +    assert calls[2][0] == ["git", "stash", "list", "--format=%gd %H"]
    +    assert calls[3][0] == ["git", "stash", "drop", "stash@{1}"]
         out = capsys.readouterr().out
         assert "Restore local changes now? [Y/n]" in out
         assert "restored on top of the updated codebase" in out
    ```

    ```diff
    @@ -117,6 +120,8 @@ def fake_run(cmd, **kwargs):
             calls.append((cmd, kwargs))
             if cmd[1:3] == ["stash", "apply"]:
                 return SimpleNamespace(stdout="applied\n", stderr="", returncode=0)
    +        if cmd[1:3] == ["diff", "--name-only"]:
    +            return SimpleNamespace(stdout="", stderr="", returncode=0)
             if cmd[1:3] == ["stash", "list"]:
                 return SimpleNamespace(stdout="stash@{0} abc123\n", stderr="", returncode=0)
             if cmd[1:3] == ["stash", "drop"]:
    ```

    ```diff
    @@ -129,8 +134,9 @@ def fake_run(cmd, **kwargs):
     
         assert restored is True
         assert calls[0][0] == ["git", "stash", "apply", "abc123"]
    -    assert calls[1][0] == ["git", "stash", "list", "--format=%gd %H"]
    -    assert calls[2][0] == ["git", "stash", "drop", "stash@{0}"]
    +    assert calls[1][0] == ["git", "diff", "--name-only", "--diff-filter=U"]
    +    assert calls[2][0] == ["git", "stash", "list", "--format=%gd %H"]
    +    assert calls[3][0] == ["git", "stash", "drop", "stash@{0}"]
         assert "Restore local changes now?" not in capsys.readouterr().out
     
     
    ```

    ```diff
    @@ -152,6 +158,8 @@ def fake_run(cmd, **kwargs):
             calls.append((cmd, kwargs))
             if cmd[1:3] == ["stash", "apply"]:
                 return SimpleNamespace(stdout="applied\n", stderr="", returncode=0)
    +        if cmd[1:3] == ["diff", "--name-only"]:
    +            return SimpleNamespace(stdout="", stderr="", returncode=0)
             if cmd[1:3] == ["stash", "list"]:
                 return SimpleNamespace(stdout="stash@{0} def456\n", stderr="", returncode=0)
             raise AssertionError(f"unexpected command: {cmd}")
    ```

    ```diff
    @@ -161,10 +169,9 @@ def fake_run(cmd, **kwargs):
         restored = hermes_main._restore_stashed_changes(["git"], tmp_path, "abc123", prompt_user=False)
     
         assert restored is True
    -    assert calls == [
    -        (["git", "stash", "apply", "abc123"], {"cwd": tmp_path, "capture_output": True, "text": True}),
    -        (["git", "stash", "list", "--format=%gd %H"], {"cwd": tmp_path, "capture_output": True, "text": True, "check": True}),
    -    ]
    +    assert calls[0] == (["git", "stash", "apply", "abc123"], {"cwd": tmp_path, "capture_output": True, "text": True})
    +    assert calls[1] == (["git", "diff", "--name-only", "--diff-filter=U"], {"cwd": tmp_path, "capture_output": True, "text": True})
    +    assert calls[2] == (["git", "stash", "list", "--format=%gd %H"], {"cwd": tmp_path, "capture_output": True, "text": True, "check": True})
         out = capsys.readouterr().out
         assert "couldn't find the stash entry to drop" in out
         assert "stash was left in place" in out
    ```

    ```diff
    @@ -181,6 +188,8 @@ def fake_run(cmd, **kwargs):
             calls.append((cmd, kwargs))
             if cmd[1:3] == ["stash", "apply"]:
                 return SimpleNamespace(stdout="applied\n", stderr="", returncode=0)
    +        if cmd[1:3] == ["diff", "--name-only"]:
    +            return SimpleNamespace(stdout="", stderr="", returncode=0)
             if cmd[1:3] == ["stash", "list"]:
                 return SimpleNamespace(stdout="stash@{0} abc123\n", stderr="", returncode=0)
             if cmd[1:3] == ["stash", "drop"]:
    ```

    ```diff
    @@ -192,7 +201,7 @@ def fake_run(cmd, **kwargs):
         restored = hermes_main._restore_stashed_changes(["git"], tmp_path, "abc123", prompt_user=False)
     
         assert restored is True
    -    assert calls[2][0] == ["git", "stash", "drop", "stash@{0}"]
    +    assert calls[3][0] == ["git", "stash", "drop", "stash@{0}"]
         out = capsys.readouterr().out
         assert "couldn't drop the saved stash entry" in out
         assert "drop failed" in out
    ```

    ```diff
    @@ -208,6 +217,10 @@ def fake_run(cmd, **kwargs):
             calls.append((cmd, kwargs))
             if cmd[1:3] == ["stash", "apply"]:
                 return SimpleNamespace(stdout="conflict output\n", stderr="conflict stderr\n", returncode=1)
    +        if cmd[1:3] == ["diff", "--name-only"]:
    +            return SimpleNamespace(stdout="hermes_cli/main.py\n", stderr="", returncode=0)
    +        if cmd[1:3] == ["reset", "--hard"]:
    +            return SimpleNamespace(stdout="", stderr="", returncode=0)
             raise AssertionError(f"unexpected command: {cmd}")
     
         monkeypatch.setattr(hermes_main.subprocess, "run", fake_run)
    ```

    ```diff
    @@ -219,7 +232,34 @@ def fake_run(cmd, **kwargs):
         out = capsys.readouterr().out
         assert "Your changes are still preserved in git stash." in out
         assert "git stash apply abc123" in out
    -    assert calls == [(["git", "stash", "apply", "abc123"], {"cwd": tmp_path, "capture_output": True, "text": True})]
    +    assert "working tree has been reset to a clean state" in out
    +    # Verify reset --hard was called to clean up conflict markers
    +    reset_calls = [c for c, _ in calls if c[1:3] == ["reset", "--hard"]]
    +    assert len(reset_calls) == 1
    +
    +
    +def test_restore_stashed_changes_resets_when_unmerged_files_detected(monkeypatch, tmp_path, capsys):
    +    """Even if stash apply returns 0, conflict markers must be cleaned up."""
    +    calls = []
    +
    +    def fake_run(cmd, **kwargs):
    +        calls.append((cmd, kwargs))
    +        if cmd[1:3] == ["stash", "apply"]:
    +            return SimpleNamespace(stdout="applied\n", stderr="", returncode=0)
    +        if cmd[1:3] == ["diff", "--name-only"]:
    +            return SimpleNamespace(stdout="cli.py\n", stderr="", returncode=0)
    +        if cmd[1:3] == ["reset", "--hard"]:
    +            return SimpleNamespace(stdout="", stderr="", returncode=0)
    +        raise AssertionError(f"unexpected command: {cmd}")
    +
    +    monkeypatch.setattr(hermes_main.subprocess, "run", fake_run)
    +
    +    with pytest.raises(SystemExit, match="1"):
    +        hermes_main._restore_stashed_changes(["git"], tmp_path, "abc123", prompt_user=False)
    +
    +    out = capsys.readouterr().out
    +    assert "working tree has been reset to a clean state" in out
    +    assert "git stash apply abc123" in out
     
     
     def test_stash_local_changes_if_needed_raises_when_stash_ref_missing(monkeypatch, tmp_path):
    ```
- **#2396: fix(auth): gate Anthropic token refresh to native Anthropic endpoints only**. closed without merge · fix tier `suggested`
  - maintainer direction: category `uncategorized`; implication `needs_llm`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#2100: Fix: Resolve Minimax 401 Auth error by defaulting to anthropic_messages**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#2103`
- **#2375: fix: MiniMax (/anthropic) auth gets overridden by Anthropic token refr**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#2388`

## Fix status

- Merged to main in PR #2103 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-03-21`. This confirmation is old and may be out of date (a revert/reopen would not have invalidated it here); re-verify.
- Subsystem: `agent`
