fix(proxy): report sources only on the read endpoints main does not cover

/config/field/info and /config/list already report per-key source on main,
so this drops the branch's versions of those and keeps /alerting/settings,
/get/ui_settings and /router/settings.

Read endpoints no longer write the freshly read database row back into the
shared settings store; the reload path already keeps it current, and a GET
that mutates global state leaks across callers.

Regenerates the lazy OpenAPI snapshot on Python 3.12, matching CI, and the
dashboard API types for the two new response fields.
This commit is contained in:
Yuneng Jiang 2026-09-21 13:25:39 -07:00
parent 86c4021e83
commit be2f0d081b
No known key found for this signature in database
6 changed files with 80 additions and 55 deletions

View file

@ -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": {

View file

@ -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,

View file

@ -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(
{

View file

@ -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,

View file

@ -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(

View file

@ -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;