-
Notifications
You must be signed in to change notification settings - Fork 307
feat: bundle google-cloud-aiplatform for Vertex partner models #2910
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
5b11763
8120d23
4ba8204
1490482
c5ebe31
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,7 @@ import os | |
| import site | ||
| import sys | ||
| from PyInstaller.utils.hooks import ( | ||
| collect_all, | ||
| collect_submodules, | ||
| collect_data_files, | ||
| copy_metadata, | ||
|
|
@@ -17,6 +18,58 @@ from PyInstaller.utils.hooks import ( | |
| # and cause LoadLibrary to fail at runtime with "Invalid access to memory location". | ||
| IS_WINDOWS = sys.platform == "win32" | ||
|
|
||
| # Optional Vertex AI bundle. google-cloud-aiplatform is an opt-in extra | ||
| # (`openhands-sdk[vertex]`) and is NOT bundled in the default agent-server | ||
| # build. To produce a binary that supports `vertex_ai/*` partner models | ||
| # (MiniMax, Qwen, Kimi MaaS endpoints): | ||
| # | ||
| # - Docker: docker build --build-arg ENABLE_VERTEX=1 ... | ||
| # - From src: uv sync --frozen --dev --no-editable --extra boto3 --extra vertex | ||
| # uv run pyinstaller .../agent-server.spec | ||
| # | ||
| # When `vertexai` is importable we use collect_all(...) for the Vertex SDK | ||
| # and its google.cloud.* namespace siblings: the imports happen inside | ||
| # function bodies AND traverse PEP-420 google.cloud namespace packages, so | ||
| # collect_submodules alone misses everything below the namespace root. | ||
| # collect_all walks the actual installed dirs. | ||
| import importlib.util as _vertex_importlib_util | ||
|
|
||
| _VERTEX_AVAILABLE = _vertex_importlib_util.find_spec("vertexai") is not None | ||
|
|
||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Suggestion: The loop with repeated unpacking and |
||
| _vertex_pkgs = ( | ||
| "vertexai", | ||
| "google.cloud.aiplatform", | ||
| "google.cloud.aiplatform_v1", | ||
| "google.cloud.aiplatform_v1beta1", | ||
| "google.cloud.bigquery", | ||
| "google.cloud.storage", | ||
| "google.cloud.resourcemanager", | ||
| "google.api_core", | ||
| "google.auth", | ||
| "google.rpc", | ||
| "google.genai", | ||
| "proto", | ||
| "grpc_status", | ||
| ) | ||
| _vertex_datas = [] | ||
| _vertex_binaries = [] | ||
| _vertex_hiddenimports = [] | ||
| if _VERTEX_AVAILABLE: | ||
| for _pkg in _vertex_pkgs: | ||
| _d, _b, _h = collect_all(_pkg) | ||
| _vertex_datas += _d | ||
| _vertex_binaries += _b | ||
| _vertex_hiddenimports += _h | ||
| # google.rpc.status_pb2 is a gRPC proto stub imported dynamically; only pin | ||
| # it when the SDK is actually present. | ||
| _vertex_hiddenimports.append("google.rpc.status_pb2") | ||
| else: | ||
| print( | ||
| "[agent-server.spec] vertexai not installed; " | ||
| "skipping Vertex AI bundle collection. " | ||
| "Install openhands-sdk[vertex] before building to include it." | ||
| ) | ||
|
|
||
| # Get the project root directory (current working directory when running PyInstaller) | ||
| project_root = Path.cwd() | ||
| # Namespace roots must be in pathex so PyInstaller can find 'openhands/...' | ||
|
|
@@ -64,7 +117,10 @@ def get_fakeredis_data(): | |
| a = Analysis( | ||
| [ENTRY], | ||
| pathex=PATHEX, | ||
| binaries=[], | ||
| binaries=[ | ||
| # Vertex AI SDK binaries (collected via collect_all above) | ||
| *_vertex_binaries, | ||
| ], | ||
| datas=[ | ||
| # Third-party packages that ship data | ||
| *collect_data_files("tiktoken"), | ||
|
|
@@ -99,6 +155,9 @@ a = Analysis( | |
| *copy_metadata("openhands-workspace"), | ||
| *copy_metadata("fastmcp"), | ||
| *copy_metadata("litellm"), | ||
|
|
||
| # Vertex AI SDK datas (collected via collect_all above) | ||
| *_vertex_datas, | ||
| ], | ||
| hiddenimports=[ | ||
| # Pull all OpenHands modules from the namespace (PEP 420 safe once pathex is correct) | ||
|
|
@@ -118,6 +177,10 @@ a = Analysis( | |
| # unicodedata.unidata_version (e.g. unicode17_0_0 on Python 3.13). | ||
| *collect_submodules("rich"), | ||
|
|
||
| # Vertex AI SDK hidden imports (collected via collect_all above; empty | ||
| # if openhands-sdk[vertex] is not installed in the build env). | ||
| *_vertex_hiddenimports, | ||
|
|
||
| # mcp subpackages used at runtime (avoid CLI) | ||
| "mcp.types", | ||
| "mcp.client", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| """Preflight check for Vertex AI partner-model dependencies. | ||
|
|
||
| `google-cloud-aiplatform` is an optional extra (`openhands-sdk[vertex]`). When a | ||
| caller targets a `vertex_ai/*` model without the extra installed, LiteLLM fails | ||
| with a low-level `ModuleNotFoundError` from inside its provider handler. We | ||
| catch that earlier and surface a friendly install hint instead. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import importlib.util | ||
|
|
||
| from openhands.sdk.llm.exceptions import LLMBadRequestError | ||
|
|
||
|
|
||
| _INSTALL_HINT = ( | ||
| "Vertex AI partner models require the Vertex SDK. " | ||
| 'Install with: pip install "openhands-sdk[vertex]"' | ||
| ) | ||
|
|
||
|
|
||
| def _vertex_sdk_available() -> bool: | ||
| return importlib.util.find_spec("vertexai") is not None | ||
|
|
||
|
|
||
| def assert_vertex_sdk_available(provider: str | None) -> None: | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Suggestion: Consider adding a type hint for the |
||
| """Raise a friendly error if the caller is targeting Vertex without the SDK. | ||
|
|
||
| No-op for any non-`vertex_ai` provider, so it's safe to call unconditionally | ||
| from the transport path. | ||
| """ | ||
| if provider != "vertex_ai": | ||
| return | ||
| if _vertex_sdk_available(): | ||
| return | ||
| raise LLMBadRequestError(_INSTALL_HINT) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| """Tests for the Vertex AI optional-extra preflight check.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import pytest | ||
|
|
||
| from openhands.sdk.llm.exceptions import LLMBadRequestError | ||
| from openhands.sdk.llm.utils import vertex_preflight | ||
| from openhands.sdk.llm.utils.vertex_preflight import assert_vertex_sdk_available | ||
|
|
||
|
|
||
| def test_noop_for_non_vertex_providers(monkeypatch: pytest.MonkeyPatch) -> None: | ||
| # Even with the SDK absent, non-vertex providers must not raise. | ||
| monkeypatch.setattr(vertex_preflight, "_vertex_sdk_available", lambda: False) | ||
| assert_vertex_sdk_available(None) | ||
| assert_vertex_sdk_available("openai") | ||
| assert_vertex_sdk_available("bedrock") | ||
|
|
||
|
|
||
| def test_passes_when_sdk_installed(monkeypatch: pytest.MonkeyPatch) -> None: | ||
| monkeypatch.setattr(vertex_preflight, "_vertex_sdk_available", lambda: True) | ||
| assert_vertex_sdk_available("vertex_ai") | ||
|
|
||
|
|
||
| def test_raises_with_install_hint_when_sdk_missing( | ||
| monkeypatch: pytest.MonkeyPatch, | ||
| ) -> None: | ||
| monkeypatch.setattr(vertex_preflight, "_vertex_sdk_available", lambda: False) | ||
| with pytest.raises(LLMBadRequestError) as excinfo: | ||
| assert_vertex_sdk_available("vertex_ai") | ||
| assert "openhands-sdk[vertex]" in str(excinfo.value) |
Uh oh!
There was an error while loading. Please reload this page.