From ab5b2192ccd04d6e6ffe1f6d195a91a243788ade Mon Sep 17 00:00:00 2001 From: mateo Date: Mon, 21 Sep 2026 22:33:13 +0000 Subject: [PATCH] feat(proxy): add frozen ProxyRuntimeConfig model Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/proxy/_types.py | 67 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 3f6ef89fbc6..49fe41d985c 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -15,6 +15,7 @@ from pydantic import ( Json, JsonValue, PositiveInt, + PrivateAttr, field_validator, model_validator, ) @@ -3118,6 +3119,72 @@ class ConfigYAML(LiteLLMPydanticObjectBase): model_config = ConfigDict(protected_namespaces=()) +ConfigSection: TypeAlias = Mapping[str, object] +ConfigSectionList: TypeAlias = tuple[Mapping[str, object], ...] + + +class ProxyRuntimeConfig(LiteLLMPydanticObjectBase): + """ + The loaded, secret-resolved config.yaml that `ProxyConfig.get_config` returns. + + Frozen: build a changed copy with `with_section` (or `model_copy(update=...)`) and hand it to + `ProxyConfig.save_config`. Top-level sections this model does not name are kept as extras so a + round trip through `to_mapping` loses nothing. `baseline` is the snapshot taken at load time + that `save_config` diffs against to find the sections that changed. + """ + + model_list: ConfigSectionList = () + general_settings: ConfigSection = Field(default_factory=dict) + litellm_settings: ConfigSection = Field(default_factory=dict) + router_settings: ConfigSection = Field(default_factory=dict) + environment_variables: ConfigSection = Field(default_factory=dict) + callback_settings: ConfigSection = Field(default_factory=dict) + assistant_settings: ConfigSection = Field(default_factory=dict) + default_vertex_config: ConfigSection = Field(default_factory=dict) + mcp_servers: ConfigSection = Field(default_factory=dict) + policies: ConfigSection = Field(default_factory=dict) + search_tools: ConfigSectionList = () + sandbox_tools: ConfigSectionList = () + finetune_settings: ConfigSectionList = () + files_settings: ConfigSectionList = () + guardrails: ConfigSectionList = () + prompts: ConfigSectionList = () + credential_list: ConfigSectionList = () + mcp_tools: ConfigSectionList = () + agents: ConfigSectionList = () + agent_list: ConfigSectionList = () + vector_store_registry: ConfigSectionList = () + worker_registry: ConfigSectionList = () + policy_attachments: ConfigSectionList = () + + _baseline: Mapping[str, object] = PrivateAttr(default_factory=dict) + + model_config = ConfigDict(frozen=True, extra="allow", protected_namespaces=()) + + @model_validator(mode="before") + @classmethod + def _drop_null_sections(cls, data: object) -> object: + if not isinstance(data, Mapping): + return data + return {key: value for key, value in data.items() if value is not None} + + @classmethod + def from_resolved(cls, raw: Mapping[str, object]) -> "ProxyRuntimeConfig": + loaded: Final = cls.model_validate(raw) + loaded._baseline = loaded.to_mapping() + return loaded + + @property + def baseline(self) -> Mapping[str, object]: + return self._baseline + + def to_mapping(self) -> Mapping[str, object]: + return MappingProxyType(self.model_dump(mode="json", exclude_defaults=True)) + + def with_section(self, name: str, value: ConfigSection | ConfigSectionList) -> "ProxyRuntimeConfig": + return self.model_copy(update={name: value}) + + from litellm.models.verification_token import ( # noqa: E402 LiteLLM_DeletedVerificationToken as LiteLLM_DeletedVerificationToken, )