Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
445 changes: 351 additions & 94 deletions hermes_cli/auth.py

Large diffs are not rendered by default.

164 changes: 83 additions & 81 deletions hermes_cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2869,87 +2869,6 @@ def _model_flow_openai_codex(config, current_model=""):
print("No change.")


def _model_flow_xai_oauth(_config, current_model=""):
"""xAI Grok OAuth (SuperGrok Subscription) provider: ensure logged in, then pick model."""
from hermes_cli.auth import (
get_xai_oauth_auth_status,
_prompt_model_selection,
_save_model_choice,
_update_config_for_provider,
resolve_xai_oauth_runtime_credentials,
_login_xai_oauth,
DEFAULT_XAI_OAUTH_BASE_URL,
PROVIDER_REGISTRY,
)
from hermes_cli.models import _PROVIDER_MODELS

status = get_xai_oauth_auth_status()
if status.get("logged_in"):
print(" xAI Grok OAuth (SuperGrok Subscription) credentials: ✓")
print()
print(" 1. Use existing credentials")
print(" 2. Reauthenticate (new OAuth login)")
print(" 3. Cancel")
print()
try:
choice = input(" Choice [1/2/3]: ").strip()
except (KeyboardInterrupt, EOFError):
choice = "1"

if choice == "2":
print("Starting a fresh xAI OAuth login...")
print()
try:
mock_args = argparse.Namespace()
_login_xai_oauth(
mock_args,
PROVIDER_REGISTRY["xai-oauth"],
force_new_login=True,
)
except SystemExit:
print("Login cancelled or failed.")
return
except Exception as exc:
print(f"Login failed: {exc}")
return
elif choice == "3":
return
else:
print("Not logged into xAI Grok OAuth (SuperGrok Subscription). Starting login...")
print()
try:
mock_args = argparse.Namespace()
_login_xai_oauth(mock_args, PROVIDER_REGISTRY["xai-oauth"])
except SystemExit:
print("Login cancelled or failed.")
return
except Exception as exc:
print(f"Login failed: {exc}")
return

# Resolve a usable base URL. ``resolve_xai_oauth_runtime_credentials``
# only reads from the auth.json singleton — but credentials may legitimately
# live only in the pool (e.g. after ``hermes auth add xai-oauth``). Fall
# back to the default base URL in that case so the model picker still
# completes successfully instead of bailing out with
# ``Could not resolve xAI OAuth credentials``.
base_url = DEFAULT_XAI_OAUTH_BASE_URL
try:
creds = resolve_xai_oauth_runtime_credentials()
base_url = (creds.get("base_url") or "").strip().rstrip("/") or base_url
except Exception:
pass

models = list(_PROVIDER_MODELS.get("xai-oauth") or _PROVIDER_MODELS.get("xai") or [])
selected = _prompt_model_selection(models, current_model=current_model or (models[0] if models else "grok-4.3"))
if selected:
_save_model_choice(selected)
_update_config_for_provider("xai-oauth", base_url)
print(f"Default model set to: {selected} (via xAI Grok OAuth — SuperGrok Subscription)")
else:
print("No change.")


