mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-07 08:26:10 +00:00
fix(proxy): keep WIF secret pointers unresolved when expanding os.environ references
This commit is contained in:
parent
142092ae87
commit
f2240df6da
6 changed files with 109 additions and 5 deletions
|
|
@ -698,6 +698,7 @@ from litellm.types.router import (
|
|||
RouterGeneralSettings,
|
||||
RoutingPlugin,
|
||||
SearchToolTypedDict,
|
||||
holds_secret_pointer,
|
||||
updateDeployment,
|
||||
)
|
||||
from litellm.types.router import ModelInfo as RouterModelInfo
|
||||
|
|
@ -4497,7 +4498,7 @@ class ProxyConfig:
|
|||
if isinstance(item, dict):
|
||||
item = self._check_for_os_environ_vars(config=item, depth=depth + 1, max_depth=max_depth)
|
||||
# if the value is a string and starts with "os.environ/" - then it's an environment variable
|
||||
elif isinstance(value, str) and value.startswith("os.environ/"):
|
||||
elif isinstance(value, str) and value.startswith("os.environ/") and not holds_secret_pointer(key):
|
||||
resolved = get_secret(value)
|
||||
if resolved is None and secret_manager_would_be_consulted(value):
|
||||
verbose_proxy_logger.warning("%s is absent from the configured secret manager", value)
|
||||
|
|
@ -5530,7 +5531,7 @@ class ProxyConfig:
|
|||
for model in model_list:
|
||||
### LOAD FROM os.environ/ ###
|
||||
for k, v in model["litellm_params"].items():
|
||||
if isinstance(v, str) and v.startswith("os.environ/"):
|
||||
if isinstance(v, str) and v.startswith("os.environ/") and not holds_secret_pointer(k):
|
||||
model["litellm_params"][k] = get_secret(v)
|
||||
validate_deployment_max_agentic_loops(model)
|
||||
validate_deployment_complexity_router_placement(model)
|
||||
|
|
@ -5926,7 +5927,7 @@ class ProxyConfig:
|
|||
for model in model_list:
|
||||
### LOAD FROM os.environ/ ###
|
||||
for k, v in model["litellm_params"].items():
|
||||
if isinstance(v, str) and v.startswith("os.environ/"):
|
||||
if isinstance(v, str) and v.startswith("os.environ/") and not holds_secret_pointer(k):
|
||||
model["litellm_params"][k] = get_secret(v)
|
||||
|
||||
## check if they have model-id's ##
|
||||
|
|
@ -5954,7 +5955,11 @@ class ProxyConfig:
|
|||
return value
|
||||
|
||||
decrypted_value: Final = decrypt_value_helper(value=value, key=key, return_original_value=True)
|
||||
if isinstance(decrypted_value, str) and decrypted_value.startswith("os.environ/"):
|
||||
if (
|
||||
isinstance(decrypted_value, str)
|
||||
and decrypted_value.startswith("os.environ/")
|
||||
and not holds_secret_pointer(key)
|
||||
):
|
||||
return get_secret(decrypted_value)
|
||||
return decrypted_value
|
||||
|
||||
|
|
|
|||
|
|
@ -217,6 +217,7 @@ from litellm.types.router import (
|
|||
RoutingStrategy,
|
||||
SearchToolTypedDict,
|
||||
TaggedPreRoutingStrategy,
|
||||
holds_secret_pointer,
|
||||
)
|
||||
from litellm.types.services import ServiceTypes
|
||||
from litellm.types.utils import (
|
||||
|
|
@ -8757,7 +8758,7 @@ class Router:
|
|||
## check if litellm params in os.environ
|
||||
if isinstance(_litellm_params, dict):
|
||||
for k, v in _litellm_params.items():
|
||||
if isinstance(v, str) and v.startswith("os.environ/"):
|
||||
if isinstance(v, str) and v.startswith("os.environ/") and not holds_secret_pointer(k):
|
||||
_litellm_params[k] = get_secret(v)
|
||||
|
||||
_model_info: dict = model.pop("model_info", {})
|
||||
|
|
|
|||
|
|
@ -329,6 +329,17 @@ def anthropic_wif_fields_named(keys: Container[str]) -> tuple[str, ...]:
|
|||
return tuple(name for name in _anthropic_wif_litellm_params if name in keys)
|
||||
|
||||
|
||||
_ANTHROPIC_WIF_POINTER_FIELDS: Final = frozenset(
|
||||
name for name in _anthropic_wif_litellm_params if name.endswith("_ref")
|
||||
)
|
||||
|
||||
|
||||
def holds_secret_pointer(param_name: str) -> bool:
|
||||
"""A ``*_ref`` federation field is a secret POINTER the identity source dereferences at use
|
||||
time, so a loader expanding ``os.environ/`` values must leave it as written."""
|
||||
return param_name in _ANTHROPIC_WIF_POINTER_FIELDS
|
||||
|
||||
|
||||
_RESERVED_INIT_KEYS: Final = frozenset({"self", "params", "__class__"})
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -11756,3 +11756,54 @@ async def test_load_config_router_authorizes_fallback_targets_against_the_callin
|
|||
router, _, _ = await ProxyConfig().load_config(router=None, config_file_path=str(config_file))
|
||||
|
||||
assert router.fallback_access_check is router_fallback_access_check
|
||||
|
||||
|
||||
def test_resolve_db_litellm_param_keeps_wif_secret_pointers(monkeypatch):
|
||||
from litellm.proxy.proxy_server import ProxyConfig
|
||||
|
||||
monkeypatch.setenv("WIF_TEST_KC_SECRET", "kc-secret")
|
||||
proxy_config = ProxyConfig()
|
||||
|
||||
pointer = proxy_config._resolve_db_litellm_param(
|
||||
"anthropic_keycloak_client_secret_ref", "os.environ/WIF_TEST_KC_SECRET"
|
||||
)
|
||||
dereferenced = proxy_config._resolve_db_litellm_param("api_key", "os.environ/WIF_TEST_KC_SECRET")
|
||||
|
||||
assert pointer == "os.environ/WIF_TEST_KC_SECRET"
|
||||
assert dereferenced == "kc-secret"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_config_keeps_wif_secret_pointers_on_config_models(tmp_path, monkeypatch):
|
||||
from litellm.proxy.proxy_server import ProxyConfig
|
||||
|
||||
monkeypatch.setenv("WIF_TEST_SIGNING_KEY", "-----BEGIN PRIVATE KEY-----")
|
||||
monkeypatch.setenv("WIF_TEST_FDRL", "fdrl_from_env")
|
||||
config_file = tmp_path / "config.yaml"
|
||||
config_file.write_text(
|
||||
yaml.dump(
|
||||
{
|
||||
"model_list": [
|
||||
{
|
||||
"model_name": "claude-wif",
|
||||
"litellm_params": {
|
||||
"model": "anthropic/claude-haiku-4-5",
|
||||
"anthropic_federation_rule_id": "os.environ/WIF_TEST_FDRL",
|
||||
"anthropic_identity_source": "internal_issuer",
|
||||
"anthropic_issuer_url": "https://litellm.example",
|
||||
"anthropic_issuer_audience": "https://api.anthropic.com",
|
||||
"anthropic_issuer_signing_key_ref": "os.environ/WIF_TEST_SIGNING_KEY",
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
_router, model_list, _general_settings = await ProxyConfig().load_config(
|
||||
router=None, config_file_path=str(config_file)
|
||||
)
|
||||
|
||||
litellm_params = model_list[0]["litellm_params"]
|
||||
assert litellm_params["anthropic_federation_rule_id"] == "fdrl_from_env"
|
||||
assert litellm_params["anthropic_issuer_signing_key_ref"] == "os.environ/WIF_TEST_SIGNING_KEY"
|
||||
|
|
|
|||
|
|
@ -11564,3 +11564,28 @@ class TestTierParamsTheTargetAccepts:
|
|||
accepted = router._tier_params_the_target_accepts("no-such-group", {"reasoning_effort": "max"}, {})
|
||||
|
||||
assert accepted == {"reasoning_effort": "max"}
|
||||
|
||||
|
||||
def test_router_keeps_wif_secret_pointers_unresolved(monkeypatch):
|
||||
monkeypatch.setenv("WIF_TEST_KC_SECRET", "kc-secret")
|
||||
monkeypatch.setenv("WIF_TEST_FDRL", "fdrl_from_env")
|
||||
router = Router(
|
||||
model_list=[
|
||||
{
|
||||
"model_name": "claude-wif",
|
||||
"litellm_params": {
|
||||
"model": "anthropic/claude-haiku-4-5",
|
||||
"anthropic_federation_rule_id": "os.environ/WIF_TEST_FDRL",
|
||||
"anthropic_identity_source": "keycloak",
|
||||
"anthropic_keycloak_token_url": "https://keycloak.example/token",
|
||||
"anthropic_keycloak_client_id": "litellm",
|
||||
"anthropic_keycloak_client_secret_ref": "os.environ/WIF_TEST_KC_SECRET",
|
||||
},
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
litellm_params = router.get_model_list()[0]["litellm_params"]
|
||||
|
||||
assert litellm_params["anthropic_federation_rule_id"] == "fdrl_from_env"
|
||||
assert litellm_params["anthropic_keycloak_client_secret_ref"] == "os.environ/WIF_TEST_KC_SECRET"
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ from litellm.types.router import (
|
|||
ModelInfo,
|
||||
anthropic_wif_fields_named,
|
||||
anthropic_wif_fields_present,
|
||||
holds_secret_pointer,
|
||||
)
|
||||
from litellm.types.utils import (
|
||||
CustomPricingLiteLLMParams,
|
||||
|
|
@ -146,3 +147,13 @@ def test_anthropic_wif_fields_named_reports_keys_whatever_their_value():
|
|||
|
||||
def test_anthropic_wif_fields_named_is_derived_from_the_shared_list():
|
||||
assert set(anthropic_wif_fields_named(frozenset(anthropic_wif_litellm_params))) == set(anthropic_wif_litellm_params)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("param_name", ["anthropic_issuer_signing_key_ref", "anthropic_keycloak_client_secret_ref"])
|
||||
def test_wif_ref_fields_hold_secret_pointers(param_name: str):
|
||||
assert holds_secret_pointer(param_name)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("param_name", ["api_key", "anthropic_federation_rule_id", "anthropic_identity_token"])
|
||||
def test_dereferenced_fields_do_not_hold_secret_pointers(param_name: str):
|
||||
assert not holds_secret_pointer(param_name)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue