Coverage for .venv/lib/python3.13/site-packages/litellm/proxy/config_resolvers/settings_rules.py: 89%

48 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-10 12:01 +0000

1from __future__ import annotations 

2 

3from collections.abc import Mapping 

4from dataclasses import dataclass 

5from types import MappingProxyType 

6from typing import Final, Literal, TypeAlias 

7 

8from litellm.proxy.config_resolvers._descriptors import FieldSource 

9 

10JsonValue: TypeAlias = None | bool | int | float | str | list["JsonValue"] | dict[str, "JsonValue"] 

11Section: TypeAlias = Literal[ 

12 "general_settings", 

13 "router_settings", 

14 "litellm_settings", 

15 "environment_variables", 

16 "ui_settings", 

17] 

18DbRow: TypeAlias = Section 

19 

20 

21@dataclass(frozen=True, slots=True) 

22class Absent: 

23 pass 

24 

25 

26ABSENT: Final = Absent() 

27SettingValue: TypeAlias = JsonValue | Absent 

28 

29 

30@dataclass(frozen=True, slots=True) 

31class KeyRule: 

32 """Which stored row carries this key. Precedence no longer varies per key.""" 

33 

34 db_row: DbRow 

35 

36 

37@dataclass(frozen=True, slots=True) 

38class Resolved: 

39 value: SettingValue 

40 source: FieldSource 

41 

42 

43_UI_SETTINGS_FIELDS: Final[tuple[str, ...]] = ( 

44 "allow_public_health_readiness_details", 

45 "forward_client_headers_to_llm_api", 

46 "forward_llm_provider_auth_headers", 

47 "disable_agents_for_internal_users", 

48 "allow_agents_for_team_admins", 

49 "disable_vector_stores_for_internal_users", 

50 "allow_vector_stores_for_team_admins", 

51 "disable_custom_api_keys", 

52 "disable_key_generate_for_org_admin", 

53 "team_admin_editable_team_fields", 

54) 

55 

56 

57def _rules_for( 

58 section: Section, keys: tuple[str, ...], db_row: DbRow 

59) -> tuple[tuple[tuple[Section, str], KeyRule], ...]: 

60 return tuple(((section, key), KeyRule(db_row=db_row)) for key in keys) 

61 

62 

63def _build_dual_source_keys() -> Mapping[tuple[Section, str], KeyRule]: 

64 """Maps a key to the stored row that carries it, for the keys whose row is not their own section.""" 

65 return MappingProxyType( 

66 dict( 

67 ( 

68 *_rules_for("general_settings", _UI_SETTINGS_FIELDS, "ui_settings"), 

69 *( 

70 ((section, "*"), KeyRule(db_row=section)) 

71 for section in ("general_settings", "router_settings", "litellm_settings", "environment_variables") 

72 ), 

73 ) 

74 ) 

75 ) 

76 

77 

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

79 

80 

81RESOURCE_LIST_KEYS: Final[frozenset[tuple[Section, str]]] = frozenset({("general_settings", "pass_through_endpoints")}) 

82 

83 

84def is_resource_list(section: Section, key: str) -> bool: 

85 return (section, key) in RESOURCE_LIST_KEYS 

86 

87 

88def rule_for(section: Section, key: str) -> KeyRule: 

89 return DUAL_SOURCE_KEYS.get((section, key), DUAL_SOURCE_KEYS[(section, "*")]) 

90 

91 

92def coerce_bool(value: JsonValue) -> JsonValue: 

93 if value is None or isinstance(value, bool): 93 ↛ 95line 93 didn't jump to line 95 because the condition on line 93 was always true

94 return value 

95 if isinstance(value, str): 

96 return value.lower() == "true" 

97 return bool(value) 

98 

99 

100def resolve(yaml_value: SettingValue, db_value: SettingValue) -> Resolved: 

101 """Config wins. A key the config file declares is config-owned, whatever the database holds. 

102 

103 A stored ``null`` still counts as absent, so clearing a row does not erase a value 

104 the file never declared. 

105 """ 

106 if yaml_value is not ABSENT: 

107 return Resolved(value=yaml_value, source="config") 

108 if _db_is_present(db_value): 

109 return Resolved(value=db_value, source="db") 

110 return Resolved(value=ABSENT, source="unset") 

111 

112 

113def is_absent(value: SettingValue) -> bool: 

114 return value is ABSENT 

115 

116 

117def _db_is_present(value: SettingValue) -> bool: 

118 return not is_absent(value) and value is not None