diff --git a/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py b/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py index a972f08b8bf..7bdadeadf86 100644 --- a/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py +++ b/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py @@ -506,6 +506,16 @@ class WebSearchInterceptionSettings(BaseModel): class WebSearchInterceptionSettingsResponse(SettingsResponse): """Response model for web search interception settings""" + active_on_this_pod: bool = Field( + default=False, + description=( + "Whether the process answering this request has the interception callback " + "registered. Read-only: it reports what is running here, while values.enabled " + "is the cluster-wide setting, and the two disagree while a pod is still " + "applying a change or failed to apply it." + ), + ) + def _with_websearch_enabled_resolved(config: Mapping[str, object]) -> dict[str, object]: """ @@ -1496,11 +1506,22 @@ async def get_websearch_interception_settings( config: Final = await proxy_config.get_config() - return await _get_settings_with_schema( + from litellm.integrations.websearch_interception.handler import ( + WebSearchInterceptionLogger, + ) + + settings: Final = await _get_settings_with_schema( settings_key="websearch_interception_params", settings_class=WebSearchInterceptionSettings, config=_with_websearch_enabled_resolved(config), ) + return WebSearchInterceptionSettingsResponse( + values=settings["values"], + field_schema=settings["field_schema"], + active_on_this_pod=bool( + litellm.logging_callback_manager.get_custom_loggers_for_type(WebSearchInterceptionLogger) + ), + ) @router.patch( diff --git a/tests/test_litellm/proxy/ui_crud_endpoints/test_proxy_setting_endpoints.py b/tests/test_litellm/proxy/ui_crud_endpoints/test_proxy_setting_endpoints.py index 9af559e3660..c305f7262da 100644 --- a/tests/test_litellm/proxy/ui_crud_endpoints/test_proxy_setting_endpoints.py +++ b/tests/test_litellm/proxy/ui_crud_endpoints/test_proxy_setting_endpoints.py @@ -3263,6 +3263,38 @@ class TestWebSearchInterceptionSettingsEndpoints: assert resp.status_code == 200, resp.text assert resp.json()["values"]["enabled"] is True + def test_get_flags_a_pod_that_has_not_applied_the_stored_setting( + self, mock_proxy_config, mock_auth, monkeypatch + ): + import litellm + + monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", object()) + monkeypatch.setattr(litellm, "callbacks", []) + mock_proxy_config["config"]["litellm_settings"]["websearch_interception_params"] = {"enabled": True} + + resp = client.get("/get/websearch_interception_settings") + + assert resp.status_code == 200, resp.text + assert resp.json()["values"]["enabled"] is True + assert resp.json()["active_on_this_pod"] is False + + def test_get_reports_the_pod_as_active_once_the_callback_is_registered( + self, mock_proxy_config, mock_auth, monkeypatch + ): + import litellm + from litellm.integrations.websearch_interception.handler import ( + WebSearchInterceptionLogger, + ) + + monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", object()) + monkeypatch.setattr(litellm, "callbacks", [WebSearchInterceptionLogger(search_tool_name="running")]) + mock_proxy_config["config"]["litellm_settings"]["websearch_interception_params"] = {"enabled": True} + + resp = client.get("/get/websearch_interception_settings") + + assert resp.status_code == 200, resp.text + assert resp.json()["active_on_this_pod"] is True + def test_get_reports_no_database_instead_of_empty_settings(self, mock_proxy_config, mock_auth, monkeypatch): monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", None) diff --git a/ui/litellm-dashboard/src/components/Settings/AdminSettings/WebSearchInterceptionSettings/WebSearchInterceptionSettings.test.tsx b/ui/litellm-dashboard/src/components/Settings/AdminSettings/WebSearchInterceptionSettings/WebSearchInterceptionSettings.test.tsx index ae981aa9767..a3d09c8f6d6 100644 --- a/ui/litellm-dashboard/src/components/Settings/AdminSettings/WebSearchInterceptionSettings/WebSearchInterceptionSettings.test.tsx +++ b/ui/litellm-dashboard/src/components/Settings/AdminSettings/WebSearchInterceptionSettings/WebSearchInterceptionSettings.test.tsx @@ -127,6 +127,32 @@ describe("WebSearchInterceptionSettings", () => { expect(mockMutate.mock.calls[0][0]).toEqual(ENABLED_PAYLOAD); }); + it("warns when the cluster has it on but the serving pod has not applied it", async () => { + vi.mocked(useWebSearchInterceptionSettings).mockReturnValue({ + data: { ...storedSettings, values: { ...storedSettings.values, enabled: true }, active_on_this_pod: false }, + isLoading: false, + isError: false, + error: null, + } as any); + + await renderSettings(); + + expect(screen.getByText(/has not applied it/i)).toBeInTheDocument(); + }); + + it("stays quiet when the serving pod has applied the cluster setting", async () => { + vi.mocked(useWebSearchInterceptionSettings).mockReturnValue({ + data: { ...storedSettings, values: { ...storedSettings.values, enabled: true }, active_on_this_pod: true }, + isLoading: false, + isError: false, + error: null, + } as any); + + await renderSettings(); + + expect(screen.queryByText(/has not applied it/i)).not.toBeInTheDocument(); + }); + it("ignores stored values whose types do not match the field", async () => { vi.mocked(useWebSearchInterceptionSettings).mockReturnValue({ data: { diff --git a/ui/litellm-dashboard/src/components/Settings/AdminSettings/WebSearchInterceptionSettings/WebSearchInterceptionSettings.tsx b/ui/litellm-dashboard/src/components/Settings/AdminSettings/WebSearchInterceptionSettings/WebSearchInterceptionSettings.tsx index 3e9e04e720b..93b9cbffd32 100644 --- a/ui/litellm-dashboard/src/components/Settings/AdminSettings/WebSearchInterceptionSettings/WebSearchInterceptionSettings.tsx +++ b/ui/litellm-dashboard/src/components/Settings/AdminSettings/WebSearchInterceptionSettings/WebSearchInterceptionSettings.tsx @@ -5,7 +5,7 @@ import { useUpdateWebSearchInterceptionSettings } from "@/app/(dashboard)/hooks/ import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; import { toast } from "@/lib/toast"; import { Skeleton } from "@/components/ui/skeleton"; -import { CircleHelp, Info, Save } from "lucide-react"; +import { CircleHelp, Info, Save, TriangleAlert } from "lucide-react"; import { Alert, AlertDescription, AlertTitle } from "@/components/shared/Alert"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; @@ -293,9 +293,22 @@ export default function WebSearchInterceptionSettings() { } const values: WebSearchInterceptionStoredValues = toStoredValues(data?.values ?? NO_STORED_VALUES); + const notAppliedHere = values.enabled === true && data?.active_on_this_pod === false; return (