mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-09 08:18:12 +00:00
* chore(typing): validate provider payloads at the boundary to clear 464 basedpyright Any errors Replaces untyped reads of decoded JSON in 48 provider, guardrail and proxy modules with one pydantic validation at the point the payload enters the module, so every value read afterwards carries a real type. reportAny 6111 -> 5663 and reportExplicitAny 1416 -> 1400. No basedpyright, ruff-strict or type-discipline rule is higher repo-wide or in any single file, and no cast, ignore or suppression marker was added. * chore(typing): replace 47 Any annotations that every caller already satisfies with object Each annotation here is a parameter or return type whose value is only stored, compared, logged or forwarded, so object describes it exactly and no function body changes. A site was kept only if no basedpyright, ruff-strict or type-discipline rule rose in any file that references it. reportExplicitAny 1400 -> 1353 and reportAny 5663 -> 5655. * chore(typing): type upstream responses and loose annotations across providers, proxy and integrations Second pass of the Any reduction. Decoded upstream JSON is validated once at the provider boundary and read through typed views afterwards, annotations that every caller already satisfies move from Any to a concrete type, and the nine search transformations validate each result item on its own so error text no longer carries the item index. No cast, no type: ignore, no suppression marker and no budget file edit * ci(unit): run the brave, google_pse, nlp_cloud and opensandbox tests in the providers shard
326 lines
12 KiB
Python
326 lines
12 KiB
Python
import base64
|
|
import binascii
|
|
import json
|
|
from dataclasses import dataclass
|
|
from typing import Final
|
|
|
|
import google.auth
|
|
import httpx
|
|
import litellm
|
|
import litellm.proxy.proxy_server
|
|
import pytest
|
|
import respx
|
|
from google.auth.credentials import Credentials
|
|
from pydantic import ValidationError
|
|
|
|
from litellm.secret_managers.google_secret_manager import GoogleSecretManager
|
|
from litellm.secret_managers.main import get_secret
|
|
from litellm.types.secret_managers.main import KeyManagementSettings
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class _CachedVertexCredentials:
|
|
token: str
|
|
quota_project_id: str | None
|
|
expired: bool = False
|
|
|
|
def refresh(self, request: object) -> None:
|
|
return None
|
|
|
|
|
|
def _google_secret_manager(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
project_id: str,
|
|
) -> GoogleSecretManager:
|
|
monkeypatch.setenv("GOOGLE_SECRET_MANAGER_PROJECT_ID", project_id)
|
|
monkeypatch.setattr("litellm.proxy.proxy_server.premium_user", True)
|
|
secret_manager: Final = GoogleSecretManager()
|
|
credentials: Final = _CachedVertexCredentials(
|
|
token="test-gsm-token",
|
|
quota_project_id=project_id,
|
|
)
|
|
vertex_chat_completion: Final = litellm.vertex_chat_completion
|
|
monkeypatch.setitem(
|
|
vertex_chat_completion._credentials_project_mapping,
|
|
(None, project_id),
|
|
(credentials, project_id),
|
|
)
|
|
return secret_manager
|
|
|
|
|
|
def test_google_secret_manager_decodes_secret_and_requests_latest_version(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
project_id: Final = "test-secret-project"
|
|
secret_manager: Final = _google_secret_manager(monkeypatch, project_id)
|
|
secret_url: Final = (
|
|
f"https://secretmanager.googleapis.com/v1/projects/{project_id}/secrets/OPENAI_API_KEY/versions/latest:access"
|
|
)
|
|
encoded_secret: Final = base64.b64encode(b"anything").decode("ascii")
|
|
upstream: Final[respx.MockRouter]
|
|
|
|
with respx.mock(assert_all_called=True) as upstream:
|
|
secret_route: Final = upstream.get(secret_url).respond(
|
|
200,
|
|
json={"payload": {"data": encoded_secret}},
|
|
)
|
|
|
|
result: Final = secret_manager.get_secret_from_google_secret_manager("OPENAI_API_KEY")
|
|
|
|
assert result == "anything"
|
|
assert secret_route.called
|
|
assert len(upstream.calls) == 1
|
|
assert str(upstream.calls.last.request.url) == secret_url
|
|
assert upstream.calls.last.request.headers["Authorization"] == "Bearer test-gsm-token"
|
|
|
|
|
|
def test_google_secret_manager_returns_cached_values_without_http(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
project_id: Final = "test-secret-project"
|
|
secret_manager: Final = _google_secret_manager(monkeypatch, project_id)
|
|
secret_manager.cache.set_cache("cached-none", None)
|
|
secret_manager.cache.set_cache("cached-string", "lite-llm")
|
|
upstream: Final[respx.MockRouter]
|
|
|
|
with respx.mock() as upstream:
|
|
missing_value: Final = secret_manager.get_secret_from_google_secret_manager("cached-none")
|
|
cached_value: Final = secret_manager.get_secret_from_google_secret_manager("cached-string")
|
|
|
|
assert missing_value is None
|
|
assert cached_value == "lite-llm"
|
|
assert upstream.calls.call_count == 0
|
|
|
|
|
|
PROJECT_ID: Final = "unit-test-project"
|
|
SECRET_NAME: Final = "UNIT_TEST_GOOGLE_SECRET"
|
|
ACCESS_URL: Final = (
|
|
f"https://secretmanager.googleapis.com/v1/projects/{PROJECT_ID}/secrets/{SECRET_NAME}/versions/latest:access"
|
|
)
|
|
SECRET_VALUE: Final = "s3cr3t-value"
|
|
ENCODED_SECRET: Final = base64.b64encode(SECRET_VALUE.encode()).decode("ascii")
|
|
|
|
|
|
class _StaticCredentials(Credentials):
|
|
def refresh(self, request: object) -> None:
|
|
self.token = "unit-test-token"
|
|
|
|
|
|
def _default_credentials(scopes: object = None) -> tuple[Credentials, str]:
|
|
return _StaticCredentials(), PROJECT_ID
|
|
|
|
|
|
def _access_response(body: object) -> httpx.Response:
|
|
return httpx.Response(200, content=json.dumps(body).encode())
|
|
|
|
|
|
@pytest.fixture
|
|
def manager(monkeypatch: pytest.MonkeyPatch) -> GoogleSecretManager:
|
|
monkeypatch.setattr(litellm.proxy.proxy_server, "premium_user", True)
|
|
monkeypatch.setattr(litellm, "secret_manager_client", None)
|
|
monkeypatch.setattr(litellm, "_key_management_system", None)
|
|
monkeypatch.setattr(litellm, "_key_management_settings", KeyManagementSettings())
|
|
monkeypatch.setattr(litellm.vertex_chat_completion, "_credentials_project_mapping", {})
|
|
monkeypatch.setattr(google.auth, "default", _default_credentials)
|
|
monkeypatch.setenv("GOOGLE_SECRET_MANAGER_PROJECT_ID", PROJECT_ID)
|
|
monkeypatch.delenv("GOOGLE_SECRET_MANAGER_ALWAYS_READ_SECRET_MANAGER", raising=False)
|
|
monkeypatch.delenv("GOOGLE_SECRET_MANAGER_REFRESH_INTERVAL", raising=False)
|
|
monkeypatch.delenv("GCS_PATH_SERVICE_ACCOUNT", raising=False)
|
|
monkeypatch.delenv("GCS_MOCK", raising=False)
|
|
monkeypatch.delenv(SECRET_NAME, raising=False)
|
|
return GoogleSecretManager()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("body", "expected"),
|
|
[
|
|
({"payload": {"data": ENCODED_SECRET}}, SECRET_VALUE),
|
|
(
|
|
{
|
|
"name": f"projects/123456/secrets/{SECRET_NAME}/versions/7",
|
|
"payload": {"data": ENCODED_SECRET, "dataCrc32c": "2932599711"},
|
|
},
|
|
SECRET_VALUE,
|
|
),
|
|
({"payload": {"data": ENCODED_SECRET, "labels": {"nested": [1, None]}}, "replication": [1, 2]}, SECRET_VALUE),
|
|
({"payload": {"data": "czNj\ncjN0 LXZhbHVl"}}, SECRET_VALUE),
|
|
({"payload": {"data": base64.b64encode("clé ☃".encode()).decode("ascii")}}, "clé ☃"),
|
|
({"payload": {"data": ""}}, ""),
|
|
],
|
|
)
|
|
@respx.mock
|
|
def test_secret_is_decoded_from_the_access_response(manager: GoogleSecretManager, body: object, expected: str) -> None:
|
|
route: Final = respx.get(ACCESS_URL).mock(return_value=_access_response(body))
|
|
|
|
secret: Final = manager.get_secret_from_google_secret_manager(SECRET_NAME)
|
|
|
|
assert secret == expected
|
|
assert type(secret) is str
|
|
assert manager.cache.cache_dict[SECRET_NAME] is secret
|
|
assert route.calls.last.request.headers["Authorization"] == "Bearer unit-test-token"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"body",
|
|
[
|
|
{},
|
|
{"name": f"projects/123456/secrets/{SECRET_NAME}/versions/7"},
|
|
{"payload": {}},
|
|
{"payload": {"dataCrc32c": "2932599711"}},
|
|
{"payload": {"data": None}},
|
|
],
|
|
)
|
|
@respx.mock
|
|
def test_response_without_secret_data_reports_not_found_and_caches_the_miss(
|
|
manager: GoogleSecretManager, body: object
|
|
) -> None:
|
|
respx.get(ACCESS_URL).mock(return_value=_access_response(body))
|
|
|
|
with pytest.raises(ValueError, match=rf"^secret {SECRET_NAME} not found in Google Secret Manager$") as raised:
|
|
manager.get_secret_from_google_secret_manager(SECRET_NAME)
|
|
|
|
assert type(raised.value) is ValueError
|
|
assert manager.cache.cache_dict[SECRET_NAME] is None
|
|
assert manager.get_secret_from_google_secret_manager(SECRET_NAME) is None
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"body",
|
|
[
|
|
[],
|
|
[{"payload": {"data": ENCODED_SECRET}}],
|
|
[["payload", {"data": ENCODED_SECRET}]],
|
|
ENCODED_SECRET,
|
|
7,
|
|
None,
|
|
True,
|
|
{"payload": None},
|
|
{"payload": []},
|
|
{"payload": [["data", ENCODED_SECRET]]},
|
|
{"payload": ENCODED_SECRET},
|
|
{"payload": ""},
|
|
{"payload": 0},
|
|
{"payload": False},
|
|
],
|
|
)
|
|
@respx.mock
|
|
def test_response_that_is_not_a_json_object_is_rejected_without_caching(
|
|
manager: GoogleSecretManager, body: object
|
|
) -> None:
|
|
respx.get(ACCESS_URL).mock(return_value=_access_response(body))
|
|
|
|
with pytest.raises(ValidationError) as raised:
|
|
manager.get_secret_from_google_secret_manager(SECRET_NAME)
|
|
|
|
assert [error["type"] for error in raised.value.errors()] == ["dict_type"]
|
|
assert ENCODED_SECRET not in str(raised.value)
|
|
assert SECRET_NAME not in manager.cache.cache_dict
|
|
|
|
|
|
@pytest.mark.parametrize("data", [5, 0, 1.5, True, False, [], [ENCODED_SECRET], {}, {"value": ENCODED_SECRET}])
|
|
@respx.mock
|
|
def test_secret_data_that_is_not_text_is_rejected_without_caching(manager: GoogleSecretManager, data: object) -> None:
|
|
respx.get(ACCESS_URL).mock(return_value=_access_response({"payload": {"data": data}}))
|
|
|
|
with pytest.raises(ValidationError) as raised:
|
|
manager.get_secret_from_google_secret_manager(SECRET_NAME)
|
|
|
|
assert [error["type"] for error in raised.value.errors()] == ["string_type"]
|
|
assert ENCODED_SECRET not in str(raised.value)
|
|
assert SECRET_NAME not in manager.cache.cache_dict
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"body",
|
|
[
|
|
[{"payload": {"data": ENCODED_SECRET}}] * 403 + [7],
|
|
{"payload": [{"data": ENCODED_SECRET}] * 403 + [None]},
|
|
{"payload": {"data": [ENCODED_SECRET] * 403 + [429]}},
|
|
{"payload": {"data": {str(position): "Request timed out" for position in range(403, 430)}}},
|
|
],
|
|
)
|
|
@respx.mock
|
|
def test_rejection_of_a_long_response_names_no_position_or_content_from_it(
|
|
manager: GoogleSecretManager, body: object
|
|
) -> None:
|
|
respx.get(ACCESS_URL).mock(return_value=_access_response(body))
|
|
|
|
with pytest.raises(ValidationError) as raised:
|
|
manager.get_secret_from_google_secret_manager(SECRET_NAME)
|
|
|
|
message: Final = str(raised.value)
|
|
assert [error["loc"] for error in raised.value.errors()] == [()]
|
|
assert [fragment for fragment in ("403", "429", "Request timed out", ENCODED_SECRET) if fragment in message] == []
|
|
assert SECRET_NAME not in manager.cache.cache_dict
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("data", "error_class"),
|
|
[
|
|
("abc", binascii.Error),
|
|
(base64.b64encode(b"\xff\xfe").decode("ascii"), UnicodeDecodeError),
|
|
("éééé", ValueError),
|
|
],
|
|
)
|
|
@respx.mock
|
|
def test_undecodable_secret_text_keeps_raising_the_decoder_error(
|
|
manager: GoogleSecretManager, data: str, error_class: type[Exception]
|
|
) -> None:
|
|
respx.get(ACCESS_URL).mock(return_value=_access_response({"payload": {"data": data}}))
|
|
|
|
with pytest.raises(error_class) as raised:
|
|
manager.get_secret_from_google_secret_manager(SECRET_NAME)
|
|
|
|
assert type(raised.value) is error_class
|
|
assert SECRET_NAME not in manager.cache.cache_dict
|
|
|
|
|
|
@pytest.mark.usefixtures("manager")
|
|
@respx.mock
|
|
def test_get_secret_reads_through_the_configured_manager() -> None:
|
|
respx.get(ACCESS_URL).mock(return_value=_access_response({"payload": {"data": ENCODED_SECRET}}))
|
|
|
|
assert get_secret(SECRET_NAME) == SECRET_VALUE
|
|
|
|
|
|
@pytest.mark.usefixtures("manager")
|
|
@pytest.mark.parametrize(
|
|
"body",
|
|
[
|
|
[],
|
|
{"payload": None},
|
|
{"payload": ENCODED_SECRET},
|
|
{"payload": {"data": 5}},
|
|
{"payload": {"data": [ENCODED_SECRET]}},
|
|
{"payload": {}},
|
|
[{"payload": {"data": ENCODED_SECRET}}] * 403 + [7],
|
|
{"payload": {"data": [ENCODED_SECRET] * 403 + [429]}},
|
|
{"payload": "Request timed out"},
|
|
],
|
|
)
|
|
@respx.mock
|
|
def test_get_secret_falls_back_to_the_environment_when_the_response_is_unusable(
|
|
monkeypatch: pytest.MonkeyPatch, body: object
|
|
) -> None:
|
|
monkeypatch.setenv(SECRET_NAME, "from-environment")
|
|
respx.get(ACCESS_URL).mock(return_value=_access_response(body))
|
|
|
|
assert get_secret(SECRET_NAME) == "from-environment"
|
|
|
|
|
|
@pytest.mark.usefixtures("manager")
|
|
@pytest.mark.parametrize(
|
|
"body",
|
|
[
|
|
[{"payload": {"data": ENCODED_SECRET}}] * 403 + [7],
|
|
{"payload": None},
|
|
{"payload": {"data": [ENCODED_SECRET] * 403 + [429]}},
|
|
{"payload": {"data": 429}},
|
|
{"payload": {}},
|
|
],
|
|
)
|
|
@respx.mock
|
|
def test_get_secret_returns_none_when_the_response_is_unusable_and_the_environment_has_no_value(body: object) -> None:
|
|
respx.get(ACCESS_URL).mock(return_value=_access_response(body))
|
|
|
|
assert get_secret(SECRET_NAME) is None
|