Skip to content

Commit 85d5881

Browse files
teknium1nickdlkk
authored andcommitted
feat(minimax-oauth): full integration with peer OAuth providers
Close integration gaps discovered by auditing qwen-oauth's file coverage. These are surfaces the original salvage missed — they all existed on main and were added in the 747 commits since PR NousResearch#15203 was opened. Coverage added: - agent/credential_pool.py: seed pool from auth.json providers.minimax-oauth so `hermes auth list` reflects logged-in state and `hermes auth remove minimax-oauth <N>` works through the standard flow. - agent/credential_sources.py: register RemovalStep for minimax-oauth with suppression-aware `_clear_auth_store_provider`. - agent/models_dev.py: PROVIDER_TO_MODELS_DEV mapping (-> 'minimax' family). - hermes_cli/providers.py: HermesOverlay entry (anthropic_messages transport, oauth_external auth_type, api.minimax.io/anthropic base). - hermes_cli/model_normalize.py: add to _MATCHING_PREFIX_STRIP_PROVIDERS so `minimax-oauth/MiniMax-M2.7` in config.yaml gets correctly repaired. - hermes_cli/status.py: render MiniMax OAuth block in `hermes doctor` (logged-in / region / expires_at / error). - hermes_cli/web_server.py: register in OAUTH_PROVIDER_REGISTRY + dispatch branch in _resolve_provider_status so the dashboard auth page shows it. - website/docs/integrations/providers.md: full 'MiniMax (OAuth)' section. - website/docs/reference/cli-commands.md: --provider enum. - website/docs/user-guide/features/fallback-providers.md: fallback table row. - scripts/release.py AUTHOR_MAP: amanning3390 mapping (CI gate).
1 parent 1417f04 commit 85d5881

11 files changed

Lines changed: 137 additions & 3 deletions

File tree

agent/credential_pool.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1299,6 +1299,48 @@ def _is_suppressed(_p, _s): # type: ignore[misc]
12991299
except Exception as exc:
13001300
logger.debug("Qwen OAuth token seed failed: %s", exc)
13011301

1302+
elif provider == "minimax-oauth":
1303+
# MiniMax OAuth tokens live in ~/.hermes/auth.json providers.minimax-oauth.
1304+
# Seed the pool so `/auth list` reflects the logged-in state and the
1305+
# standard `hermes auth remove minimax-oauth <N>` flow works.
1306+
# Use refresh_if_expiring=False equivalent: resolve_minimax_oauth_runtime_credentials
1307+
# always refreshes on expiry, so instead read raw state here to avoid
1308+
# surprise network calls during provider discovery.
1309+
try:
1310+
from hermes_cli.auth import get_provider_auth_state
1311+
state = get_provider_auth_state("minimax-oauth")
1312+
if state and state.get("access_token"):
1313+
source_name = "oauth"
1314+
if not _is_suppressed(provider, source_name):
1315+
active_sources.add(source_name)
1316+
expires_at_ms = None
1317+
try:
1318+
from datetime import datetime as _dt
1319+
raw = state.get("expires_at", "")
1320+
if raw:
1321+
expires_at_ms = int(_dt.fromisoformat(raw).timestamp() * 1000)
1322+
except Exception:
1323+
expires_at_ms = None
1324+
base_url = str(state.get("inference_base_url", "") or "").rstrip("/")
1325+
changed |= _upsert_entry(
1326+
entries,
1327+
provider,
1328+
source_name,
1329+
{
1330+
"source": source_name,
1331+
"auth_type": AUTH_TYPE_OAUTH,
1332+
"access_token": state["access_token"],
1333+
"refresh_token": state.get("refresh_token"),
1334+
"expires_at_ms": expires_at_ms,
1335+
"base_url": base_url,
1336+
"label": state.get("label", "") or label_from_token(
1337+
state.get("access_token", ""), source_name
1338+
),
1339+
},
1340+
)
1341+
except Exception as exc:
1342+
logger.debug("MiniMax OAuth token seed failed: %s", exc)
1343+
13021344
elif provider == "openai-codex":
13031345
# Respect user suppression — `hermes auth remove openai-codex` marks
13041346
# the device_code source as suppressed so it won't be re-seeded from

agent/credential_sources.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,19 @@ def _remove_nous_device_code(provider: str, removed) -> RemovalResult:
252252
return result
253253

