fix(vertex): explain Vertex failures in-band, correct the model/region matrix - #2640
fix(vertex): explain Vertex failures in-band, correct the model/region matrix#2640zeroasterisk wants to merge 8 commits into
Conversation
TAG=agy CONV=8f23f59a-66a3-4520-aefb-88cb127386c8
…gion matrix
Vertex rejects onboarding requests for a few recurring reasons -- expired ADC, a
partner model that was never enabled in Model Garden, a model that simply is not
served in the requested location -- and its own error text names none of the
fixes. Users saw a bare 404 through their SDK with no way to tell a proxy bug
from a project-config gap.
Annotate those failures where the proxy already knows the location, publisher
and model: a WARNING in the log, an x-headroom-hint header, and a note appended
to error.message (the only field most SDKs surface). Successful responses are
untouched and annotation is idempotent. Routes opt in with a one-line decorator.
The same treatment covers backend setup failures, where `--backend vertex_ai`
surfaced LiteLLM's raw "No module named 'vertexai'" as an opaque 500; the hint
now names both remedies, including that the native publisher routes need no
--backend flag at all.
Also correct guidance and fixtures that could not have worked:
- google-genai takes http_options={"base_url": ...}, not api_endpoint, and
ThinkingConfig(thinking_budget=), not thinking_budget_tokens.
- The `global` endpoint requires contents[].role; omitting it 400s.
- Replace the model x location cross-product with servable pairs. Gemini 3.x has
no US regional endpoint and the -latest aliases are global-only, so a pinned
us-central1 Gemini case can never pass. No Gemini 2.5 (retires 2026-10-16).
- test_thinking_levels asserted only status < 500 while sending an ignored
thinkingBudgetTokens key, so it passed on 400s and proved nothing.
Cases the caller's project genuinely cannot serve now skip with an actionable
reason instead of reporting a red proxy.
PR governanceThis PR follows the template and is marked ready for human review. |
…annot serve `--backend vertex` needs `vertexai` from google-cloud-aiplatform, which no extra provided. The backend still constructed cleanly without it and then failed on every request with an opaque LiteLLM string. Add a dedicated `[vertex]` extra rather than folding the dependency into `[proxy]`: google-cloud-aiplatform and its transitive tree measure ~175MB installed, and the native Vertex passthrough routes never needed it -- only the LiteLLM translation backend does. This follows the existing `proxy-prod` split. Preflight the import when the resolved provider is vertex_ai, so the failure lands once at startup with the remedy instead of on every request. The check keys on the resolved provider, so all aliases (vertex, vertex_ai, litellm-vertex, google-vertex, googlevertex) are covered, and it is skipped when a backend class is injected so tests can keep using fakes. A new BackendUnavailableError lets the CLI print the message and exit 2 rather than dumping a traceback through startup internals. Docs corrected while verifying this: - proxy.mdx claimed publisher=anthropic passthrough "routes through the same LiteLLM-Vertex path as --backend vertex_ai". It does not; without the flag it is a direct forward, which is why it works on a bare [proxy] install. - Combining the flag with native Vertex URLs is now called out as a trap: LiteLLM takes project/region/model from its own config and ignores the URL, turning a working request into a 404 for a model the caller never named. - installation.mdx and wiki/vertex.md said the SDK is "not included in any extra".
…touches ruff check . flagged 19 issues on this branch; 15 sit in the demo script and the Vertex integration test, both rewritten here. Clears them so the PR leaves ruff clean everywhere it reaches. The remaining 4 are in plugins/headroom-oauth2 and are untouched by this work.
|
Integration test against #1044 (headroom wrap agy) I did a local temp-branch merge of this PR + davdittrich/headroom#1044 to check compatibility before either lands. Merge result: Two conflicts, both trivial — Shared files that merged cleanly: agy unit tests on the merged branch: 97 passed, 5 skipped. One blocker found in #1044 itself: This module was retired in #2499 ( Also found one missing re-export: No action needed on this PR. Flagging here for visibility and tagging #1044. |
JerrettDavis
left a comment
There was a problem hiding this comment.
Reviewed the current diff with focus on the new Vertex diagnostics, LiteLLM Vertex preflight, route decorators, Anthropic backend error annotation, docs, and the new tests. I did not find a blocking issue in the source changes.\n\nLocal verification: pytest tests/test_vertex_diagnostics.py -q passes (36 passed). I also attempted pytest tests/test_vertex_diagnostics.py tests/test_vertex_claude_compression.py -q; the second module could not collect locally because this checkout does not have the native headroom._core extension built, so I could not use that broader existing test as local evidence.
|
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
JerrettDavis
left a comment
There was a problem hiding this comment.
Thanks for the Vertex diagnostic work. I need to pull back my earlier approval because fresh CI exposed a branch-local regression.
test (3) is failing on the latest head in two existing LiteLLM Vertex backend-path tests:
FAILED tests/test_openai_streaming_backend.py::TestOpenAIStreamingMock::test_litellm_vertex_streaming_preserves_max_tokens_and_vendor_fields
FAILED tests/test_proxy/test_openai_backend_path.py::test_litellm_vertex_backend_path_preserves_max_tokens_and_vendor_fields
E headroom.providers.registry.BackendUnavailableError: Vertex backend selected but the Vertex SDK is missing...
The new startup preflight is firing before those tests can exercise the mocked/backend-path behavior. The production guard makes sense, but the test surface needs to be updated so CI can still validate the LiteLLM Vertex request-shaping path without requiring google-cloud-aiplatform in the default proxy environment. A small targeted fix would be to inject/mock the backend class or patch the preflight in those existing tests, while keeping the new CLI/startup failure-path coverage for the missing SDK case.
…x tests The new startup preflight (ensure_vertex_sdk_available) fires during create_app(), which these tests call with backend='litellm-vertex' to prove request-shaping. Neither test has google-cloud-aiplatform installed, so the preflight raises BackendUnavailableError before any mock can run. Fix by patching vertex_sdk_available to return True in those two tests. The preflight's own behaviour is already covered by TestVertexSdkPreflight in tests/test_vertex_diagnostics.py.
JerrettDavis
left a comment
There was a problem hiding this comment.
The previous blocker is fixed at the current head. The new 52686520 commit patches vertex_sdk_available in the two existing LiteLLM Vertex backend-path tests, so those tests can keep validating request shaping without requiring the optional Vertex SDK in the default proxy environment. GitHub is also green now, including the test (3) shard that failed when I requested changes.
Verified locally:
.venv\\Scripts\\python.exe -m pytest tests/test_vertex_diagnostics.py tests/test_proxy_vertex_native_integration.py tests/test_openai_streaming_backend.py::TestOpenAIStreamingMock::test_litellm_vertex_streaming_preserves_max_tokens_and_vendor_fields tests/test_proxy/test_openai_backend_path.py::test_litellm_vertex_backend_path_preserves_max_tokens_and_vendor_fields -q-> 38 passed, 8 skipped.venv\\Scripts\\python.exe -m ruff check ...on the touched Vertex/proxy/test files -> passedgit diff --check upstream/main...HEAD-> passed
Full GitHub check rollup is green on 5268652007c6068445b1fd251d303d21c9d77249.
# Conflicts: # headroom/proxy/handlers/anthropic.py
|
Refreshed and cleanly merged with latest |
JerrettDavis
left a comment
There was a problem hiding this comment.
Re-reviewed the exact current head after the main merges. The Vertex failure annotation remains confined to supported failing statuses and preserves successful bodies; the optional-SDK preflight still keys on resolved Vertex aliases and is bypassed only for injected test backends. The prior CI regression fixes remain in both LiteLLM Vertex request-shaping tests. The later Anthropic conflict resolution integrates current main without removing those diagnostics. I also added the missing factual Runtime Rollout Safety section. No new source blocker; fork workflows remain authorization-gated rather than failing.
Description
Vertex rejects onboarding requests for a handful of recurring reasons — expired ADC, a partner model never enabled in Model Garden, a model not served in the requested location, a missing optional SDK — and its own error text names none of the fixes. Users see a bare
404or an opaque500through their SDK and cannot tell a proxy bug from a project-config gap.This makes the proxy explain those failures where it happens, adds a
[vertex]extra so the LiteLLM Vertex backend can actually start, and corrects an example, a test matrix, and three documentation claims that could not have worked as written.Closes #2632
Type of Change
Changes Made
Explain Vertex failures in-band (
headroom/providers/vertex/diagnostics.py, new)401/403/404/429, attach an actionable hint three ways: a WARNING in the proxy log, anx-headroom-hintresponse header, and a note appended toerror.message. The last is load-bearing — SDKs raise with the message string and discard headers.@with_vertex_diagnosticsdecorator reading path params FastAPI already injects (+8 lines inproxy_routes.py, no re-indentation).--backend vertexsurfaced LiteLLM's rawNo module named 'vertexai'as an opaque 500.Make
--backend vertexable to start (pyproject.toml,registry.py,cli/proxy.py)[vertex]extra carryinggoogle-cloud-aiplatform>=1.38. Deliberately not folded into[proxy]: measured at ~175 MB installed, and the native passthrough routes never needed it. Follows the existingproxy-prodsplit.registry.py:229(vertex,vertex_ai,litellm-vertex,google-vertex,googlevertex) is covered. Skipped when a backend class is injected, so existing fake-based tests are unaffected.BackendUnavailableErrorso the CLI prints the remedy and exits2instead of dumping a traceback through startup internals.Correct things that could not have worked
http_options={"api_endpoint": ...}{"base_url": "http://..."}—api_endpointis not a field onHttpOptions; pydantic rejects it before any request is sentThinkingConfig(thinking_budget_tokens=)thinking_budget=contents: [{parts: [...]}]role: "user"— theglobalendpoint enforces it, regional was silently lenientgcloud auth print-access-tokenapplication-default— the plain user token gets401 ACCESS_TOKEN_TYPE_UNSUPPORTEDglobal/gemini-flash-latest,global/claude-sonnet-4-6,europe-west2/gemini-3.5-flash,us-east5/claude-sonnet-4-6. Gemini 3.x has no US regional endpoint and-latestaliases are global-only, so a pinnedus-central1Gemini case can never pass. No Gemini 2.5 — it retires 2026-10-16.test_thinking_levelsasserted onlystatus_code < 500while sending an ignoredthinkingBudgetTokenskey: it passed on 400s and proved nothing. Now asserts 200 plus scored usage metadata with the correctthinkingBudgetspelling.Docs (
proxy.mdx,installation.mdx,wiki/vertex.md)proxy.mdxclaimedpublisher=anthropicpassthrough "routes through the same LiteLLM-Vertex path as--backend vertex_ai". It does not — without the flag it is a direct forward, which is why it works on a bare[proxy]install where the LiteLLM path cannot even import.--backend vertexwith native Vertex URLs: LiteLLM takes project/region/model from its own config and ignores the URL.installation.mdxandwiki/vertex.mdsaid the SDK is "not included in any extra".rufffindings in the two files this PR rewrites (4 remain inplugins/headroom-oauth2, untouched by this work).Testing
pytest)ruff check .)mypy headroom)Test Output
New tests in
tests/test_vertex_diagnostics.py(36, no credentials required): status coverage, per-publisher remedy divergence, idempotency,content-lengthconsistency after body rewrite, non-JSON and body-less/streaming responses, every backend alias preflighted, other providers unaffected, injected backend bypasses the preflight.Real Behavior Proof
alanblount-sandbox) withaiplatform.googleapis.comenabled and Claude enabled in Model Garden; ADC viagcloud auth application-default loginexport GCP_PROJECT_ID=alanblount-sandbox && export GCP_ACCESS_TOKEN=$(gcloud auth application-default print-access-token) && uv run --extra proxy --with google-genai python3 examples/vertex_genai_sdk_demo.py && uv run --extra proxy --with pytest-asyncio pytest tests/test_proxy_vertex_native_integration.py tests/test_vertex_diagnostics.py -q && headroom proxy --backend vertex(exits 2, then retried with[proxy,vertex]extra installed)✓ Standard response received successfully! > 1, 2, 3, 4, 5and✓ Thinking response received successfully!; 44 tests passed (8 live integration, 36 diagnostics), 0 skipped;--backend vertexexited 2 with the remedy message; same request with the extra installed returned 200; invalid region produced[headroom] hint:in the response body andx-headroom-hintheader; success responses were byte-cleanus/eumulti-region endpoints and Claude 4.7+ (project cannot serve them); Provisioned Throughput; Rust proxy path;--backend vertexdropping URL project/model on native Vertex routes (documented as known gap in Additional Notes)Review Readiness
Checklist
CHANGELOG.md— it is generated by release-please from my Conventional Commit PR title (a CI guard enforces this)Screenshots (if applicable)
N/A — proxy and CLI behavior; terminal output is quoted above.
Additional Notes
uv.lockintentionally not included: the new extra is opt-in and does not change default resolution.registry.py) rather than at the CLI, so programmatic embedders get the same guarantee; the CLI only formats it.test_hermes_passthrough_compression,test_observability_metrics,test_observability_tracing). They predate this work and are unrelated, so they were excluded rather than fixed — happy to open a separate issue.[vertex]was chosen over adding the dependency to[proxy]after measuring the footprint at ~175 MB. If maintainers prefer it in[proxy], that is a one-line change.Runtime Rollout Safety