diff --git a/CHANGELOG.md b/CHANGELOG.md index dfba370d7..6b0690211 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased ### Fixed +- `--backend bedrock` now fails fast with an actionable error when temporary + AWS credentials (`AWS_SESSION_TOKEN`) are used but botocore is not installed + (e.g. the slim default Docker image). litellm's session-token auth path + imports botocore, so the missing dependency previously surfaced only at + request time as a misleading `authentication_error: No module named + 'botocore'`. The proxy now tells the user to install the `bedrock` extra up + front ([#1551](https://github.com/headroomlabs-ai/headroom/issues/1551)). - Content detection no longer crashes the proxy on text containing an orphaned `+++ ` target line with no preceding `--- ` source line (common in `set -x` xtrace output and partial diffs). The bundled `unidiff` 0.4.0 parser diff --git a/headroom/backends/litellm.py b/headroom/backends/litellm.py index ff7cb5e27..7a410ed79 100644 --- a/headroom/backends/litellm.py +++ b/headroom/backends/litellm.py @@ -12,8 +12,10 @@ LiteLLM handles all the auth and format translation internally. from __future__ import annotations +import importlib.util import json import logging +import os import uuid from collections.abc import AsyncIterator from dataclasses import dataclass, field @@ -423,6 +425,19 @@ class LiteLLMBackend(Backend): # For Bedrock, fetch model map dynamically from AWS API if provider == "bedrock": + # litellm takes the botocore-backed `_auth_with_aws_session_token` + # path as soon as temporary credentials (AWS_SESSION_TOKEN) are + # present. botocore is an optional dependency (the `bedrock` + # extra); when it is absent — as in the slim default Docker image — + # the failure only surfaces at request time as a misleading + # `authentication_error: No module named 'botocore'` (#1551). Fail + # fast at startup with an actionable message instead. + if os.environ.get("AWS_SESSION_TOKEN") and importlib.util.find_spec("botocore") is None: + raise ImportError( + "Bedrock with temporary credentials (AWS_SESSION_TOKEN) requires " + "botocore, which is not installed. Install the bedrock extra: " + "pip install 'headroom-ai[bedrock]' (or pip install botocore)." + ) self._model_map = _fetch_bedrock_inference_profiles(region) litellm.set_verbose = False # Reduce noise else: diff --git a/tests/test_backends/test_bedrock_botocore_preflight.py b/tests/test_backends/test_bedrock_botocore_preflight.py new file mode 100644 index 000000000..acfbb5c8c --- /dev/null +++ b/tests/test_backends/test_bedrock_botocore_preflight.py @@ -0,0 +1,49 @@ +"""Preflight guard for Bedrock + temporary credentials without botocore (#1551). + +litellm takes the botocore-backed ``_auth_with_aws_session_token`` path as +soon as ``AWS_SESSION_TOKEN`` is present. botocore ships only with the +``bedrock`` extra, so on a slim install the failure used to surface at request +time as a misleading ``authentication_error: No module named 'botocore'``. +``LiteLLMBackend`` now fails fast at startup with an actionable message. +""" + +from __future__ import annotations + +from unittest.mock import patch + +import pytest + +from headroom.backends import litellm as litellm_mod +from headroom.backends.litellm import LiteLLMBackend + + +def test_bedrock_session_token_without_botocore_raises_actionable( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setattr(litellm_mod, "LITELLM_AVAILABLE", True) + monkeypatch.setenv("AWS_SESSION_TOKEN", "tmp-session-token") + + # Simulate botocore not installed (the slim default-image case). + with patch("importlib.util.find_spec", return_value=None): + with pytest.raises(ImportError, match="botocore") as exc: + LiteLLMBackend(provider="bedrock", region="us-west-2") + + # The message must point at the fix, not just name the missing module. + assert "headroom-ai[bedrock]" in str(exc.value) + + +def test_bedrock_without_session_token_does_not_trip_botocore_guard( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Static-credential Bedrock users don't need botocore — guard stays quiet.""" + monkeypatch.setattr(litellm_mod, "LITELLM_AVAILABLE", True) + monkeypatch.delenv("AWS_SESSION_TOKEN", raising=False) + + # botocore missing, but no session token → guard must NOT raise. Stub the + # downstream model-map fetch and litellm handle so construction completes. + with patch("importlib.util.find_spec", return_value=None): + with patch.object(litellm_mod, "_fetch_bedrock_inference_profiles", return_value={}): + with patch.object(litellm_mod, "litellm", create=True): + backend = LiteLLMBackend(provider="bedrock", region="us-west-2") + + assert backend.provider == "bedrock"