Repository navigation
fix(proxy): restore pre-config-wins handling of pass-through endpoints #43962
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
bdd19db
4e2b098
b7fbc2f
56fc0a0
ae40803
a982b02
e5ef842
2144aef
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -500,9 +500,13 @@ def generate_feedback_box(): | |
| ) | ||
| from litellm.proxy.config_resolvers.changed_section_keys import changed_section_keys | ||
| from litellm.proxy.config_resolvers.settings_rules import ( | ||
| ABSENT, | ||
| DbRow, | ||
| Section, | ||
| SettingValue, | ||
| coerce_bool, | ||
| is_absent, | ||
| is_resource_list, | ||
| ) | ||
| from litellm.proxy.config_resolvers.settings_rules import ( | ||
| JsonValue as SettingsJsonValue, | ||
|
|
@@ -5218,6 +5222,8 @@ def update_baseline(self, config: Mapping[str, object]) -> None: | |
|
|
||
| _EMPTY_SETTINGS_MAPPING: Final[Mapping[str, SettingsJsonValue]] = MappingProxyType({}) | ||
| _SETTINGS_MAPPING: Final = TypeAdapter(dict[str, SettingsJsonValue]) | ||
| _SETTINGS_LIST: Final = TypeAdapter(list[SettingsJsonValue]) | ||
| _ENDPOINT_DICTS: Final = TypeAdapter(list[dict[str, object]]) | ||
|
|
||
|
|
||
| def _as_settings_mapping(value: object) -> Mapping[str, SettingsJsonValue]: | ||
|
|
@@ -5232,6 +5238,40 @@ def _get_field_default(field_info: FieldInfo) -> JsonValue: | |
| return cast(JsonValue, field_info.default) # cast-ok: Pydantic field defaults are JSON values at runtime | ||
|
|
||
|
|
||
| def _pass_through_endpoints_beside_db(db_endpoints: object, config_endpoints: object) -> list[SettingsJsonValue]: | ||
| stored: Final = db_endpoints if isinstance(db_endpoints, list) else () | ||
| declared: Final = config_endpoints if isinstance(config_endpoints, list) else () | ||
| db_paths: Final = frozenset(endpoint.get("path") for endpoint in stored if isinstance(endpoint, dict)) | ||
| beside_db: Final = ( | ||
| endpoint for endpoint in declared if not isinstance(endpoint, dict) or endpoint.get("path") not in db_paths | ||
| ) | ||
| return _SETTINGS_LIST.validate_python((*stored, *beside_db)) | ||
|
|
||
|
|
||
| def _with_config_file_pass_through_endpoints( | ||
| section_config: object, resolved: Mapping[str, SettingsJsonValue], db_endpoints: SettingValue | ||
| ) -> Mapping[str, object]: | ||
| config_endpoints: Final = ( | ||
| section_config.get("pass_through_endpoints") if isinstance(section_config, Mapping) else None | ||
| ) | ||
| if config_endpoints is None and not isinstance(db_endpoints, list) and "pass_through_endpoints" not in resolved: | ||
| return resolved | ||
| return MappingProxyType( | ||
| { | ||
| **resolved, | ||
| "pass_through_endpoints": _pass_through_endpoints_beside_db(db_endpoints, config_endpoints), | ||
| } | ||
| ) | ||
|
|
||
|
|
||
| def _reload_settings_store(section: Section, store: SettingsStore, section_config: object) -> None: | ||
| serving_pass_throughs: Final = store.get("pass_through_endpoints") | ||
| store.load_yaml(_as_settings_mapping(section_config)) | ||
| store.apply_db_row(section, _EMPTY_SETTINGS_MAPPING) | ||
| if is_resource_list(section, "pass_through_endpoints") and serving_pass_throughs is not None: | ||
| store["pass_through_endpoints"] = serving_pass_throughs | ||
|
|
||
|
|
||
| def _bind_general_settings_store(settings: SettingsStore) -> None: | ||
| global general_settings | ||
| general_settings = settings # pyright: ignore[reportAssignmentType] # legacy global accepts mappings | ||
|
|
@@ -5364,22 +5404,18 @@ def __init__(self) -> None: | |
| ) | ||
|
|
||
| def _load_yaml_settings_stores(self, config: Mapping[str, object]) -> None: | ||
| global config_passthrough_endpoints | ||
| for section, store in self._settings_stores.items(): | ||
| store.load_yaml(_as_settings_mapping(config.get(section))) | ||
| store.apply_db_row(section, _EMPTY_SETTINGS_MAPPING) | ||
| yaml_endpoints: Final = self.settings.config_value("pass_through_endpoints") | ||
| config_passthrough_endpoints = ( | ||
| [dict(endpoint) for endpoint in yaml_endpoints if isinstance(endpoint, dict)] | ||
| if isinstance(yaml_endpoints, list) | ||
| else None | ||
| ) | ||
| _reload_settings_store(section, store, config.get(section)) | ||
|
|
||
| def _config_with_resolved_settings(self, config: Mapping[str, object]) -> dict[str, object]: | ||
| return { # mutable-ok: get_config preserves the mutable mapping contract used by existing loaders | ||
| **config, | ||
| **{ | ||
| section: dict(store.resolved()) | ||
| section: dict( | ||
| _with_config_file_pass_through_endpoints( | ||
| config.get(section), store.resolved(), store.db_value("pass_through_endpoints") | ||
| ) | ||
| ) | ||
| for section, store in self._settings_stores.items() | ||
| if isinstance(config.get(section), Mapping) or len(store) > 0 | ||
| }, | ||
|
|
@@ -6743,6 +6779,7 @@ async def load_config(self, router: litellm.Router | None, config_file_path: str | |
|
|
||
| ## pass through endpoints | ||
| if general_settings.get("pass_through_endpoints", None) is not None: | ||
| config_passthrough_endpoints = general_settings["pass_through_endpoints"] | ||
|
greptile-apps[bot] marked this conversation as resolved.
|
||
| await initialize_pass_through_endpoints( | ||
| pass_through_endpoints=general_settings["pass_through_endpoints"], | ||
| config_file_path=config_file_path, | ||
|
|
@@ -7758,14 +7795,12 @@ async def _update_general_settings(self, db_general_settings: Mapping[str, Setti | |
| self.settings.load_yaml(_as_settings_mapping(general_settings)) | ||
| cache_size_was_db: Final = self.settings.source("user_api_key_cache_max_size") == "db" | ||
| previous_cleanup_schedule: Final = self._resolved_cleanup_schedule() | ||
| previous_pass_through_endpoints: Final = self.settings.get("pass_through_endpoints") | ||
| self.settings.apply_db_row("general_settings", db_general_settings) | ||
| _bind_general_settings_store(self.settings) | ||
| await self._apply_general_settings_side_effects( | ||
| db_general_settings, | ||
| cache_size_was_db, | ||
| previous_cleanup_schedule, | ||
| previous_pass_through_endpoints, | ||
| ) | ||
|
|
||
| def _resolved_cleanup_schedule(self) -> tuple[object, ...]: | ||
|
|
@@ -7779,11 +7814,10 @@ async def _apply_general_settings_side_effects( | |
| db_values: Mapping[str, SettingsJsonValue], | ||
| cache_size_was_db: bool, | ||
| previous_cleanup_schedule: tuple[object, ...], | ||
| previous_pass_through_endpoints: SettingsJsonValue | None, | ||
| ) -> None: | ||
| effects: Final = ( | ||
| self._apply_alerting_settings, | ||
| partial(self._apply_pass_through_settings, previous_endpoints=previous_pass_through_endpoints), | ||
| self._apply_pass_through_settings, | ||
| self._apply_boolean_settings, | ||
| partial(self._apply_cache_size_setting, cache_size_was_db=cache_size_was_db), | ||
| self._apply_store_model_in_db_setting, | ||
|
|
@@ -7816,19 +7850,23 @@ async def _apply_alerting_settings(self, db_values: Mapping[str, SettingsJsonVal | |
| if "plugins" in db_values and self.settings.source("plugins") == "db": | ||
| register_plugins_from_config(self.settings) | ||
|
|
||
| async def _apply_pass_through_settings( | ||
| self, | ||
| db_values: Mapping[str, SettingsJsonValue], | ||
| previous_endpoints: SettingsJsonValue | None, | ||
| ) -> None: | ||
| del db_values | ||
| resolved_endpoints: Final = self.settings.get("pass_through_endpoints") | ||
| if resolved_endpoints == previous_endpoints: | ||
| async def _apply_pass_through_settings(self, db_values: Mapping[str, SettingsJsonValue]) -> None: | ||
| db_endpoints: Final = db_values.get("pass_through_endpoints") | ||
| if isinstance(db_endpoints, list): | ||
| await self._serve_pass_through_endpoints(db_endpoints) | ||
|
greptile-apps[bot] marked this conversation as resolved.
|
||
| return | ||
| await initialize_pass_through_endpoints( | ||
| pass_through_endpoints=resolved_endpoints if isinstance(resolved_endpoints, list) else [] | ||
| if "pass_through_endpoints" not in self.settings: | ||
| self._publish_pass_through_endpoints(()) | ||
|
|
||
| def _publish_pass_through_endpoints(self, db_endpoints: Sequence[SettingsJsonValue]) -> None: | ||
| self.settings["pass_through_endpoints"] = _pass_through_endpoints_beside_db( | ||
| list(db_endpoints), config_passthrough_endpoints | ||
|
cursor[bot] marked this conversation as resolved.
|
||
| ) | ||
|
|
||
| async def _serve_pass_through_endpoints(self, db_endpoints: Sequence[SettingsJsonValue]) -> None: | ||
| self._publish_pass_through_endpoints(db_endpoints) | ||
| await initialize_pass_through_endpoints(pass_through_endpoints=_ENDPOINT_DICTS.validate_python(db_endpoints)) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. High: Pass-through overrides authorize the wrong upstream For a YAML
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Pre-existing: pre-#41779 and v1.102 behave the same way. This PR reverts to that handling to fix a regression, so fixing route registration is a follow-up |
||
|
|
||
| async def _apply_boolean_settings(self, db_values: Mapping[str, SettingsJsonValue]) -> None: | ||
| for key in ( | ||
| "store_prompts_in_spend_logs", | ||
|
|
@@ -18312,6 +18350,9 @@ async def update_config_general_settings( | |
| ) | ||
| await invalidate_config_param("general_settings") | ||
| proxy_config.settings.apply_db_row("general_settings", general_settings) | ||
| if is_resource_list("general_settings", data.field_name): | ||
| stored_endpoints: Final = general_settings.get("pass_through_endpoints") | ||
| await proxy_config._serve_pass_through_endpoints(stored_endpoints if isinstance(stored_endpoints, list) else ()) | ||
| asyncio.create_task( | ||
| create_config_audit_log( | ||
| "general_settings", "updated", before_general_settings, general_settings, user_api_key_dict | ||
|
|
@@ -18463,6 +18504,20 @@ def _apply_webhook_role_gate(webhook_map, is_full_admin: bool): | |
| return {alert_type: "REDACTED" for alert_type in webhook_map} | ||
|
|
||
|
|
||
| async def _declared_general_setting( | ||
| settings: SettingsStore, field_name: str, prisma_client: PrismaClient | ||
| ) -> SettingValue: | ||
| if is_resource_list("general_settings", field_name): | ||
| row: Final = await ConfigRepository(prisma_client, use_writer=True).table.find_first( | ||
| where={"param_name": "general_settings"} | ||
| ) | ||
| stored: Final = row.param_value if row is not None and isinstance(row.param_value, Mapping) else {} | ||
| return stored.get(field_name, ABSENT) if stored.get(field_name) is not None else ABSENT | ||
| if field_name not in settings: | ||
| return ABSENT | ||
| return settings.config_value(field_name) if settings.owned_by_config(field_name) else settings[field_name] | ||
|
|
||
|
|
||
| @router.get( | ||
| "/config/field/info", | ||
| tags=["config.yaml"], | ||
|
|
@@ -18501,15 +18556,12 @@ async def get_config_general_settings( | |
| ) | ||
|
|
||
| settings: Final = proxy_config.settings | ||
| if field_name not in settings: | ||
| declared: Final = await _declared_general_setting(settings, field_name, prisma_client) | ||
| if is_absent(declared): | ||
| raise HTTPException( | ||
| status_code=400, | ||
| detail={"error": f"Field name={field_name} is not set"}, | ||
| ) | ||
|
|
||
| declared: Final = ( | ||
| settings.config_value(field_name) if settings.owned_by_config(field_name) else settings[field_name] | ||
| ) | ||
| field_value = _redact_general_setting_value( | ||
| field_name, | ||
| declared, | ||
|
|
@@ -18920,6 +18972,9 @@ async def delete_config_general_settings( | |
| ) | ||
| await invalidate_config_param("general_settings") | ||
| proxy_config.settings.apply_db_row("general_settings", general_settings) | ||
| if is_resource_list("general_settings", data.field_name): | ||
| stored_endpoints: Final = general_settings.get("pass_through_endpoints") | ||
| await proxy_config._serve_pass_through_endpoints(stored_endpoints if isinstance(stored_endpoints, list) else ()) | ||
| asyncio.create_task( | ||
| create_config_audit_log( | ||
| "general_settings", "deleted", before_general_settings, general_settings, user_api_key_dict | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.