MiniMax auth broken by Anthropic token refresh logic

NousResearch/hermes-agent · cluster 2094 · agent

Data as of

Nothing open on GitHub. Fix merged in #2103. Check it covers your case.

The short answer

0open issues
0open PRs
8items grouped
Known?
Yes. 8 related items grouped here, 2 issues already closed.
Fix event
A fix was merged to main in PR #2103. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this partially resolved, deciding PR #2103. That is separate from the fix event.
Still open
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.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #2103 fix: resolve MiniMax 401 auth error by defaulting to anthropic_messages

    merged to main

    files: hermes_cli/auth.py, hermes_cli/runtime_provider.py, tests/test_api_key_providers.py, tests/test_runtime_provider_resolution.py

    Show the diff
    • hermes_cli/auth.py
      @@ -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",
           ),
      @@ -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
      @@ -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
      @@ -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"
       
      @@ -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
      @@ -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")
      @@ -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):
  • #2388 fix(provider): prevent Anthropic fallback from inheriting non-Anthropic base_url + fix(update): reset on stash conflict

    merged to main

    files: agent/auxiliary_client.py, hermes_cli/main.py, hermes_cli/runtime_provider.py, tests/hermes_cli/test_update_autostash.py

    Show the diff
    • agent/auxiliary_client.py
      @@ -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
      @@ -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
      @@ -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
      @@ -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"]:
      @@ -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
      @@ -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"]:
      @@ -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
       
       
      @@ -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}")
      @@ -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
      @@ -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"]:
      @@ -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
      @@ -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)
      @@ -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):

4 PRs closed without merging

From the close comments: merged via another change 2 · other 2

  • #2100 Fix: Resolve Minimax 401 Auth error by defaulting to anthropic_messages · merged via another change → #2103
  • #2375 fix: MiniMax (/anthropic) auth gets overridden by Anthropic token refr · merged via another change → #2388
  • #2377 fix(auth): gate Anthropic token refresh to native Anthropic endpoints only · other
  • #2396 fix(auth): gate Anthropic token refresh to native Anthropic endpoints only · other
Closed issues · 2
  • Closed#2094[Bug]: Error code: 401 - Authorization field missing. Minimax configuration.
  • Closed#2374MiniMax (/anthropic) auth gets overridden by Anthropic token refresh, causing 401
How this cluster was grouped
  • Link evidence (partly structural links): 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 item 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.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Subsystem
agent
Merged PRs
#2103, #2388
Closed issues
2
Attempts
4 PRs closed without merging
Fix link checked
21 Mar 2026. This confirmation is old and may be out of date (a revert or reopen would not have invalidated it here); re-verify.

For your agent

The same facts, typed.