mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-23 10:42:00 +00:00
feat(cli): add native Antigravity OAuth provider
This commit is contained in:
parent
29176ffecf
commit
8baa4e9976
25 changed files with 2371 additions and 18 deletions
|
|
@ -142,6 +142,9 @@ SERVICE_PROVIDER_NAMES: Dict[str, str] = {
|
|||
DEFAULT_GEMINI_CLOUDCODE_BASE_URL = "cloudcode-pa://google"
|
||||
GEMINI_OAUTH_ACCESS_TOKEN_REFRESH_SKEW_SECONDS = 60 # refresh 60s before expiry
|
||||
|
||||
# Google Antigravity OAuth (Antigravity Code Assist backend)
|
||||
DEFAULT_ANTIGRAVITY_CLOUDCODE_BASE_URL = "antigravity-pa://google"
|
||||
|
||||
# LM Studio's default no-auth mode still requires *some* non-empty bearer for
|
||||
# the API-key code paths (auxiliary_client, runtime resolver) to treat the
|
||||
# provider as configured. This sentinel is sent only to LM Studio, never to
|
||||
|
|
@ -212,6 +215,12 @@ PROVIDER_REGISTRY: Dict[str, ProviderConfig] = {
|
|||
auth_type="oauth_external",
|
||||
inference_base_url=DEFAULT_GEMINI_CLOUDCODE_BASE_URL,
|
||||
),
|
||||
"google-antigravity": ProviderConfig(
|
||||
id="google-antigravity",
|
||||
name="Google Antigravity (OAuth)",
|
||||
auth_type="oauth_external",
|
||||
inference_base_url=DEFAULT_ANTIGRAVITY_CLOUDCODE_BASE_URL,
|
||||
),
|
||||
"lmstudio": ProviderConfig(
|
||||
id="lmstudio",
|
||||
name="LM Studio",
|
||||
|
|
@ -1530,6 +1539,7 @@ def resolve_provider(
|
|||
"github-copilot-acp": "copilot-acp", "copilot-acp-agent": "copilot-acp",
|
||||
"opencode": "opencode-zen", "zen": "opencode-zen",
|
||||
"qwen-portal": "qwen-oauth", "qwen-cli": "qwen-oauth", "qwen-oauth": "qwen-oauth", "google-gemini-cli": "google-gemini-cli", "gemini-cli": "google-gemini-cli", "gemini-oauth": "google-gemini-cli",
|
||||
"google-antigravity": "google-antigravity", "google-antigravity-oauth": "google-antigravity", "antigravity": "google-antigravity", "antigravity-oauth": "google-antigravity", "antigravity-cli": "google-antigravity", "agy": "google-antigravity", "agy-cli": "google-antigravity",
|
||||
"hf": "huggingface", "hugging-face": "huggingface", "huggingface-hub": "huggingface",
|
||||
"mimo": "xiaomi", "xiaomi-mimo": "xiaomi",
|
||||
"tencent": "tencent-tokenhub", "tokenhub": "tencent-tokenhub",
|
||||
|
|
@ -2246,6 +2256,72 @@ def get_gemini_oauth_auth_status() -> Dict[str, Any]:
|
|||
"email": creds.email,
|
||||
"project_id": creds.project_id,
|
||||
}
|
||||
|
||||
|
||||
def resolve_antigravity_oauth_runtime_credentials(
|
||||
*,
|
||||
force_refresh: bool = False,
|
||||
) -> Dict[str, Any]:
|
||||
"""Resolve runtime OAuth creds for google-antigravity."""
|
||||
try:
|
||||
from agent.antigravity_oauth import (
|
||||
AntigravityOAuthError,
|
||||
_credentials_path,
|
||||
get_valid_access_token,
|
||||
load_credentials,
|
||||
)
|
||||
except ImportError as exc:
|
||||
raise AuthError(
|
||||
f"agent.antigravity_oauth is not importable: {exc}",
|
||||
provider="google-antigravity",
|
||||
code="antigravity_oauth_module_missing",
|
||||
) from exc
|
||||
|
||||
try:
|
||||
access_token = get_valid_access_token(force_refresh=force_refresh)
|
||||
except AntigravityOAuthError as exc:
|
||||
raise AuthError(
|
||||
str(exc),
|
||||
provider="google-antigravity",
|
||||
code=exc.code,
|
||||
) from exc
|
||||
|
||||
creds = load_credentials()
|
||||
return {
|
||||
"provider": "google-antigravity",
|
||||
"base_url": DEFAULT_ANTIGRAVITY_CLOUDCODE_BASE_URL,
|
||||
"api_key": access_token,
|
||||
"source": "antigravity-oauth",
|
||||
"expires_at_ms": (creds.expires_ms if creds else None),
|
||||
"auth_file": str(_credentials_path()),
|
||||
"email": (creds.email if creds else "") or "",
|
||||
"project_id": (creds.project_id if creds else "") or "",
|
||||
}
|
||||
|
||||
|
||||
def get_antigravity_oauth_auth_status() -> Dict[str, Any]:
|
||||
"""Return a status dict for `hermes auth list` / `hermes status`."""
|
||||
try:
|
||||
from agent.antigravity_oauth import _credentials_path, load_credentials
|
||||
except ImportError:
|
||||
return {"logged_in": False, "error": "agent.antigravity_oauth unavailable"}
|
||||
auth_path = _credentials_path()
|
||||
creds = load_credentials()
|
||||
if creds is None or not creds.access_token:
|
||||
return {
|
||||
"logged_in": False,
|
||||
"auth_file": str(auth_path),
|
||||
"error": "not logged in",
|
||||
}
|
||||
return {
|
||||
"logged_in": True,
|
||||
"auth_file": str(auth_path),
|
||||
"source": "antigravity-oauth",
|
||||
"api_key": creds.access_token,
|
||||
"expires_at_ms": creds.expires_ms,
|
||||
"email": creds.email,
|
||||
"project_id": creds.project_id,
|
||||
}
|
||||
# Spotify auth — PKCE tokens stored in ~/.hermes/auth.json
|
||||
# =============================================================================
|
||||
|
||||
|
|
@ -6191,6 +6267,8 @@ def get_auth_status(provider_id: Optional[str] = None) -> Dict[str, Any]:
|
|||
return get_qwen_auth_status()
|
||||
if target == "google-gemini-cli":
|
||||
return get_gemini_oauth_auth_status()
|
||||
if target == "google-antigravity":
|
||||
return get_antigravity_oauth_auth_status()
|
||||
if target == "minimax-oauth":
|
||||
return get_minimax_oauth_auth_status()
|
||||
if target == "copilot-acp":
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ from hermes_cli.secret_prompt import masked_secret_prompt
|
|||
|
||||
|
||||
# Providers that support OAuth login in addition to API keys.
|
||||
_OAUTH_CAPABLE_PROVIDERS = {"anthropic", "nous", "openai-codex", "xai-oauth", "qwen-oauth", "google-gemini-cli", "minimax-oauth"}
|
||||
_OAUTH_CAPABLE_PROVIDERS = {"anthropic", "nous", "openai-codex", "xai-oauth", "qwen-oauth", "google-gemini-cli", "google-antigravity", "minimax-oauth"}
|
||||
|
||||
|
||||
def _get_custom_provider_names() -> list:
|
||||
|
|
@ -386,6 +386,27 @@ def auth_add_command(args) -> None:
|
|||
print(f'Added {provider} OAuth credential #{len(pool.entries())}: "{entry.label}"')
|
||||
return
|
||||
|
||||
if provider == "google-antigravity":
|
||||
from agent.antigravity_oauth import run_antigravity_oauth_login_pure
|
||||
|
||||
creds = run_antigravity_oauth_login_pure()
|
||||
label = (getattr(args, "label", None) or "").strip() or (
|
||||
creds.get("email") or _oauth_default_label(provider, len(pool.entries()) + 1)
|
||||
)
|
||||
entry = PooledCredential(
|
||||
provider=provider,
|
||||
id=uuid.uuid4().hex[:6],
|
||||
label=label,
|
||||
auth_type=AUTH_TYPE_OAUTH,
|
||||
priority=0,
|
||||
source=f"{SOURCE_MANUAL}:antigravity_pkce",
|
||||
access_token=creds["access_token"],
|
||||
refresh_token=creds.get("refresh_token"),
|
||||
)
|
||||
pool.add_entry(entry)
|
||||
print(f'Added {provider} OAuth credential #{len(pool.entries())}: "{entry.label}"')
|
||||
return
|
||||
|
||||
if provider == "qwen-oauth":
|
||||
creds = auth_mod.resolve_qwen_runtime_credentials(refresh_if_expiring=False)
|
||||
auth_mod._mark_qwen_oauth_active(creds)
|
||||
|
|
|
|||
|
|
@ -3100,6 +3100,38 @@ OPTIONAL_ENV_VARS = {
|
|||
"category": "provider",
|
||||
"advanced": True,
|
||||
},
|
||||
"HERMES_ANTIGRAVITY_CLIENT_ID": {
|
||||
"description": "Google OAuth client ID for google-antigravity (optional; discovered from agy when omitted)",
|
||||
"prompt": "Antigravity OAuth client ID (optional — leave empty to discover from agy)",
|
||||
"url": "https://console.cloud.google.com/apis/credentials",
|
||||
"password": False,
|
||||
"category": "provider",
|
||||
"advanced": True,
|
||||
},
|
||||
"HERMES_ANTIGRAVITY_CLIENT_SECRET": {
|
||||
"description": "Google OAuth client secret for google-antigravity (optional)",
|
||||
"prompt": "Antigravity OAuth client secret (optional)",
|
||||
"url": "https://console.cloud.google.com/apis/credentials",
|
||||
"password": True,
|
||||
"category": "provider",
|
||||
"advanced": True,
|
||||
},
|
||||
"HERMES_ANTIGRAVITY_CLI_PATH": {
|
||||
"description": "Path to agy/Antigravity CLI for OAuth client credential discovery",
|
||||
"prompt": "Antigravity CLI path (leave empty to search PATH/default locations)",
|
||||
"url": None,
|
||||
"password": False,
|
||||
"category": "provider",
|
||||
"advanced": True,
|
||||
},
|
||||
"HERMES_ANTIGRAVITY_PROJECT_ID": {
|
||||
"description": "GCP project ID for Antigravity OAuth (auto-discovered when omitted)",
|
||||
"prompt": "GCP project ID for Antigravity OAuth (leave empty to auto-discover)",
|
||||
"url": None,
|
||||
"password": False,
|
||||
"category": "provider",
|
||||
"advanced": True,
|
||||
},
|
||||
"OPENCODE_ZEN_API_KEY": {
|
||||
"description": "OpenCode Zen API key (pay-as-you-go access to curated models)",
|
||||
"prompt": "OpenCode Zen API key",
|
||||
|
|
|
|||
|
|
@ -3074,6 +3074,8 @@ def select_provider_and_model(args=None):
|
|||
_model_flow_minimax_oauth(config, current_model, args=args)
|
||||
elif selected_provider == "google-gemini-cli":
|
||||
_model_flow_google_gemini_cli(config, current_model)
|
||||
elif selected_provider == "google-antigravity":
|
||||
_model_flow_google_antigravity(config, current_model)
|
||||
elif selected_provider == "copilot-acp":
|
||||
_model_flow_copilot_acp(config, current_model)
|
||||
elif selected_provider == "copilot":
|
||||
|
|
@ -3609,6 +3611,271 @@ _DEFAULT_QWEN_PORTAL_MODELS = [
|
|||
|
||||
|
||||
|
||||
def _model_flow_google_antigravity(_config, current_model=""):
|
||||
"""Google Antigravity OAuth via Antigravity Code Assist."""
|
||||
from hermes_cli.auth import (
|
||||
DEFAULT_ANTIGRAVITY_CLOUDCODE_BASE_URL,
|
||||
get_antigravity_oauth_auth_status,
|
||||
resolve_antigravity_oauth_runtime_credentials,
|
||||
_prompt_model_selection,
|
||||
_save_model_choice,
|
||||
_update_config_for_provider,
|
||||
)
|
||||
from hermes_cli.models import provider_model_ids
|
||||
|
||||
status = get_antigravity_oauth_auth_status()
|
||||
if not status.get("logged_in"):
|
||||
try:
|
||||
from agent.antigravity_oauth import resolve_project_id_from_env, start_oauth_flow
|
||||
|
||||
env_project = resolve_project_id_from_env()
|
||||
start_oauth_flow(force_relogin=True, project_id=env_project)
|
||||
except Exception as exc:
|
||||
print(f"OAuth login failed: {exc}")
|
||||
return
|
||||
|
||||
try:
|
||||
creds = resolve_antigravity_oauth_runtime_credentials(force_refresh=False)
|
||||
project_id = creds.get("project_id", "")
|
||||
if project_id:
|
||||
print(f" Using Antigravity project: {project_id}")
|
||||
except Exception as exc:
|
||||
print(f"Failed to resolve Antigravity credentials: {exc}")
|
||||
return
|
||||
|
||||
models = provider_model_ids("google-antigravity")
|
||||
default = current_model or (models[0] if models else "gemini-3-flash-agent")
|
||||
selected = _prompt_model_selection(models, current_model=default)
|
||||
if selected:
|
||||
_save_model_choice(selected)
|
||||
_update_config_for_provider(
|
||||
"google-antigravity", DEFAULT_ANTIGRAVITY_CLOUDCODE_BASE_URL
|
||||
)
|
||||
print(
|
||||
f"Default model set to: {selected} (via Google Antigravity OAuth / Code Assist)"
|
||||
)
|
||||
else:
|
||||
print("No change.")
|
||||
|
||||
|
||||
def _model_flow_custom(config):
|
||||
"""Custom endpoint: collect URL, API key, and model name.
|
||||
|
||||
Automatically saves the endpoint to ``custom_providers`` in config.yaml
|
||||
so it appears in the provider menu on subsequent runs.
|
||||
"""
|
||||
from hermes_cli.auth import _save_model_choice, deactivate_provider
|
||||
from hermes_cli.config import get_env_value, load_config, save_config
|
||||
|
||||
current_url = get_env_value("OPENAI_BASE_URL") or ""
|
||||
current_key = get_env_value("OPENAI_API_KEY") or ""
|
||||
|
||||
print("Custom OpenAI-compatible endpoint configuration:")
|
||||
if current_url:
|
||||
print(f" Current URL: {current_url}")
|
||||
if current_key:
|
||||
print(f" Current key: {current_key[:8]}...")
|
||||
print()
|
||||
|
||||
try:
|
||||
base_url = input(
|
||||
f"API base URL [{current_url or 'e.g. https://api.example.com/v1'}]: "
|
||||
).strip()
|
||||
import getpass
|
||||
|
||||
api_key = getpass.getpass(
|
||||
f"API key [{current_key[:8] + '...' if current_key else 'optional'}]: "
|
||||
).strip()
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
print("\nCancelled.")
|
||||
return
|
||||
|
||||
if not base_url and not current_url:
|
||||
print("No URL provided. Cancelled.")
|
||||
return
|
||||
|
||||
# Validate URL format
|
||||
effective_url = base_url or current_url
|
||||
if not effective_url.startswith(("http://", "https://")):
|
||||
print(f"Invalid URL: {effective_url} (must start with http:// or https://)")
|
||||
return
|
||||
|
||||
effective_key = api_key or current_key
|
||||
|
||||
# Hint: most local model servers (Ollama, vLLM, llama.cpp) require /v1
|
||||
# in the base URL for OpenAI-compatible chat completions. Prompt the
|
||||
# user if the URL looks like a local server without /v1.
|
||||
_url_lower = effective_url.rstrip("/").lower()
|
||||
_looks_local = any(
|
||||
h in _url_lower
|
||||
for h in ("localhost", "127.0.0.1", "0.0.0.0", ":11434", ":8080", ":5000")
|
||||
)
|
||||
if _looks_local and not _url_lower.endswith("/v1"):
|
||||
print()
|
||||
print(f" Hint: Did you mean to add /v1 at the end?")
|
||||
print(f" Most local model servers (Ollama, vLLM, llama.cpp) require it.")
|
||||
print(f" e.g. {effective_url.rstrip('/')}/v1")
|
||||
try:
|
||||
_add_v1 = input(" Add /v1? [Y/n]: ").strip().lower()
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
_add_v1 = "n"
|
||||
if _add_v1 in {"", "y", "yes"}:
|
||||
effective_url = effective_url.rstrip("/") + "/v1"
|
||||
if base_url:
|
||||
base_url = effective_url
|
||||
print(f" Updated URL: {effective_url}")
|
||||
print()
|
||||
|
||||
from hermes_cli.models import probe_api_models
|
||||
|
||||
probe = probe_api_models(effective_key, effective_url)
|
||||
if probe.get("used_fallback") and probe.get("resolved_base_url"):
|
||||
print(
|
||||
f"Warning: endpoint verification worked at {probe['resolved_base_url']}/models, "
|
||||
f"not the exact URL you entered. Saving the working base URL instead."
|
||||
)
|
||||
effective_url = probe["resolved_base_url"]
|
||||
if base_url:
|
||||
base_url = effective_url
|
||||
elif probe.get("models") is not None:
|
||||
print(
|
||||
f"Verified endpoint via {probe.get('probed_url')} "
|
||||
f"({len(probe.get('models') or [])} model(s) visible)"
|
||||
)
|
||||
else:
|
||||
print(
|
||||
f"Warning: could not verify this endpoint via {probe.get('probed_url')}. "
|
||||
f"Hermes will still save it."
|
||||
)
|
||||
if probe.get("suggested_base_url"):
|
||||
suggested = probe["suggested_base_url"]
|
||||
if suggested.endswith("/v1"):
|
||||
print(
|
||||
f" If this server expects /v1 in the path, try base URL: {suggested}"
|
||||
)
|
||||
else:
|
||||
print(f" If /v1 should not be in the base URL, try: {suggested}")
|
||||
|
||||
# Prompt for API compatibility mode explicitly so codex-compatible custom
|
||||
# providers don't silently fall back to chat_completions.
|
||||
current_model_cfg = config.get("model")
|
||||
current_api_mode = ""
|
||||
if isinstance(current_model_cfg, dict):
|
||||
current_api_mode = str(current_model_cfg.get("api_mode") or "").strip()
|
||||
api_mode = _prompt_custom_api_mode_selection(
|
||||
effective_url,
|
||||
current_api_mode=current_api_mode,
|
||||
)
|
||||
if api_mode:
|
||||
print(f" API mode: {api_mode}")
|
||||
else:
|
||||
print(" API mode: auto-detect")
|
||||
|
||||
# Select model — use probe results when available, fall back to manual input
|
||||
model_name = ""
|
||||
detected_models = probe.get("models") or []
|
||||
try:
|
||||
if len(detected_models) == 1:
|
||||
print(f" Detected model: {detected_models[0]}")
|
||||
confirm = input(" Use this model? [Y/n]: ").strip().lower()
|
||||
if confirm in {"", "y", "yes"}:
|
||||
model_name = detected_models[0]
|
||||
else:
|
||||
model_name = input("Model name (e.g. gpt-4, llama-3-70b): ").strip()
|
||||
elif len(detected_models) > 1:
|
||||
print(" Available models:")
|
||||
for i, m in enumerate(detected_models, 1):
|
||||
print(f" {i}. {m}")
|
||||
pick = input(
|
||||
f" Select model [1-{len(detected_models)}] or type name: "
|
||||
).strip()
|
||||
if pick.isdigit() and 1 <= int(pick) <= len(detected_models):
|
||||
model_name = detected_models[int(pick) - 1]
|
||||
elif pick:
|
||||
model_name = pick
|
||||
else:
|
||||
model_name = input("Model name (e.g. gpt-4, llama-3-70b): ").strip()
|
||||
|
||||
context_length_str = input(
|
||||
"Context length in tokens [leave blank for auto-detect]: "
|
||||
).strip()
|
||||
|
||||
# Prompt for a display name — shown in the provider menu on future runs
|
||||
default_name = _auto_provider_name(effective_url)
|
||||
display_name = input(f"Display name [{default_name}]: ").strip() or default_name
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
print("\nCancelled.")
|
||||
return
|
||||
|
||||
context_length = None
|
||||
if context_length_str:
|
||||
try:
|
||||
context_length = int(
|
||||
context_length_str.replace(",", "")
|
||||
.replace("k", "000")
|
||||
.replace("K", "000")
|
||||
)
|
||||
if context_length <= 0:
|
||||
context_length = None
|
||||
except ValueError:
|
||||
print(f"Invalid context length: {context_length_str} — will auto-detect.")
|
||||
context_length = None
|
||||
|
||||
if model_name:
|
||||
_save_model_choice(model_name)
|
||||
|
||||
# Update config and deactivate any OAuth provider
|
||||
cfg = load_config()
|
||||
model = cfg.get("model")
|
||||
if not isinstance(model, dict):
|
||||
model = {"default": model} if model else {}
|
||||
cfg["model"] = model
|
||||
model["provider"] = "custom"
|
||||
model["base_url"] = effective_url
|
||||
if effective_key:
|
||||
model["api_key"] = effective_key
|
||||
if api_mode:
|
||||
model["api_mode"] = api_mode
|
||||
else:
|
||||
model.pop("api_mode", None)
|
||||
save_config(cfg)
|
||||
deactivate_provider()
|
||||
|
||||
# Sync the caller's config dict so the setup wizard's final
|
||||
# save_config(config) preserves our model settings. Without
|
||||
# this, the wizard overwrites model.provider/base_url with
|
||||
# the stale values from its own config dict (#4172).
|
||||
config["model"] = dict(model)
|
||||
|
||||
print(f"Default model set to: {model_name} (via {effective_url})")
|
||||
else:
|
||||
if base_url or api_key:
|
||||
deactivate_provider()
|
||||
# Even without a model name, persist the custom endpoint on the
|
||||
# caller's config dict so the setup wizard doesn't lose it.
|
||||
_caller_model = config.get("model")
|
||||
if not isinstance(_caller_model, dict):
|
||||
_caller_model = {"default": _caller_model} if _caller_model else {}
|
||||
_caller_model["provider"] = "custom"
|
||||
_caller_model["base_url"] = effective_url
|
||||
if effective_key:
|
||||
_caller_model["api_key"] = effective_key
|
||||
if api_mode:
|
||||
_caller_model["api_mode"] = api_mode
|
||||
else:
|
||||
_caller_model.pop("api_mode", None)
|
||||
config["model"] = _caller_model
|
||||
print("Endpoint saved. Use `/model` in chat or `hermes model` to set a model.")
|
||||
|
||||
# Auto-save to custom_providers so it appears in the menu next time
|
||||
_save_custom_provider(
|
||||
effective_url,
|
||||
effective_key,
|
||||
model_name or "",
|
||||
context_length=context_length,
|
||||
name=display_name,
|
||||
api_mode=api_mode,
|
||||
)
|
||||
|
||||
|
||||
def _prompt_custom_api_mode_selection(base_url: str, current_api_mode: str = "") -> Optional[str]:
|
||||
|
|
@ -11248,6 +11515,24 @@ def cmd_logs(args):
|
|||
since=getattr(args, "since", None),
|
||||
component=getattr(args, "component", None),
|
||||
)
|
||||
|
||||
|
||||
def _build_provider_choices() -> list[str]:
|
||||
"""Build the --provider choices list from CANONICAL_PROVIDERS + 'auto'."""
|
||||
try:
|
||||
from hermes_cli.models import CANONICAL_PROVIDERS as _cp
|
||||
return ["auto"] + [p.slug for p in _cp]
|
||||
except Exception:
|
||||
# Fallback: static list guarantees the CLI always works
|
||||
return [
|
||||
"auto", "openrouter", "nous", "openai-codex", "xai-oauth", "copilot-acp", "copilot",
|
||||
"anthropic", "gemini", "google-gemini-cli", "google-antigravity", "xai", "bedrock", "azure-foundry",
|
||||
"ollama-cloud", "huggingface", "zai", "kimi-coding", "kimi-coding-cn",
|
||||
"stepfun", "minimax", "minimax-cn", "kilocode", "novita", "xiaomi", "arcee",
|
||||
"nvidia", "deepseek", "alibaba", "qwen-oauth", "opencode-zen", "opencode-go",
|
||||
]
|
||||
|
||||
|
||||
# Top-level subcommands that argparse knows about WITHOUT running plugin
|
||||
# discovery. Used to short-circuit eager plugin imports (which can take
|
||||
# 500ms+ pulling in google.cloud.pubsub_v1, aiohttp, grpc, etc.) when the
|
||||
|
|
|
|||
|
|
@ -276,6 +276,15 @@ _PROVIDER_MODELS: dict[str, list[str]] = {
|
|||
"gemini-3-flash-preview",
|
||||
"gemini-3.5-flash",
|
||||
],
|
||||
"google-antigravity": [
|
||||
"gemini-3-flash-agent",
|
||||
"gemini-3.5-flash-low",
|
||||
"gemini-pro-agent",
|
||||
"gemini-3.1-pro-low",
|
||||
"claude-sonnet-4-6",
|
||||
"claude-opus-4-6-thinking",
|
||||
"gpt-oss-120b-medium",
|
||||
],
|
||||
"zai": [
|
||||
"glm-5.2",
|
||||
"glm-5.1",
|
||||
|
|
@ -1029,6 +1038,7 @@ CANONICAL_PROVIDERS: list[ProviderEntry] = [
|
|||
ProviderEntry("huggingface", "Hugging Face", "Hugging Face Inference Providers"),
|
||||
ProviderEntry("gemini", "Google AI Studio", "Google AI Studio (Native Gemini API)"),
|
||||
ProviderEntry("google-gemini-cli", "Google Gemini (OAuth)", "Google Gemini via OAuth + Code Assist (Code Assist OAuth flow)"),
|
||||
ProviderEntry("google-antigravity", "Google Antigravity (OAuth)", "Google Antigravity via OAuth + Code Assist (Gemini 3.5/3.1, Claude, GPT-OSS where entitled)"),
|
||||
ProviderEntry("deepseek", "DeepSeek", "DeepSeek (V3, R1, coder, direct API)"),
|
||||
ProviderEntry("xai", "xAI", "xAI Grok (Direct API)"),
|
||||
ProviderEntry("zai", "Z.AI / GLM", "Z.AI / GLM (Zhipu direct API)"),
|
||||
|
|
@ -1222,6 +1232,12 @@ _PROVIDER_ALIASES = {
|
|||
"qwen-portal": "qwen-oauth",
|
||||
"gemini-cli": "google-gemini-cli",
|
||||
"gemini-oauth": "google-gemini-cli",
|
||||
"antigravity": "google-antigravity",
|
||||
"antigravity-oauth": "google-antigravity",
|
||||
"antigravity-cli": "google-antigravity",
|
||||
"google-antigravity-oauth": "google-antigravity",
|
||||
"agy": "google-antigravity",
|
||||
"agy-cli": "google-antigravity",
|
||||
"hf": "huggingface",
|
||||
"hugging-face": "huggingface",
|
||||
"huggingface-hub": "huggingface",
|
||||
|
|
@ -2192,6 +2208,32 @@ def _merge_with_models_dev(provider: str, curated: list[str]) -> list[str]:
|
|||
return merged
|
||||
|
||||
|
||||
def _fetch_antigravity_models(*, force_refresh: bool = False) -> list[str]:
|
||||
try:
|
||||
from agent import antigravity_oauth
|
||||
from agent.antigravity_code_assist import (
|
||||
fetch_available_models_with_fallbacks,
|
||||
load_code_assist,
|
||||
parse_agent_model_ids,
|
||||
)
|
||||
from hermes_cli.auth import resolve_antigravity_oauth_runtime_credentials
|
||||
|
||||
creds = resolve_antigravity_oauth_runtime_credentials(force_refresh=force_refresh)
|
||||
access_token = str(creds.get("api_key") or "").strip()
|
||||
project_id = str(creds.get("project_id") or "").strip()
|
||||
if not access_token:
|
||||
return []
|
||||
if not project_id:
|
||||
info = load_code_assist(access_token)
|
||||
project_id = info.project_id
|
||||
if project_id:
|
||||
antigravity_oauth.update_project_ids(project_id=project_id, managed_project_id=project_id)
|
||||
payload = fetch_available_models_with_fallbacks(access_token, project_id=project_id)
|
||||
return parse_agent_model_ids(payload)
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
def provider_model_ids(provider: Optional[str], *, force_refresh: bool = False) -> list[str]:
|
||||
"""Return the best known model catalog for a provider.
|
||||
|
||||
|
|
@ -2222,6 +2264,10 @@ def provider_model_ids(provider: Optional[str], *, force_refresh: bool = False)
|
|||
return get_codex_model_ids(access_token=access_token)
|
||||
if normalized == "xai-oauth":
|
||||
return list(_PROVIDER_MODELS.get("xai-oauth", _PROVIDER_MODELS.get("xai", [])))
|
||||
if normalized == "google-antigravity":
|
||||
live = _fetch_antigravity_models(force_refresh=force_refresh)
|
||||
if live:
|
||||
return live
|
||||
if normalized in {"copilot", "copilot-acp"}:
|
||||
try:
|
||||
live = _fetch_github_models(_resolve_copilot_catalog_api_key())
|
||||
|
|
|
|||
|
|
@ -81,6 +81,11 @@ HERMES_OVERLAYS: Dict[str, HermesOverlay] = {
|
|||
auth_type="oauth_external",
|
||||
base_url_override="cloudcode-pa://google",
|
||||
),
|
||||
"google-antigravity": HermesOverlay(
|
||||
transport="openai_chat",
|
||||
auth_type="oauth_external",
|
||||
base_url_override="antigravity-pa://google",
|
||||
),
|
||||
"lmstudio": HermesOverlay(
|
||||
transport="openai_chat",
|
||||
auth_type="api_key",
|
||||
|
|
@ -314,6 +319,13 @@ ALIASES: Dict[str, str] = {
|
|||
"gemini-cli": "google-gemini-cli",
|
||||
"gemini-oauth": "google-gemini-cli",
|
||||
|
||||
# google-antigravity (OAuth + Antigravity Code Assist)
|
||||
"antigravity": "google-antigravity",
|
||||
"antigravity-oauth": "google-antigravity",
|
||||
"antigravity-cli": "google-antigravity",
|
||||
"google-antigravity-oauth": "google-antigravity",
|
||||
"agy": "google-antigravity",
|
||||
"agy-cli": "google-antigravity",
|
||||
|
||||
# huggingface
|
||||
"hf": "huggingface",
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ from hermes_cli.auth import (
|
|||
resolve_xai_oauth_runtime_credentials,
|
||||
resolve_qwen_runtime_credentials,
|
||||
resolve_gemini_oauth_runtime_credentials,
|
||||
resolve_antigravity_oauth_runtime_credentials,
|
||||
resolve_api_key_provider_credentials,
|
||||
resolve_external_process_provider_credentials,
|
||||
has_usable_secret,
|
||||
|
|
@ -334,6 +335,9 @@ def _resolve_runtime_from_pool_entry(
|
|||
elif provider == "google-gemini-cli":
|
||||
api_mode = "chat_completions"
|
||||
base_url = base_url or "cloudcode-pa://google"
|
||||
elif provider == "google-antigravity":
|
||||
api_mode = "chat_completions"
|
||||
base_url = base_url or "antigravity-pa://google"
|
||||
elif provider == "minimax-oauth":
|
||||
# MiniMax OAuth tokens are valid only against the Anthropic Messages
|
||||
# compatible endpoint. Do not honor stale model.api_mode values from a
|
||||
|
|
@ -1634,6 +1638,26 @@ def resolve_runtime_provider(
|
|||
logger.info("Google Gemini OAuth credentials failed; "
|
||||
"falling through to next provider.")
|
||||
|
||||
if provider == "google-antigravity":
|
||||
try:
|
||||
creds = resolve_antigravity_oauth_runtime_credentials()
|
||||
return {
|
||||
"provider": "google-antigravity",
|
||||
"api_mode": "chat_completions",
|
||||
"base_url": creds.get("base_url", ""),
|
||||
"api_key": creds.get("api_key", ""),
|
||||
"source": creds.get("source", "antigravity-oauth"),
|
||||
"expires_at_ms": creds.get("expires_at_ms"),
|
||||
"email": creds.get("email", ""),
|
||||
"project_id": creds.get("project_id", ""),
|
||||
"requested_provider": requested_provider,
|
||||
}
|
||||
except AuthError:
|
||||
if requested_provider != "auto":
|
||||
raise
|
||||
logger.info("Google Antigravity OAuth credentials failed; "
|
||||
"falling through to next provider.")
|
||||
|
||||
if provider == "copilot-acp":
|
||||
creds = resolve_external_process_provider_credentials(provider)
|
||||
return {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue