From 01ce54d4e43c05d4b5f0c7c50cdaf764a479153c Mon Sep 17 00:00:00 2001 From: mitnick Date: Fri, 26 Jun 2026 01:44:05 +0000 Subject: [PATCH] feat: add bedrock + vertex optional extras with install docs and import hints (#574) Declare [project.optional-dependencies] with vertex (google-auth) and bedrock (boto3) extras so "strix-agent[vertex]" / "strix-agent[bedrock]" install the provider SDKs. Add an Installation section to the Bedrock docs mirroring Vertex, and a _provider_import_hint helper in warm_up_llm that surfaces a pip-install hint when a provider dependency is missing. Fixes #574, #573 Co-Authored-By: Claude Opus 4.8 --- docs/llm-providers/bedrock.mdx | 8 +++ pyproject.toml | 4 ++ strix/interface/main.py | 25 +++++++ tests/test_optional_deps.py | 26 ++++++++ tests/test_provider_hints.py | 30 +++++++++ uv.lock | 115 +++++++++++++++++++++++++++++++++ 6 files changed, 208 insertions(+) create mode 100644 tests/test_optional_deps.py create mode 100644 tests/test_provider_hints.py diff --git a/docs/llm-providers/bedrock.mdx b/docs/llm-providers/bedrock.mdx index 2189e987..d7cfb84c 100644 --- a/docs/llm-providers/bedrock.mdx +++ b/docs/llm-providers/bedrock.mdx @@ -3,6 +3,14 @@ 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/pyproject.toml b/pyproject.toml index 4485d779..d431368a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,6 +44,10 @@ dependencies = [ "caido-sdk-client>=0.2.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/interface/main.py b/strix/interface/main.py index 4eae0527..f2b73b0f 100644 --- a/strix/interface/main.py +++ b/strix/interface/main.py @@ -213,10 +213,31 @@ def check_docker_installed() -> None: logger.debug("Docker CLI present") +def _provider_import_hint(exc: BaseException, model: str) -> str | None: + """Return a pip-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 raises an + ``ImportError``/``ModuleNotFoundError`` naming the missing package. Map that + back to the matching extra so the user knows what to install. Returns + ``None`` for any unrelated error. + """ + if not isinstance(exc, ImportError): + return None + message = str(exc) + model_name = model.lower() + if "boto3" in message and model_name.startswith("bedrock/"): + return 'Bedrock support is optional. Install it with: pip install "strix-agent[bedrock]"' + if "google" in message and "vertex" in model_name: + return 'Vertex AI support is optional. Install it with: pip install "strix-agent[vertex]"' + return None + + async def warm_up_llm() -> None: console = Console() logger.info("Warming up LLM connection") + raw_model = "" try: settings = load_settings() configure_sdk_model_defaults(settings) @@ -279,6 +300,9 @@ async def warm_up_llm() -> 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(e, raw_model) + if hint is not None: + error_text.append(f"\n{hint}\n", style="bold yellow") error_text.append(f"\nError: {e}", style="dim white") panel = Panel( @@ -310,6 +334,7 @@ def _positive_budget(value: str) -> float: except ValueError as exc: raise argparse.ArgumentTypeError(f"invalid float value: {value!r}") from exc import math + if not math.isfinite(budget) or budget <= 0: raise argparse.ArgumentTypeError("must be a finite number greater than 0") return budget diff --git a/tests/test_optional_deps.py b/tests/test_optional_deps.py new file mode 100644 index 00000000..43911dca --- /dev/null +++ b/tests/test_optional_deps.py @@ -0,0 +1,26 @@ +"""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")) + return data["project"]["optional-dependencies"] + + +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_hints.py b/tests/test_provider_hints.py new file mode 100644 index 00000000..3bad7d99 --- /dev/null +++ b/tests/test_provider_hints.py @@ -0,0 +1,30 @@ +"""Tests for the provider import-error hint helper in interface/main.py.""" + +from __future__ import annotations + +from strix.interface.main import _provider_import_hint + + +def test_bedrock_boto3_hint() -> None: + exc = ModuleNotFoundError("No module named 'boto3'") + hint = _provider_import_hint(exc, "bedrock/anthropic.claude-4-5-sonnet") + assert hint is not None + assert "pip install" in hint + assert "bedrock" in hint + + +def test_vertex_google_hint() -> None: + exc = ImportError("No module named 'google'") + hint = _provider_import_hint(exc, "vertex_ai/gemini-3-pro-preview") + assert hint is not None + assert "pip install" in hint + assert "vertex" 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 cc7cb0a1..8aef715e 100644 --- a/uv.lock +++ b/uv.lock @@ -193,6 +193,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/05/a4/a26d5b25671d27e03afb5401a0be5899d94ff8fab6a698b1ac5be3ec29ef/bandit-1.9.4-py3-none-any.whl", hash = "sha256:f89ffa663767f5a0585ea075f01020207e966a9c0f2b9ef56a57c7963a3f6f8e", size = 134741, upload-time = "2026-02-25T06:44:13.694Z" }, ] +[[package]] +name = "boto3" +version = "1.43.36" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, + { name = "jmespath" }, + { name = "s3transfer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ff/9f/897287e955db0f50b12fd69ef45956e4fd2c7ddb48c736872f7ea2314443/boto3-1.43.36.tar.gz", hash = "sha256:587d7ee92a12e440ad12b0e7f11f3358f0c4d65b19f64726efc94aaf194aff28", size = 112690, upload-time = "2026-06-23T02:47:14.561Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/f1/274303f52483ecf199eae6f8d9b6f5951670397ee4d72c06cfd4eb644612/boto3-1.43.36-py3-none-any.whl", hash = "sha256:42942dde254673abcbc9e6e60017c88341a4f49d99d24e1f2e290fb38138c26f", size = 140031, upload-time = "2026-06-23T02:47:13.178Z" }, +] + +[[package]] +name = "botocore" +version = "1.43.36" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jmespath" }, + { name = "python-dateutil" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7c/37/da9e7f6ca73ac73afd7f0bb7f238aa5daba35c081e98d7f48a7c399599c0/botocore-1.43.36.tar.gz", hash = "sha256:4cae47d1b2d426316b85a0087d9e69e048f13bc003b5177d74639fe9dfd28205", size = 15625488, upload-time = "2026-06-23T02:47:03.192Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/19/934f81592527a3f7f9b943c893e334c721a4644948642bc33885d584e9ec/botocore-1.43.36-py3-none-any.whl", hash = "sha256:3c65fdc39ed01d8dfde1e961b34038aed03c459f8ddf80717a12ac006475e49d", size = 15313630, upload-time = "2026-06-23T02:46:59.327Z" }, +] + [[package]] name = "caido-sdk-client" version = "0.2.0" @@ -606,6 +634,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl", hash = "sha256:98de475b5cb3bd66bedd5c4679e87b4fdfe1a3bf4d707b151b3c07e58c9a2437", size = 202505, upload-time = "2026-02-05T21:50:51.819Z" }, ] +[[package]] +name = "google-auth" +version = "2.55.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "pyasn1-modules" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/6f/f3f4ac177c67bbee8fe8e88f2ab4f36af88c44a096e165c5217accf6e5d3/google_auth-2.55.1.tar.gz", hash = "sha256:fb2d9b730f2c9b8d326ec8d7222f21aef2ead15bf0513793d6442485d87af0a1", size = 349527, upload-time = "2026-06-25T23:39:27.182Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/1d/f6d3ca1ad0725f2e08a1c6915640748a52de2e66596160a4d53b010cccf0/google_auth-2.55.1-py3-none-any.whl", hash = "sha256:eada68dfd52b3b81191827601e2a0c3fa12540c818534b630ddc5355769c3995", size = 252349, upload-time = "2026-06-25T23:38:52.946Z" }, +] + [[package]] name = "gql" version = "4.0.0" @@ -864,6 +905,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/67/8a/a342b2f0251f3dac4ca17618265d93bf244a2a4d089126e81e4c1056ac50/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb00b6d26db67a05fe3e12c76edc75f32077fb51deed13822dc648fa373bc19", size = 343768, upload-time = "2026-02-02T12:37:55.055Z" }, ] +[[package]] +name = "jmespath" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, +] + [[package]] name = "jsonschema" version = "4.23.0" @@ -1465,6 +1515,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, ] +[[package]] +name = "pyasn1" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/5f/6583902b6f79b399c9c40674ac384fd9cd77805f9e6205075f828ef11fb2/pyasn1-0.6.3.tar.gz", hash = "sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf", size = 148685, upload-time = "2026-03-17T01:06:53.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/a0/7d793dce3fa811fe047d6ae2431c672364b462850c6235ae306c0efd025f/pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde", size = 83997, upload-time = "2026-03-17T01:06:52.036Z" }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, +] + [[package]] name = "pycparser" version = "3.0" @@ -1680,6 +1751,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1", size = 16930, upload-time = "2026-05-26T09:56:02.576Z" }, ] +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + [[package]] name = "python-discovery" version = "1.2.0" @@ -2018,6 +2101,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8f/e8/726643a3ea68c727da31570bde48c7a10f1aa60eddd628d94078fec586ff/ruff-0.15.7-py3-none-win_arm64.whl", hash = "sha256:18e8d73f1c3fdf27931497972250340f92e8c861722161a9caeb89a58ead6ed2", size = 11023304, upload-time = "2026-03-19T16:26:51.669Z" }, ] +[[package]] +name = "s3transfer" +version = "0.19.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/94/dcdaeb1713cab9c84def276cfac7388b17c7d9855bbcfe88d77e4dbafd44/s3transfer-0.19.0.tar.gz", hash = "sha256:ce436931687addc4c1712d52d40b32f53e88315723f107ffa20ba82b05a0f685", size = 165171, upload-time = "2026-06-16T19:44:51.599Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/5f/4c174edad94f82de888ac00a5ddd8d07b35609b6c94f0bdf4d74af57703e/s3transfer-0.19.0-py3-none-any.whl", hash = "sha256:777cc2415536f1debadb5c2ef7779275d0fc0fe0e042411cdd6caebeb2685262", size = 90101, upload-time = "2026-06-16T19:44:50.439Z" }, +] + [[package]] name = "setuptools" version = "82.0.1" @@ -2036,6 +2131,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, ] +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + [[package]] name = "sniffio" version = "1.3.1" @@ -2096,6 +2200,14 @@ dependencies = [ { name = "textual" }, ] +[package.optional-dependencies] +bedrock = [ + { name = "boto3" }, +] +vertex = [ + { name = "google-auth" }, +] + [package.dev-dependencies] dev = [ { name = "bandit" }, @@ -2110,9 +2222,11 @@ dev = [ [package.metadata] requires-dist = [ + { name = "boto3", marker = "extra == 'bedrock'", specifier = ">=1.28.0" }, { name = "caido-sdk-client", specifier = ">=0.2.0" }, { name = "cvss", specifier = ">=3.2" }, { name = "docker", specifier = ">=7.1.0" }, + { name = "google-auth", marker = "extra == 'vertex'", specifier = ">=2.0.0" }, { name = "openai-agents", extras = ["litellm"], specifier = "==0.14.6" }, { name = "pydantic", specifier = ">=2.11.3" }, { name = "pydantic-settings", specifier = ">=2.13.0" }, @@ -2120,6 +2234,7 @@ requires-dist = [ { name = "rich" }, { name = "textual", specifier = ">=6.0.0" }, ] +provides-extras = ["vertex", "bedrock"] [package.metadata.requires-dev] dev = [