diff --git a/agent/credential_pool.py b/agent/credential_pool.py index d11b0186e4d..004b5749889 100644 --- a/agent/credential_pool.py +++ b/agent/credential_pool.py @@ -1299,6 +1299,48 @@ def _seed_from_singletons(provider: str, entries: List[PooledCredential]) -> Tup except Exception as exc: logger.debug("Qwen OAuth token seed failed: %s", exc) + elif provider == "minimax-oauth": + # MiniMax OAuth tokens live in ~/.hermes/auth.json providers.minimax-oauth. + # Seed the pool so `/auth list` reflects the logged-in state and the + # standard `hermes auth remove minimax-oauth ` flow works. + # Use refresh_if_expiring=False equivalent: resolve_minimax_oauth_runtime_credentials + # always refreshes on expiry, so instead read raw state here to avoid + # surprise network calls during provider discovery. + try: + from hermes_cli.auth import get_provider_auth_state + state = get_provider_auth_state("minimax-oauth") + if state and state.get("access_token"): + source_name = "oauth" + if not _is_suppressed(provider, source_name): + active_sources.add(source_name) + expires_at_ms = None + try: + from datetime import datetime as _dt + raw = state.get("expires_at", "") + if raw: + expires_at_ms = int(_dt.fromisoformat(raw).timestamp() * 1000) + except Exception: + expires_at_ms = None + base_url = str(state.get("inference_base_url", "") or "").rstrip("/") + changed |= _upsert_entry( + entries, + provider, + source_name, + { + "source": source_name, + "auth_type": AUTH_TYPE_OAUTH, + "access_token": state["access_token"], + "refresh_token": state.get("refresh_token"), + "expires_at_ms": expires_at_ms, + "base_url": base_url, + "label": state.get("label", "") or label_from_token( + state.get("access_token", ""), source_name + ), + }, + ) + except Exception as exc: + logger.debug("MiniMax OAuth token seed failed: %s", exc) + elif provider == "openai-codex": # Respect user suppression — `hermes auth remove openai-codex` marks # the device_code source as suppressed so it won't be re-seeded from diff --git a/agent/credential_sources.py b/agent/credential_sources.py index dce6293526c..74204919248 100644 --- a/agent/credential_sources.py +++ b/agent/credential_sources.py @@ -252,6 +252,19 @@ def _remove_nous_device_code(provider: str, removed) -> RemovalResult: return result +def _remove_minimax_oauth(provider: str, removed) -> RemovalResult: + """MiniMax OAuth lives in auth.json providers.minimax-oauth — clear it. + + Same pattern as Nous: single-source OAuth state with refresh tokens. + Suppression of the `oauth` source ensures the pool reseed path + (_seed_from_singletons) doesn't instantly undo the removal. + """ + result = RemovalResult() + if _clear_auth_store_provider(provider): + result.cleaned.append(f"Cleared {provider} OAuth tokens from auth store") + return result + + def _remove_codex_device_code(provider: str, removed) -> RemovalResult: """Codex tokens live in TWO places: our auth store AND ~/.codex/auth.json. @@ -389,6 +402,11 @@ def _register_all_sources() -> None: remove_fn=_remove_qwen_cli, description="~/.qwen/oauth_creds.json", )) + register(RemovalStep( + provider="minimax-oauth", source_id="oauth", + remove_fn=_remove_minimax_oauth, + description="auth.json providers.minimax-oauth", + )) register(RemovalStep( provider="*", source_id="config:", match_fn=lambda src: src.startswith("config:") or src == "model_config", diff --git a/agent/models_dev.py b/agent/models_dev.py index 236dd582f92..79cfa90ca95 100644 --- a/agent/models_dev.py +++ b/agent/models_dev.py @@ -149,6 +149,7 @@ PROVIDER_TO_MODELS_DEV: Dict[str, str] = { "stepfun": "stepfun", "kimi-coding-cn": "kimi-for-coding", "minimax": "minimax", + "minimax-oauth": "minimax", "minimax-cn": "minimax-cn", "deepseek": "deepseek", "alibaba": "alibaba", diff --git a/hermes_cli/model_normalize.py b/hermes_cli/model_normalize.py index 99e6c34e481..433e3427964 100644 --- a/hermes_cli/model_normalize.py +++ b/hermes_cli/model_normalize.py @@ -96,6 +96,7 @@ _MATCHING_PREFIX_STRIP_PROVIDERS: frozenset[str] = frozenset({ "kimi-coding", "kimi-coding-cn", "minimax", + "minimax-oauth", "minimax-cn", "alibaba", "qwen-oauth", diff --git a/hermes_cli/providers.py b/hermes_cli/providers.py index 60f8dd8eaa7..49098709546 100644 --- a/hermes_cli/providers.py +++ b/hermes_cli/providers.py @@ -111,6 +111,11 @@ HERMES_OVERLAYS: Dict[str, HermesOverlay] = { transport="anthropic_messages", base_url_env_var="MINIMAX_BASE_URL", ), + "minimax-oauth": HermesOverlay( + transport="anthropic_messages", + auth_type="oauth_external", + base_url_override="https://api.minimax.io/anthropic", + ), "minimax-cn": HermesOverlay( transport="anthropic_messages", base_url_env_var="MINIMAX_CN_BASE_URL", diff --git a/hermes_cli/status.py b/hermes_cli/status.py index b412fb56a45..2f072c0e317 100644 --- a/hermes_cli/status.py +++ b/hermes_cli/status.py @@ -159,14 +159,21 @@ def show_status(args): print(color("◆ Auth Providers", Colors.CYAN, Colors.BOLD)) try: - from hermes_cli.auth import get_nous_auth_status, get_codex_auth_status, get_qwen_auth_status + from hermes_cli.auth import ( + get_nous_auth_status, + get_codex_auth_status, + get_qwen_auth_status, + get_minimax_oauth_auth_status, + ) nous_status = get_nous_auth_status() codex_status = get_codex_auth_status() qwen_status = get_qwen_auth_status() + minimax_status = get_minimax_oauth_auth_status() except Exception: nous_status = {} codex_status = {} qwen_status = {} + minimax_status = {} nous_logged_in = bool(nous_status.get("logged_in")) nous_error = nous_status.get("error") @@ -219,6 +226,20 @@ def show_status(args): if qwen_status.get("error") and not qwen_logged_in: print(f" Error: {qwen_status.get('error')}") + minimax_logged_in = bool(minimax_status.get("logged_in")) + print( + f" {'MiniMax OAuth':<12} {check_mark(minimax_logged_in)} " + f"{'logged in' if minimax_logged_in else 'not logged in (run: hermes auth add minimax-oauth)'}" + ) + minimax_region = minimax_status.get("region") + if minimax_logged_in and minimax_region: + print(f" Region: {minimax_region}") + minimax_exp = minimax_status.get("expires_at") + if minimax_exp: + print(f" Access exp: {minimax_exp}") + if minimax_status.get("error") and not minimax_logged_in: + print(f" Error: {minimax_status.get('error')}") + # ========================================================================= # Nous Subscription Features # ========================================================================= diff --git a/hermes_cli/web_server.py b/hermes_cli/web_server.py index c45375cde2b..965566bce99 100644 --- a/hermes_cli/web_server.py +++ b/hermes_cli/web_server.py @@ -1221,6 +1221,14 @@ _OAUTH_PROVIDER_CATALOG: tuple[Dict[str, Any], ...] = ( "docs_url": "https://github.com/QwenLM/qwen-code", "status_fn": None, # dispatched via auth.get_qwen_auth_status }, + { + "id": "minimax-oauth", + "name": "MiniMax (OAuth)", + "flow": "pkce", + "cli_command": "hermes auth add minimax-oauth", + "docs_url": "https://www.minimax.io", + "status_fn": None, # dispatched via auth.get_minimax_oauth_auth_status + }, ) @@ -1264,6 +1272,16 @@ def _resolve_provider_status(provider_id: str, status_fn) -> Dict[str, Any]: "expires_at": raw.get("expires_at"), "has_refresh_token": bool(raw.get("has_refresh_token")), } + if provider_id == "minimax-oauth": + raw = hauth.get_minimax_oauth_auth_status() + return { + "logged_in": bool(raw.get("logged_in")), + "source": "minimax_oauth", + "source_label": f"MiniMax ({raw.get('region', 'global')})", + "token_preview": None, + "expires_at": raw.get("expires_at"), + "has_refresh_token": True, + } except Exception as e: return {"logged_in": False, "error": str(e)} return {"logged_in": False} diff --git a/scripts/release.py b/scripts/release.py index 9a0096cbb16..c9fdfbbcb70 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -103,6 +103,7 @@ AUTHOR_MAP = { "keifergu@tencent.com": "keifergu", "kshitijk4poor@users.noreply.github.com": "kshitijk4poor", "abner.the.foreman@agentmail.to": "Abnertheforeman", + "adam.manning@pro-serveinc.com": "amanning3390", "thomasgeorgevii09@gmail.com": "tochukwuada", "harryykyle1@gmail.com": "hharry11", "kshitijk4poor@gmail.com": "kshitijk4poor", diff --git a/website/docs/integrations/providers.md b/website/docs/integrations/providers.md index cc2a8dac6d0..a989d938fed 100644 --- a/website/docs/integrations/providers.md +++ b/website/docs/integrations/providers.md @@ -411,6 +411,32 @@ Set `HERMES_QWEN_BASE_URL` only if the portal endpoint relocates (default: `http `qwen-oauth` uses the consumer-facing Qwen Portal with OAuth login — ideal for individual users. The `alibaba` provider uses DashScope's enterprise API with a `DASHSCOPE_API_KEY` — ideal for programmatic / production workloads. Both route to Qwen-family models but live at different endpoints. ::: +### MiniMax (OAuth) + +MiniMax-M2.7 via browser OAuth login — no API key needed. Pick **MiniMax (OAuth)** in `hermes model`, sign in through the browser, and Hermes persists the access + refresh tokens. Uses the Anthropic Messages-compatible endpoint (`/anthropic`) under the hood. + +```bash +hermes model +# → pick "MiniMax (OAuth)" +# → browser opens; sign in with your MiniMax account (global or CN region) +# → confirm — credentials are saved to ~/.hermes/auth.json + +hermes chat # uses api.minimax.io/anthropic endpoint +``` + +Or configure `config.yaml`: +```yaml +model: + provider: "minimax-oauth" + default: "MiniMax-M2.7" +``` + +Supported models: `MiniMax-M2.7` (main) and `MiniMax-M2.7-highspeed` (wired as the default auxiliary model). The OAuth path ignores `MINIMAX_API_KEY` / `MINIMAX_BASE_URL`. + +:::tip MiniMax OAuth vs API key +`minimax-oauth` uses MiniMax's consumer-facing portal with OAuth login — no billing setup required. The `minimax` and `minimax-cn` providers use `MINIMAX_API_KEY` / `MINIMAX_CN_API_KEY` — for programmatic access. See the [MiniMax OAuth guide](/docs/guides/minimax-oauth) for a full walkthrough. +::: + ### NVIDIA NIM Nemotron and other open source models via [build.nvidia.com](https://build.nvidia.com) (free API key) or a local NIM endpoint. @@ -1194,7 +1220,7 @@ fallback_model: When activated, the fallback swaps the model and provider mid-session without losing your conversation. It fires **at most once** per session. -Supported providers: `openrouter`, `nous`, `openai-codex`, `copilot`, `copilot-acp`, `anthropic`, `gemini`, `google-gemini-cli`, `qwen-oauth`, `huggingface`, `zai`, `kimi-coding`, `kimi-coding-cn`, `minimax`, `minimax-cn`, `deepseek`, `nvidia`, `xai`, `ollama-cloud`, `bedrock`, `ai-gateway`, `opencode-zen`, `opencode-go`, `kilocode`, `xiaomi`, `arcee`, `gmi`, `alibaba`, `tencent-tokenhub`, `custom`. +Supported providers: `openrouter`, `nous`, `openai-codex`, `copilot`, `copilot-acp`, `anthropic`, `gemini`, `google-gemini-cli`, `qwen-oauth`, `huggingface`, `zai`, `kimi-coding`, `kimi-coding-cn`, `minimax`, `minimax-cn`, `minimax-oauth`, `deepseek`, `nvidia`, `xai`, `ollama-cloud`, `bedrock`, `ai-gateway`, `opencode-zen`, `opencode-go`, `kilocode`, `xiaomi`, `arcee`, `gmi`, `alibaba`, `tencent-tokenhub`, `custom`. :::tip Fallback is configured exclusively through `config.yaml` — there are no environment variables for it. For full details on when it triggers, supported providers, and how it interacts with auxiliary tasks and delegation, see [Fallback Providers](/docs/user-guide/features/fallback-providers). diff --git a/website/docs/reference/cli-commands.md b/website/docs/reference/cli-commands.md index ca1fb0817a9..074f7ee830a 100644 --- a/website/docs/reference/cli-commands.md +++ b/website/docs/reference/cli-commands.md @@ -85,7 +85,7 @@ Common options: | `-q`, `--query "..."` | One-shot, non-interactive prompt. | | `-m`, `--model ` | Override the model for this run. | | `-t`, `--toolsets ` | Enable a comma-separated set of toolsets. | -| `--provider ` | Force a provider: `auto`, `openrouter`, `nous`, `openai-codex`, `copilot-acp`, `copilot`, `anthropic`, `gemini`, `google-gemini-cli`, `huggingface`, `zai`, `kimi-coding`, `kimi-coding-cn`, `minimax`, `minimax-cn`, `kilocode`, `xiaomi`, `arcee`, `gmi`, `alibaba`, `deepseek`, `nvidia`, `ollama-cloud`, `xai` (alias `grok`), `qwen-oauth`, `bedrock`, `opencode-zen`, `opencode-go`, `ai-gateway`, `azure-foundry`. | +| `--provider ` | Force a provider: `auto`, `openrouter`, `nous`, `openai-codex`, `copilot-acp`, `copilot`, `anthropic`, `gemini`, `google-gemini-cli`, `huggingface`, `zai`, `kimi-coding`, `kimi-coding-cn`, `minimax`, `minimax-cn`, `minimax-oauth`, `kilocode`, `xiaomi`, `arcee`, `gmi`, `alibaba`, `deepseek`, `nvidia`, `ollama-cloud`, `xai` (alias `grok`), `qwen-oauth`, `bedrock`, `opencode-zen`, `opencode-go`, `ai-gateway`, `azure-foundry`. | | `-s`, `--skills ` | Preload one or more skills for the session (can be repeated or comma-separated). | | `-v`, `--verbose` | Verbose output. | | `-Q`, `--quiet` | Programmatic mode: suppress banner/spinner/tool previews. | diff --git a/website/docs/user-guide/features/fallback-providers.md b/website/docs/user-guide/features/fallback-providers.md index a0d699dfb2f..ea8fc3fc8b9 100644 --- a/website/docs/user-guide/features/fallback-providers.md +++ b/website/docs/user-guide/features/fallback-providers.md @@ -54,6 +54,7 @@ Both `provider` and `model` are **required**. If either is missing, the fallback | xAI (Grok) | `xai` (alias `grok`) | `XAI_API_KEY` (optional: `XAI_BASE_URL`) | | AWS Bedrock | `bedrock` | Standard boto3 auth (`AWS_REGION` + `AWS_PROFILE` or `AWS_ACCESS_KEY_ID`) | | Qwen Portal (OAuth) | `qwen-oauth` | `hermes model` (Qwen Portal OAuth; optional: `HERMES_QWEN_BASE_URL`) | +| MiniMax (OAuth) | `minimax-oauth` | `hermes model` (MiniMax portal OAuth) | | OpenCode Zen | `opencode-zen` | `OPENCODE_ZEN_API_KEY` | | OpenCode Go | `opencode-go` | `OPENCODE_GO_API_KEY` | | Kilo Code | `kilocode` | `KILOCODE_API_KEY` |