Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2579,7 +2579,7 @@ def _jwt_auth_issuers() -> list:
if env_issuer:
issuers.append(env_issuer)

jwtauth: Final = general_settings.get("litellm_jwtauth") if isinstance(general_settings, dict) else None
jwtauth: Final = general_settings.get("litellm_jwtauth") if isinstance(general_settings, Mapping) else None
raw_issuers: Final = jwtauth.get("issuers") if isinstance(jwtauth, dict) else getattr(jwtauth, "issuers", None)
for cfg in raw_issuers or []:
issuer = cfg.get("issuer") if isinstance(cfg, dict) else getattr(cfg, "issuer", None)
Expand Down
4 changes: 4 additions & 0 deletions litellm/proxy/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -2423,6 +2423,8 @@ class ConfigList(LiteLLMPydanticObjectBase):
nested_fields: list[FieldDetail] | None = None # For nested dictionary or Pydantic fields
field_options: list[str] | None = None # Allowed values, for field_type == "Select"
field_tab: str | None = None # Admin UI sub-tab this field renders under; None groups it with the rest
source: Literal["config", "db", "env", "default", "unset"] = "unset"
editable: bool = True


class UserHeaderMapping(LiteLLMPydanticObjectBase):
Expand Down Expand Up @@ -3693,6 +3695,8 @@ class InvitationClaim(LiteLLMPydanticObjectBase):
class ConfigFieldInfo(LiteLLMPydanticObjectBase):
field_name: str
field_value: Any
source: Literal["config", "db", "env", "default", "unset"] = "unset"
editable: bool = True


class CallbackOnUI(LiteLLMPydanticObjectBase):
Expand Down
3 changes: 2 additions & 1 deletion litellm/proxy/client/cli/commands/autoroute/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import secrets
import signal
import threading
from collections.abc import Mapping
from types import FrameType
from typing import Final

Expand Down Expand Up @@ -67,7 +68,7 @@ def _ensure_master_key() -> str:
master_key: Final = secrets.token_urlsafe(32)
general_settings: Final = generated.get("general_settings")
updated_settings: Final[dict[str, JsonValue]] = {
**(general_settings if isinstance(general_settings, dict) else {}),
**(general_settings if isinstance(general_settings, Mapping) else {}),
"master_key": master_key,
}
updated: Final[dict[str, JsonValue]] = {**generated, "general_settings": updated_settings}
Expand Down
3 changes: 2 additions & 1 deletion litellm/proxy/client/cli/commands/autoroute/config.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from collections.abc import Mapping
from typing import Final, Literal

from pydantic import BaseModel, ConfigDict, Field, JsonValue, TypeAdapter
Expand Down Expand Up @@ -221,7 +222,7 @@ def master_key_from_config(config: dict[str, JsonValue]) -> str | None:
normalized copy here would diverge from what the proxy expects.
"""
general_settings: Final = config.get("general_settings")
if not isinstance(general_settings, dict):
if not isinstance(general_settings, Mapping):
return None
master_key: Final = general_settings.get("master_key")
if isinstance(master_key, str) and master_key.strip():
Expand Down
3 changes: 2 additions & 1 deletion litellm/proxy/config_resolvers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
FieldSource,
resolve_fields,
)
from litellm.proxy.config_resolvers.settings_store import SettingsStore

__all__ = ["FieldDescriptor", "FieldSource", "resolve_fields"]
__all__ = ("FieldDescriptor", "FieldSource", "SettingsStore", "resolve_fields")
6 changes: 4 additions & 2 deletions litellm/proxy/config_resolvers/_descriptors.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from dataclasses import dataclass
from typing import Final, Literal

FieldSource = Literal["db", "env", "default", "unset"]
FieldSource = Literal["config", "db", "env", "default", "unset"]


@dataclass(frozen=True, slots=True)
Expand Down Expand Up @@ -69,5 +69,7 @@ def resolve_fields(
"""
resolved: Final = tuple(_resolve_one(descriptor, db_values, env, empty_db_is_set) for descriptor in descriptors)
values: Final = {field_name: value for field_name, value, _ in resolved}
provenance: Final = {field_name: source for field_name, _, source in resolved}
provenance: Final[dict[str, FieldSource]] = dict( # mutable-ok: public resolver contract returns a plain dict
(field_name, source) for field_name, _, source in resolved
)
return values, provenance
110 changes: 110 additions & 0 deletions litellm/proxy/config_resolvers/settings_rules.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
from __future__ import annotations

