Skip to content
Open
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
58 changes: 58 additions & 0 deletions agent/anthropic_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,64 @@ def _is_oauth_token(key: str) -> bool:
return True


def build_vertex_client(project: str, region: str = "us-east5"):
"""Build an AnthropicVertex client using GCP Application Default Credentials.

Uses the same Messages API as Anthropic direct, but authenticated via
GCP IAM (attached service account, ADC, or explicit key file).

Args:
project: GCP project ID (required)
region: GCP region where Claude on Vertex AI is generally available
(default: us-east5)

Returns:
AnthropicVertex client instance (same interface as Anthropic)
"""
try:
from anthropic import AnthropicVertex
except ImportError:
raise ImportError(
"anthropic[vertex] is required for Vertex AI provider. "
"Install with: pip install 'anthropic[vertex]'"
)
from httpx import Timeout

kwargs = {
"project_id": project,
"region": region,
"timeout": Timeout(timeout=900.0, connect=10.0),
}

# If GOOGLE_APPLICATION_CREDENTIALS is set, the SDK uses it automatically.
# Otherwise, ADC picks up the attached service account on GCP VMs.

# Note: Beta headers are not forwarded to Vertex AI — feature availability
# may differ from the direct Anthropic API. The AnthropicVertex SDK handles
# feature negotiation with the Vertex endpoint.

return AnthropicVertex(**kwargs)


def resolve_vertex_credentials():
"""Resolve Vertex AI credentials from environment.

Returns:
Tuple of (project_id, region) or (None, None) if not configured.
"""
project = (
os.environ.get("VERTEX_PROJECT")
or os.environ.get("GOOGLE_CLOUD_PROJECT")
or os.environ.get("GCP_PROJECT_ID")
)
region = os.environ.get("VERTEX_REGION", "us-east5")

if not project:
return None, None

return project, region


