Skip to content

Feat/anthropic on vertex - #61859

Open
haizaar wants to merge 7 commits into
NousResearch:mainfrom
zarmory:feat/anthropic-on-vertex
Open

Feat/anthropic on vertex#61859
haizaar wants to merge 7 commits into
NousResearch:mainfrom
zarmory:feat/anthropic-on-vertex

Conversation

@haizaar

@haizaar haizaar commented Jul 10, 2026

Copy link
Copy Markdown

What does this PR do?

Adds first-class Anthropic Claude on Google Vertex AI as a supported runtime path under the existing vertex provider name. Users who already run Gemini on Vertex can flip to Claude with a one-line config change:

model:
  default: anthropic/claude-opus-4-8   # was google/gemini-3.1-pro-preview
  provider: vertex

vertex:
  project_id: my-gcp-project
  region: global

Anthropic and Google document the offering on both vendor sides:

Related Issue

Integrating this feature on a Vertex-only deployment surfaced a pre-existing bug in agent/auxiliary_client.py that silently disabled every auxiliary task (vision, compression, curator, session_search, title_generation, web_extract) on any provider: vertex deployment, regardless of model family. The main-chat surface has its own dispatch path (hermes_cli/runtime_provider.py) that special-cases vertex up front, so Gemini-on-Vertex worked at the main-chat level while auxiliary tasks were quietly failing.

That fix is filed as sibling #61853. The middle commit of this PR (fix(auxiliary_client): route vertex through the auth_type dispatch) is bit-identical to the above's single commit — either PR can land first:

Both PRs stand alone. Merging just this PR is fully self-contained; merging just #61853 fixes Gemini-on-Vertex aux tooling without the Claude feature.

Type of Change

Non-breaking. Routes through the existing vertex provider; users who don't set model.default to an anthropic/-prefixed name see zero behavior change.

Changes Made

Commit 1 — feat(providers): Anthropic on Google Vertex AI (e978baec3)

New adapter module — agent/anthropic_vertex_adapter.py:

  • build_anthropic_vertex_client(project_id, region, timeout) constructs an anthropic.AnthropicVertex with a fresh google-auth Credentials object. Beta-header policy mirrors build_anthropic_bedrock_client: the common Anthropic betas are attached (interleaved-thinking-2025-05-14, fine-grained-tool-streaming-2025-05-14); context-1m-2025-08-07 is not attached — see Design decisions Architecture planning #3. SDK-level retries are disabled; Hermes's outer loop handles Retry-After.
  • get_anthropic_vertex_config(), has_anthropic_vertex_credentials(), build_anthropic_vertex_base_url() are helpers for resolve_runtime_provider and the doctor / setup-status UI. They reuse agent/vertex_adapter.py's credential-resolution helpers (_resolve_credentials_path, _resolve_project_override, _resolve_region) so both wire paths under vertex share one config surface.
  • is_anthropic_vertex_model(model_id) is the runtime dispatch classifier. Matches anthropic/<model> only (case-insensitive, whitespace-tolerant); non-string inputs safely return False. See Design decisions Support passing morph snapshot id #2.

Runtime dispatch — hermes_cli/runtime_provider.py:

Extended the vertex branch of resolve_runtime_provider with a model-name dispatch. When is_anthropic_vertex_model(target_model) is true, we return an anthropic_messages runtime dict (placeholder api_key="vertex-adc" since the SDK mints its own tokens per request; vertex_project_id / vertex_region fields for the client-construction sites). Everything else takes the existing chat_completions path. Raises AuthError with an actionable message pointing at the Vertex Model Garden enablement flow when credentials or the project can't be resolved for a Claude request.

The model config is sourced via _get_model_config() into a locally-scoped variable, deliberately named _vertex_model_cfg rather than the more obvious model_cfg — Python's static scoping makes any bare model_cfg name in this function local for the whole function body (because a later fallback branch assigns to it), which would trip UnboundLocalError on any call path that passes target_model=None (cron scheduler + gateway per-turn agent resolve both do). Regression test in test_vertex_dispatch_when_target_model_is_none.

Client construction and rebuild sites — parallel branches mirroring the existing bedrock branches:

  • agent/agent_init.py — new branch inside the anthropic_messages transport-selection block constructs AnthropicVertex when agent.provider == "vertex". Stashes _vertex_project_id / _vertex_region on the agent for rebuild-site consumption.
  • run_agent.py::_rebuild_anthropic_client — parallel branch reconstructs AnthropicVertex on interrupt / stale-call recovery.
  • agent/agent_runtime_helpers.py (2 rebuild sites plus the switch_model snapshot) — parallel branches so restore / fallback / mid-session model swap all recreate the client from the _primary_runtime snapshot without hitting disk.

Model-name normalization — hermes_cli/model_normalize.py:

New vertex branch strips the anthropic/ vendor prefix on the way to the wire. AnthropicVertex substitutes the request body's model field verbatim into .../publishers/anthropic/models/{model}:rawPredict, so a leading anthropic/ corrupts the URL. Gemini's google/gemini-… naming passes through unchanged — the OpenAI-compat aggregator wants the prefix intact.

Small housekeeping — hermes_cli/models.py, tools/vision_tools.py:

ProviderEntry description on vertex updated to reflect that the provider now hosts Gemini + Claude on the same platform. No new aliases, no new auth-type entries, no picker changes — the existing vertex auth flow covers both wire paths.

Docs — website/docs/:

  • guides/anthropic-vertex.md — new guide with quick start, configuration, dispatch table, per-region model-availability caveat, feature parity notes, and common failure modes. Explicitly documents that 1M context is GA on Vertex-hosted Opus 4.6+ / Sonnet 4.6+ with no beta header required.
  • integrations/providers.md — Anthropic-on-Vertex entry pointing at the same vertex provider row as Gemini.

Commit 2 — fix(auxiliary_client): route vertex through the auth_type dispatch (d211336a5)

Bit-identical to #61853 single commit. Summary: resolve_provider_client("vertex", …) silently returned (None, None) on any Vertex deployment because PROVIDER_REGISTRY.get("vertex") is None (the auto-extension in hermes_cli/auth.py filters out non-api_key providers), so the existing elif pconfig.auth_type == "vertex": handler below was unreachable dead code.

The fix is a plugin-catalog fallback for pconfig=None: when the registry lookup misses, consult providers.get_provider_profile(name) and synthesize a minimal pconfig so the downstream auth_type dispatch runs. Genuinely unknown providers still bail cleanly.