from collections.abc import Mapping
from dataclasses import dataclass
from types import MappingProxyType
from typing import Final, Literal, TypeAlias

from litellm.proxy.config_resolvers._descriptors import FieldSource

JsonValue: TypeAlias = None | bool | int | float | str | list["JsonValue"] | dict[str, "JsonValue"]
Section: TypeAlias = Literal[
"general_settings",
"router_settings",
"litellm_settings",
"environment_variables",
"ui_settings",
]
DbRow: TypeAlias = Section


@dataclass(frozen=True, slots=True)
class Absent:
pass


ABSENT: Final = Absent()
SettingValue: TypeAlias = JsonValue | Absent


@dataclass(frozen=True, slots=True)
class KeyRule:
"""Which stored row carries this key. Precedence no longer varies per key."""

db_row: DbRow


@dataclass(frozen=True, slots=True)
class Resolved:
value: SettingValue
source: FieldSource


_UI_SETTINGS_FIELDS: Final[tuple[str, ...]] = (
"allow_public_health_readiness_details",
"forward_client_headers_to_llm_api",
"forward_llm_provider_auth_headers",
"disable_agents_for_internal_users",
"allow_agents_for_team_admins",
"disable_vector_stores_for_internal_users",
"allow_vector_stores_for_team_admins",
"disable_key_generate_for_org_admin",
"team_admin_editable_team_fields",
)


def _rules_for(
section: Section, keys: tuple[str, ...], db_row: DbRow
) -> tuple[tuple[tuple[Section, str], KeyRule], ...]:
return tuple(((section, key), KeyRule(db_row=db_row)) for key in keys)


def _build_dual_source_keys() -> Mapping[tuple[Section, str], KeyRule]:
"""Maps a key to the stored row that carries it, for the keys whose row is not their own section."""
return MappingProxyType(
dict(
(
*_rules_for("general_settings", _UI_SETTINGS_FIELDS, "ui_settings"),
*(
((section, "*"), KeyRule(db_row=section))
for section in ("general_settings", "router_settings", "litellm_settings", "environment_variables")
),
)
)
)


DUAL_SOURCE_KEYS: Final[Mapping[tuple[Section, str], KeyRule]] = _build_dual_source_keys()


def rule_for(section: Section, key: str) -> KeyRule:
return DUAL_SOURCE_KEYS.get((section, key), DUAL_SOURCE_KEYS[(section, "*")])


def coerce_bool(value: JsonValue) -> JsonValue:
if value is None or isinstance(value, bool):
return value
if isinstance(value, str):
return value.lower() == "true"
return bool(value)


def resolve(yaml_value: SettingValue, db_value: SettingValue) -> Resolved:
"""Config wins. A key the config file declares is config-owned, whatever the database holds.

A stored ``null`` still counts as absent, so clearing a row does not erase a value
the file never declared.
"""
if yaml_value is not ABSENT:
return Resolved(value=yaml_value, source="config")
if _db_is_present(db_value):
return Resolved(value=db_value, source="db")
return Resolved(value=ABSENT, source="unset")


def is_absent(value: SettingValue) -> bool:
return value is ABSENT


def _db_is_present(value: SettingValue) -> bool:
return not is_absent(value) and value is not None
128 changes: 128 additions & 0 deletions litellm/proxy/config_resolvers/settings_store.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
from __future__ import annotations

from collections.abc import Iterator, Mapping, MutableMapping
from types import MappingProxyType
from typing import Final

from litellm.proxy.config_resolvers._descriptors import FieldSource
from litellm.proxy.config_resolvers.settings_rules import (
ABSENT,
Absent,
DbRow,
JsonValue,
Resolved,
Section,
SettingValue,
resolve,
rule_for,
)

_EMPTY_VALUES: Final[Mapping[str, JsonValue]] = MappingProxyType({})
_EMPTY_ROWS: Final[Mapping[DbRow, Mapping[str, JsonValue]]] = MappingProxyType({})


class SettingsStore(MutableMapping[str, JsonValue]):
def __init__(self, section: Section) -> None:
self._section: Final = section
self._yaml_values: Mapping[str, JsonValue] = _EMPTY_VALUES
self._database_rows: Mapping[DbRow, Mapping[str, JsonValue]] = _EMPTY_ROWS
self._runtime_values: Mapping[str, JsonValue] = _EMPTY_VALUES
self._deleted_runtime_keys: frozenset[str] = frozenset()

def load_yaml(self, mapping: Mapping[str, JsonValue]) -> None:
self._yaml_values = MappingProxyType(dict(mapping))
self._clear_runtime()

def config_value(self, key: str) -> JsonValue:
return self._yaml_values.get(key)

def owned_by_config(self, key: str) -> bool:
return key in self._yaml_values

def rejected_writes(self, incoming: Mapping[str, JsonValue]) -> tuple[str, ...]:
return tuple(
sorted(
key for key, value in incoming.items() if self.owned_by_config(key) and value != self._yaml_values[key]
)
)

def apply_db_row(self, row: DbRow, db_row: Mapping[str, JsonValue]) -> None:
previous_row: Final = self._database_rows.get(row, _EMPTY_VALUES)
self._database_rows = MappingProxyType({**self._database_rows, row: MappingProxyType(dict(db_row))})
self._clear_runtime_keys(frozenset((*previous_row, *db_row)))

def resolved(self) -> Mapping[str, JsonValue]:
return MappingProxyType(dict(self))

def apply_runtime_values(self, values: Mapping[str, JsonValue]) -> None:
self._runtime_values = MappingProxyType(dict(values))
self._deleted_runtime_keys = frozenset()

def source(self, key: str) -> FieldSource:
return self._resolution_for(key).source

def __getitem__(self, key: str) -> JsonValue:
if key in self._deleted_runtime_keys:
raise KeyError(key)
if key in self._runtime_values:
return self._runtime_values[key]
resolved: Final = self._resolution_for(key)
if isinstance(resolved.value, Absent):
raise KeyError(key)
return resolved.value

def __setitem__(self, key: str, value: JsonValue) -> None:
if self.owned_by_config(key):
return
self._runtime_values = MappingProxyType({**self._runtime_values, key: value})
self._deleted_runtime_keys = self._deleted_runtime_keys - frozenset((key,))

def __delitem__(self, key: str) -> None:
if key not in self:
raise KeyError(key)
if self.owned_by_config(key):
return
self._runtime_values = MappingProxyType(
{key_: value for key_, value in self._runtime_values.items() if key_ != key}
)
self._deleted_runtime_keys = self._deleted_runtime_keys | frozenset((key,))

def __iter__(self) -> Iterator[str]:
return iter(
key
for key in self._keys()
if key not in self._deleted_runtime_keys
and (key in self._runtime_values or not isinstance(self._resolution_for(key).value, Absent))
)

def __len__(self) -> int:
return sum(1 for _ in self)

def _clear_runtime(self) -> None:
self._runtime_values = _EMPTY_VALUES
self._deleted_runtime_keys = frozenset()

def _clear_runtime_keys(self, keys: frozenset[str]) -> None:
if not keys:
return
self._runtime_values = MappingProxyType(
{key: value for key, value in self._runtime_values.items() if key not in keys}
)
self._deleted_runtime_keys = self._deleted_runtime_keys - keys

def _keys(self) -> tuple[str, ...]:
return tuple(
dict.fromkeys(
(
*self._yaml_values,
*(key for row in self._database_rows.values() for key in row),
*self._runtime_values,
)
)
)

def _resolution_for(self, key: str) -> Resolved:
rule: Final = rule_for(self._section, key)
yaml_value: Final[SettingValue] = self._yaml_values.get(key, ABSENT)
db_value: Final[SettingValue] = self._database_rows.get(rule.db_row, _EMPTY_VALUES).get(key, ABSENT)
return resolve(yaml_value, db_value)
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ async def _current_coordination_redis_settings() -> dict[str, object] | None:

config_state: Final = _SETTINGS_ADAPTER.validate_python(proxy_config.get_config_state())
general_settings: Final = config_state.get(_GENERAL_SETTINGS_PARAM_NAME)
if not isinstance(general_settings, dict):
if not isinstance(general_settings, Mapping):
return None
from_file: Final = general_settings.get(_COORDINATION_REDIS_KEY)
if isinstance(from_file, dict):
Expand Down
2 changes: 1 addition & 1 deletion litellm/proxy/plugin_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def _configured_key_header_names() -> frozenset[str]:
except Exception:
return frozenset()
general_settings: Final = getattr(proxy_server, "general_settings", None)
if not isinstance(general_settings, dict):
if not isinstance(general_settings, Mapping):
return frozenset()
name: Final[object] = general_settings.get("litellm_key_header_name")
return frozenset({name.lower()}) if isinstance(name, str) and name else frozenset()
Expand Down
Loading
Loading