_DEFAULT_QWEN_PORTAL_MODELS = [
"qwen3-coder-plus",
"qwen3-coder",
Expand Down Expand Up @@ -3123,6 +3042,89 @@ def _model_flow_google_gemini_cli(_config, current_model=""):
print("No change.")


def _model_flow_xai_oauth(config, current_model=""):
"""
xAI (Grok) OAuth flow — primarily uses import from the official Grok CLI.

This is the recommended path. If you are already logged into the official
Grok CLI / Grok Build, Hermes will automatically import and use those
credentials (no browser login needed).
"""
from hermes_cli.auth import (
get_xai_oauth_auth_status,
_login_xai_oauth,
resolve_xai_oauth_runtime_credentials,
_prompt_model_selection,
_save_model_choice,
_update_config_for_provider,
_import_grok_cli_into_hermes,
PROVIDER_REGISTRY,
)
from hermes_cli.models import _xai_curated_models, fetch_api_models, _PROVIDER_MODELS

print("Checking for existing Grok CLI login...")

# === Step 1: Auto-import from official Grok CLI (best experience) ===
imported = _import_grok_cli_into_hermes("xai-oauth")
if imported:
print("✓ Imported credentials from your existing Grok CLI login.")

# === Step 2: Check if we now have usable credentials ===
status = get_xai_oauth_auth_status()
has_creds = status.get("logged_in", False)

if has_creds and status.get("source") == "grok-cli-import":
print("✓ Using credentials imported from Grok CLI.")

if not has_creds:
print("\nNo xAI credentials found.")
print("Recommended: Log in with the official Grok CLI (`grok login`), then run `hermes model` again.")
print("Hermes will automatically import your credentials.")
print("\nAlternative: Perform a fresh browser login now? (y/N): ", end="")

try:
choice = input().strip().lower()
except EOFError:
choice = "n"

if choice == "y":
try:
import argparse
pconfig = PROVIDER_REGISTRY.get(selected_provider) or PROVIDER_REGISTRY.get("xai-oauth")
mock_args = argparse.Namespace(force=True)
_login_xai_oauth(mock_args, pconfig, force_new_login=True)
except Exception as e:
print(f"\nBrowser login failed: {e}")
return
else:
print("\nPlease run `grok login` (or the official Grok CLI), then run `hermes model` again.")
print("Hermes will automatically detect and use your Grok CLI login.")
return

# === Step 3: We have credentials — fetch models and let user choose ===
print("\nFetching available Grok models...")

models = None
try:
creds = resolve_xai_oauth_runtime_credentials()
models = fetch_api_models(creds.get("api_key"), creds.get("base_url"))
except Exception:
pass

if not models:
models = list(_PROVIDER_MODELS.get("xai-oauth", _xai_curated_models() or ["grok-build", "grok-4.3"]))

default = current_model or (models[0] if models else "grok-4")
selected = _prompt_model_selection(models, current_model=default)

if selected:
_save_model_choice(selected)
_update_config_for_provider("xai-oauth", "https://api.x.ai/v1")
print(f"\n✓ Default model set to: {selected} (via xAI / Grok OAuth)")
else:
print("\nNo change.")


def _model_flow_custom(config):
"""Custom endpoint: collect URL, API key, and model name.

Expand Down
10 changes: 9 additions & 1 deletion hermes_cli/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,13 @@ def _xai_curated_models() -> list[str]:
"glm-4.5-flash",
],
"xai": _xai_curated_models(),
"xai-oauth": [
"grok-build",
"grok-4-1-fast",
"grok-code-fast-1",
"grok-4.3",
"grok-4.3-latest",
],
"nvidia": [
# NVIDIA flagship reasoning models
"nvidia/nemotron-3-super-120b-a12b",
Expand Down Expand Up @@ -939,7 +946,8 @@ class ProviderEntry(NamedTuple):
ProviderEntry("gemini", "Google AI Studio", "Google AI Studio (Gemini models — native Gemini API)"),
ProviderEntry("google-gemini-cli", "Google Gemini (OAuth)", "Google Gemini via OAuth + Code Assist (free tier supported; no API key needed)"),
ProviderEntry("deepseek", "DeepSeek", "DeepSeek (DeepSeek-V3, R1, coder — direct API)"),
ProviderEntry("xai", "xAI", "xAI (Grok models — direct API)"),
ProviderEntry("xai", "xAI (API key)", "xAI Grok (direct API key — XAI_API_KEY)"),
ProviderEntry("xai-oauth", "xAI (OAuth login)", "xAI Grok — uses your existing official Grok CLI login (recommended) or browser login"),
ProviderEntry("zai", "Z.AI / GLM", "Z.AI / GLM (Zhipu AI direct API)"),
ProviderEntry("kimi-coding", "Kimi / Kimi Coding Plan", "Kimi Coding Plan (api.kimi.com) & Moonshot API"),
ProviderEntry("kimi-coding-cn", "Kimi / Moonshot (China)", "Kimi / Moonshot China (Moonshot CN direct API)"),
Expand Down
7 changes: 7 additions & 0 deletions hermes_cli/providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,13 @@ class ProviderDef:
"xai-oauth": "xai-oauth",
"x-ai-oauth": "xai-oauth",
"xai-grok-oauth": "xai-oauth",
"xai-coding-plan": "xai-oauth",
"xai coding plan": "xai-oauth",
"x.ai coding plan": "xai-oauth",
"grok-plan": "xai-oauth",
"grok-code": "xai-oauth",
"xai-grok-build": "xai-oauth",
"xai-oauth-plan": "xai-oauth",

# nvidia
"nim": "nvidia",
Expand Down
14 changes: 11 additions & 3 deletions hermes_cli/runtime_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,9 +240,6 @@ def _resolve_runtime_from_pool_entry(
if provider == "openai-codex":
api_mode = "codex_responses"
base_url = base_url or DEFAULT_CODEX_BASE_URL
elif provider == "xai-oauth":
api_mode = "codex_responses"
base_url = base_url or DEFAULT_XAI_OAUTH_BASE_URL
elif provider == "qwen-oauth":
api_mode = "chat_completions"
base_url = base_url or DEFAULT_QWEN_BASE_URL
Expand All @@ -257,6 +254,17 @@ def _resolve_runtime_from_pool_entry(
api_mode = "anthropic_messages"
pconfig = PROVIDER_REGISTRY.get(provider)
base_url = base_url or (pconfig.inference_base_url if pconfig else "")
elif provider == "xai-oauth":
api_mode = "codex_responses"
base_url = base_url or "https://api.x.ai/v1"
try:
from hermes_cli.auth import resolve_xai_oauth_runtime_credentials
creds = resolve_xai_oauth_runtime_credentials()
api_key = creds.get("api_key") or api_key
base_url = creds.get("base_url") or base_url
except Exception as e:
logger.warning("xai-oauth credential resolution failed: %s", e)
raise
elif provider == "anthropic":
api_mode = "anthropic_messages"
cfg_provider = str(model_cfg.get("provider") or "").strip().lower()
Expand Down
61 changes: 61 additions & 0 deletions plugins/model-providers/xai-oauth/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# xAI (Grok) OAuth Provider

This provider adds first-class support for [xAI Grok](https://grok.com) models in Hermes Agent using OAuth.

## Features

- Uses the official Responses API (`codex_responses`) for native reasoning support on Grok 4+ models.
- Automatically benefits from xAI's prompt caching via the `x-grok-conv-id` header.
- **Primary authentication method**: Seamless import from the official Grok CLI / Grok Build login (`~/.grok/auth.json`).
- Optional browser-based OAuth login to `auth.x.ai` as a fallback.

## Recommended Setup

The recommended way to authenticate is to first log in with the official Grok CLI:

```bash
grok login
```

Then in Hermes, select **"xAI (OAuth login)"** in `hermes model`. Hermes will automatically detect and import your existing Grok CLI credentials.

## Alternative: Browser Login

If you do not have the Grok CLI installed, you can authenticate directly via browser when selecting the provider in `hermes model`.

**Note**: Browser login uses xAI's public desktop OAuth client. Redirect URI restrictions may apply depending on your environment.

## Environment Variables

| Variable | Description | Priority |
|--------------------------------|--------------------------------------|----------|
| `XAI_API_KEY` | Fallback API key (if using `xai` provider) | Low |

## Aliases

This provider responds to the following names:

- `xai-oauth`
- `grok-oauth`
- `xai-portal`
- `grok-login`

## Model Examples

- `grok-4`
- `grok-3`
- `grok-3-mini`

## Related Providers

- `xai` — The simpler API-key version of the xAI provider (uses `XAI_API_KEY`).

## Implementation Notes

- `api_mode`: `codex_responses`
- Prompt caching is handled automatically by the transport layer when the base URL contains `x.ai`.
- Credential import logic lives in `hermes_cli/auth.py` (`_import_grok_cli_into_hermes`).

## Contributing

This provider was contributed to make Grok a first-class citizen in Hermes Agent, with a focus on a great experience for users who already use the official Grok CLI.
64 changes: 64 additions & 0 deletions plugins/model-providers/xai-oauth/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""xAI (Grok) OAuth Provider for Hermes Agent.

This provider adds first-class support for xAI's Grok models using OAuth
authentication against auth.x.ai.

Primary authentication method:
Automatic import from the official Grok CLI / Grok Build login
(~/.grok/auth.json). This provides the best experience for users who
already use the official Grok tools.

Fallback:
Full browser-based PKCE OAuth login flow.

Technical details:
- Uses `codex_responses` API mode for native reasoning support.
- Benefits from xAI's built-in prompt caching via the `x-grok-conv-id` header
(automatically handled by the transport layer).
"""

from providers import register_provider
from providers.base import ProviderProfile


class XaiOAuthProfile(ProviderProfile):
"""xAI Grok via OAuth.

Prefers credentials imported from the official Grok CLI.
Falls back to browser OAuth login when needed.
"""

# Note: xAI prompt caching (x-grok-conv-id) is handled automatically
# in the transport layer when the base URL contains "x.ai".
# No custom prepare_messages or build_extra_body hooks are required
# at this time.


xai_oauth = XaiOAuthProfile(
name="xai-oauth",
aliases=(
"xai-oauth",
"grok-oauth",
"xai-portal",
"grok-login",
"grok-oauth-login",
"xai-browser",
),
api_mode="codex_responses",
env_vars=(),
base_url="https://api.x.ai/v1",
auth_type="oauth_external",
default_aux_model="grok-3-mini",
default_max_tokens=32768,
fallback_models=(
"grok-build",
"grok-4-1-fast",
"grok-code-fast-1",
"grok-4.3",
"grok-4.3-latest",
),
description="xAI Grok — OAuth (auto-imports from official Grok CLI login)",
signup_url="https://grok.com",
)

register_provider(xai_oauth)
5 changes: 5 additions & 0 deletions plugins/model-providers/xai-oauth/plugin.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
name: xai-oauth
kind: model-provider
version: 1.0.0
description: xAI Grok via OAuth (recommended - auto-imports from official Grok CLI)
author: xAI + Hermes contributors
Loading