diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 86e3da4c..0c76841f 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -72,7 +72,10 @@ jobs: TUI_NAME="strix-tui" dist/strix --version fi - uv run pyi-archive_viewer -l "$PYI_BINARY" | grep -E "strix[/\\]+bin[/\\]+$TUI_NAME" >/dev/null + uv run pyi-archive_viewer -l -r "$PYI_BINARY" > archive.txt + grep -E "strix[/\\]+bin[/\\]+$TUI_NAME" archive.txt >/dev/null + grep -E "'google\.auth\.transport\.requests'" archive.txt >/dev/null + grep -E "'boto3\.session'" archive.txt >/dev/null if [[ "${{ matrix.target }}" == "linux-arm64" ]]; then file dist/strix diff --git a/.github/workflows/ci-package.yml b/.github/workflows/ci-package.yml index 942b9844..c01389e7 100644 --- a/.github/workflows/ci-package.yml +++ b/.github/workflows/ci-package.yml @@ -36,6 +36,8 @@ jobs: uv pip install --python /tmp/strix-smoke/bin/python dist/*.whl /tmp/strix-smoke/bin/strix --version /tmp/strix-smoke/bin/strix --help >/dev/null + - name: Vertex AI and Bedrock dependencies install with the wheel + run: /tmp/strix-smoke/bin/python -c "import google.auth.transport.requests, boto3" binary: runs-on: ubuntu-latest @@ -51,5 +53,9 @@ jobs: - run: make tui-build - run: uv run --frozen pyinstaller strix.spec --noconfirm - run: dist/strix --version - - name: Binary bundles the TUI sidecar - run: uv run --frozen pyi-archive_viewer -l dist/strix | grep -E "strix/bin/strix-tui" >/dev/null + - name: Binary bundles the TUI sidecar and the provider dependencies + run: | + uv run --frozen pyi-archive_viewer -l -r dist/strix > archive.txt + grep -E "strix/bin/strix-tui" archive.txt >/dev/null + grep -E "'google\.auth\.transport\.requests'" archive.txt >/dev/null + grep -E "'boto3\.session'" archive.txt >/dev/null diff --git a/docs/llm-providers/bedrock.mdx b/docs/llm-providers/bedrock.mdx index d7cfb84c..2189e987 100644 --- a/docs/llm-providers/bedrock.mdx +++ b/docs/llm-providers/bedrock.mdx @@ -3,14 +3,6 @@ title: "AWS Bedrock" description: "Configure Strix with models via AWS Bedrock" --- -## Installation - -Bedrock requires the AWS SDK dependency. Install Strix with the bedrock extra: - -```bash -pipx install "strix-agent[bedrock]" -``` - ## Setup ```bash diff --git a/docs/llm-providers/vertex.mdx b/docs/llm-providers/vertex.mdx index d7ed9710..72a71872 100644 --- a/docs/llm-providers/vertex.mdx +++ b/docs/llm-providers/vertex.mdx @@ -3,14 +3,6 @@ title: "Google Vertex AI" description: "Configure Strix with Gemini models via Google Cloud" --- -## Installation - -Vertex AI requires the Google Cloud dependency. Install Strix with the vertex extra: - -```bash -pipx install "strix-agent[vertex]" -``` - ## Setup ```bash diff --git a/pyproject.toml b/pyproject.toml index ed8d2cdc..ffa2aa51 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,7 @@ dependencies = [ "openai-agents[litellm]>=0.19.0,<0.20", "openai>=2.45.0,<3", "litellm>=1.101.0", + "google-auth>=2.0.0", "pydantic>=2.11.3", "pydantic-settings>=2.13.0", "rich", @@ -52,10 +53,6 @@ dependencies = [ "pyyaml>=6.0", ] -[project.optional-dependencies] -vertex = ["google-auth>=2.0.0"] -bedrock = ["boto3>=1.28.0"] - [project.scripts] strix = "strix.interface.main:main" diff --git a/strix.spec b/strix.spec index 827e5e2c..987d8b96 100644 --- a/strix.spec +++ b/strix.spec @@ -180,6 +180,8 @@ hiddenimports = [ ] hiddenimports += collect_submodules('litellm') +hiddenimports += collect_submodules('google.auth') +hiddenimports += collect_submodules('google.oauth2') hiddenimports += collect_submodules('rich') hiddenimports += collect_submodules('pydantic') hiddenimports += collect_submodules('pygments') @@ -211,8 +213,6 @@ excludes = [ 'google.cloud', 'google.cloud.aiplatform', 'google.api_core', - 'google.auth', - 'google.oauth2', 'google.protobuf', 'grpc', 'grpcio', diff --git a/strix/interface/main.py b/strix/interface/main.py index fa8da3fa..1ebccffb 100644 --- a/strix/interface/main.py +++ b/strix/interface/main.py @@ -6,6 +6,7 @@ Strix Agent Interface import argparse import asyncio import contextlib +import logging import sys from pathlib import Path from typing import Any @@ -53,21 +54,6 @@ from strix.telemetry import posthog, report_error, scarf, set_scan_phase from strix.telemetry.logging import setup_console_logging -BEDROCK_MODEL_PREFIX = "bedrock/" -BEDROCK_MISSING_MODULE_ERROR = "No module named 'boto3'" -BEDROCK_EXTRA_HINT = ( - 'Bedrock support is optional. Install it with: pipx install "strix-agent[bedrock]"' -) -VERTEX_MODEL_MARKER = "vertex" -VERTEX_MISSING_MODULE_ERROR = "No module named 'google" -VERTEX_EXTRA_HINT = ( - 'Vertex AI support is optional. Install it with: pipx install "strix-agent[vertex]"' -) - - -import logging # noqa: E402 - - logger = logging.getLogger(__name__) _ROOT_SUBCOMMAND_HELP = """ @@ -96,29 +82,6 @@ def _exception_messages(exc: BaseException) -> tuple[str, ...]: return tuple(messages) -def _provider_import_hint(exc: BaseException, model: str) -> str | None: - """Return an install hint when *exc* is a missing provider dependency. - - Bedrock and Vertex AI ship as optional extras: Bedrock needs ``boto3`` and - Vertex AI needs ``google-auth``. When either is absent, litellm may raise an - ``ImportError``/``ModuleNotFoundError`` directly or wrap it in a connection - error. Map the missing module back to the matching extra so the user knows - what to install. Returns ``None`` for any unrelated error. - """ - model_name = model.lower() - messages = _exception_messages(exc) - if any( - BEDROCK_MISSING_MODULE_ERROR in message for message in messages - ) and model_name.startswith(BEDROCK_MODEL_PREFIX): - return BEDROCK_EXTRA_HINT - if ( - any(VERTEX_MISSING_MODULE_ERROR in message for message in messages) - and VERTEX_MODEL_MARKER in model_name - ): - return VERTEX_EXTRA_HINT - return None - - def _subscription_error_hint(exc: BaseException) -> str | None: """Return an actionable hint for a known ChatGPT-subscription error, or None.""" if not codex.subscription_model(load_settings().llm.model): @@ -364,7 +327,7 @@ def _print_error_panel(title: str, message: str) -> None: console.print() -def _print_model_connection_error(exc: BaseException, model_name: str) -> None: +def _print_model_connection_error(exc: BaseException) -> None: console = Console() error_text = Text() sub_hint = _subscription_error_hint(exc) @@ -380,9 +343,6 @@ def _print_model_connection_error(exc: BaseException, model_name: str) -> None: error_text.append("\n\n", style="white") error_text.append("Could not establish connection to the language model.\n", style="white") error_text.append("Please check your configuration and try again.\n", style="white") - hint = _provider_import_hint(exc, model_name) - if hint is not None: - error_text.append(f"\n{hint}\n", style="bold yellow") error_text.append(f"\nError: {exc}", style="dim white") panel = Panel( @@ -435,7 +395,7 @@ def _bootstrap_scan(args: argparse.Namespace) -> None: asyncio.run(warm_up_llm()) except ModelConnectionError as exc: report_error("model_connection_failed", exc) - _print_model_connection_error(exc, exc.model_name) + _print_model_connection_error(exc) sys.exit(1) persist_current() try: diff --git a/tests/test_main_launch.py b/tests/test_main_launch.py index 0651d126..4965cd42 100644 --- a/tests/test_main_launch.py +++ b/tests/test_main_launch.py @@ -79,7 +79,7 @@ def test_direct_launch_with_a_bad_key_prints_the_panel_and_exits_before_the_tui( monkeypatch.setattr( cli_main, "_print_model_connection_error", - lambda exc, model: calls.append(f"panel:{model}:{exc}"), + lambda exc: calls.append(f"panel:{exc}"), ) monkeypatch.setattr(cli_main, "persist_current", lambda: calls.append("persist")) monkeypatch.setattr(cli_main, "prepare_run", lambda _args: calls.append("prepare")) @@ -89,7 +89,7 @@ def test_direct_launch_with_a_bad_key_prints_the_panel_and_exits_before_the_tui( cli_main._bootstrap_scan(argparse.Namespace(non_interactive=False, needs_setup=False)) assert exit_info.value.code == 1 - assert calls == ["panel:openai/gpt-4o:Error code: 401"] + assert calls == ["panel:Error code: 401"] def test_tui_startup_failure_marks_the_prepared_run_failed(monkeypatch: pytest.MonkeyPatch) -> None: diff --git a/tests/test_optional_deps.py b/tests/test_optional_deps.py deleted file mode 100644 index 7acf18f3..00000000 --- a/tests/test_optional_deps.py +++ /dev/null @@ -1,27 +0,0 @@ -"""Tests for the optional-dependency extras declared in pyproject.toml.""" - -from __future__ import annotations - -import tomllib -from pathlib import Path - - -PYPROJECT = Path(__file__).resolve().parent.parent / "pyproject.toml" - - -def _optional_dependencies() -> dict[str, list[str]]: - data = tomllib.loads(PYPROJECT.read_text(encoding="utf-8")) - extras: dict[str, list[str]] = data["project"]["optional-dependencies"] - return extras - - -def test_vertex_extra_pins_google_auth() -> None: - extras = _optional_dependencies() - assert "vertex" in extras - assert any(req.startswith("google-auth") for req in extras["vertex"]) - - -def test_bedrock_extra_pins_boto3() -> None: - extras = _optional_dependencies() - assert "bedrock" in extras - assert any(req.startswith("boto3") for req in extras["bedrock"]) diff --git a/tests/test_provider_dependencies.py b/tests/test_provider_dependencies.py new file mode 100644 index 00000000..3e8ef9eb --- /dev/null +++ b/tests/test_provider_dependencies.py @@ -0,0 +1,45 @@ +"""Vertex AI and Bedrock support ship with every install, not as extras.""" + +from __future__ import annotations + +import importlib +import tomllib +from pathlib import Path + +import pytest + + +ROOT = Path(__file__).resolve().parent.parent + + +def _pyproject() -> dict[str, object]: + return tomllib.loads((ROOT / "pyproject.toml").read_text(encoding="utf-8")) + + +def test_google_auth_is_a_regular_dependency() -> None: + project = _pyproject()["project"] + assert isinstance(project, dict) + assert any(req.startswith("google-auth") for req in project["dependencies"]) + assert "optional-dependencies" not in project + + +@pytest.mark.parametrize( + "module", + [ + "google.auth", + "google.auth.transport.requests", + "google.oauth2.service_account", + "boto3", + "litellm.llms.vertex_ai.vertex_llm_base", + "litellm.llms.bedrock.base_aws_llm", + ], +) +def test_provider_modules_import(module: str) -> None: + importlib.import_module(module) + + +def test_pyinstaller_spec_bundles_google_auth() -> None: + spec = (ROOT / "strix.spec").read_text(encoding="utf-8") + assert "collect_submodules('google.auth')" in spec + assert "'google.auth'," not in spec + assert "'google.oauth2'," not in spec diff --git a/tests/test_provider_hints.py b/tests/test_provider_hints.py deleted file mode 100644 index 437c1e10..00000000 --- a/tests/test_provider_hints.py +++ /dev/null @@ -1,74 +0,0 @@ -"""Tests for the provider import-error hint helper in interface/main.py.""" - -from __future__ import annotations - -from strix.interface.main import _provider_import_hint - - -VERTEX_MODEL = "vertex_ai/gemini-3-pro-preview" -BEDROCK_MODEL = "bedrock/anthropic.claude-4-5-sonnet" -VERTEX_EXTRA_NAME = "vertex" -BEDROCK_EXTRA_NAME = "bedrock" -INSTALL_EXTRA_COMMAND_FRAGMENT = 'pipx install "strix-agent[' -WRAPPED_VERTEX_GOOGLE_ERROR = "litellm.APIConnectionError: No module named 'google'" -WRAPPED_BEDROCK_BOTO3_ERROR = "litellm.APIConnectionError: No module named 'boto3'" - - -def test_bedrock_boto3_hint() -> None: - exc = ModuleNotFoundError("No module named 'boto3'") - hint = _provider_import_hint(exc, BEDROCK_MODEL) - assert hint is not None - assert INSTALL_EXTRA_COMMAND_FRAGMENT in hint - assert BEDROCK_EXTRA_NAME in hint - - -def test_vertex_google_hint() -> None: - exc = ImportError("No module named 'google'") - hint = _provider_import_hint(exc, VERTEX_MODEL) - assert hint is not None - assert INSTALL_EXTRA_COMMAND_FRAGMENT in hint - assert VERTEX_EXTRA_NAME in hint - - -def test_vertex_google_hint_for_litellm_wrapped_connection_error() -> None: - exc = ConnectionError(WRAPPED_VERTEX_GOOGLE_ERROR) - hint = _provider_import_hint(exc, VERTEX_MODEL) - assert hint is not None - assert INSTALL_EXTRA_COMMAND_FRAGMENT in hint - assert VERTEX_EXTRA_NAME in hint - - -def test_bedrock_boto3_hint_for_litellm_wrapped_connection_error() -> None: - exc = ConnectionError(WRAPPED_BEDROCK_BOTO3_ERROR) - hint = _provider_import_hint(exc, BEDROCK_MODEL) - assert hint is not None - assert INSTALL_EXTRA_COMMAND_FRAGMENT in hint - assert BEDROCK_EXTRA_NAME in hint - - -def test_vertex_google_submodule_hint() -> None: - exc = ModuleNotFoundError("No module named 'google.auth'") - hint = _provider_import_hint(exc, VERTEX_MODEL) - assert hint is not None - assert INSTALL_EXTRA_COMMAND_FRAGMENT in hint - assert VERTEX_EXTRA_NAME in hint - - -def test_vertex_google_hint_for_deeply_chained_error() -> None: - root = ModuleNotFoundError("No module named 'google.auth'") - middle = RuntimeError("provider init failed") - middle.__cause__ = root - exc = ConnectionError("litellm.APIConnectionError: request failed") - exc.__cause__ = middle - hint = _provider_import_hint(exc, VERTEX_MODEL) - assert hint is not None - assert VERTEX_EXTRA_NAME in hint - - -def test_non_import_error_returns_none() -> None: - assert _provider_import_hint(ConnectionError("boom"), "bedrock/whatever") is None - - -def test_unrelated_provider_returns_none() -> None: - exc = ImportError("No module named 'something'") - assert _provider_import_hint(exc, "openai/gpt-4") is None diff --git a/uv.lock b/uv.lock index 3c3a6b41..0936229c 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 5 requires-python = ">=3.12" resolution-markers = [ "python_full_version >= '3.15'", @@ -2393,6 +2393,7 @@ dependencies = [ { name = "cryptography" }, { name = "cvss" }, { name = "docker" }, + { name = "google-auth" }, { name = "litellm" }, { name = "markdown-it-py" }, { name = "openai" }, @@ -2406,14 +2407,6 @@ dependencies = [ { name = "rich" }, ] -[package.optional-dependencies] -bedrock = [ - { name = "boto3" }, -] -vertex = [ - { name = "google-auth" }, -] - [package.dev-dependencies] dev = [ { name = "bandit" }, @@ -2429,12 +2422,11 @@ dev = [ [package.metadata] requires-dist = [ - { name = "boto3", marker = "extra == 'bedrock'", specifier = ">=1.28.0" }, { name = "caido-sdk-client", specifier = ">=0.2.0" }, { name = "cryptography", specifier = ">=48.0.1,<49" }, { name = "cvss", specifier = ">=3.2" }, { name = "docker", specifier = ">=7.1.0" }, - { name = "google-auth", marker = "extra == 'vertex'", specifier = ">=2.0.0" }, + { name = "google-auth", specifier = ">=2.0.0" }, { name = "litellm", specifier = ">=1.101.0" }, { name = "markdown-it-py", specifier = ">=3.0.0" }, { name = "openai", specifier = ">=2.45.0,<3" }, @@ -2447,7 +2439,6 @@ requires-dist = [ { name = "requests", specifier = ">=2.32.0" }, { name = "rich" }, ] -provides-extras = ["vertex", "bedrock"] [package.metadata.requires-dev] dev = [