mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-27 01:22:18 +00:00
* fix(secrets): verify provider API request and payload contracts * wip * fix(secrets): unify backend reads and route secret resolution * feat(secrets): bind built-in managers to retained Rust backends * refactor(secrets): centralize catalog dispatch and native binding * test(secrets): split provider integration tests * refactor(secrets): enforce cache and rotation contracts * test(secrets): stub parent packages in failing resolver fixture Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * refactor(secrets): pass manager settings through the interop boundary Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(secrets): align cloud KMS auth and harden provider reads * ci(rust): raise native wheel size gate to 40 MB for secrets backends Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(proxy): treat unset google kms flag as disabled like the old loader Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(secrets): preserve certificate credentials and disabled KMS flags * test(secrets): cover certificate validation and bounded auth retries * test(secrets): cover Python dispatch without the native extension * test(proxy): skip legacy secret manager cases when the optional SDK is missing Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(secrets): port Python parity tests and preserve provider behavior * fix(secrets): store the captured native config without setattr to satisfy the strict lint budget Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(secrets): preserve missing Azure manager values Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(secrets): pin typed values and recovery failure precedence * refactor(secrets): organize provider internals and behavioral test suites * refactor(secrets): simplify recovery and isolate Python compatibility * fix(secrets): distinguish Azure callback absence from HTTP not found * fix(secrets): preserve Python AWS read results at the bridge * fix(secrets): route public reads through the native catalog bridge * fix(secrets): keep JSON selection outside the bridge Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(secrets): preserve provider JSON reads at the bridge Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(secrets): preserve Python primary JSON semantics Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(secrets): preserve CyberArk mutation behavior through the native bridge * docs(secrets): record public API replacement gaps * refactor(secrets): share Vault write payload preparation * feat(secrets): route Vault mutations through the native bridge * fix(secrets): preserve typed Vault rotation failures * refactor(secrets): move Python dispatch into bridge * refactor(secrets): move CyberArk Python policy into bridge * refactor(secrets): move Vault Python policy into bridge * test(secrets): assert Vault rotation request paths * fix(secrets): keep bridge JSON interop centralized Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Yujong Lee <yujong@berri.ai> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
221 lines
9.1 KiB
Python
221 lines
9.1 KiB
Python
from typing import Final
|
|
|
|
import httpx
|
|
import pytest
|
|
|
|
import litellm
|
|
from litellm.integrations.custom_secret_manager import CustomSecretManager
|
|
from litellm.llms.custom_httpx.http_handler import default_user_agent
|
|
from litellm.rust_bridge import catalog, settings
|
|
from litellm.rust_bridge.catalog import SecretManagerRule
|
|
from litellm.rust_bridge.configuration import Rollout
|
|
from litellm.secret_managers.main import get_secret_str
|
|
from litellm.types.secret_managers.main import KeyManagementSettings, KeyManagementSystem
|
|
|
|
|
|
def test_url_policy_reads_the_litellm_globals(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setattr(litellm, "user_url_validation", False)
|
|
monkeypatch.setattr(litellm, "user_url_allowed_hosts", ["docs.internal:8443"])
|
|
|
|
assert settings.url_policy() == settings.UrlPolicy(
|
|
user_url_validation=False,
|
|
user_url_allowed_hosts=["docs.internal:8443"],
|
|
)
|
|
|
|
|
|
def test_http_settings_reads_the_litellm_globals(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setattr(litellm, "ssl_verify", "/etc/ssl/corp.pem")
|
|
monkeypatch.setattr(litellm, "ssl_certificate", "/etc/ssl/client.pem")
|
|
monkeypatch.setattr(litellm, "ssl_security_level", "DEFAULT@SECLEVEL=1")
|
|
monkeypatch.setattr(litellm, "ssl_ecdh_curve", "X25519")
|
|
monkeypatch.setattr(litellm, "force_ipv4", True)
|
|
monkeypatch.setattr(litellm, "http2", True)
|
|
monkeypatch.setattr(litellm, "aiohttp_trust_env", True)
|
|
monkeypatch.setattr(litellm, "disable_aiohttp_trust_env", True)
|
|
monkeypatch.setattr(litellm, "disable_aiohttp_transport", True)
|
|
|
|
assert settings.http_settings() == settings.HttpSettings(
|
|
ssl_verify="/etc/ssl/corp.pem",
|
|
ssl_certificate="/etc/ssl/client.pem",
|
|
ssl_security_level="DEFAULT@SECLEVEL=1",
|
|
ssl_ecdh_curve="X25519",
|
|
force_ipv4=True,
|
|
http2=True,
|
|
aiohttp_trust_env=True,
|
|
disable_aiohttp_trust_env=True,
|
|
disable_aiohttp_transport=True,
|
|
user_agent=default_user_agent(),
|
|
)
|
|
|
|
|
|
def test_http_settings_ignores_environment_overrides(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setenv("LITELLM_USER_AGENT", "operator/1")
|
|
monkeypatch.setenv("SSL_VERIFY", "false")
|
|
monkeypatch.setattr(litellm, "ssl_verify", True)
|
|
|
|
result: Final = settings.http_settings()
|
|
|
|
assert result.user_agent == default_user_agent()
|
|
assert result.ssl_verify is True
|
|
|
|
|
|
class _VaultSecrets(CustomSecretManager):
|
|
def __init__(self, secrets: dict[str, str]) -> None:
|
|
super().__init__(secret_manager_name="rust_bridge_settings_test")
|
|
self.secrets = secrets
|
|
|
|
async def async_read_secret(
|
|
self,
|
|
secret_name: str,
|
|
optional_params: dict[str, object] | None = None,
|
|
timeout: float | httpx.Timeout | None = None,
|
|
) -> str | None:
|
|
return self.secrets.get(secret_name)
|
|
|
|
def sync_read_secret(
|
|
self,
|
|
secret_name: str,
|
|
optional_params: dict[str, object] | None = None,
|
|
timeout: float | httpx.Timeout | None = None,
|
|
) -> str | None:
|
|
return self.secrets.get(secret_name)
|
|
|
|
|
|
_RUST_FOR_CUSTOM: Final = (
|
|
SecretManagerRule(Rollout.RUST_REQUIRED, systems=frozenset({KeyManagementSystem.CUSTOM.value})),
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("access_mode", "readable"),
|
|
[("read_only", True), ("read_and_write", True), ("write_only", False)],
|
|
)
|
|
def test_secret_manager_is_readable_only_when_litellm_would_read_secrets_from_it(
|
|
monkeypatch: pytest.MonkeyPatch, access_mode: str, readable: bool
|
|
) -> None:
|
|
monkeypatch.setenv("MISTRAL_API_KEY", "env-key")
|
|
monkeypatch.setattr(litellm, "secret_manager_client", _VaultSecrets({"MISTRAL_API_KEY": "vault-key"}))
|
|
monkeypatch.setattr(litellm, "_key_management_system", KeyManagementSystem.CUSTOM)
|
|
monkeypatch.setattr(litellm, "_key_management_settings", KeyManagementSettings(access_mode=access_mode))
|
|
|
|
assert settings.secret_manager(rules=()) == settings.SecretManager(readable=readable, native=False)
|
|
assert (get_secret_str("MISTRAL_API_KEY") == "vault-key") is readable
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("system", "access_mode", "rules", "native"),
|
|
[
|
|
(KeyManagementSystem.CUSTOM, "read_only", _RUST_FOR_CUSTOM, True),
|
|
(KeyManagementSystem.CUSTOM, "read_only", (), False),
|
|
(
|
|
KeyManagementSystem.CUSTOM,
|
|
"read_only",
|
|
(SecretManagerRule(Rollout.PYTHON_ONLY, systems=frozenset({KeyManagementSystem.CUSTOM.value})),),
|
|
False,
|
|
),
|
|
(KeyManagementSystem.CUSTOM, "write_only", _RUST_FOR_CUSTOM, False),
|
|
(None, "read_only", _RUST_FOR_CUSTOM, False),
|
|
(KeyManagementSystem.AWS_SECRET_MANAGER, "read_only", _RUST_FOR_CUSTOM, False),
|
|
],
|
|
)
|
|
def test_secret_manager_is_native_only_when_the_rules_select_rust_for_its_system(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
system: KeyManagementSystem | None,
|
|
access_mode: str,
|
|
rules: catalog.Rules,
|
|
native: bool,
|
|
) -> None:
|
|
monkeypatch.setattr(litellm, "secret_manager_client", _VaultSecrets({}))
|
|
monkeypatch.setattr(litellm, "_key_management_system", system)
|
|
monkeypatch.setattr(litellm, "_key_management_settings", KeyManagementSettings(access_mode=access_mode))
|
|
|
|
assert settings.secret_manager(rules=rules) == settings.SecretManager(
|
|
readable=access_mode != "write_only", native=native
|
|
)
|
|
|
|
|
|
def test_secret_manager_is_not_readable_without_a_client(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setattr(litellm, "secret_manager_client", None)
|
|
|
|
assert settings.secret_manager(rules=_RUST_FOR_CUSTOM) == settings.SecretManager(readable=False, native=False)
|
|
|
|
|
|
def test_secret_manager_projects_custom_settings(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
manager_settings: Final = KeyManagementSettings(
|
|
access_mode="read_and_write",
|
|
hosted_keys=["MISTRAL_API_KEY"],
|
|
primary_secret_name="primary",
|
|
aws_region_name="us-east-1",
|
|
)
|
|
client: Final = _VaultSecrets({"MISTRAL_API_KEY": "vault-key"})
|
|
monkeypatch.setattr(litellm, "secret_manager_client", client)
|
|
monkeypatch.setattr(litellm, "_key_management_system", KeyManagementSystem.CUSTOM)
|
|
monkeypatch.setattr(litellm, "_key_management_settings", manager_settings)
|
|
|
|
assert settings.secret_manager_binding() == settings.SecretManagerBinding(
|
|
system="custom",
|
|
access_mode="read_and_write",
|
|
hosted_keys=["MISTRAL_API_KEY"],
|
|
primary_secret_name="primary",
|
|
store_virtual_keys=manager_settings.store_virtual_keys,
|
|
prefix_for_stored_virtual_keys=manager_settings.prefix_for_stored_virtual_keys,
|
|
kms_key_id=manager_settings.kms_key_id,
|
|
custom_secret_manager=manager_settings.custom_secret_manager,
|
|
aws_region_name="us-east-1",
|
|
aws_role_name=manager_settings.aws_role_name,
|
|
aws_session_name=manager_settings.aws_session_name,
|
|
aws_external_id=manager_settings.aws_external_id,
|
|
aws_profile_name=manager_settings.aws_profile_name,
|
|
aws_web_identity_token=manager_settings.aws_web_identity_token,
|
|
aws_sts_endpoint=manager_settings.aws_sts_endpoint,
|
|
replica_regions=manager_settings.replica_regions,
|
|
client=client,
|
|
settings_object=manager_settings,
|
|
)
|
|
|
|
|
|
def test_secret_manager_without_a_client_has_no_system(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setattr(litellm, "secret_manager_client", None)
|
|
|
|
assert settings.secret_manager_binding().system is None
|
|
|
|
|
|
def test_secret_manager_uses_key_management_defaults(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setattr(litellm, "secret_manager_client", None)
|
|
monkeypatch.setattr(litellm, "_key_management_settings", None)
|
|
|
|
defaults: Final = KeyManagementSettings()
|
|
result: Final = settings.secret_manager_binding()
|
|
|
|
assert result == settings.SecretManagerBinding(
|
|
system=None,
|
|
access_mode=defaults.access_mode,
|
|
hosted_keys=defaults.hosted_keys,
|
|
primary_secret_name=defaults.primary_secret_name,
|
|
store_virtual_keys=defaults.store_virtual_keys,
|
|
prefix_for_stored_virtual_keys=defaults.prefix_for_stored_virtual_keys,
|
|
kms_key_id=defaults.kms_key_id,
|
|
custom_secret_manager=defaults.custom_secret_manager,
|
|
aws_region_name=defaults.aws_region_name,
|
|
aws_role_name=defaults.aws_role_name,
|
|
aws_session_name=defaults.aws_session_name,
|
|
aws_external_id=defaults.aws_external_id,
|
|
aws_profile_name=defaults.aws_profile_name,
|
|
aws_web_identity_token=defaults.aws_web_identity_token,
|
|
aws_sts_endpoint=defaults.aws_sts_endpoint,
|
|
replica_regions=defaults.replica_regions,
|
|
client=None,
|
|
settings_object=None,
|
|
)
|
|
|
|
|
|
def test_provider_defaults_read_the_litellm_globals(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setattr(litellm, "vertex_project", "configured-project")
|
|
monkeypatch.setattr(litellm, "vertex_location", "europe-west4")
|
|
monkeypatch.setattr(litellm, "enable_azure_ad_token_refresh", True)
|
|
|
|
assert settings.provider_defaults() == settings.ProviderDefaults(
|
|
vertex_project="configured-project",
|
|
vertex_location="europe-west4",
|
|
enable_azure_ad_token_refresh=True,
|
|
)
|