def build_anthropic_client(api_key: str, base_url: str = None):
"""Create an Anthropic client, auto-detecting setup-tokens vs API keys.

Expand Down
28 changes: 28 additions & 0 deletions agent/auxiliary_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"minimax": "MiniMax-M2.7-highspeed",
"minimax-cn": "MiniMax-M2.7-highspeed",
"anthropic": "claude-haiku-4-5-20251001",
"vertex-ai": "claude-haiku-4-5-20251001",
"ai-gateway": "google/gemini-3-flash",
"opencode-zen": "gemini-3-flash",
"opencode-go": "glm-5",
Expand Down Expand Up @@ -512,6 +513,8 @@ def _resolve_api_key_provider() -> Tuple[Optional[OpenAI], Optional[str]]:
continue
if provider_id == "anthropic":
return _try_anthropic()
if provider_id == "vertex-ai":
return _try_vertex()

creds = resolve_api_key_provider_credentials(provider_id)
api_key = str(creds.get("api_key", "")).strip()
Expand Down Expand Up @@ -667,6 +670,23 @@ def _try_codex() -> Tuple[Optional[Any], Optional[str]]:
return CodexAuxiliaryClient(real_client, _CODEX_AUX_MODEL), _CODEX_AUX_MODEL


def _try_vertex() -> Tuple[Optional[Any], Optional[str]]:
"""Try to build a Vertex AI auxiliary client (Claude via GCP)."""
try:
from agent.anthropic_adapter import build_vertex_client, resolve_vertex_credentials
except ImportError:
return None, None

project, region = resolve_vertex_credentials()
if not project:
return None, None

model = _API_KEY_PROVIDER_AUX_MODELS.get("vertex-ai", "claude-haiku-4-5-20251001")
logger.debug("Auxiliary client: Vertex AI (%s) project=%s region=%s", model, project, region)
real_client = build_vertex_client(project, region)
return AnthropicAuxiliaryClient(real_client, model, "", ""), model


def _try_anthropic() -> Tuple[Optional[Any], Optional[str]]:
try:
from agent.anthropic_adapter import build_anthropic_client, resolve_anthropic_token
Expand Down Expand Up @@ -968,6 +988,14 @@ def resolve_provider_client(
final_model = model or default_model
return (_to_async_client(client, final_model) if async_mode else (client, final_model))

if provider == "vertex-ai":
client, default_model = _try_vertex()
if client is None:
logger.warning("resolve_provider_client: vertex-ai requested but no GCP credentials found (set VERTEX_PROJECT)")
return None, None
final_model = model or default_model
return (_to_async_client(client, final_model) if async_mode else (client, final_model))

creds = resolve_api_key_provider_credentials(provider)
api_key = str(creds.get("api_key", "")).strip()
if not api_key:
Expand Down
7 changes: 7 additions & 0 deletions hermes_cli/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,13 @@ class ProviderConfig:
inference_base_url="https://api.anthropic.com",
api_key_env_vars=("ANTHROPIC_API_KEY", "ANTHROPIC_TOKEN", "CLAUDE_CODE_OAUTH_TOKEN"),
),
"vertex-ai": ProviderConfig(
id="vertex-ai",
name="Google Vertex AI (Claude)",
auth_type="api_key",
inference_base_url="", # Region-dependent, resolved at runtime
api_key_env_vars=("VERTEX_PROJECT",), # Uses ADC, not API key
),
"alibaba": ProviderConfig(
id="alibaba",
name="Alibaba Cloud (DashScope)",
Expand Down
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ mcp = ["mcp>=1.2.0,<2"]
homeassistant = ["aiohttp>=3.9.0,<4"]
sms = ["aiohttp>=3.9.0,<4"]
acp = ["agent-client-protocol>=0.8.1,<0.9"]
vertex = ["anthropic[vertex]>=0.39.0,<1"]
dingtalk = ["dingtalk-stream>=0.1.0,<1"]
feishu = ["lark-oapi>=1.5.3,<2"]
rl = [
Expand Down Expand Up @@ -85,6 +86,7 @@ all = [
"hermes-agent[acp]",
"hermes-agent[voice]",
"hermes-agent[dingtalk]",
"hermes-agent[vertex]",
"hermes-agent[feishu]",
]

Expand Down
18 changes: 17 additions & 1 deletion run_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,8 @@ def __init__(
elif (provider_name is None) and "chatgpt.com/backend-api/codex" in self._base_url_lower:
self.api_mode = "codex_responses"
self.provider = "openai-codex"
elif self.provider == "vertex-ai":
self.api_mode = "anthropic_messages"
elif self.provider == "anthropic" or (provider_name is None and "api.anthropic.com" in self._base_url_lower):
self.api_mode = "anthropic_messages"
self.provider = "anthropic"
Expand Down Expand Up @@ -782,7 +784,21 @@ def __init__(
self._anthropic_client = None
self._is_anthropic_oauth = False

if self.api_mode == "anthropic_messages":
if self.api_mode == "anthropic_messages" and self.provider == "vertex-ai":
from agent.anthropic_adapter import build_vertex_client, resolve_vertex_credentials
project, region = resolve_vertex_credentials()
if not project:
raise ValueError("VERTEX_PROJECT environment variable is required for vertex-ai provider")
self._anthropic_client = build_vertex_client(project, region)
self._anthropic_api_key = ""
self._anthropic_base_url = ""
self._is_anthropic_oauth = False
self.api_key = ""
self.client = None
self._client_kwargs = {}
if not self.quiet_mode:
print(f"🤖 AI Agent initialized with model: {self.model} (Vertex AI, project={project}, region={region})")
elif self.api_mode == "anthropic_messages":
from agent.anthropic_adapter import build_anthropic_client, resolve_anthropic_token
# Only fall back to ANTHROPIC_TOKEN when the provider is actually Anthropic.
# Other anthropic_messages providers (MiniMax, Alibaba, etc.) must use their own API key.
Expand Down