diff --git a/litellm/proxy/_lazy_openapi_snapshot.json b/litellm/proxy/_lazy_openapi_snapshot.json index 391f0042ed0..06e157498aa 100644 --- a/litellm/proxy/_lazy_openapi_snapshot.json +++ b/litellm/proxy/_lazy_openapi_snapshot.json @@ -19632,7 +19632,7 @@ } } }, - "description": "\nUnified rate-limit error.\n\nEvery rate-limit condition surfaced by litellm \u2014 whether it originated from\nan upstream LLM provider, a vendor batch endpoint, or one of litellm's own\nproxy-side limiters (parallel-requests, dynamic-rate, batch-rate, budget,\nmax-iterations, etc.) \u2014 is raised as an instance of this class.\n\nThe :attr:`category` attribute lets callers distinguish the source. See\n:class:`RateLimitErrorCategory` for the available values.\n" + "description": "\n Unified rate-limit error.\n\n Every rate-limit condition surfaced by litellm \u2014 whether it originated from\n an upstream LLM provider, a vendor batch endpoint, or one of litellm's own\n proxy-side limiters (parallel-requests, dynamic-rate, batch-rate, budget,\n max-iterations, etc.) \u2014 is raised as an instance of this class.\n\n The :attr:`category` attribute lets callers distinguish the source. See\n :class:`RateLimitErrorCategory` for the available values.\n " }, "500": { "content": { diff --git a/litellm/proxy/management_endpoints/router_settings_endpoints.py b/litellm/proxy/management_endpoints/router_settings_endpoints.py index 019ac68ae23..d6d74ada35a 100644 --- a/litellm/proxy/management_endpoints/router_settings_endpoints.py +++ b/litellm/proxy/management_endpoints/router_settings_endpoints.py @@ -8,7 +8,9 @@ GET /router/fields - Get router settings field definitions without values (for U """ import inspect -from typing import Any, Final, cast, get_args +from collections.abc import Mapping +from types import MappingProxyType +from typing import Any, Final, get_args from fastapi import APIRouter, Depends from pydantic import BaseModel, Field @@ -127,19 +129,20 @@ async def get_router_settings( if field.field_name in current_values: field.field_value = current_values[field.field_name] - field_defaults: Final[dict[str, object]] = { - field.field_name: cast(object, field.field_default) # cast-ok: Pydantic field defaults are untyped - for field in router_fields - } - source: Final[dict[str, FieldSource]] = { - key: _router_setting_source( - proxy_config.router_settings, - key, - cast(object, current_values[key]), # cast-ok: current values are stored in a typed response map - field_defaults.get(key), - ) - for key in current_values - } + field_defaults: Final[Mapping[str, object]] = MappingProxyType( + {field.field_name: field.field_default for field in router_fields} + ) + source: Final[Mapping[str, FieldSource]] = MappingProxyType( + { + key: _router_setting_source( + proxy_config.router_settings, + key, + current_values[key], + field_defaults.get(key), + ) + for key in current_values + } + ) return RouterSettingsResponse( fields=router_fields, current_values=current_values, diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 4b2781e031b..24af6d7f7d3 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -15991,7 +15991,7 @@ def _nested_setting_source( field_default: JsonValue, ) -> FieldSource: db_value: Final = db_values.get(field_name) - if db_value is not None and db_value != []: + if db_value is not None and not (isinstance(db_value, list) and len(db_value) == 0): return "db" parent_value: Final = settings.config_value(parent_key) if isinstance(parent_value, Mapping) and field_name in parent_value: @@ -16036,13 +16036,13 @@ async def alerting_settings( where={"param_name": "general_settings"} ) - db_general_settings_dict: Final[Mapping[str, JsonValue]] = ( - dict(db_general_settings.param_value) + db_general_settings_dict: Final[Mapping[str, JsonValue]] = MappingProxyType( + dict(db_general_settings.param_value) # mutable-ok: Prisma returns the JSON column as a plain dict if db_general_settings is not None and db_general_settings.param_value is not None else {} ) alerting_args_value: Final = db_general_settings_dict.get("alerting_args") - alerting_args_dict: Final[Mapping[str, JsonValue]] = ( + alerting_args_dict: Final[Mapping[str, JsonValue]] = MappingProxyType( alerting_args_value if isinstance(alerting_args_value, dict) else {} ) alerting_values: Final = cast( # cast-ok: alerting is stored as a JSON list when present @@ -16050,7 +16050,6 @@ async def alerting_settings( ) settings: Final = proxy_config.settings - settings.apply_db_row("general_settings", db_general_settings_dict) allowed_args: Final = MappingProxyType( { diff --git a/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py b/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py index 4505f3144ee..ed626bdb624 100644 --- a/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py +++ b/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py @@ -1755,18 +1755,21 @@ async def get_ui_settings(): ui_settings: Final = {k: v for k, v in parsed.items() if k in ALLOWED_UI_SETTINGS_FIELDS} apply_runtime_general_settings_flags(ui_settings) - proxy_config.settings.apply_db_row("ui_settings", ui_settings) # Refresh DualCache so other code paths (e.g. /user/filter/ui) see fresh values from litellm.proxy.proxy_server import user_api_key_cache await user_api_key_cache.async_set_cache(key=UI_SETTINGS_CACHE_KEY, value=ui_settings, ttl=UI_SETTINGS_CACHE_TTL) - effective_ui_settings: Final = { - **{key: proxy_config.settings[key] for key in ALLOWED_UI_SETTINGS_FIELDS if key in proxy_config.settings}, - **ui_settings, - } - config: Final[dict[str, object]] = {"litellm_settings": {"ui_settings": effective_ui_settings}} + effective_ui_settings: Final[Mapping[str, object]] = MappingProxyType( + { + **{key: proxy_config.settings[key] for key in ALLOWED_UI_SETTINGS_FIELDS if key in proxy_config.settings}, + **ui_settings, + } + ) + config: Final[Mapping[str, object]] = MappingProxyType( + {"litellm_settings": MappingProxyType({"ui_settings": effective_ui_settings})} + ) settings_class: Final = _get_effective_ui_settings_class() resolved_settings: Final = _SettingsWithSchema.model_validate( await _get_settings_with_schema( @@ -1775,16 +1778,22 @@ async def get_ui_settings(): config=config, ) ) - values: Final = { - **resolved_settings.values, - ENABLE_PTU_COST_ATTRIBUTION_UI_SETTING: is_ptu_cost_attribution_enabled(), - } - source: Final[dict[str, FieldSource]] = { - key: ( - "db" if key in ui_settings else _ui_setting_source(key, values[key], proxy_config.settings, settings_class) - ) - for key in values - } + values: Final[Mapping[str, object]] = MappingProxyType( + { + **resolved_settings.values, + ENABLE_PTU_COST_ATTRIBUTION_UI_SETTING: is_ptu_cost_attribution_enabled(), + } + ) + source: Final[Mapping[str, FieldSource]] = MappingProxyType( + { + key: ( + "db" + if key in ui_settings + else _ui_setting_source(key, values[key], proxy_config.settings, settings_class) + ) + for key in values + } + ) return UISettingsResponse( values=values, field_schema=resolved_settings.field_schema, diff --git a/tests/test_litellm/proxy/management_endpoints/test_router_settings_endpoints.py b/tests/test_litellm/proxy/management_endpoints/test_router_settings_endpoints.py index 3af7de62abe..51c8679e89e 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_router_settings_endpoints.py +++ b/tests/test_litellm/proxy/management_endpoints/test_router_settings_endpoints.py @@ -15,12 +15,24 @@ from litellm.proxy._types import LitellmUserRoles, UserAPIKeyAuth from litellm.proxy.management_endpoints.router_settings_endpoints import ( get_router_settings, ) +from litellm.proxy.config_resolvers import SettingsStore from litellm.proxy.proxy_server import app from litellm.router import Router client = TestClient(app) +def _stub_proxy_config(router_settings, config_router_settings): + class _StubProxyConfig: + def __init__(self): + self.router_settings = router_settings + + async def get_config(self, config_file_path=None): + return {"router_settings": dict(config_router_settings)} + + return _StubProxyConfig() + + class TestRouterSettingsEndpoints: """Test suite for router settings endpoints""" @@ -77,25 +89,18 @@ class TestRouterSettingsEndpoints: @pytest.mark.asyncio async def test_get_router_settings_reports_sources(self, monkeypatch): - from litellm.proxy.config_resolvers import SettingsStore - store = SettingsStore("router_settings") store.load_yaml({"routing_strategy": "simple-shuffle"}) store.apply_db_row("router_settings", {"num_retries": 3}) - monkeypatch.setattr(proxy_server.proxy_config, "router_settings", store) - monkeypatch.setattr(proxy_server, "llm_router", None) - - async def fake_get_config(self, config_file_path=None): - return { - "router_settings": { - "routing_strategy": "simple-shuffle", - "num_retries": 3, - } - } - monkeypatch.setattr( - proxy_server.ProxyConfig, "get_config", fake_get_config, raising=True + proxy_server, + "proxy_config", + _stub_proxy_config( + store, + {"routing_strategy": "simple-shuffle", "num_retries": 3}, + ), ) + monkeypatch.setattr(proxy_server, "llm_router", None) admin_user = UserAPIKeyAuth( user_role=LitellmUserRoles.PROXY_ADMIN, api_key="sk-x" @@ -132,12 +137,10 @@ class TestRouterSettingsEndpoints: ) monkeypatch.setattr(proxy_server, "llm_router", llm_router) - - async def fake_get_config(self, config_file_path=None): - return {} - monkeypatch.setattr( - proxy_server.ProxyConfig, "get_config", fake_get_config, raising=True + proxy_server, + "proxy_config", + _stub_proxy_config(SettingsStore("router_settings"), {}), ) admin_user = UserAPIKeyAuth( diff --git a/ui/litellm-dashboard/src/lib/http/schema.d.ts b/ui/litellm-dashboard/src/lib/http/schema.d.ts index 6211eeaf962..ac89d676921 100644 --- a/ui/litellm-dashboard/src/lib/http/schema.d.ts +++ b/ui/litellm-dashboard/src/lib/http/schema.d.ts @@ -37069,6 +37069,13 @@ export interface components { routing_strategy_descriptions: { [key: string]: string; }; + /** + * Source + * @description Source of each current router setting + */ + source: { + [key: string]: "config" | "db" | "env" | "default" | "unset"; + }; }; /** * RoutingGroup @@ -39532,6 +39539,10 @@ export interface components { field_schema: { [key: string]: unknown; }; + /** Source */ + source: { + [key: string]: "config" | "db" | "env" | "default" | "unset"; + }; /** Values */ values: { [key: string]: unknown;