This commit alone fixes Gemini-on-Vertex aux tooling for every existing user. It's a prerequisite for the Anthropic-on-Vertex aux dispatch in commit 3.

Commit 3 — feat(auxiliary_client): route Anthropic-on-Vertex through the native SDK (0addf0c8e)

Extends the vertex handler (which commit 2 made reachable) with an Anthropic-vs-Gemini split, mirroring bedrock_adapter.is_anthropic_bedrock_model's aws_sdk-branch pattern:

try:
    from agent.anthropic_vertex_adapter import is_anthropic_vertex_model
except ImportError:
    def is_anthropic_vertex_model(_m: str) -> bool:
        return False

_model_lc = (model or "").strip().lower()
_is_anthropic = (
    is_anthropic_vertex_model(model)
    or _model_lc.startswith("claude-")
)

if _is_anthropic:
    # Claude on Vertex → AnthropicVertex SDK (Anthropic Messages wire),
    # wrapped in AnthropicAuxiliaryClient with api_key="vertex-adc" as the
    # sentinel (matching the runtime_provider convention).
    ...
else:
    # Existing Gemini OpenAI-compat aggregator path.
    ...

The lazy try/except ImportError keeps commit 2 fully standalone — on main (or with #61853 merged alone), the classifier defaults to lambda: False and every vertex aux call routes to Gemini. Once this commit is applied, the classifier activates.

Why the aux dispatch widens to bare claude-* too: is_anthropic_vertex_model intentionally requires the anthropic/<model> vendor prefix on the main-agent side — that's the loud-fail contract for config typos (see Design decisions #2). But the auxiliary path sees the model AFTER agent_init.py::normalize_model_for_provider(model, "vertex") has stripped the prefix and set_runtime_main has recorded the bare form. _read_main_model() returns bare claude-opus-4-8. If the aux dispatch also required the prefix, every Vertex-Anthropic aux call would silently misroute to the Gemini path and 400 with "Malformed publisher model". The widening mirrors is_anthropic_bedrock_model's dual-form acceptance in the one place where it's actually necessary — the strict main-agent classifier is untouched.

Test file extension — tests/agent/test_auxiliary_client_vertex_dispatch.py (adds 11 cases on top of the 10 from commit 2):

  • TestVertexAnthropicDispatch (9)anthropic/-prefixed model routes to AnthropicAuxiliaryClient wrapping AnthropicVertex; model normalized to bare form on the wire; placeholder api_key="vertex-adc"; base_url shape matches the Vertex publisher endpoint (for billing attribution); missing-creds / missing-project / missing-SDK bail paths; uppercase prefix; bare claude-* widening (case-insensitive); async wrapper.

Total for the vertex dispatch test file: 21 cases, all hermetic (mock the credential seams and SDK factory only).

How to Test

Focused test files:

scripts/run_tests.sh tests/agent/test_anthropic_vertex_adapter.py \
                     tests/hermes_cli/test_anthropic_vertex_provider.py \
                     tests/hermes_cli/test_model_normalize.py \
                     tests/agent/test_auxiliary_client_vertex_dispatch.py \
                     -q
# ─── All tests pass.

Broader regression sweep across sibling test files (provider dispatch, runtime resolution, anthropic_messages transport, primary-runtime restore, model switch, vision routing, provider profiles, bedrock parity):

scripts/run_tests.sh tests/hermes_cli/test_anthropic_vertex_provider.py \
                     tests/agent/test_anthropic_vertex_adapter.py \
                     tests/agent/test_auxiliary_client_vertex_dispatch.py \
                     tests/agent/test_auxiliary_client.py \
                     tests/agent/test_auxiliary_main_first.py \
                     tests/agent/test_vertex_adapter.py \
                     tests/agent/test_auxiliary_client_azure_foundry.py \
                     tests/agent/test_auxiliary_client_anthropic_custom.py \
                     tests/agent/test_auxiliary_client_resolve_dedup.py \
                     tests/agent/test_bedrock_adapter.py \
                     tests/agent/test_bedrock_integration.py \
                     tests/hermes_cli/test_model_normalize.py \
                     tests/hermes_cli/test_models.py \
                     tests/hermes_cli/test_gemini_provider.py \
                     tests/hermes_cli/test_setup.py \
                     tests/hermes_cli/test_cli_provider_resolution.py \
                     tests/hermes_cli/test_status_model_provider.py \
                     tests/providers/test_provider_profiles.py \
                     tests/providers/test_profile_wiring.py \
                     tests/tools/test_vision_tools.py \
                     tests/tools/test_vision_native_fast_path.py \
                     tests/agent/test_vision_routing_31179.py \
                     tests/agent/test_context_compressor.py \
                     tests/tools/test_session_search.py \
                     tests/agent/test_auxiliary_config_bridge.py \
                     tests/run_agent/test_provider_fallback.py \
                     tests/run_agent/test_primary_runtime_restore.py \
                     tests/run_agent/test_streaming.py \
                     tests/agent/transports/ \
                     tests/gateway/test_runtime_footer.py \
                     -q
# ─── 37 files, 1776/1776 tests pass in ~25s.

Real-world validation. Deployed on a Vertex-only GCE fleet (NixOS, ADC via VM service-account) with model.default: anthropic/claude-opus-4-8. hermes -z "what LLM are you?" confirms claude-opus-4-8 served via the Vertex AI provider. The main-chat loop handles live tool-use turns; prompt caching and interleaved thinking on Opus verified via session traces. vision_analyze, title_generation, compression, session_search, curator, and web_extract all round-trip through the Anthropic path in <10s each.

Large-context sanity check. A 40-turn multi-turn accumulator test drove a peak single-call payload of ~675K tokens through hermes → AnthropicVertex → Vertex → claude-opus-4-8. Latency stayed flat at ~27s per turn across all 40 turns (prompt caching dominant); the model correctly recalled sentinel tokens from both the first and last chunks in its final answer. That's 3.4× over the 200K pre-GA cap and matches Anthropic's public GA statement that the beta header is a no-op on Vertex.

Manual reproduction on a fresh checkout:

# One-time GCP-side setup:
#   1. gcloud auth application-default login
#      (or export GOOGLE_APPLICATION_CREDENTIALS=/path/to/sa.json)
#   2. Enable at least one Claude SKU (Opus 4.8 / Sonnet 4.5 / Haiku 4.5)
#      in Vertex Model Garden for your GCP project — one-time TOS-accept
#      click, starts the Marketplace subscription.

cat >> ~/.hermes/config.yaml <<'YAML'
model:
  default: anthropic/claude-opus-4-8
  provider: vertex

vertex:
  project_id: your-gcp-project-id
  region: global
YAML

hermes -z "One sentence: what model are you and what provider does your runtime report?"
# → I'm the claude-opus-4-8 model, served via the Vertex AI provider.

Checklist

Code

  • I've read the Contributing Guide.
  • My commit messages follow Conventional Commits (feat(providers): …, fix(auxiliary_client): …).
  • I searched existing PRs to make sure this isn't a duplicate.
  • My PR contains only changes related to this feature + the aux-dispatch fix it depends on. Three focused commits, no unrelated changes.
  • I've run scripts/run_tests.sh on the affected files (all pass — 1776/1776 across the broader sweep).
  • I've added tests — new adapter, runtime-dispatch, normalization, and aux-dispatch coverage. Including regression guards for each of the three design decisions above and a TestHistoricalRegression that pins the aux-dispatch invariant against future refactors of hermes_cli/auth.py.
  • I've tested on my platform: NixOS 25.05 (GCE VMs, real Vertex backend), plus local dev on macOS 15 against the same Vertex project.

Documentation & Housekeeping

  • Documentation updated: new guide at website/docs/guides/anthropic-vertex.md; new entry in website/docs/integrations/providers.md.
  • cli-config.yaml.example — N/A. No new config keys; model.default / model.provider / vertex.* all already exist.
  • CONTRIBUTING.md / AGENTS.md — N/A. No architecture or workflow changes.
  • Cross-platform impact considered — no OS-specific primitives; google-auth + anthropic SDKs are cross-platform.
  • Tool descriptions / schemas — N/A. This is a model-provider path, not a tool.

Follow-ups (out of scope for this PR)

Filing here for reviewer visibility, not asking to bundle in:

  1. Setup-wizard's curated Vertex model list. hermes_cli/setup.py::_VERTEX_MODELS currently lists only google/gemini-* SKUs. Extending it to also surface anthropic/claude-opus-4-8 / -sonnet-4-5 / -haiku-4-5 in the picker would make the wizard aware of both families. Would benefit from a small UI hint (which SKUs are Anthropic vs. Google) so users understand the Model-Garden-enablement gate applies to Anthropic entries.
  2. Compression cost. With the aux dispatch fix (commit 2 + 3), compression on Vertex-Anthropic deployments now routes to the main Claude model by default (via _resolve_auto Step 1). That's the intended behaviour, but Opus is expensive for what's essentially summarisation. Deployments where compression fires frequently may want to pin auxiliary.compression.provider: vertex, auxiliary.compression.model: google/gemini-3.5-flash explicitly. Not this PR's concern — it's a config-time choice for operators.

Screenshots / Logs

Startup banner on the running agent:

🤖 AI Agent initialized with model: anthropic/claude-opus-4-8 (Anthropic on Vertex AI, <project>/<region>)

Turn round-trip against Opus 4.8 via Vertex:

$ hermes -z "One sentence: what model are you?"
I'm the claude-opus-4-8 model, served via the Vertex AI provider.

@alt-glitch alt-glitch added type/feature New feature or request comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/cli CLI entry point, hermes_cli/, setup wizard provider/anthropic Anthropic native Messages API area/auth Authentication, OAuth, credential pools P3 Low — cosmetic, nice to have labels Jul 10, 2026

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the focused Vertex integration and for identifying the auxiliary resolver gap.

Problems

  • Blocking: agent/agent_runtime_helpers.py:1891-1927 still constructs every anthropic_messages switch with build_anthropic_client(...). The added Vertex snapshot block in this PR is later in the same function (agent/agent_runtime_helpers.py, new diff line 2077) and does not change that construction. A running session switched to Vertex Claude would therefore not create AnthropicVertex. Please add the parallel Vertex branch here and test a Gemini-on-Vertex → Claude-on-Vertex switch plus restore.
  • website/docs/guides/anthropic-vertex.md:13 says bare claude-* routes through AnthropicVertex, contradicting the intended strict-prefix behavior documented at lines 76 and 90. The implementation classifier also only accepts anthropic/.

Suggested changes

  • Build AnthropicVertex in the switch_model() Anthropic Messages branch using the resolved Vertex project/region, then snapshot the same fields for restore/recovery.
  • Correct the guide’s introductory dispatch statement.

Automated hermes-sweeper review.

Comment thread agent/agent_runtime_helpers.py
Comment thread website/docs/guides/anthropic-vertex.md
@teknium1 teknium1 added sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-contained Sweeper blast radius: contained — one narrow path / opt-in / few users labels Jul 11, 2026
msampathkumar pushed a commit to msampathkumar/hermes-agent-google-cloud that referenced this pull request Jul 12, 2026
Addresses review feedback on PR NousResearch#61859 (@teknium1 via hermes-sweeper):
the anthropic_messages branch in agent_runtime_helpers.switch_model()
was unconditionally calling build_anthropic_client, so a Gemini-on-
Vertex session that ran /model anthropic/claude-opus-4-8 mid-turn
built the wrong client — native Anthropic against api.anthropic.com
instead of AnthropicVertex against the project's Vertex endpoint. The
restore/recover branches already had the parallel vertex construction
(added in the original commit at agent_runtime_helpers.py:1020 and
:1206) but switch_model was missed.

Adds the parallel new_provider == "vertex" branch inside the
anthropic_messages guard. Resolves project/region freshly via
get_anthropic_vertex_config() (switch_model has no runtime dict to
consume from) and stashes _vertex_project_id / _vertex_region on the
agent so subsequent restore/recover sites can rebuild without a
config re-read. Placeholder api_key="vertex-adc" matches the runtime-
provider convention.

Also corrects the guide intro paragraph the same review flagged: the
:::info block at website/docs/guides/anthropic-vertex.md:13 was
left over from an earlier draft that accepted bare claude-* names.
The classifier is strict-prefix (only anthropic/... routes through
AnthropicVertex); the intro now says so explicitly, matching lines
76 and 90 of the same guide and the is_anthropic_vertex_model
implementation.

New tests in tests/run_agent/test_switch_model_vertex_anthropic.py
cover the exact scenario the reviewer asked for:

 - Gemini-on-Vertex -> Claude-on-Vertex mid-session switch builds
 AnthropicVertex, NOT native Anthropic. Verifies the resolved
 project/region reach the client factory positionally and the
 agent state (api_key placeholder, _vertex_project_id/region,
 _is_anthropic_oauth=False, client=None) is coherent.
 - region=None from get_anthropic_vertex_config falls back to
 "global" (matches the runtime-provider default).
 - Regression guard: a switch onto the native Anthropic provider
 still uses build_anthropic_client, unaffected by the fix.

Verification: scripts/run_tests.sh on the impacted file set (169
tests across 8 files including primary_runtime_restore,
user_providers_model_switch, copilot_api_mode, and the existing
switch_model pool-reload regression) all pass with no new failures.

(cherry picked from commit e78e8ba31f6ed293ccb3987784b6d308bd1180c7)
msampathkumar added a commit to msampathkumar/hermes-agent-google-cloud that referenced this pull request Jul 12, 2026
The NousResearch#47828 guard rejects a /model switch to a new provider with an empty
base_url. Claude-on-Vertex (api_mode=anthropic_messages, provider=vertex)
legitimately has no base_url — the AnthropicVertex SDK builds its endpoint
from project/region — so a mid-session Gemini-on-Vertex -> Claude-on-Vertex
switch raised instead of constructing the client. Exempt that one case.

Integration fix on top of cherry-picked PR NousResearch#61859 (Anthropic on Vertex).
@haizaar

haizaar commented Jul 13, 2026

Copy link
Copy Markdown
Author

@teknium1 Thanks for the review! - all should be fixed now. Can you please have another look?

@ch-muhammad-asim

Copy link
Copy Markdown

While this PR is still pending, I’ve implemented and tested a working alternative that runs Hermes with Claude on Vertex AI through a dedicated compatibility bridge.

It supports tool calls, prompt caching, retries, Workload Identity, and production-style GKE deployment without requiring changes to Hermes core:

https://github.com/ch-muhammad-asim/hermes-claude-code-bridge/tree/main/vertex-ai

@haizaar

haizaar commented Jul 22, 2026

Copy link
Copy Markdown
Author

@teknium1 Did you have a chance to review my updates?

@haizaar

haizaar commented Aug 10, 2026

Copy link
Copy Markdown
Author

@teknium1 pinging your again too see how we can advance this PR.

Route Anthropic Claude models on Google Vertex AI through the existing
``vertex`` provider, dispatching on the requested model name. Same
Anthropic Messages wire as native Anthropic (prompt caching, adaptive
thinking, tool-use streaming, ``xhigh`` effort), but authenticated with
Google-cloud OAuth2 (ADC or service-account JSON) and billed through
GCP.

Rationale
=========
Vertex AI has hosted Anthropic Claude models as partner models since
2024, but Hermes's existing ``vertex`` provider is Gemini-only (it uses
Vertex's OpenAI-compatible aggregator at ``.../endpoints/openapi``).
Reaching Claude on Vertex from Hermes today requires operators to fall
back to third-party proxies or forgo the GCP billing / quotas /
compliance surface entirely — even though Anthropic ships the
``AnthropicVertex`` SDK client that handles it cleanly and Hermes
already knows how to talk the Anthropic Messages protocol through
``api_mode="anthropic_messages"``.

Design
======
Vertex hosts multiple model families on one platform. Rather than
introduce a second provider name, treat the ``vertex`` provider as the
GCP-hosted aggregator and dispatch the wire protocol from the requested
model name:

* ``anthropic/<model>`` → ``anthropic_messages`` transport via the
  ``AnthropicVertex`` SDK.
* ``google/gemini-*`` (or anything else) → existing ``chat_completions``
  transport via Vertex's OpenAI-compat aggregator.

This mirrors Bedrock's dual-path shape (``is_anthropic_bedrock_model``),
with one deliberate difference: Vertex requires the fully-qualified
``anthropic/`` vendor prefix. Bedrock accepts bare ``claude-*`` as a
legacy shortcut from the era when Bedrock was Anthropic-only; Vertex
Model Garden is multi-vendor from day one, so a bare Claude name has
no unambiguous meaning and the classifier deliberately rejects it. A
user writing ``model: claude-opus-4-8`` under ``provider: vertex``
falls through to the OpenAI-compat aggregator and gets a clear Vertex
404 that names the missing prefix, rather than silent, model-shape-
guessing dispatch. One provider, one config surface
(``vertex.project_id`` / ``vertex.region``), one explicit wire
selector.

Users who already have Gemini on Vertex working get Anthropic on Vertex
with a one-line ``model.default`` change.

New adapter module ``agent/anthropic_vertex_adapter.py`` exposes:

- ``build_anthropic_vertex_client(project_id, region, timeout)`` —
  constructs an ``anthropic.AnthropicVertex`` client with a fresh
  google-auth Credentials object. Mirrors ``build_anthropic_bedrock_
  client(region)`` in shape and beta-header policy: common Anthropic
  betas attached (``interleaved-thinking-2025-05-14``, ``fine-grained-
  tool-streaming-2025-05-14``); ``context-1m-2025-08-07`` NOT attached
  by default (Vertex-hosted Claude gates 1M context on the same
  Anthropic-side subscription flag as native Anthropic). SDK-level
  retries disabled — Hermes's outer loop handles Retry-After.
- ``get_anthropic_vertex_config(region=None)`` — returns
  ``(project_id, region)`` from the shared ``vertex_adapter`` config
  chain (VERTEX_PROJECT_ID / vertex.project_id / credentials-embedded).
- ``has_anthropic_vertex_credentials()`` — fast presence check for
  auto-detection and setup-status UI, no network calls.
- ``build_anthropic_vertex_base_url(project_id, region)`` — display-only
  URL for the runtime dict / diagnostics; the AnthropicVertex SDK
  builds its own real request URLs internally.
- ``is_anthropic_vertex_model(model_id)`` — the dispatch classifier
  used by ``resolve_runtime_provider``. Matches vendor-prefixed
  ``anthropic/<model>`` only (case-insensitive, whitespace-tolerant);
  bare Claude names and non-string inputs safely return False.

Auth reuses ``agent/vertex_adapter.py``'s credential-resolution helpers
(``_resolve_credentials_path``, ``_resolve_project_override``,
``_resolve_region``). Gemini-on-Vertex and Anthropic-on-Vertex share
one config surface — the two families are different wire protocols on
the same underlying platform.

Wiring
======
- ``hermes_cli/runtime_provider.py``: extended the ``vertex`` branch
  with a model-name dispatch — an ``anthropic/`` prefixed model →
  returns an ``anthropic_messages`` runtime dict (placeholder
  ``api_key="vertex-adc"`` since the SDK mints its own tokens;
  ``vertex_project_id`` / ``vertex_region`` fields for the client-
  construction sites to consume); everything else → existing
  ``chat_completions`` path. Raises ``AuthError`` with an actionable
  message pointing at the Vertex Model Garden enablement flow when
  credentials or the project can't be resolved for a Claude request.
- ``agent/agent_init.py``: new branch in the ``anthropic_messages``
  transport-selection block that constructs ``AnthropicVertex`` instead
  of the regular ``Anthropic`` client when ``agent.provider == "vertex"``
  (unambiguous inside the anthropic_messages guard — Gemini on Vertex
  uses ``chat_completions`` and never reaches this branch). Stashes
  ``_vertex_project_id`` / ``_vertex_region`` on the agent for
  rebuild-site consumption. Startup banner says "Anthropic on Vertex
  AI, <project>/<region>".
- ``run_agent._rebuild_anthropic_client``: parallel branch mirroring the
  bedrock branch — reconstructs an ``AnthropicVertex`` on interrupt /
  stale-call recovery.
- ``agent/agent_runtime_helpers.py`` (2 rebuild sites +
  ``switch_model`` snapshot): parallel branches so restore / fallback /
  mid-session model swap all recreate the vertex client from the
  ``_primary_runtime`` snapshot without hitting disk.
- ``hermes_cli/model_normalize.py``: new ``vertex`` branch strips the
  ``anthropic/`` vendor prefix on the way to the wire (the
  AnthropicVertex SDK substitutes the request-body ``model`` field
  verbatim into ``publishers/anthropic/models/{model}:rawPredict``, so a
  leading ``anthropic/`` would corrupt the URL). ``google/gemini-*``
  and every other form passes through unchanged — the OpenAI-compat
  aggregator wants the ``google/`` prefix intact.
- ``hermes_cli/models.py``: ``vertex`` provider description updated to
  reflect the mixed-family dispatch.
- ``tools/vision_tools.py``: ``vertex`` in the vision-capable
  aggregator set already covers both Claude and Gemini on Vertex, no
  new entry needed.

Docs + tests
============
- ``website/docs/guides/anthropic-vertex.md`` — quick start,
  configuration (``provider: vertex``, ``model: anthropic/claude-…``),
  dispatch table (including the "bare Claude falls through" row so
  users understand the failure mode), per-region model availability
  caveat, feature parity note, common failure modes.
- ``website/docs/integrations/providers.md`` — Anthropic-on-Vertex
  entry pointing at the same ``vertex`` provider row as Gemini.
- ``tests/agent/test_anthropic_vertex_adapter.py`` — unit tests
  covering base-URL construction, credential resolution (missing
  google-auth / ADC / explicit override), client construction
  (missing SDK / old SDK / missing creds / expected kwargs), and the
  ``is_anthropic_vertex_model`` classifier (vendor-prefixed accepted,
  bare Claude rejected as regression guard, case-insensitive,
  whitespace-tolerant, negative and non-string cases).
- ``tests/hermes_cli/test_anthropic_vertex_provider.py`` — end-to-end
  runtime-dispatch tests: ``anthropic/`` model on all ``vertex``
  aliases routes through anthropic_messages; bare ``claude-*`` on
  ``vertex`` falls through to chat_completions (regression guard for
  the strict-prefix decision); Gemini on ``vertex`` still routes
  through chat_completions (regression guard confirming the Anthropic
  adapter is not consulted); removed provider name and aliases no
  longer resolve; model-normalization contract (strip
  ``anthropic/``, preserve everything else); actionable AuthErrors
  when credentials / project_id can't be resolved.

Model IDs on Vertex use Anthropic's native names with an optional
``@YYYYMMDD`` suffix (``claude-opus-4-8``, ``claude-sonnet-4-5``,
``claude-haiku-4-5``, …). The exact set enabled for a given GCP
project is gated on a one-time Model Garden click per model —
undocumented models 404 with the same shape as unknown model names,
so the guide calls this out explicitly under Prerequisites and Common
Failure Modes.

Auxiliary tasks (title generation, memory extraction, vision) are NOT
wired to route through Vertex when the primary provider is
``vertex + anthropic/*`` in this PR — the existing ``_try_anthropic``
fallback in ``agent/auxiliary_client.py`` targets the native Anthropic
API. Operators who want anthropic-vertex for aux tasks can set
``auxiliary.<task>.provider = "vertex"`` with an ``anthropic/`` model
explicitly; the codepath will pick it up in a follow-up.
Silent regression on any `provider: vertex` deployment:
`resolve_provider_client("vertex", ...)` returned `(None, None)` because
`PROVIDER_REGISTRY.get("vertex")` was None, and the existing
`elif pconfig.auth_type == "vertex":` handler below was unreachable
dead code.

Every auxiliary task on a Vertex-only deployment — `vision_analyze`,
context compression, curator's LLM review pass, `session_search`,
`title_generation`, `web_extract` — fell through to the aggregator
fallback chain (openrouter → nous → local/custom → api-key) and
terminated with:

    RuntimeError: No LLM provider configured for
        task=<task> provider=auto. Run: hermes setup

Vision was hit hardest: `check_vision_requirements()` uses the same
resolver, so it returned False and `vision_analyze` was stripped from
the model-facing tool schema. Fleets with `OPENROUTER_API_KEY` set
never noticed because Step 3 of `_resolve_auto` caught the miss;
Vertex-only deployments hit it terminally.

Root cause is in `hermes_cli/auth.py`'s auto-extension of
`PROVIDER_REGISTRY` from the provider-plugin catalog:

    if _pp.auth_type != "api_key" or not _pp.env_vars:
        continue

The vertex `ProviderProfile` declares `auth_type="vertex"` (OAuth2 via
ADC, not a static key) and `env_vars=()`, so both clauses reject it.
Same shape applies to any future non-api_key provider profile
(aws_sdk, oauth_device_code, oauth_external).

Fix: plugin-catalog fallback for `pconfig=None`. When the registry
lookup misses, consult `providers.get_provider_profile(provider)`. If
the profile's `auth_type` is one of the well-known non-api_key
families (`vertex`, `aws_sdk`, `oauth_device_code`, `oauth_external`),
synthesize a `SimpleNamespace(auth_type=<profile.auth_type>)` and let
the existing dispatch branches downstream fire. Genuinely unknown
providers still bail cleanly with the unchanged "unknown provider"
debug log.

No touch to `hermes_cli/auth.py` — keeping `PROVIDER_REGISTRY`
api-key-only preserves invariants elsewhere in the codebase.

Once the fallback is in place, the pre-existing
`elif pconfig.auth_type == "vertex":` handler serves Gemini traffic
via the OpenAI-compat endpoint as it was originally written to do —
no other changes needed here.

Tests: new `tests/agent/test_auxiliary_client_vertex_dispatch.py` (10
cases, all hermetic — mock the credential seams only):

  - `TestPluginCatalogFallback` (3): vertex reaches dispatch through
    the fallback; aliases (`google-vertex`, `vertex-ai`, `gcp-vertex`)
    resolve; genuinely-unknown providers still bail.
  - `TestVertexGeminiDispatch` (6): `google/`-prefixed model builds an
    OpenAI client with the right base_url + token; no-model default;
    bare `gemini-*` still routes to the Gemini handler (Vertex's
    404 for the missing publisher stays the loud-fail diagnostic);
    credential / token failure paths; async wrapper.
  - `TestHistoricalRegression` (1): pins the invariant with `vertex`
    forcibly removed from `PROVIDER_REGISTRY`, so a future refactor
    of the `auth.py` auto-extension filter cannot silently
    reintroduce the bug.

No user-facing behaviour change on non-Vertex deployments.
Follow-up to the plugin-catalog fallback fix (previous commit) and the
Anthropic-on-Vertex feature (`feat(providers): Anthropic on Google
Vertex AI`). Extends the vertex handler in
``agent.auxiliary_client.resolve_provider_client`` so that Anthropic
Claude auxiliary calls route through ``AnthropicVertex`` (native
Messages wire) instead of the Gemini OpenAI-compat aggregator path,
mirroring ``hermes_cli/runtime_provider.py``'s main-agent dispatch.

Without this commit, an operator with ``model.provider: vertex,
model.default: anthropic/claude-*`` gets the main chat working
correctly (main-agent path builds ``AnthropicVertex``) but every
auxiliary task (vision_analyze, compression, curator, session_search,
title_generation, web_extract) routes through the Gemini handler and
400s with "Malformed publisher model (`model`: 'claude-...') for the
'openapi' request endpoint ID".

Two hunks in ``agent/auxiliary_client.py``, plus a follow-up widening
for prefix-stripping:

  1. Anthropic-vs-Gemini split inside the vertex handler. Mirrors
     ``bedrock_adapter.is_anthropic_bedrock_model``'s aws_sdk-branch
     pattern:

         if model and is_anthropic_vertex_model(model):
             # Claude on Vertex → AnthropicVertex SDK
             ...
         else:
             # Existing Gemini OpenAI-compat aggregator path
             ...

     ``is_anthropic_vertex_model`` is imported lazily via
     ``try/except ImportError`` so the aux-dispatch commit above
     stands alone even if this commit is not applied — the classifier
     defaults to ``lambda: False`` and every vertex aux call falls
     through to Gemini, which is the correct behaviour when the
     Anthropic-on-Vertex feature is absent.

  2. Widen aux-side detection to bare ``claude-*`` (case-insensitive):

         _model_lc = (model or "").strip().lower()
         _is_anthropic = (
             is_anthropic_vertex_model(model)
             or _model_lc.startswith("claude-")
         )

     ``is_anthropic_vertex_model`` (defined in the feature commit)
     intentionally requires the fully-qualified ``anthropic/<model>``
     form so main-agent config typos surface as a loud Vertex 404
     (``publisher: google, model: claude-...``) instead of silently
     misrouting. The auxiliary path sees the model AFTER
     ``agent_init.py::normalize_model_for_provider(model, "vertex")``
     has stripped the ``anthropic/`` prefix, so
     ``_read_main_model()`` returns bare ``claude-opus-4-8``. If aux
     dispatch also required the prefix, every Vertex-Anthropic aux
     call would silently misroute to the Gemini path.

     The strict classifier stays strict for main-agent — the widening
     is scoped to the aux dispatch site where prefix stripping has
     already happened. Mirrors ``is_anthropic_bedrock_model``'s
     dual-form acceptance (``anthropic.claude-*`` AND bare
     ``claude-*``) in the one place where it's actually necessary.

Tests: extends the existing ``tests/agent/test_auxiliary_client_
vertex_dispatch.py`` with ``TestVertexAnthropicDispatch`` (9 cases
covering the SDK dispatch, prefix stripping, sentinel api_key, base
URL for billing attribution, missing-creds / missing-project /
missing-SDK bail paths, uppercase prefix, bare ``claude-*``, and the
async wrapper). All 19 cases in the file pass hermetically with only
the credential seams and SDK factory mocked.

No user-facing behaviour change unless the operator has configured
``model.provider: vertex, model.default: anthropic/*``.
Addresses review feedback on PR NousResearch#61859 (@teknium1 via hermes-sweeper):
the anthropic_messages branch in agent_runtime_helpers.switch_model()
was unconditionally calling build_anthropic_client, so a Gemini-on-
Vertex session that ran /model anthropic/claude-opus-4-8 mid-turn
built the wrong client — native Anthropic against api.anthropic.com
instead of AnthropicVertex against the project's Vertex endpoint. The
restore/recover branches already had the parallel vertex construction
(added in the original commit at agent_runtime_helpers.py:1020 and
:1206) but switch_model was missed.

Adds the parallel new_provider == "vertex" branch inside the
anthropic_messages guard. Resolves project/region freshly via
get_anthropic_vertex_config() (switch_model has no runtime dict to
consume from) and stashes _vertex_project_id / _vertex_region on the
agent so subsequent restore/recover sites can rebuild without a
config re-read. Placeholder api_key="vertex-adc" matches the runtime-
provider convention.

Also corrects the guide intro paragraph the same review flagged: the
:::info block at website/docs/guides/anthropic-vertex.md:13 was
left over from an earlier draft that accepted bare claude-* names.
The classifier is strict-prefix (only anthropic/... routes through
AnthropicVertex); the intro now says so explicitly, matching lines
76 and 90 of the same guide and the is_anthropic_vertex_model
implementation.

New tests in tests/run_agent/test_switch_model_vertex_anthropic.py
cover the exact scenario the reviewer asked for:

 - Gemini-on-Vertex -> Claude-on-Vertex mid-session switch builds
 AnthropicVertex, NOT native Anthropic. Verifies the resolved
 project/region reach the client factory positionally and the
 agent state (api_key placeholder, _vertex_project_id/region,
 _is_anthropic_oauth=False, client=None) is coherent.
 - region=None from get_anthropic_vertex_config falls back to
 "global" (matches the runtime-provider default).
 - Regression guard: a switch onto the native Anthropic provider
 still uses build_anthropic_client, unaffected by the fix.

Verification: scripts/run_tests.sh on the impacted file set (169
tests across 8 files including primary_runtime_restore,
user_providers_model_switch, copilot_api_mode, and the existing
switch_model pool-reload regression) all pass with no new failures.
Extends the previous commit's Vertex branch to the sibling provider with the
identical defect. ``switch_model``'s ``anthropic_messages`` branch now
dispatches bedrock as well, matching ``agent_init``,
``run_agent._rebuild_anthropic_client`` and
``run_agent._create_request_anthropic_client``, all three of which already
special-case ``provider == "bedrock"``.

Bedrock-hosted Claude speaks the Anthropic Messages protocol but authenticates
through the AWS SDK, against a base_url that has no ``/v1/messages`` route. So
``/model claude-sonnet-4-5`` in a live Bedrock session replaced a working
AnthropicBedrock client with a direct Anthropic one and every call after the
switch failed. Restarting the session recovered it (agent_init gets it right),
which is likely why it went unnoticed longer than the Vertex variant.

Region resolution prefers the endpoint being switched TO — the same regex
agent_init.py runs over the resolved base_url — then the region stashed at
init, then ``us-east-1``. ``_bedrock_region`` is refreshed on the agent so the
``_primary_runtime`` snapshot taken later in this same function records the
destination rather than the origin.

Adds tests/agent/test_switch_model_anthropic_provider_dispatch.py, which
asserts the cross-site invariant behind both halves of this bug — that
``switch_model`` and ``_rebuild_anthropic_client`` agree on which SDK each
provider gets — rather than snapshotting either implementation. It covers
vertex, bedrock and native Anthropic, that no client is ever built against the
display-only Vertex publisher base_url, and that switching INTO vertex works
without the init-stashed attributes. The narrower
tests/run_agent/test_switch_model_vertex_anthropic.py from the previous commit
is kept as-is.

This also reconciles the branch with the deployment fork (zarmory/hermes-agent
`khala`), which had independently grown the same bedrock+vertex fix while
chasing a production 404. Both now carry equivalent coverage, so a future
rebase of this branch onto current upstream/main won't have to untangle two
divergent versions of one fix.

Verification: scripts/run_tests.sh over the new file plus the existing vertex,
anthropic-adapter and model-switch suites — 342 passed. The 3 failures in
TestRunOauthSetupToken are pre-existing on this branch's base and reproduce
with this change stashed.
…client

Upstream NousResearch#67142 (42c240f) added ``AIAgent._create_request_anthropic_client``
— a per-request Anthropic client so the stale/interrupt watchdog can abort a
socket without closing the shared client out from under a worker thread. It
became the client that carries every in-flight ``anthropic_messages`` call
(both call sites in ``agent/chat_completion_helpers.py``: the non-streaming
``make_client`` lambda and the ``anthropic_stream_request`` path).

Its docstring says it mirrors ``_rebuild_anthropic_client``, but it only
reproduced the direct-Anthropic and Bedrock branches — the Claude-on-Vertex
branch was missing. On a ``provider: vertex`` + ``model: anthropic/claude-*``
deployment every turn fell through to ``build_anthropic_client()`` with
``api_key="vertex-adc"`` and ``base_url`` set to the *display-only* Vertex
publisher URL, so the SDK POSTed to

    …/publishers/anthropic/v1/messages            -> HTTP 404

instead of the real publisher route

    …/publishers/anthropic/models/<model>:rawPredict

Symptom: "The model provider failed after retries" on every message, with
``NotFoundError`` / ``HTTP 404`` and ``provider=vertex`` in errors.log.

The failure was invisible to init-time and auxiliary checks. The shared
``_anthropic_client`` is correctly an ``AnthropicVertex`` (verified in situ:
constructing the agent with the gateway's own ``_resolve_runtime_agent_kwargs``
yields ``anthropic.lib.vertex._client.AnthropicVertex``), and the auxiliary
path builds its own Vertex client in ``resolve_provider_client``. So agent
startup, ``hermes --version``, and all six auxiliary probes pass while the
main conversation loop 404s on its first API call.

Add the ``elif _provider == "vertex"`` branch, mirroring
``_rebuild_anthropic_client`` (project + region read from the
``_vertex_project_id`` / ``_vertex_region`` attributes stashed during init,
region defaulting to ``global``), and note in the docstring that the two
builders' provider dispatch must stay in sync.

Tests: tests/agent/test_request_anthropic_client_vertex_dispatch.py. Rather
than snapshot either implementation, the central test asserts the invariant
that broke — that ``_create_request_anthropic_client`` and
``_rebuild_anthropic_client`` agree on which SDK each provider gets — plus
that no client is ever built against the display-only publisher base_url.
All four fail without this change (the invariant test reports
``{'vertex': 0, 'direct': 1}`` vs ``{'vertex': 1, 'direct': 0}``).

Upstream-PR candidate: this is a gap in NousResearch#67142 that affects any
Claude-on-Vertex user, independent of our fork's other commits.
…h guard

`switch_model` gained a guard (NousResearch#47828) that raises when a provider change
resolves an empty `base_url`, on the premise that empty means upstream
resolution failed and keeping the previous provider's endpoint would send
every subsequent request to the wrong host.

That premise does not hold for the Anthropic cloud partner SDKs.
`AnthropicVertex` and `AnthropicBedrock` derive their endpoint from
project/region internally
(`…/publishers/anthropic/models/<model>:rawPredict`), so `switch_model()`
correctly resolves no base_url for them. With the guard as-is,
`/model anthropic/claude-*` on `provider: vertex` raises
ValueError instead of switching:

    ValueError: switch_model: no base_url resolved for provider 'vertex'
    (switching from 'openrouter'); refusing to keep the previous provider's
    endpoint

Nothing is inherited in the exempted case either: the branch leaves
`agent.base_url` untouched and the Anthropic client is rebuilt from
project/region, not from `agent.base_url`.

Scoped deliberately to `api_mode == "anthropic_messages"` and
provider in {vertex, bedrock}. Gemini-on-Vertex goes through the
OpenAI-compat aggregator and does need a real base_url, so it stays under
the guard.

Tests pin the interaction from both sides: the Vertex switch survives an
empty base_url, and the guard still fires for an ordinary provider change
and for Vertex outside `anthropic_messages`. Upstream's own guard test
(tests/run_agent/test_switch_model_stale_base_url.py) still passes
unchanged.
@haizaar
haizaar force-pushed the feat/anthropic-on-vertex branch from 8b418ed to cd44d48 Compare August 14, 2026 11:41
@haizaar

haizaar commented Aug 14, 2026

Copy link
Copy Markdown
Author

Rebased onto current main (was conflicting). Two things had to change beyond conflict resolution, and one of them was a real hole in this PR — details below so you don't have to take the rebase on faith.

The rebase conflict

One conflict, in agent/agent_runtime_helpers.py::restore_primary_runtime: main added the moa virtual-provider branch (#53802) ahead of the anthropic_messages branch this PR rewrites. Kept main's moa branch intact and made ours the elif; the build_anthropic_client import moved into the non-Vertex sub-branch so the Vertex path no longer pulls in the direct-Anthropic adapter.

Added: the request-local client was still undispatched

main has since gained AIAgent._create_request_anthropic_client (#67142) — the per-request client that now carries every in-flight anthropic_messages call. It dispatches Bedrock but not Vertex, and this PR did not cover it, so as it stood, merging this would still have 404'd every conversation turn against …/publishers/anthropic/v1/messages. Added the dispatch plus a regression test.

That gap is invisible to a clean rebase, so I verified it mechanically rather than by reading. Classifying every build_anthropic_* construction site by whether its enclosing function dispatches on provider:

total sites flagged
this branch, before 27 8
this branch, now 28 7

The removed flag is exactly run_agent.py::_create_request_anthropic_client. The remaining flags are the pre-existing undispatched sites on mainchat_completion_helpers.try_activate_fallback (bedrock-only) and run_agent._swap_credential (no dispatch) — which this PR deliberately leaves alone: both are only reachable with a credential pool or a fallback provider chain configured, and fixing them is a separate change with its own reachability argument. Happy to fold them in here if you'd rather.

Added: the empty-base_url guard blocks Claude on Vertex

main also gained a guard in switch_model (#47828) that raises when a provider change resolves an empty base_url. The premise — "empty means resolution failed upstream" — doesn't hold for the Anthropic cloud partner SDKs: AnthropicVertex and AnthropicBedrock build their endpoint from project/region internally, so there is legitimately no URL to pass. On current main + this PR, /model anthropic/claude-* on provider: vertex raised:

ValueError: switch_model: no base_url resolved for provider 'vertex'
(switching from 'openrouter'); refusing to keep the previous provider's endpoint

Exempted, scoped to api_mode == "anthropic_messages" and provider in {vertex, bedrock}. Gemini-on-Vertex uses the OpenAI-compat aggregator and does need a real base_url, so it stays under the guard. Nothing is inherited in the exempted path: agent.base_url is left untouched and the client is rebuilt from project/region.

Tests pin it from both sides — the Vertex switch survives an empty base_url, and the guard still fires for an ordinary provider change and for Vertex outside anthropic_messages. Your original guard test (tests/run_agent/test_switch_model_stale_base_url.py) passes unchanged.

Your earlier review

Both points from the 2026-07-10 review are in and still hold after the rebase:

  • the switch_model() Anthropic Messages branch builds AnthropicVertex from the resolved project/region and snapshots the same fields for restore/recovery (ce6474075), and the request-local site above is now covered too;
  • website/docs/guides/anthropic-vertex.md no longer contradicts itself — the intro now states the anthropic/ prefix is required and that bare claude-… falls through to the aggregator and 404s on purpose, matching the dispatch table.

Verification

scripts/run_tests.sh over the PR's suites plus your guard suite and the transports suite: 164 passed, 0 failed across

tests/agent/test_anthropic_vertex_adapter.py
tests/agent/test_auxiliary_client_vertex_dispatch.py
tests/agent/test_switch_model_anthropic_provider_dispatch.py
tests/agent/test_request_anthropic_client_vertex_dispatch.py
tests/hermes_cli/test_anthropic_vertex_provider.py
tests/run_agent/test_switch_model_vertex_anthropic.py
tests/run_agent/test_switch_model_stale_base_url.py
tests/agent/transports/test_chat_completions.py

This is running in production on a two-VM fleet against anthropic/claude-opus-4-8 on Vertex with ADC from the GCE metadata server, no API key anywhere in the tree.

shanemcd pushed a commit to shanemcd/hermes-agent that referenced this pull request Aug 18, 2026
Addresses review feedback on PR NousResearch#61859 (@teknium1 via hermes-sweeper):
the anthropic_messages branch in agent_runtime_helpers.switch_model()
was unconditionally calling build_anthropic_client, so a Gemini-on-
Vertex session that ran /model anthropic/claude-opus-4-8 mid-turn
built the wrong client — native Anthropic against api.anthropic.com
instead of AnthropicVertex against the project's Vertex endpoint. The
restore/recover branches already had the parallel vertex construction
(added in the original commit at agent_runtime_helpers.py:1020 and
:1206) but switch_model was missed.

Adds the parallel new_provider == "vertex" branch inside the
anthropic_messages guard. Resolves project/region freshly via
get_anthropic_vertex_config() (switch_model has no runtime dict to
consume from) and stashes _vertex_project_id / _vertex_region on the
agent so subsequent restore/recover sites can rebuild without a
config re-read. Placeholder api_key="vertex-adc" matches the runtime-
provider convention.

Also corrects the guide intro paragraph the same review flagged: the
:::info block at website/docs/guides/anthropic-vertex.md:13 was
left over from an earlier draft that accepted bare claude-* names.
The classifier is strict-prefix (only anthropic/... routes through
AnthropicVertex); the intro now says so explicitly, matching lines
76 and 90 of the same guide and the is_anthropic_vertex_model
implementation.

New tests in tests/run_agent/test_switch_model_vertex_anthropic.py
cover the exact scenario the reviewer asked for:

 - Gemini-on-Vertex -> Claude-on-Vertex mid-session switch builds
 AnthropicVertex, NOT native Anthropic. Verifies the resolved
 project/region reach the client factory positionally and the
 agent state (api_key placeholder, _vertex_project_id/region,
 _is_anthropic_oauth=False, client=None) is coherent.
 - region=None from get_anthropic_vertex_config falls back to
 "global" (matches the runtime-provider default).
 - Regression guard: a switch onto the native Anthropic provider
 still uses build_anthropic_client, unaffected by the fix.

Verification: scripts/run_tests.sh on the impacted file set (169
tests across 8 files including primary_runtime_restore,
user_providers_model_switch, copilot_api_mode, and the existing
switch_model pool-reload regression) all pass with no new failures.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/auth Authentication, OAuth, credential pools comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/cli CLI entry point, hermes_cli/, setup wizard P3 Low — cosmetic, nice to have provider/anthropic Anthropic native Messages API sweeper:blast-contained Sweeper blast radius: contained — one narrow path / opt-in / few users sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants