mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
## Description Adds first-class GCP Vertex AI proxy routing for publisher REST endpoints so Vertex requests are forwarded to a configurable regional Vertex host instead of falling through to the generic OpenAI/Anthropic/Gemini passthrough selection. Fixes #792 ## Type of Change - [x] New feature (non-breaking change that adds functionality) - [x] Documentation update ## Changes Made - Added a `vertex` provider target with `VERTEX_TARGET_API_URL` and `--vertex-api-url` support. - Registered explicit Vertex publisher routes for Google `generateContent`, `streamGenerateContent`, `countTokens` and Anthropic publisher `rawPredict`, `streamRawPredict` passthrough. - Added startup banner/routing output for Vertex AI. - Added focused tests for provider target resolution, CLI/env config, banner output, and route delegation. - Added `wiki/vertex.md` with usage examples and Google Cloud source links. ## Sources - Vertex AI Gemini inference reference: https://docs.cloud.google.com/vertex-ai/generative-ai/docs/model-reference/inference - Google Cloud REST authentication: https://docs.cloud.google.com/docs/authentication/rest - Google Application Default Credentials: https://docs.cloud.google.com/docs/authentication/application-default-credentials ## Testing - [x] Linting passes (`python -m ruff check .`) - [x] New tests added for new functionality - [x] Focused unit tests pass - [ ] Full unit suite completed locally - [ ] Rust tests completed locally - [ ] Type checking passes locally ## Test Output ```text $ python -m ruff check . All checks passed! $ python -m pytest tests/test_provider_registry.py tests/test_provider_proxy_routes.py tests/test_cli_proxy_env.py tests/test_banner_upstream_targets.py -q 57 passed, 1 warning in 13.75s ``` Local limitations: - `python -m pytest tests scripts/tests -q` timed out after 1 hour on this Windows machine before completing. - `cargo test -p headroom-proxy --test integration_vertex_raw_predict` could not run because `cargo` is not installed on PATH in this environment. - The commit hook's `mypy` step fails locally on an existing Windows `fcntl` typing issue in `headroom/subscription/tracker.py`; `ruff`, `ruff-format`, and plugin-version hooks passed, and the commit was made with only `mypy` skipped. ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] I have made corresponding changes to the documentation - [x] I have added tests that prove the feature works - [ ] New and existing unit tests pass locally with my changes - [ ] I have updated the CHANGELOG.md if applicable
133 lines
5.4 KiB
Python
133 lines
5.4 KiB
Python
"""Tests for upstream API targets in the proxy startup banner.
|
|
|
|
Verifies that:
|
|
1. Default API targets appear in the banner when no overrides are set
|
|
2. Custom API targets (via ProxyConfig) appear correctly in the banner
|
|
3. The banner is suppressed when print_banner=False
|
|
|
|
Closes #583.
|
|
"""
|
|
|
|
from io import StringIO
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
pytest.importorskip("fastapi")
|
|
|
|
from headroom.providers.claude import DEFAULT_API_URL as DEFAULT_ANTHROPIC_API_URL # noqa: E402
|
|
from headroom.providers.codex import DEFAULT_API_URL as DEFAULT_OPENAI_API_URL # noqa: E402
|
|
from headroom.providers.gemini import DEFAULT_API_URL as DEFAULT_GEMINI_API_URL # noqa: E402
|
|
from headroom.providers.registry import ( # noqa: E402
|
|
DEFAULT_CLOUDCODE_API_URL,
|
|
DEFAULT_VERTEX_API_URL,
|
|
)
|
|
from headroom.proxy.models import ProxyConfig # noqa: E402
|
|
from headroom.proxy.server import run_server # noqa: E402
|
|
|
|
|
|
class TestBannerUpstreamTargets:
|
|
"""Verify resolved upstream API targets are displayed in the startup banner."""
|
|
|
|
def _capture_banner(self, config: ProxyConfig | None = None) -> str:
|
|
"""Run the server with print_banner=True, intercepting stdout and uvicorn."""
|
|
buf = StringIO()
|
|
config = config or ProxyConfig()
|
|
with (
|
|
patch("sys.stdout", buf),
|
|
patch("headroom.proxy.server.uvicorn") as mock_uvicorn,
|
|
patch("headroom.proxy.server.create_app"),
|
|
):
|
|
mock_uvicorn.run = lambda *a, **kw: None
|
|
run_server(config, print_banner=True)
|
|
return buf.getvalue()
|
|
|
|
def test_default_targets_in_banner(self):
|
|
"""Default API targets should appear when no overrides are configured."""
|
|
output = self._capture_banner()
|
|
|
|
assert "UPSTREAM TARGETS:" in output
|
|
assert DEFAULT_ANTHROPIC_API_URL in output
|
|
assert DEFAULT_OPENAI_API_URL in output
|
|
assert DEFAULT_GEMINI_API_URL in output
|
|
assert DEFAULT_CLOUDCODE_API_URL in output
|
|
assert DEFAULT_VERTEX_API_URL in output
|
|
|
|
def test_custom_anthropic_target_in_banner(self):
|
|
"""A custom Anthropic API URL should be resolved and shown in the banner."""
|
|
config = ProxyConfig(anthropic_api_url="https://litellm.internal/v1")
|
|
output = self._capture_banner(config)
|
|
|
|
# resolve_api_targets strips trailing /v1
|
|
assert "https://litellm.internal" in output
|
|
# Other providers should keep their defaults
|
|
assert DEFAULT_OPENAI_API_URL in output
|
|
|
|
def test_custom_openai_target_in_banner(self):
|
|
"""A custom OpenAI API URL should be resolved and shown in the banner."""
|
|
config = ProxyConfig(openai_api_url="http://my-vllm:4000")
|
|
output = self._capture_banner(config)
|
|
|
|
assert "http://my-vllm:4000" in output
|
|
assert DEFAULT_ANTHROPIC_API_URL in output
|
|
|
|
def test_custom_gemini_target_in_banner(self):
|
|
"""A custom Gemini API URL should be resolved and shown in the banner."""
|
|
config = ProxyConfig(gemini_api_url="http://my-gemini:5000")
|
|
output = self._capture_banner(config)
|
|
|
|
assert "http://my-gemini:5000" in output
|
|
|
|
def test_custom_cloudcode_target_in_banner(self):
|
|
"""A custom Cloud Code API URL should be resolved and shown in the banner."""
|
|
config = ProxyConfig(cloudcode_api_url="https://custom-cloudcode.example.com")
|
|
output = self._capture_banner(config)
|
|
|
|
assert "https://custom-cloudcode.example.com" in output
|
|
|
|
def test_custom_vertex_target_in_banner(self):
|
|
"""A custom Vertex AI API URL should be resolved and shown in the banner."""
|
|
config = ProxyConfig(vertex_api_url="https://europe-west4-aiplatform.googleapis.com")
|
|
output = self._capture_banner(config)
|
|
|
|
assert "https://europe-west4-aiplatform.googleapis.com" in output
|
|
|
|
def test_multiple_custom_targets_in_banner(self):
|
|
"""Multiple custom targets should all appear correctly in the banner."""
|
|
config = ProxyConfig(
|
|
anthropic_api_url="https://anthropic.internal",
|
|
openai_api_url="https://openai.internal",
|
|
gemini_api_url="https://gemini.internal",
|
|
cloudcode_api_url="https://cloudcode.internal",
|
|
vertex_api_url="https://vertex.internal",
|
|
)
|
|
output = self._capture_banner(config)
|
|
|
|
assert "https://anthropic.internal" in output
|
|
assert "https://openai.internal" in output
|
|
assert "https://gemini.internal" in output
|
|
assert "https://cloudcode.internal" in output
|
|
assert "https://vertex.internal" in output
|
|
|
|
def test_banner_suppressed_when_disabled(self):
|
|
"""When print_banner=False, upstream targets should NOT be printed."""
|
|
buf = StringIO()
|
|
with (
|
|
patch("sys.stdout", buf),
|
|
patch("headroom.proxy.server.uvicorn") as mock_uvicorn,
|
|
patch("headroom.proxy.server.create_app"),
|
|
):
|
|
mock_uvicorn.run = lambda *a, **kw: None
|
|
run_server(ProxyConfig(), print_banner=False)
|
|
output = buf.getvalue()
|
|
|
|
assert "UPSTREAM TARGETS:" not in output
|
|
|
|
def test_trailing_v1_stripped_in_banner(self):
|
|
"""URLs ending in /v1 should be normalized (stripped) in the banner."""
|
|
config = ProxyConfig(openai_api_url="http://my-proxy:8000/v1")
|
|
output = self._capture_banner(config)
|
|
|
|
# resolve_api_targets normalizes /v1 suffix
|
|
assert "http://my-proxy:8000" in output
|
|
assert "http://my-proxy:8000/v1" not in output
|