254254

255+
def _remove_minimax_oauth(provider: str, removed) -> RemovalResult:
256+
"""MiniMax OAuth lives in auth.json providers.minimax-oauth — clear it.
257+
258+
Same pattern as Nous: single-source OAuth state with refresh tokens.
259+
Suppression of the `oauth` source ensures the pool reseed path
260+
(_seed_from_singletons) doesn't instantly undo the removal.
261+
"""
262+
result = RemovalResult()
263+
if _clear_auth_store_provider(provider):
264+
result.cleaned.append(f"Cleared {provider} OAuth tokens from auth store")
265+
return result
266+
267+
255268
def _remove_codex_device_code(provider: str, removed) -> RemovalResult:
256269
"""Codex tokens live in TWO places: our auth store AND ~/.codex/auth.json.
257270
@@ -389,6 +402,11 @@ def _register_all_sources() -> None:
389402
remove_fn=_remove_qwen_cli,
390403
description="~/.qwen/oauth_creds.json",
391404
))
405+
register(RemovalStep(
406+
provider="minimax-oauth", source_id="oauth",
407+
remove_fn=_remove_minimax_oauth,
408+
description="auth.json providers.minimax-oauth",
409+
))
392410
register(RemovalStep(
393411
provider="*", source_id="config:",
394412
match_fn=lambda src: src.startswith("config:") or src == "model_config",

agent/models_dev.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ class ProviderInfo:
149149
"stepfun": "stepfun",
150150
"kimi-coding-cn": "kimi-for-coding",
151151
"minimax": "minimax",
152+
"minimax-oauth": "minimax",
152153
"minimax-cn": "minimax-cn",
153154
"deepseek": "deepseek",
154155
"alibaba": "alibaba",

hermes_cli/model_normalize.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@
9696
"kimi-coding",
9797
"kimi-coding-cn",
9898
"minimax",
99+
"minimax-oauth",
99100
"minimax-cn",
100101
"alibaba",
101102
"qwen-oauth",

hermes_cli/providers.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,11 @@ class HermesOverlay:
111111
transport="anthropic_messages",
112112
base_url_env_var="MINIMAX_BASE_URL",
113113
),
114+
"minimax-oauth": HermesOverlay(
115+
transport="anthropic_messages",
116+
auth_type="oauth_external",
117+
base_url_override="https://api.minimax.io/anthropic",
118+
),
114119
"minimax-cn": HermesOverlay(
115120
transport="anthropic_messages",
116121
base_url_env_var="MINIMAX_CN_BASE_URL",

hermes_cli/status.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,14 +159,21 @@ def show_status(args):
159159
print(color("◆ Auth Providers", Colors.CYAN, Colors.BOLD))
160160

161161
try:
162-
from hermes_cli.auth import get_nous_auth_status, get_codex_auth_status, get_qwen_auth_status
162+
from hermes_cli.auth import (
163+
get_nous_auth_status,
164+
get_codex_auth_status,
165+
get_qwen_auth_status,
166+
get_minimax_oauth_auth_status,
167+
)
163168
nous_status = get_nous_auth_status()
164169
codex_status = get_codex_auth_status()
165170
qwen_status = get_qwen_auth_status()
171+
minimax_status = get_minimax_oauth_auth_status()
166172
except Exception:
167173
nous_status = {}
168174
codex_status = {}
169175
qwen_status = {}
176+
minimax_status = {}
170177

171178
nous_logged_in = bool(nous_status.get("logged_in"))
172179
nous_error = nous_status.get("error")
@@ -219,6 +226,20 @@ def show_status(args):
219226
if qwen_status.get("error") and not qwen_logged_in:
220227
print(f" Error: {qwen_status.get('error')}")
221228

229+
minimax_logged_in = bool(minimax_status.get("logged_in"))
230+
print(
231+
f" {'MiniMax OAuth':<12} {check_mark(minimax_logged_in)} "
232+
f"{'logged in' if minimax_logged_in else 'not logged in (run: hermes auth add minimax-oauth)'}"
233+
)
234+
minimax_region = minimax_status.get("region")
235+
if minimax_logged_in and minimax_region:
236+
print(f" Region: {minimax_region}")
237+
minimax_exp = minimax_status.get("expires_at")
238+
if minimax_exp:
239+
print(f" Access exp: {minimax_exp}")
240+
if minimax_status.get("error") and not minimax_logged_in:
241+
print(f" Error: {minimax_status.get('error')}")
242+
222243
# =========================================================================
223244
# Nous Subscription Features
224245
# =========================================================================

hermes_cli/web_server.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1221,6 +1221,14 @@ def _claude_code_only_status() -> Dict[str, Any]:
12211221
"docs_url": "https://github.com/QwenLM/qwen-code",
12221222
"status_fn": None, # dispatched via auth.get_qwen_auth_status
12231223
},
1224+
{
1225+
"id": "minimax-oauth",
1226+
"name": "MiniMax (OAuth)",
1227+
"flow": "pkce",
1228+
"cli_command": "hermes auth add minimax-oauth",
1229+
"docs_url": "https://www.minimax.io",
1230+
"status_fn": None, # dispatched via auth.get_minimax_oauth_auth_status
1231+
},
12241232
)
12251233

12261234

@@ -1264,6 +1272,16 @@ def _resolve_provider_status(provider_id: str, status_fn) -> Dict[str, Any]:
12641272
"expires_at": raw.get("expires_at"),
12651273
"has_refresh_token": bool(raw.get("has_refresh_token")),
12661274
}
1275+
if provider_id == "minimax-oauth":
1276+
raw = hauth.get_minimax_oauth_auth_status()
1277+
return {
1278+
"logged_in": bool(raw.get("logged_in")),
1279+
"source": "minimax_oauth",
1280+
"source_label": f"MiniMax ({raw.get('region', 'global')})",
1281+
"token_preview": None,
1282+
"expires_at": raw.get("expires_at"),
1283+
"has_refresh_token": True,
1284+
}
12671285
except Exception as e:
12681286
return {"logged_in": False, "error": str(e)}
12691287
return {"logged_in": False}

scripts/release.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@
103103
"keifergu@tencent.com": "keifergu",
104104
"kshitijk4poor@users.noreply.github.com": "kshitijk4poor",
105105
"abner.the.foreman@agentmail.to": "Abnertheforeman",
106+
"adam.manning@pro-serveinc.com": "amanning3390",
106107
"thomasgeorgevii09@gmail.com": "tochukwuada",
107108
"harryykyle1@gmail.com": "hharry11",
108109
"kshitijk4poor@gmail.com": "kshitijk4poor",

website/docs/integrations/providers.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,32 @@ Set `HERMES_QWEN_BASE_URL` only if the portal endpoint relocates (default: `http
411411
`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.
412412
:::
413413

414+
### MiniMax (OAuth)
415+
416+
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.
417+
418+
```bash
419+
hermes model
420+
# → pick "MiniMax (OAuth)"
421+
# → browser opens; sign in with your MiniMax account (global or CN region)
422+
# → confirm — credentials are saved to ~/.hermes/auth.json
423+
424+
hermes chat # uses api.minimax.io/anthropic endpoint
425+
```
426+
427+
Or configure `config.yaml`:
428+
```yaml
429+
model:
430+
provider: "minimax-oauth"
431+
default: "MiniMax-M2.7"
432+
```
433+
434+
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`.
435+
436+
:::tip MiniMax OAuth vs API key
437+
`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.
438+
:::
439+
414440
### NVIDIA NIM
415441

416442
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:
11941220

11951221
When activated, the fallback swaps the model and provider mid-session without losing your conversation. It fires **at most once** per session.
11961222

1197-
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`.
1223+
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`.
11981224

11991225
:::tip
12001226
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).

website/docs/reference/cli-commands.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ Common options:
8585
| `-q`, `--query "..."` | One-shot, non-interactive prompt. |
8686
| `-m`, `--model <model>` | Override the model for this run. |
8787
| `-t`, `--toolsets <csv>` | Enable a comma-separated set of toolsets. |
88-
| `--provider <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`. |
88+
| `--provider <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`. |
8989
| `-s`, `--skills <name>` | Preload one or more skills for the session (can be repeated or comma-separated). |
9090
| `-v`, `--verbose` | Verbose output. |
9191
| `-Q`, `--quiet` | Programmatic mode: suppress banner/spinner/tool previews. |

0 commit comments

Comments
 (0)