From 2d5ae35a8517aa480049d37600a531d0eb6d85b8 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Thu, 6 Nov 2025 12:38:47 -0800 Subject: [PATCH 001/370] Show all callbacks on UI --- litellm/proxy/proxy_server.py | 123 ++++--- tests/proxy_unit_tests/test_proxy_server.py | 163 ++++++++ .../src/components/settings.test.tsx | 136 +++++++ .../src/components/settings.tsx | 348 ++++++++++-------- 4 files changed, 548 insertions(+), 222 deletions(-) create mode 100644 ui/litellm-dashboard/src/components/settings.test.tsx diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index bed3d218df4..4a0d39f4518 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -9572,8 +9572,61 @@ async def get_config(): # noqa: PLR0915 _general_settings = config_data.get("general_settings", {}) environment_variables = config_data.get("environment_variables", {}) - # check if "langfuse" in litellm_settings + # Helper function to process callbacks and get environment variables + def process_callback(_callback: str, callback_type: str) -> dict: + """Process a single callback and return its data with environment variables""" + if _callback == "langfuse" or _callback == "langfuse_otel": + env_vars = [ + "LANGFUSE_PUBLIC_KEY", + "LANGFUSE_SECRET_KEY", + "LANGFUSE_HOST", + ] + elif _callback == "openmeter": + env_vars = [ + "OPENMETER_API_KEY", + ] + elif _callback == "braintrust": + env_vars = [ + "BRAINTRUST_API_KEY", + "BRAINTRUST_API_BASE", + ] + elif _callback == "traceloop": + env_vars = ["TRACELOOP_API_KEY"] + elif _callback == "custom_callback_api": + env_vars = ["GENERIC_LOGGER_ENDPOINT"] + elif _callback == "otel": + env_vars = ["OTEL_EXPORTER", "OTEL_ENDPOINT", "OTEL_HEADERS"] + elif _callback == "langsmith": + env_vars = [ + "LANGSMITH_API_KEY", + "LANGSMITH_PROJECT", + "LANGSMITH_DEFAULT_RUN_NAME", + ] + else: + env_vars = [] + + env_vars_dict = {} + for _var in env_vars: + env_variable = environment_variables.get(_var, None) + if env_variable is None: + env_vars_dict[_var] = None + else: + # decode + decrypt the value + decrypted_value = decrypt_value_helper( + value=env_variable, key=_var + ) + env_vars_dict[_var] = decrypted_value + + return { + "name": _callback, + "variables": env_vars_dict, + "type": callback_type + } + _success_callbacks = _litellm_settings.get("success_callback", []) + _failure_callbacks = _litellm_settings.get("failure_callback", []) + _generic_callbacks = _litellm_settings.get("callbacks", []) + _data_to_return = [] """ [ @@ -9584,70 +9637,20 @@ async def get_config(): # noqa: PLR0915 "LANGFUSE_SECRET_KEY": "value", "LANGFUSE_HOST": "value" }, + "type": "success" } ] """ + for _callback in _success_callbacks: - if _callback != "langfuse": - if _callback == "openmeter": - env_vars = [ - "OPENMETER_API_KEY", - ] - elif _callback == "braintrust": - env_vars = [ - "BRAINTRUST_API_KEY", - "BRAINTRUST_API_BASE", - ] - elif _callback == "traceloop": - env_vars = ["TRACELOOP_API_KEY"] - elif _callback == "custom_callback_api": - env_vars = ["GENERIC_LOGGER_ENDPOINT"] - elif _callback == "otel": - env_vars = ["OTEL_EXPORTER", "OTEL_ENDPOINT", "OTEL_HEADERS"] - elif _callback == "langsmith": - env_vars = [ - "LANGSMITH_API_KEY", - "LANGSMITH_PROJECT", - "LANGSMITH_DEFAULT_RUN_NAME", - ] - else: - env_vars = [] - - env_vars_dict = {} - for _var in env_vars: - env_variable = environment_variables.get(_var, None) - if env_variable is None: - env_vars_dict[_var] = None - else: - # decode + decrypt the value - decrypted_value = decrypt_value_helper( - value=env_variable, key=_var - ) - env_vars_dict[_var] = decrypted_value - - _data_to_return.append({"name": _callback, "variables": env_vars_dict}) - elif _callback == "langfuse": - _langfuse_vars = [ - "LANGFUSE_PUBLIC_KEY", - "LANGFUSE_SECRET_KEY", - "LANGFUSE_HOST", - ] - _langfuse_env_vars = {} - for _var in _langfuse_vars: - env_variable = environment_variables.get(_var, None) - if env_variable is None: - _langfuse_env_vars[_var] = None - else: - # decode + decrypt the value - decrypted_value = decrypt_value_helper( - value=env_variable, key=_var - ) - _langfuse_env_vars[_var] = decrypted_value - - _data_to_return.append( - {"name": _callback, "variables": _langfuse_env_vars} - ) + _data_to_return.append(process_callback(_callback, "success")) + + for _callback in _failure_callbacks: + _data_to_return.append(process_callback(_callback, "failure")) + + for _callback in _generic_callbacks: + _data_to_return.append(process_callback(_callback, "generic")) # Check if slack alerting is on _alerting = _general_settings.get("alerting", []) diff --git a/tests/proxy_unit_tests/test_proxy_server.py b/tests/proxy_unit_tests/test_proxy_server.py index 1f4bf806c16..3aeb8a840c9 100644 --- a/tests/proxy_unit_tests/test_proxy_server.py +++ b/tests/proxy_unit_tests/test_proxy_server.py @@ -2371,3 +2371,166 @@ def test_non_root_ui_path_logic(monkeypatch, tmp_path, ui_exists, ui_has_content error_calls = [call[0][0] for call in mock_logger.error.call_args_list] assert any("Path exists:" in call for call in error_calls) assert mock_logger.info.call_count == 0 + + +@pytest.mark.asyncio +async def test_get_config_callbacks_with_all_types(client_no_auth): + """ + Test that /get/config/callbacks returns all three callback types: + - success_callback with type="success" + - failure_callback with type="failure" + - callbacks (generic) with type="generic" + """ + from litellm.proxy.proxy_server import ProxyConfig + + # Create a mock config with all three callback types + mock_config_data = { + "litellm_settings": { + "success_callback": ["langfuse", "braintrust"], + "failure_callback": ["sentry"], + "callbacks": ["otel", "langsmith"] + }, + "environment_variables": { + "LANGFUSE_PUBLIC_KEY": "test-public-key", + "LANGFUSE_SECRET_KEY": "test-secret-key", + "LANGFUSE_HOST": "https://test.langfuse.com", + "BRAINTRUST_API_KEY": "test-braintrust-key", + "OTEL_EXPORTER": "otlp", + "OTEL_ENDPOINT": "http://localhost:4317", + "LANGSMITH_API_KEY": "test-langsmith-key", + }, + "general_settings": {} + } + + proxy_config = getattr(litellm.proxy.proxy_server, "proxy_config") + + with patch.object( + proxy_config, "get_config", new=AsyncMock(return_value=mock_config_data) + ), patch( + "litellm.proxy.proxy_server.decrypt_value_helper", + side_effect=lambda value, key=None: value + ): + response = client_no_auth.get("/get/config/callbacks") + + assert response.status_code == 200 + result = response.json() + + # Verify response structure + assert "status" in result + assert result["status"] == "success" + assert "callbacks" in result + + callbacks = result["callbacks"] + + # Verify we have all 5 callbacks (2 success + 1 failure + 2 generic) + assert len(callbacks) == 5 + + # Group callbacks by type + success_callbacks = [cb for cb in callbacks if cb.get("type") == "success"] + failure_callbacks = [cb for cb in callbacks if cb.get("type") == "failure"] + generic_callbacks = [cb for cb in callbacks if cb.get("type") == "generic"] + + # Verify all callbacks have required fields + for callback in callbacks: + assert "name" in callback + assert "variables" in callback + assert "type" in callback + assert callback["type"] in ["success", "failure", "generic"] + + # Verify success callbacks + assert len(success_callbacks) == 2 + success_names = [cb["name"] for cb in success_callbacks] + assert "langfuse" in success_names + assert "braintrust" in success_names + + # Verify failure callbacks + assert len(failure_callbacks) == 1 + assert failure_callbacks[0]["name"] == "sentry" + + # Verify generic callbacks + assert len(generic_callbacks) == 2 + generic_names = [cb["name"] for cb in generic_callbacks] + assert "otel" in generic_names + assert "langsmith" in generic_names + + +@pytest.mark.asyncio +async def test_get_config_callbacks_environment_variables(client_no_auth): + """ + Test that /get/config/callbacks correctly includes environment variables + for each callback type with proper decryption. + """ + from litellm.proxy.proxy_server import ProxyConfig + + # Create a mock config with callbacks and their env vars + mock_config_data = { + "litellm_settings": { + "success_callback": ["langfuse"], + "failure_callback": [], + "callbacks": ["otel"] + }, + "environment_variables": { + "LANGFUSE_PUBLIC_KEY": "encrypted-public-key", + "LANGFUSE_SECRET_KEY": "encrypted-secret-key", + "LANGFUSE_HOST": "https://cloud.langfuse.com", + "OTEL_EXPORTER": "otlp", + "OTEL_ENDPOINT": "http://localhost:4317", + "OTEL_HEADERS": "key=value", + }, + "general_settings": {} + } + + # Mock decrypt to prepend "decrypted-" to values + def mock_decrypt(value, key=None): + if value and isinstance(value, str) and "encrypted" in value: + return f"decrypted-{value}" + return value + + proxy_config = getattr(litellm.proxy.proxy_server, "proxy_config") + + with patch.object( + proxy_config, "get_config", new=AsyncMock(return_value=mock_config_data) + ), patch( + "litellm.proxy.proxy_server.decrypt_value_helper", + side_effect=mock_decrypt + ): + response = client_no_auth.get("/get/config/callbacks") + + assert response.status_code == 200 + result = response.json() + + callbacks = result["callbacks"] + + # Find langfuse callback (success type) + langfuse_callback = next( + (cb for cb in callbacks if cb["name"] == "langfuse"), None + ) + assert langfuse_callback is not None + assert langfuse_callback["type"] == "success" + assert "variables" in langfuse_callback + + # Verify langfuse env vars are present and decrypted + langfuse_vars = langfuse_callback["variables"] + assert "LANGFUSE_PUBLIC_KEY" in langfuse_vars + assert langfuse_vars["LANGFUSE_PUBLIC_KEY"] == "decrypted-encrypted-public-key" + assert "LANGFUSE_SECRET_KEY" in langfuse_vars + assert langfuse_vars["LANGFUSE_SECRET_KEY"] == "decrypted-encrypted-secret-key" + assert "LANGFUSE_HOST" in langfuse_vars + assert langfuse_vars["LANGFUSE_HOST"] == "https://cloud.langfuse.com" + + # Find otel callback (generic type) + otel_callback = next( + (cb for cb in callbacks if cb["name"] == "otel"), None + ) + assert otel_callback is not None + assert otel_callback["type"] == "generic" + assert "variables" in otel_callback + + # Verify otel env vars are present + otel_vars = otel_callback["variables"] + assert "OTEL_EXPORTER" in otel_vars + assert otel_vars["OTEL_EXPORTER"] == "otlp" + assert "OTEL_ENDPOINT" in otel_vars + assert otel_vars["OTEL_ENDPOINT"] == "http://localhost:4317" + assert "OTEL_HEADERS" in otel_vars + assert otel_vars["OTEL_HEADERS"] == "key=value" diff --git a/ui/litellm-dashboard/src/components/settings.test.tsx b/ui/litellm-dashboard/src/components/settings.test.tsx new file mode 100644 index 00000000000..72a265eadd9 --- /dev/null +++ b/ui/litellm-dashboard/src/components/settings.test.tsx @@ -0,0 +1,136 @@ +import { render, screen, fireEvent, waitFor } from "@testing-library/react"; +import { describe, expect, it, beforeAll, beforeEach, vi } from "vitest"; +import Settings from "./settings"; +import * as networking from "./networking"; + +beforeAll(() => { + Object.defineProperty(window, "matchMedia", { + writable: true, + value: (query: string) => ({ + matches: false, + media: query, + onchange: null, + addListener: () => {}, + removeListener: () => {}, + addEventListener: () => {}, + removeEventListener: () => {}, + dispatchEvent: () => true, + }), + }); +}); + +const mockCallbacksData = { + callbacks: [ + { + name: "langfuse", + type: "success", + variables: { + LANGFUSE_PUBLIC_KEY: "test_key", + LANGFUSE_SECRET_KEY: "test_secret", + }, + }, + { + name: "datadog", + type: "success", + variables: { + DD_API_KEY: "test_dd_key", + }, + }, + ], + available_callbacks: [ + { + litellm_callback_name: "langfuse", + ui_callback_name: "Langfuse", + litellm_callback_params: ["LANGFUSE_PUBLIC_KEY", "LANGFUSE_SECRET_KEY"], + }, + { + litellm_callback_name: "datadog", + ui_callback_name: "Datadog", + litellm_callback_params: ["DD_API_KEY"], + }, + ], + alerts: [], +}; + +describe("Settings", () => { + it("should render the settings page", () => { + render(); + }); +}); + +describe("Logging Callbacks Section", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("should display the list of active callbacks", async () => { + vi.spyOn(networking, "getCallbacksCall").mockResolvedValue(mockCallbacksData); + + render(); + + await waitFor( + () => { + expect(screen.getByText("langfuse")).toBeInTheDocument(); + expect(screen.getByText("datadog")).toBeInTheDocument(); + }, + { timeout: 3000 }, + ); + }); + + it("should open add callback modal and display form", async () => { + vi.spyOn(networking, "getCallbacksCall").mockResolvedValue(mockCallbacksData); + + render(); + + await waitFor(() => { + expect(screen.getByText("langfuse")).toBeInTheDocument(); + }); + + const addButton = screen.getByText("Add Callback"); + fireEvent.click(addButton); + + await waitFor(() => { + expect(screen.getByText("Add Logging Callback")).toBeInTheDocument(); + expect(screen.getByText("LiteLLM Docs: Logging")).toBeInTheDocument(); + }); + }); + + it("should successfully delete a callback", async () => { + const getCallbacksSpy = vi.spyOn(networking, "getCallbacksCall").mockResolvedValue(mockCallbacksData); + const deleteCallbackSpy = vi.spyOn(networking, "deleteCallback").mockResolvedValue(undefined); + + const { container } = render( + , + ); + + await waitFor(() => { + expect(screen.getByText("langfuse")).toBeInTheDocument(); + }); + + const trashIcons = container.querySelectorAll("svg"); + const trashIcon = Array.from(trashIcons).find((svg) => { + const parentElement = svg.parentElement; + return parentElement?.className.includes("text-red") || parentElement?.outerHTML.includes("red"); + }); + + expect(trashIcon).toBeDefined(); + if (trashIcon && trashIcon.parentElement) { + fireEvent.click(trashIcon.parentElement); + } + + await waitFor(() => { + const modalText = screen.getByText((content, element) => { + return element?.tagName.toLowerCase() === "p" && content.includes("Are you sure you want to delete"); + }); + expect(modalText).toBeInTheDocument(); + }); + + const deleteButton = screen.getByRole("button", { name: "Delete" }); + fireEvent.click(deleteButton); + + await waitFor(() => { + expect(deleteCallbackSpy).toHaveBeenCalledWith("test-token", "langfuse"); + expect(getCallbacksSpy).toHaveBeenCalledTimes(2); + }); + }); +}); diff --git a/ui/litellm-dashboard/src/components/settings.tsx b/ui/litellm-dashboard/src/components/settings.tsx index 727c8b9b511..a275214918c 100644 --- a/ui/litellm-dashboard/src/components/settings.tsx +++ b/ui/litellm-dashboard/src/components/settings.tsx @@ -19,11 +19,12 @@ import { Tab, SelectItem, Icon, + Badge, } from "@tremor/react"; import { PencilAltIcon, TrashIcon } from "@heroicons/react/outline"; -import { Modal, Typography, Form, Input, Select, Button as Button2 } from "antd"; +import { Modal, Typography, Form, Input, Select, Button as Button2, Tooltip } from "antd"; import NotificationsManager from "./molecules/notifications_manager"; import EmailSettings from "./email_settings"; @@ -32,10 +33,7 @@ const { Title, Paragraph } = Typography; import { getCallbacksCall, setCallbacksCall, serviceHealthCheck, deleteCallback } from "./networking"; import AlertingSettings from "./alerting/alerting_settings"; import FormItem from "antd/es/form/FormItem"; -import { - CALLBACK_CONFIGS, - getCallbackById, -} from "./callback_info_helpers"; +import { CALLBACK_CONFIGS, getCallbackById } from "./callback_info_helpers"; import { parseErrorMessage } from "./shared/errorUtils"; interface SettingsPageProps { accessToken: string | null; @@ -60,6 +58,7 @@ interface AlertingVariables { interface AlertingObject { name: string; + type?: "success" | "failure" | "generic"; variables: AlertingVariables; } @@ -216,10 +215,10 @@ const Settings: React.FC = ({ accessToken, userRole, userID, const handleSelectedCallbackChange = (callbackName: string) => { setSelectedCallback(callbackName); - + // Get the callback configuration using the new clean structure const callbackConfig = getCallbackById(callbackName); - + // Get the parameters from the callback configuration if (callbackConfig?.dynamic_params) { const params = Object.keys(callbackConfig.dynamic_params); @@ -228,7 +227,7 @@ const Settings: React.FC = ({ accessToken, userRole, userID, setSelectedCallbackParams([]); } }; - + const handleSaveAlerts = async () => { if (!accessToken) { return; @@ -416,54 +415,94 @@ const Settings: React.FC = ({ accessToken, userRole, userID, Active Logging Callbacks - + Callback Name - {/* Callback Env Vars */} + Callback Type + Actions - {callbacks.map((callback, index) => ( - - - {callback.name} - - - - { - setSelectedEditCallback(callback); - setShowEditCallback(true); - }} - /> - handleDeleteCallback(callback.name)} - className="text-red-500 hover:text-red-700 cursor-pointer" - /> - - - - - ))} + {callbacks.map((callback, index) => { + const canEdit = !callback.type || callback.type === "success"; + const tooltipMessage = + callback.type === "failure" + ? "Modifications and deletion of failure type callbacks are not yet supported in the UI" + : callback.type === "generic" + ? "Modifications and deletion of generic type callbacks are not yet supported in the UI" + : ""; + + const getBadgeColor = (type?: string) => { + if (type === "success") return "green"; + if (type === "failure") return "red"; + if (type === "generic") return "blue"; + return "gray"; + }; + + return ( + + + {callback.name} + + + {callback.type ? ( + {callback.type} + ) : ( + success + )} + + +
+ + { + if (canEdit) { + setSelectedEditCallback(callback); + setShowEditCallback(true); + } + }} + className={canEdit ? "cursor-pointer" : "opacity-40 cursor-not-allowed"} + /> + + + { + if (canEdit) { + handleDeleteCallback(callback.name); + } + }} + className={ + canEdit + ? "text-red-500 hover:text-red-700 cursor-pointer" + : "text-red-300 opacity-40 cursor-not-allowed" + } + /> + + +
+
+
+ ); + })}
@@ -594,124 +633,109 @@ const Settings: React.FC = ({ accessToken, userRole, userID, wrapperCol={{ span: 16 }} labelAlign="left" > - + - (option?.children?.toString() ?? "") - .toLowerCase() - .includes(input.toLowerCase()) - } - onChange={(value) => { - handleSelectedCallbackChange(value); - }} - > - {CALLBACK_CONFIGS.map((callbackConfig) => ( - -
-
- {/* eslint-disable-next-line @next/next/no-img-element */} - {`${callbackConfig.displayName} { - e.currentTarget.style.display = 'none'; - }} - /> -
- - {callbackConfig.displayName} - + {CALLBACK_CONFIGS.map((callbackConfig) => ( + +
+
+ {/* eslint-disable-next-line @next/next/no-img-element */} + {`${callbackConfig.displayName} { + e.currentTarget.style.display = "none"; + }} + />
- - ))} - - + {callbackConfig.displayName} +
+
+ ))} + + - {selectedCallbackParams && selectedCallbackParams.length > 0 && ( -
- {selectedCallbackParams.map((param) => { - // Get the callback configuration to look up parameter types - const callbackConfig = getCallbackById(selectedCallback || ''); - const paramType = callbackConfig?.dynamic_params[param] || "text"; - - const fieldLabel = param.replace(/_/g, " ").replace(/\b\w/g, l => l.toUpperCase()); - - return ( - - {fieldLabel} - * - - } - name={param} - key={param} - className="mb-4" - rules={[ - { - required: true, - message: `Please enter the ${fieldLabel.toLowerCase()}`, - }, - ]} - > - {paramType === "password" ? ( - - ) : paramType === "number" ? ( - - ) : ( - - )} - - ); - })} -
- )} + {selectedCallbackParams && selectedCallbackParams.length > 0 && ( +
+ {selectedCallbackParams.map((param) => { + // Get the callback configuration to look up parameter types + const callbackConfig = getCallbackById(selectedCallback || ""); + const paramType = callbackConfig?.dynamic_params[param] || "text"; -
- - - Add Callback - + const fieldLabel = param.replace(/_/g, " ").replace(/\b\w/g, (l) => l.toUpperCase()); + + return ( + + {fieldLabel} + * + + } + name={param} + key={param} + className="mb-4" + rules={[ + { + required: true, + message: `Please enter the ${fieldLabel.toLowerCase()}`, + }, + ]} + > + {paramType === "password" ? ( + + ) : paramType === "number" ? ( + + ) : ( + + )} + + ); + })}
+ )} + +
+ + Add Callback +
From 0af3a51a974130009027355baa51b16750741d8b Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Thu, 6 Nov 2025 12:48:04 -0800 Subject: [PATCH 002/370] Fix tests --- ui/litellm-dashboard/src/components/settings.test.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ui/litellm-dashboard/src/components/settings.test.tsx b/ui/litellm-dashboard/src/components/settings.test.tsx index 72a265eadd9..20dadf9d2a5 100644 --- a/ui/litellm-dashboard/src/components/settings.test.tsx +++ b/ui/litellm-dashboard/src/components/settings.test.tsx @@ -54,6 +54,10 @@ const mockCallbacksData = { describe("Settings", () => { it("should render the settings page", () => { + vi.spyOn(networking, "alertingSettingsCall").mockResolvedValue([]); + vi.spyOn(networking, "getEmailEventSettings").mockResolvedValue({ settings: [] }); + vi.spyOn(networking, "getCallbacksCall").mockResolvedValue(mockCallbacksData); + render(); }); }); @@ -61,6 +65,8 @@ describe("Settings", () => { describe("Logging Callbacks Section", () => { beforeEach(() => { vi.clearAllMocks(); + vi.spyOn(networking, "alertingSettingsCall").mockResolvedValue([]); + vi.spyOn(networking, "getEmailEventSettings").mockResolvedValue({ settings: [] }); }); it("should display the list of active callbacks", async () => { From 3a96c700b4173aca63a90283ecfce57bfc636f9d Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Fri, 7 Nov 2025 15:02:41 -0800 Subject: [PATCH 003/370] Adjusted based on comments --- litellm/integrations/custom_logger.py | 38 +++++++++++++++++++ litellm/proxy/_types.py | 8 ++++ litellm/proxy/proxy_server.py | 37 +++--------------- tests/local_testing/test_custom_logger.py | 18 +++++++++ .../src/components/settings.tsx | 19 +++++++--- 5 files changed, 82 insertions(+), 38 deletions(-) diff --git a/litellm/integrations/custom_logger.py b/litellm/integrations/custom_logger.py index fd8ab2bad9d..2a08408f7de 100644 --- a/litellm/integrations/custom_logger.py +++ b/litellm/integrations/custom_logger.py @@ -81,6 +81,44 @@ class CustomLogger: # https://docs.litellm.ai/docs/observability/custom_callbac self.turn_off_message_logging = turn_off_message_logging pass + @staticmethod + def get_callback_env_vars(callback_name: Optional[str] = None) -> List[str]: + """ + Return the environment variables associated with a given callback + name as defined in the proxy callback registry. + + Args: + callback_name: The name of the callback to look up. + + Returns: + List[str]: A list of required environment variable names. + """ + if callback_name is None: + return [] + + normalized_name = callback_name.lower() + + alias_map = { + "langfuse_otel": "langfuse", + } + lookup_name = alias_map.get(normalized_name, normalized_name) + + try: + from litellm.proxy._types import AllCallbacks + except Exception: + return [] + + callbacks = AllCallbacks() + callback_info = getattr(callbacks, lookup_name, None) + if callback_info is None: + return [] + + params = getattr(callback_info, "litellm_callback_params", None) + if not params: + return [] + + return list(params) + def log_pre_api_call(self, model, messages, kwargs): pass diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index d739727ccab..397bfc9c3b8 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -2503,6 +2503,14 @@ class AllCallbacks(LiteLLMPydanticObjectBase): ui_callback_name="Lago Billing", ) + traceloop: CallbackOnUI = CallbackOnUI( + litellm_callback_name="traceloop", + litellm_callback_params=[ + "TRACELoop_API_KEY", + ], + ui_callback_name="Traceloop", + ) + class SpendLogsMetadata(TypedDict): """ diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 4a0d39f4518..9a582814a55 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -153,6 +153,7 @@ from litellm.constants import ( ) from litellm.exceptions import RejectedRequestError from litellm.integrations.SlackAlerting.slack_alerting import SlackAlerting +from litellm.integrations.custom_logger import CustomLogger from litellm.litellm_core_utils.core_helpers import ( _get_parent_otel_span_from_kwargs, get_litellm_metadata_from_kwargs, @@ -9575,35 +9576,7 @@ async def get_config(): # noqa: PLR0915 # Helper function to process callbacks and get environment variables def process_callback(_callback: str, callback_type: str) -> dict: """Process a single callback and return its data with environment variables""" - if _callback == "langfuse" or _callback == "langfuse_otel": - env_vars = [ - "LANGFUSE_PUBLIC_KEY", - "LANGFUSE_SECRET_KEY", - "LANGFUSE_HOST", - ] - elif _callback == "openmeter": - env_vars = [ - "OPENMETER_API_KEY", - ] - elif _callback == "braintrust": - env_vars = [ - "BRAINTRUST_API_KEY", - "BRAINTRUST_API_BASE", - ] - elif _callback == "traceloop": - env_vars = ["TRACELOOP_API_KEY"] - elif _callback == "custom_callback_api": - env_vars = ["GENERIC_LOGGER_ENDPOINT"] - elif _callback == "otel": - env_vars = ["OTEL_EXPORTER", "OTEL_ENDPOINT", "OTEL_HEADERS"] - elif _callback == "langsmith": - env_vars = [ - "LANGSMITH_API_KEY", - "LANGSMITH_PROJECT", - "LANGSMITH_DEFAULT_RUN_NAME", - ] - else: - env_vars = [] + env_vars = CustomLogger.get_callback_env_vars(_callback) env_vars_dict = {} for _var in env_vars: @@ -9625,7 +9598,7 @@ async def get_config(): # noqa: PLR0915 _success_callbacks = _litellm_settings.get("success_callback", []) _failure_callbacks = _litellm_settings.get("failure_callback", []) - _generic_callbacks = _litellm_settings.get("callbacks", []) + _success_and_failure_callbacks = _litellm_settings.get("callbacks", []) _data_to_return = [] """ @@ -9649,8 +9622,8 @@ async def get_config(): # noqa: PLR0915 for _callback in _failure_callbacks: _data_to_return.append(process_callback(_callback, "failure")) - for _callback in _generic_callbacks: - _data_to_return.append(process_callback(_callback, "generic")) + for _callback in _success_and_failure_callbacks: + _data_to_return.append(process_callback(_callback, "success_and_failure")) # Check if slack alerting is on _alerting = _general_settings.get("alerting", []) diff --git a/tests/local_testing/test_custom_logger.py b/tests/local_testing/test_custom_logger.py index 00e7c2d5aa1..f3dc6a0a7a4 100644 --- a/tests/local_testing/test_custom_logger.py +++ b/tests/local_testing/test_custom_logger.py @@ -103,6 +103,24 @@ class TmpFunction: ) +def test_get_callback_env_vars(): + env_vars = CustomLogger.get_callback_env_vars("langfuse") + assert env_vars == [ + "LANGFUSE_PUBLIC_KEY", + "LANGFUSE_SECRET_KEY", + "LANGFUSE_HOST", + ] + + alias_env_vars = CustomLogger.get_callback_env_vars("langfuse_otel") + assert alias_env_vars == env_vars + + missing_env_vars = CustomLogger.get_callback_env_vars("does_not_exist") + assert missing_env_vars == [] + + none_env_vars = CustomLogger.get_callback_env_vars(None) + assert none_env_vars == [] + + @pytest.mark.asyncio async def test_async_chat_openai_stream(): try: diff --git a/ui/litellm-dashboard/src/components/settings.tsx b/ui/litellm-dashboard/src/components/settings.tsx index a275214918c..e8ac54e99dd 100644 --- a/ui/litellm-dashboard/src/components/settings.tsx +++ b/ui/litellm-dashboard/src/components/settings.tsx @@ -58,7 +58,7 @@ interface AlertingVariables { interface AlertingObject { name: string; - type?: "success" | "failure" | "generic"; + type?: "success" | "failure" | "success_and_failure"; variables: AlertingVariables; } @@ -430,17 +430,24 @@ const Settings: React.FC = ({ accessToken, userRole, userID, const tooltipMessage = callback.type === "failure" ? "Modifications and deletion of failure type callbacks are not yet supported in the UI" - : callback.type === "generic" - ? "Modifications and deletion of generic type callbacks are not yet supported in the UI" + : callback.type === "success_and_failure" + ? "Modifications and deletion of success and failure type callbacks are not yet supported in the UI" : ""; const getBadgeColor = (type?: string) => { if (type === "success") return "green"; if (type === "failure") return "red"; - if (type === "generic") return "blue"; + if (type === "success_and_failure") return "blue"; return "gray"; }; + const getBadgeLabel = (type?: string) => { + if (type === "success") return "Success Only"; + if (type === "failure") return "Failure Only"; + if (type === "success_and_failure") return "Success & Failure"; + return "Unknown"; + }; + return ( @@ -448,9 +455,9 @@ const Settings: React.FC = ({ accessToken, userRole, userID, {callback.type ? ( - {callback.type} + {getBadgeLabel(callback.type)} ) : ( - success + Unknown )} From 9be008a54b22d41d4f6727c10afe9335a6c7827a Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Sat, 8 Nov 2025 14:50:53 -0800 Subject: [PATCH 004/370] Fixed typo --- litellm/proxy/_types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 397bfc9c3b8..17be7c18aab 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -2506,7 +2506,7 @@ class AllCallbacks(LiteLLMPydanticObjectBase): traceloop: CallbackOnUI = CallbackOnUI( litellm_callback_name="traceloop", litellm_callback_params=[ - "TRACELoop_API_KEY", + "TRACELOOP_API_KEY", ], ui_callback_name="Traceloop", ) From 67bca6dde4aaacda4c2acadab0bb5f61aaaeaa10 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Sat, 8 Nov 2025 15:00:58 -0800 Subject: [PATCH 005/370] Fix linting --- litellm/proxy/proxy_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 9a582814a55..4acd2639d8f 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -9578,7 +9578,7 @@ async def get_config(): # noqa: PLR0915 """Process a single callback and return its data with environment variables""" env_vars = CustomLogger.get_callback_env_vars(_callback) - env_vars_dict = {} + env_vars_dict: dict[str, str | None] = {} for _var in env_vars: env_variable = environment_variables.get(_var, None) if env_variable is None: From 7833b3fdb4ed3d52dbf37d8f8c9c8923b6fd6386 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Mon, 10 Nov 2025 17:28:13 -0800 Subject: [PATCH 006/370] Addressing comments --- litellm/proxy/common_utils/callback_utils.py | 26 ++++++++++ litellm/proxy/proxy_server.py | 30 ++---------- tests/proxy_unit_tests/test_proxy_server.py | 26 +++++----- .../proxy/common_utils/test_callback_utils.py | 47 +++++++++++++++++++ 4 files changed, 90 insertions(+), 39 deletions(-) diff --git a/litellm/proxy/common_utils/callback_utils.py b/litellm/proxy/common_utils/callback_utils.py index fb7ada8ab10..eb312612779 100644 --- a/litellm/proxy/common_utils/callback_utils.py +++ b/litellm/proxy/common_utils/callback_utils.py @@ -3,8 +3,12 @@ from typing import Any, Dict, List, Literal, Optional import litellm from litellm import get_secret from litellm._logging import verbose_proxy_logger +from litellm.integrations.custom_logger import CustomLogger from litellm.proxy._types import CommonProxyErrors, LiteLLMPromptInjectionParams from litellm.proxy.types_utils.utils import get_instance_fn +from litellm.proxy.common_utils.encrypt_decrypt_utils import ( + decrypt_value_helper, +) blue_color_code = "\033[94m" reset_color_code = "\033[0m" @@ -382,3 +386,25 @@ def get_metadata_variable_name_from_kwargs( - LiteLLM is now moving to using `litellm_metadata` for our metadata """ return "litellm_metadata" if "litellm_metadata" in kwargs else "metadata" + +def process_callback(_callback: str, callback_type: str, environment_variables: dict) -> dict: + """Process a single callback and return its data with environment variables""" + env_vars = CustomLogger.get_callback_env_vars(_callback) + + env_vars_dict: dict[str, str | None] = {} + for _var in env_vars: + env_variable = environment_variables.get(_var, None) + if env_variable is None: + env_vars_dict[_var] = None + else: + # decode + decrypt the value + decrypted_value = decrypt_value_helper( + value=env_variable, key=_var + ) + env_vars_dict[_var] = decrypted_value + + return { + "name": _callback, + "variables": env_vars_dict, + "type": callback_type + } \ No newline at end of file diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 090757da4ad..205609a645c 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -47,6 +47,7 @@ from litellm.types.utils import ( TokenCountResponse, ) from litellm.utils import load_credentials_from_list +from litellm.proxy.common_utils.callback_utils import process_callback if TYPE_CHECKING: from aiohttp import ClientSession @@ -9582,29 +9583,6 @@ async def get_config(): # noqa: PLR0915 _general_settings = config_data.get("general_settings", {}) environment_variables = config_data.get("environment_variables", {}) - # Helper function to process callbacks and get environment variables - def process_callback(_callback: str, callback_type: str) -> dict: - """Process a single callback and return its data with environment variables""" - env_vars = CustomLogger.get_callback_env_vars(_callback) - - env_vars_dict: dict[str, str | None] = {} - for _var in env_vars: - env_variable = environment_variables.get(_var, None) - if env_variable is None: - env_vars_dict[_var] = None - else: - # decode + decrypt the value - decrypted_value = decrypt_value_helper( - value=env_variable, key=_var - ) - env_vars_dict[_var] = decrypted_value - - return { - "name": _callback, - "variables": env_vars_dict, - "type": callback_type - } - _success_callbacks = _litellm_settings.get("success_callback", []) _failure_callbacks = _litellm_settings.get("failure_callback", []) _success_and_failure_callbacks = _litellm_settings.get("callbacks", []) @@ -9626,13 +9604,13 @@ async def get_config(): # noqa: PLR0915 """ for _callback in _success_callbacks: - _data_to_return.append(process_callback(_callback, "success")) + _data_to_return.append(process_callback(_callback, "success", environment_variables)) for _callback in _failure_callbacks: - _data_to_return.append(process_callback(_callback, "failure")) + _data_to_return.append(process_callback(_callback, "failure", environment_variables)) for _callback in _success_and_failure_callbacks: - _data_to_return.append(process_callback(_callback, "success_and_failure")) + _data_to_return.append(process_callback(_callback, "success_and_failure", environment_variables)) # Check if slack alerting is on _alerting = _general_settings.get("alerting", []) diff --git a/tests/proxy_unit_tests/test_proxy_server.py b/tests/proxy_unit_tests/test_proxy_server.py index 71b4503ff3c..17a6f8eb9ae 100644 --- a/tests/proxy_unit_tests/test_proxy_server.py +++ b/tests/proxy_unit_tests/test_proxy_server.py @@ -2409,7 +2409,7 @@ async def test_get_config_callbacks_with_all_types(client_no_auth): Test that /get/config/callbacks returns all three callback types: - success_callback with type="success" - failure_callback with type="failure" - - callbacks (generic) with type="generic" + - callbacks (success_and_failure) with type="success_and_failure" """ from litellm.proxy.proxy_server import ProxyConfig @@ -2437,7 +2437,7 @@ async def test_get_config_callbacks_with_all_types(client_no_auth): with patch.object( proxy_config, "get_config", new=AsyncMock(return_value=mock_config_data) ), patch( - "litellm.proxy.proxy_server.decrypt_value_helper", + "litellm.proxy.common_utils.callback_utils.decrypt_value_helper", side_effect=lambda value, key=None: value ): response = client_no_auth.get("/get/config/callbacks") @@ -2452,20 +2452,20 @@ async def test_get_config_callbacks_with_all_types(client_no_auth): callbacks = result["callbacks"] - # Verify we have all 5 callbacks (2 success + 1 failure + 2 generic) + # Verify we have all 5 callbacks (2 success + 1 failure + 2 success_and_failure) assert len(callbacks) == 5 # Group callbacks by type success_callbacks = [cb for cb in callbacks if cb.get("type") == "success"] failure_callbacks = [cb for cb in callbacks if cb.get("type") == "failure"] - generic_callbacks = [cb for cb in callbacks if cb.get("type") == "generic"] + success_and_failure_callbacks = [cb for cb in callbacks if cb.get("type") == "success_and_failure"] # Verify all callbacks have required fields for callback in callbacks: assert "name" in callback assert "variables" in callback assert "type" in callback - assert callback["type"] in ["success", "failure", "generic"] + assert callback["type"] in ["success", "failure", "success_and_failure"] # Verify success callbacks assert len(success_callbacks) == 2 @@ -2477,11 +2477,11 @@ async def test_get_config_callbacks_with_all_types(client_no_auth): assert len(failure_callbacks) == 1 assert failure_callbacks[0]["name"] == "sentry" - # Verify generic callbacks - assert len(generic_callbacks) == 2 - generic_names = [cb["name"] for cb in generic_callbacks] - assert "otel" in generic_names - assert "langsmith" in generic_names + # Verify success_and_failure callbacks + assert len(success_and_failure_callbacks) == 2 + success_and_failure_names = [cb["name"] for cb in success_and_failure_callbacks] + assert "otel" in success_and_failure_names + assert "langsmith" in success_and_failure_names @pytest.mark.asyncio @@ -2521,7 +2521,7 @@ async def test_get_config_callbacks_environment_variables(client_no_auth): with patch.object( proxy_config, "get_config", new=AsyncMock(return_value=mock_config_data) ), patch( - "litellm.proxy.proxy_server.decrypt_value_helper", + "litellm.proxy.common_utils.callback_utils.decrypt_value_helper", side_effect=mock_decrypt ): response = client_no_auth.get("/get/config/callbacks") @@ -2548,12 +2548,12 @@ async def test_get_config_callbacks_environment_variables(client_no_auth): assert "LANGFUSE_HOST" in langfuse_vars assert langfuse_vars["LANGFUSE_HOST"] == "https://cloud.langfuse.com" - # Find otel callback (generic type) + # Find otel callback (success_and_failure type) otel_callback = next( (cb for cb in callbacks if cb["name"] == "otel"), None ) assert otel_callback is not None - assert otel_callback["type"] == "generic" + assert otel_callback["type"] == "success_and_failure" assert "variables" in otel_callback # Verify otel env vars are present diff --git a/tests/test_litellm/proxy/common_utils/test_callback_utils.py b/tests/test_litellm/proxy/common_utils/test_callback_utils.py index b9ed4b9b508..877f0092182 100644 --- a/tests/test_litellm/proxy/common_utils/test_callback_utils.py +++ b/tests/test_litellm/proxy/common_utils/test_callback_utils.py @@ -9,6 +9,9 @@ from litellm.proxy.common_utils.callback_utils import ( get_remaining_tokens_and_requests_from_request_data, ) +from unittest.mock import patch +from litellm.proxy.common_utils.callback_utils import process_callback + def test_get_remaining_tokens_and_requests_from_request_data(): model_group = "openrouter/google/gemini-2.0-flash-001" @@ -27,3 +30,47 @@ def test_get_remaining_tokens_and_requests_from_request_data(): f"x-litellm-key-remaining-requests-{expected_name}": 100, f"x-litellm-key-remaining-tokens-{expected_name}": 200, } + + +@patch( + "litellm.proxy.common_utils.callback_utils.CustomLogger.get_callback_env_vars", + return_value=["API_KEY", "MISSING_VAR"], +) +@patch( + "litellm.proxy.common_utils.callback_utils.decrypt_value_helper", + side_effect=lambda value, key: f"decrypted-{key}", +) +def test_process_callback_with_env_vars(mock_decrypt, mock_get_env_vars): + environment_variables = { + "API_KEY": "ENC_VALUE", + "UNUSED": "SHOULD_BE_IGNORED", + } + + result = process_callback( + _callback="my_callback", + callback_type="input", + environment_variables=environment_variables, + ) + + assert result["name"] == "my_callback" + assert result["type"] == "input" + assert result["variables"] == { + "API_KEY": "decrypted-API_KEY", + "MISSING_VAR": None, + } + + +@patch( + "litellm.proxy.common_utils.callback_utils.CustomLogger.get_callback_env_vars", + return_value=[], +) +def test_process_callback_with_no_required_env_vars(mock_get_env_vars): + result = process_callback( + _callback="another_callback", + callback_type="output", + environment_variables={"SHOULD_NOT_BE_USED": "VALUE"}, + ) + + assert result["name"] == "another_callback" + assert result["type"] == "output" + assert result["variables"] == {} From d01efcb3084a0a3bd7302294d1072ab8ab247ecd Mon Sep 17 00:00:00 2001 From: AlexsanderHamir Date: Sat, 15 Nov 2025 15:58:41 -0800 Subject: [PATCH 007/370] speech set up --- no_cache_hits.py | 48 +++++++++++++++++++++++++++++++++++++++++++++ speech.mp3 | Bin 0 -> 104 bytes speech_config.yaml | 9 +++++++++ 3 files changed, 57 insertions(+) create mode 100644 no_cache_hits.py create mode 100644 speech.mp3 create mode 100644 speech_config.yaml diff --git a/no_cache_hits.py b/no_cache_hits.py new file mode 100644 index 00000000000..1b3bf895f77 --- /dev/null +++ b/no_cache_hits.py @@ -0,0 +1,48 @@ +from locust import HttpUser, between, task + + +class MyUser(HttpUser): + """ + Minimal Locust user for repeatedly hitting `/v1/audio/speech`. + The goal is to measure server-side performance, so we avoid any extra work + (file writes, random generation, manual timing, custom event hooks, etc.) + that could inflate client-side latency. + """ + + wait_time = between(0.5, 1) + host = "http://0.0.0.0:8090" + + def on_start(self): + self.api_key = "sk-1234" + self.model_name = "fake-openai-speech" + self.headers = { + "Authorization": f"Bearer {self.api_key}", + "Content-Type": "application/json", + } + self.prompt_counter = 0 + + @task + def audio_speech_request(self): + self.prompt_counter += 1 + # Ensure prompts differ slightly so the backend can't reuse cached audio. + prompt = ( + "Generate a short spoken status update mentioning counter " + f"{self.prompt_counter}." + ) + + response = self.client.post( + "v1/audio/speech", + json={ + "model": self.model_name, + "input": prompt, + "voice": "alloy", + "format": "mp3", + }, + headers=self.headers, + name="audio_speech", + ) + + if response.status_code != 200: + # log the errors in error.txt + with open("error.txt", "a") as error_log: + error_log.write(response.text + "\n") \ No newline at end of file diff --git a/speech.mp3 b/speech.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..f4f854d9bd215e2493d48b4bc4d39804bd79c038 GIT binary patch literal 104 NcmezWdjbPJ000JT0*e3u literal 0 HcmV?d00001 diff --git a/speech_config.yaml b/speech_config.yaml new file mode 100644 index 00000000000..ad9920a2793 --- /dev/null +++ b/speech_config.yaml @@ -0,0 +1,9 @@ +model_list: + - model_name: fake-openai-speech + litellm_params: + model: openai/gpt-4o-mini-tts + api_base: http://0.0.0.0:8090/ + api_key: sk-1234 + model_info: + mode: audio_speech + \ No newline at end of file From 44f2013495c6987bef3918ca045f942777b21c34 Mon Sep 17 00:00:00 2001 From: AlexsanderHamir Date: Sat, 15 Nov 2025 17:02:15 -0800 Subject: [PATCH 008/370] fix: change chunk_size for aiter_bytes 1KB is too small for audio and is lowering the RPS when testing with medium to large files --- litellm/proxy/proxy_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index a6e73199f0e..36178652a7d 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -5305,7 +5305,7 @@ async def audio_speech( # Printing each chunk size async def generate(_response: HttpxBinaryResponseContent): - _generator = await _response.aiter_bytes(chunk_size=1024) + _generator = await _response.aiter_bytes(chunk_size=4096) async for chunk in _generator: yield chunk From 348d28d871a8c8d00ec1263d6d61caff4843cd7b Mon Sep 17 00:00:00 2001 From: AlexsanderHamir Date: Sat, 15 Nov 2025 17:15:26 -0800 Subject: [PATCH 009/370] fix: remove function definition from every request --- litellm/proxy/proxy_server.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 36178652a7d..c65c7f77f0d 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -14,6 +14,7 @@ from datetime import datetime, timedelta from typing import ( TYPE_CHECKING, Any, + AsyncGenerator, List, Literal, Optional, @@ -5231,6 +5232,14 @@ async def moderations( ) +async def _audio_speech_chunk_generator( + _response: HttpxBinaryResponseContent, +) -> AsyncGenerator[bytes, None]: + _generator = await _response.aiter_bytes(chunk_size=4096) + async for chunk in _generator: + yield chunk + + @router.post( "/v1/audio/speech", dependencies=[Depends(user_api_key_auth)], @@ -5303,12 +5312,6 @@ async def audio_speech( response_cost = hidden_params.get("response_cost", None) or "" litellm_call_id = hidden_params.get("litellm_call_id", None) or "" - # Printing each chunk size - async def generate(_response: HttpxBinaryResponseContent): - _generator = await _response.aiter_bytes(chunk_size=4096) - async for chunk in _generator: - yield chunk - custom_headers = ProxyBaseLLMRequestProcessing.get_custom_headers( user_api_key_dict=user_api_key_dict, model_id=model_id, @@ -5337,7 +5340,9 @@ async def audio_speech( media_type = "audio/wav" # Gemini TTS returns WAV format after conversion return StreamingResponse( - generate(response), media_type=media_type, headers=custom_headers # type: ignore + _audio_speech_chunk_generator(response), # type: ignore[arg-type] + media_type=media_type, + headers=custom_headers, # type: ignore ) except Exception as e: From 8ea0e31678863d2d700bf857bedac4d25338e008 Mon Sep 17 00:00:00 2001 From: AlexsanderHamir Date: Sat, 15 Nov 2025 17:30:58 -0800 Subject: [PATCH 010/370] Optimize streaming response accumulation Refactor async_data_generator to build streamed text via list accumulation and ''.join() instead of repeated string concatenation. This improves performance for long responses without changing streaming behavior. --- litellm/proxy/proxy_server.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index c65c7f77f0d..b67de4a87a7 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -4017,7 +4017,8 @@ async def async_data_generator( ): verbose_proxy_logger.debug("inside generator") try: - str_so_far = "" + # Use a list to accumulate response segments to avoid O(n^2) string concatenation + str_so_far_parts: list[str] = [] error_message: Optional[str] = None async for chunk in proxy_logging_obj.async_post_call_streaming_iterator_hook( user_api_key_dict=user_api_key_dict, @@ -4033,12 +4034,12 @@ async def async_data_generator( user_api_key_dict=user_api_key_dict, response=chunk, data=request_data, - str_so_far=str_so_far, + str_so_far="".join(str_so_far_parts), ) if isinstance(chunk, (ModelResponse, ModelResponseStream)): response_str = litellm.get_response_string(response_obj=chunk) - str_so_far += response_str + str_so_far_parts.append(response_str) if isinstance(chunk, BaseModel): chunk = chunk.model_dump_json(exclude_none=True, exclude_unset=True) From 98e2b64040f6e5f882648b433a23799582d710a1 Mon Sep 17 00:00:00 2001 From: AlexsanderHamir Date: Sat, 15 Nov 2025 17:37:01 -0800 Subject: [PATCH 011/370] Optimize response string construction Use list accumulation and join in get_response_string to avoid O(n^2) string concatenation and add a brief comment explaining the performance rationale. --- litellm/utils.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/litellm/utils.py b/litellm/utils.py index 783d462a7af..201e8145254 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -4437,17 +4437,20 @@ def get_response_string(response_obj: Union[ModelResponse, ModelResponseStream]) responses_api_response = getattr(response_obj, "response", None) if responses_api_response and hasattr(responses_api_response, "output"): output_list = responses_api_response.output - response_str = "" + # Use list accumulation to avoid O(n^2) string concatenation: + # repeatedly doing `response_str += part` copies the full string each time + # because Python strings are immutable, so total work grows with n^2. + response_output_parts: List[str] = [] for output_item in output_list: # Handle output items with content array if hasattr(output_item, "content"): for content_part in output_item.content: if hasattr(content_part, "text"): - response_str += content_part.text + response_output_parts.append(content_part.text) # Handle output items with direct text field elif hasattr(output_item, "text"): - response_str += output_item.text - return response_str + response_output_parts.append(output_item.text) + return "".join(response_output_parts) # Handle Responses API text delta events if hasattr(response_obj, "type") and hasattr(response_obj, "delta"): @@ -4461,16 +4464,17 @@ def get_response_string(response_obj: Union[ModelResponse, ModelResponseStream]) response_obj.choices ) - response_str = "" + # Use list accumulation to avoid O(n^2) string concatenation across choices + response_parts: List[str] = [] for choice in _choices: if isinstance(choice, Choices): if choice.message.content is not None: - response_str += choice.message.content + response_parts.append(str(choice.message.content)) elif isinstance(choice, StreamingChoices): if choice.delta.content is not None: - response_str += choice.delta.content + response_parts.append(str(choice.delta.content)) - return response_str + return "".join(response_parts) def get_api_key(llm_provider: str, dynamic_api_key: Optional[str]): From 4614e528dc4b3385582810f3c565d62668373d3c Mon Sep 17 00:00:00 2001 From: AlexsanderHamir Date: Mon, 17 Nov 2025 10:11:55 -0800 Subject: [PATCH 012/370] fix: remove deadcode The optimizations related to `select_data_generator` had no effect because its output which is the generator wasn't being used. --- litellm/proxy/proxy_server.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index b67de4a87a7..d6a98a1ca5b 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -5327,11 +5327,6 @@ async def audio_speech( hidden_params=hidden_params, ) - select_data_generator( - response=response, - user_api_key_dict=user_api_key_dict, - request_data=data, - ) # Determine media type based on model type media_type = "audio/mpeg" # Default for OpenAI TTS request_model = data.get("model", "") From c8c12298590885bc845088d4982c7059996f04a9 Mon Sep 17 00:00:00 2001 From: AlexsanderHamir Date: Mon, 17 Nov 2025 10:24:34 -0800 Subject: [PATCH 013/370] fix: call_type mistake & remove repetitive .lower() calls --- litellm/proxy/proxy_server.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index d6a98a1ca5b..cb43ef0cecf 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -5286,7 +5286,7 @@ async def audio_speech( ### CALL HOOKS ### - modify incoming data / reject request before calling the model data = await proxy_logging_obj.pre_call_hook( - user_api_key_dict=user_api_key_dict, data=data, call_type="image_generation" + user_api_key_dict=user_api_key_dict, data=data, call_type="aspeech" ) ## ROUTE TO CORRECT ENDPOINT ## @@ -5330,10 +5330,12 @@ async def audio_speech( # Determine media type based on model type media_type = "audio/mpeg" # Default for OpenAI TTS request_model = data.get("model", "") - if "gemini" in request_model.lower() and ( - "tts" in request_model.lower() or "preview-tts" in request_model.lower() - ): - media_type = "audio/wav" # Gemini TTS returns WAV format after conversion + if request_model: + request_model_lower = request_model.lower() + if "gemini" in request_model_lower and ( + "tts" in request_model_lower or "preview-tts" in request_model_lower + ): + media_type = "audio/wav" # Gemini TTS returns WAV format after conversion return StreamingResponse( _audio_speech_chunk_generator(response), # type: ignore[arg-type] From 697cb0906011cb84f2cc8c34b4ff7a2d7402dc13 Mon Sep 17 00:00:00 2001 From: AlexsanderHamir Date: Mon, 17 Nov 2025 10:51:18 -0800 Subject: [PATCH 014/370] fix: shared_sessions not being used --- litellm/llms/openai/openai.py | 5 +++++ litellm/main.py | 2 ++ 2 files changed, 7 insertions(+) diff --git a/litellm/llms/openai/openai.py b/litellm/llms/openai/openai.py index 2949e35e5e7..3282b7665c0 100644 --- a/litellm/llms/openai/openai.py +++ b/litellm/llms/openai/openai.py @@ -1414,6 +1414,7 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM): timeout: Union[float, httpx.Timeout], aspeech: Optional[bool] = None, client=None, + shared_session: Optional["ClientSession"] = None, ) -> HttpxBinaryResponseContent: if aspeech is not None and aspeech is True: return self.async_audio_speech( @@ -1428,6 +1429,7 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM): max_retries=max_retries, timeout=timeout, client=client, + shared_session=shared_session, ) # type: ignore openai_client = self._get_openai_client( @@ -1437,6 +1439,7 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM): timeout=timeout, max_retries=max_retries, client=client, + shared_session=shared_session, ) response = cast(OpenAI, openai_client).audio.speech.create( @@ -1460,6 +1463,7 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM): max_retries: int, timeout: Union[float, httpx.Timeout], client=None, + shared_session: Optional["ClientSession"] = None, ) -> HttpxBinaryResponseContent: openai_client = cast( AsyncOpenAI, @@ -1470,6 +1474,7 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM): timeout=timeout, max_retries=max_retries, client=client, + shared_session=shared_session, ), ) diff --git a/litellm/main.py b/litellm/main.py index 14d0b04b7b5..412d7f1c38e 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -5747,6 +5747,7 @@ def speech( # noqa: PLR0915 proxy_server_request = kwargs.get("proxy_server_request", None) extra_headers = kwargs.get("extra_headers", None) model_info = kwargs.get("model_info", None) + shared_session = kwargs.get("shared_session", None) model, custom_llm_provider, dynamic_api_key, api_base = get_llm_provider( model=model, custom_llm_provider=custom_llm_provider, api_base=api_base ) # type: ignore @@ -5856,6 +5857,7 @@ def speech( # noqa: PLR0915 timeout=timeout, client=client, # pass AsyncOpenAI, OpenAI client aspeech=aspeech, + shared_session=shared_session, ) elif custom_llm_provider == "azure": # Check if this is Azure Speech Service (Cognitive Services TTS) From f1895265e6b5643ef0e5be97b6f4cd65fcc2e78f Mon Sep 17 00:00:00 2001 From: AlexsanderHamir Date: Mon, 17 Nov 2025 12:55:58 -0800 Subject: [PATCH 015/370] fix: increase chunk_size to 8 KB for optimal latency --- litellm/proxy/proxy_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index cb43ef0cecf..16988205530 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -5236,7 +5236,7 @@ async def moderations( async def _audio_speech_chunk_generator( _response: HttpxBinaryResponseContent, ) -> AsyncGenerator[bytes, None]: - _generator = await _response.aiter_bytes(chunk_size=4096) + _generator = await _response.aiter_bytes(chunk_size=8192) async for chunk in _generator: yield chunk From 7241b4e9b505ea433563a88916cece2d062cd063 Mon Sep 17 00:00:00 2001 From: AlexsanderHamir Date: Mon, 17 Nov 2025 12:59:48 -0800 Subject: [PATCH 016/370] add: comment above optimization For anybody that would change this value for whatever reason, the comment makes the tradeoff clear. --- litellm/proxy/proxy_server.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 16988205530..da592c90720 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -5236,6 +5236,10 @@ async def moderations( async def _audio_speech_chunk_generator( _response: HttpxBinaryResponseContent, ) -> AsyncGenerator[bytes, None]: + # chunk_size has a big impact on latency, it can't be too small or too large + # too small: latency is high + # too large: latency is low, but memory usage is high + # 8192 is a good compromise _generator = await _response.aiter_bytes(chunk_size=8192) async for chunk in _generator: yield chunk From b4e25a68a4690e93110fb4ca2c4aa56f1c08a25a Mon Sep 17 00:00:00 2001 From: AlexsanderHamir Date: Tue, 18 Nov 2025 09:40:26 -0800 Subject: [PATCH 017/370] fix: remove test files --- no_cache_hits.py | 48 --------------------------------------------- speech.mp3 | Bin 104 -> 0 bytes speech_config.yaml | 9 --------- 3 files changed, 57 deletions(-) delete mode 100644 no_cache_hits.py delete mode 100644 speech.mp3 delete mode 100644 speech_config.yaml diff --git a/no_cache_hits.py b/no_cache_hits.py deleted file mode 100644 index 1b3bf895f77..00000000000 --- a/no_cache_hits.py +++ /dev/null @@ -1,48 +0,0 @@ -from locust import HttpUser, between, task - - -class MyUser(HttpUser): - """ - Minimal Locust user for repeatedly hitting `/v1/audio/speech`. - The goal is to measure server-side performance, so we avoid any extra work - (file writes, random generation, manual timing, custom event hooks, etc.) - that could inflate client-side latency. - """ - - wait_time = between(0.5, 1) - host = "http://0.0.0.0:8090" - - def on_start(self): - self.api_key = "sk-1234" - self.model_name = "fake-openai-speech" - self.headers = { - "Authorization": f"Bearer {self.api_key}", - "Content-Type": "application/json", - } - self.prompt_counter = 0 - - @task - def audio_speech_request(self): - self.prompt_counter += 1 - # Ensure prompts differ slightly so the backend can't reuse cached audio. - prompt = ( - "Generate a short spoken status update mentioning counter " - f"{self.prompt_counter}." - ) - - response = self.client.post( - "v1/audio/speech", - json={ - "model": self.model_name, - "input": prompt, - "voice": "alloy", - "format": "mp3", - }, - headers=self.headers, - name="audio_speech", - ) - - if response.status_code != 200: - # log the errors in error.txt - with open("error.txt", "a") as error_log: - error_log.write(response.text + "\n") \ No newline at end of file diff --git a/speech.mp3 b/speech.mp3 deleted file mode 100644 index f4f854d9bd215e2493d48b4bc4d39804bd79c038..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 104 NcmezWdjbPJ000JT0*e3u diff --git a/speech_config.yaml b/speech_config.yaml deleted file mode 100644 index ad9920a2793..00000000000 --- a/speech_config.yaml +++ /dev/null @@ -1,9 +0,0 @@ -model_list: - - model_name: fake-openai-speech - litellm_params: - model: openai/gpt-4o-mini-tts - api_base: http://0.0.0.0:8090/ - api_key: sk-1234 - model_info: - mode: audio_speech - \ No newline at end of file From 4b80813fc20dbab597a312c8f6e5e3b29a1ecaaa Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Wed, 19 Nov 2025 13:37:51 -0800 Subject: [PATCH 018/370] Return 404 when a user is not found --- .../internal_user_endpoints.py | 9 +- .../test_internal_user_endpoints.py | 100 ++++++++++++++++-- 2 files changed, 96 insertions(+), 13 deletions(-) diff --git a/litellm/proxy/management_endpoints/internal_user_endpoints.py b/litellm/proxy/management_endpoints/internal_user_endpoints.py index 66085b69b3d..ba71dbdc8ff 100644 --- a/litellm/proxy/management_endpoints/internal_user_endpoints.py +++ b/litellm/proxy/management_endpoints/internal_user_endpoints.py @@ -563,10 +563,15 @@ async def user_info( user_id = user_api_key_dict.user_id ## GET USER ROW ## + user_info = None if user_id is not None: user_info = await prisma_client.get_data(user_id=user_id) - else: - user_info = None + + if user_info is None: + raise HTTPException( + status_code=404, + detail=f"User {user_id} not found", + ) ## GET ALL TEAMS ## team_list = [] diff --git a/tests/test_litellm/proxy/management_endpoints/test_internal_user_endpoints.py b/tests/test_litellm/proxy/management_endpoints/test_internal_user_endpoints.py index 266056bcdd2..ebce0040684 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_internal_user_endpoints.py +++ b/tests/test_litellm/proxy/management_endpoints/test_internal_user_endpoints.py @@ -315,7 +315,32 @@ async def test_user_info_url_encoding_plus_character(mocker): # Mock the prisma client mock_prisma_client = mocker.MagicMock() - mock_prisma_client.get_data = mocker.AsyncMock() + + # Create a real LiteLLM_UserTable instance (BaseModel) so isinstance check passes + mock_user = LiteLLM_UserTable( + user_id="machine-user+alp-air-admin-b58-b@tempus.com", + user_email="machine-user+alp-air-admin-b58-b@tempus.com", + teams=[], + ) + + # Mock get_data to return user when called with user_id, empty list for keys + async def mock_get_data(*args, **kwargs): + if kwargs.get("table_name") == "key": + return [] + elif kwargs.get("table_name") == "team": + return [] + elif kwargs.get("user_id") is not None: + return mock_user + return None + + mock_prisma_client.get_data = mocker.AsyncMock(side_effect=mock_get_data) + + # Mock list_team to return None (patch it from where it's imported) + mock_list_team = mocker.AsyncMock(return_value=None) + mocker.patch( + "litellm.proxy.management_endpoints.team_endpoints.list_team", + mock_list_team, + ) # Patch the prisma client import in the endpoint mocker.patch("litellm.proxy.proxy_server.prisma_client", mock_prisma_client) @@ -335,20 +360,73 @@ async def test_user_info_url_encoding_plus_character(mocker): "machine-user alp-air-admin-b58-b@tempus.com" # What FastAPI gives us ) expected_user_id = "machine-user+alp-air-admin-b58-b@tempus.com" - try: - response = await user_info( - user_id=decoded_user_id, + + response = await user_info( + user_id=decoded_user_id, + user_api_key_dict=mock_user_api_key_dict, + request=mock_request, + ) + + # Verify that the response contains the correct user data + # Check that get_data was called with the correct user_id (first call should be for user) + user_call = None + for call in mock_prisma_client.get_data.call_args_list: + if call.kwargs.get("user_id") and not call.kwargs.get("table_name"): + user_call = call + break + + assert user_call is not None, "get_data should be called with user_id" + assert user_call.kwargs["user_id"] == expected_user_id + + +@pytest.mark.asyncio +async def test_user_info_nonexistent_user(mocker): + """ + Test that /user/info endpoint returns 404 when a non-existent user_id is provided. + """ + from fastapi import Request + + from litellm.proxy._types import ProxyException, UserAPIKeyAuth + from litellm.proxy.management_endpoints.internal_user_endpoints import user_info + + # Mock the prisma client + mock_prisma_client = mocker.MagicMock() + + # Mock get_data to return None (user doesn't exist) + async def mock_get_data(*args, **kwargs): + if kwargs.get("table_name") == "key": + return [] + elif kwargs.get("user_id") is not None: + return None # User not found + return None + + mock_prisma_client.get_data = mocker.AsyncMock(side_effect=mock_get_data) + + # Patch the prisma client import in the endpoint + mocker.patch("litellm.proxy.proxy_server.prisma_client", mock_prisma_client) + + # Create a mock request + mock_request = mocker.MagicMock(spec=Request) + + # Mock user_api_key_dict + mock_user_api_key_dict = UserAPIKeyAuth( + user_id="test_admin", user_role="proxy_admin" + ) + + # Call user_info function with a non-existent user_id + nonexistent_user_id = "nonexistent-user@example.com" + + # Should raise ProxyException with 404 status code (HTTPException is converted by decorator) + with pytest.raises(ProxyException) as exc_info: + await user_info( + user_id=nonexistent_user_id, user_api_key_dict=mock_user_api_key_dict, request=mock_request, ) - except Exception as e: - print(f"Error in user_info: {e}") - # Verify that the response contains the correct user data - print( - f"mock_prisma_client.get_data.call_args: {mock_prisma_client.get_data.call_args.kwargs}" - ) - assert mock_prisma_client.get_data.call_args.kwargs["user_id"] == expected_user_id + # Verify the exception details + assert exc_info.value.code == "404" # ProxyException.code is a string + assert f"User {nonexistent_user_id} not found" in str(exc_info.value.message) @pytest.mark.asyncio From 1c67b7e1daf28a9d9afc24d647adfb35c71d322b Mon Sep 17 00:00:00 2001 From: AlexsanderHamir Date: Thu, 20 Nov 2025 17:48:40 -0800 Subject: [PATCH 019/370] fix: place hardcoded value on constants.py --- litellm/constants.py | 1 + litellm/proxy/proxy_server.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/litellm/constants.py b/litellm/constants.py index 3f763cad926..fc26e1cf817 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -246,6 +246,7 @@ TOGETHER_AI_EMBEDDING_350_M = int(os.getenv("TOGETHER_AI_EMBEDDING_350_M", 350)) QDRANT_SCALAR_QUANTILE = float(os.getenv("QDRANT_SCALAR_QUANTILE", 0.99)) QDRANT_VECTOR_SIZE = int(os.getenv("QDRANT_VECTOR_SIZE", 1536)) CACHED_STREAMING_CHUNK_DELAY = float(os.getenv("CACHED_STREAMING_CHUNK_DELAY", 0.02)) +AUDIO_SPEECH_CHUNK_SIZE = 8192 # chunk_size for audio speech streaming. Balance between latency and memory usage MAX_SIZE_PER_ITEM_IN_MEMORY_CACHE_IN_KB = int( os.getenv("MAX_SIZE_PER_ITEM_IN_MEMORY_CACHE_IN_KB", 512) ) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index da592c90720..47c30e9ce84 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -32,6 +32,7 @@ from litellm.constants import ( AIOHTTP_CONNECTOR_LIMIT, AIOHTTP_KEEPALIVE_TIMEOUT, AIOHTTP_TTL_DNS_CACHE, + AUDIO_SPEECH_CHUNK_SIZE, BASE_MCP_ROUTE, DEFAULT_MAX_RECURSE_DEPTH, DEFAULT_SHARED_HEALTH_CHECK_LOCK_TTL, @@ -5240,7 +5241,7 @@ async def _audio_speech_chunk_generator( # too small: latency is high # too large: latency is low, but memory usage is high # 8192 is a good compromise - _generator = await _response.aiter_bytes(chunk_size=8192) + _generator = await _response.aiter_bytes(chunk_size=AUDIO_SPEECH_CHUNK_SIZE) async for chunk in _generator: yield chunk From e49f21c918efd6ee8d54800981d767e417081994 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Wed, 26 Nov 2025 18:57:57 +0530 Subject: [PATCH 020/370] Make sure that media resolution is only for gemini 3 model --- .../llms/vertex_ai/gemini/transformation.py | 18 ++++--- ...test_vertex_and_google_ai_studio_gemini.py | 47 +++++++++++++++++-- 2 files changed, 56 insertions(+), 9 deletions(-) diff --git a/litellm/llms/vertex_ai/gemini/transformation.py b/litellm/llms/vertex_ai/gemini/transformation.py index e4fcd35b954..04fde04f2c1 100644 --- a/litellm/llms/vertex_ai/gemini/transformation.py +++ b/litellm/llms/vertex_ai/gemini/transformation.py @@ -28,7 +28,6 @@ from litellm.types.files import ( get_file_type_from_extension, is_gemini_1_5_accepted_file_type, ) -from litellm.types.utils import LlmProviders from litellm.types.llms.openai import ( AllMessageValues, ChatCompletionAssistantMessage, @@ -48,7 +47,7 @@ from litellm.types.llms.vertex_ai import ( ToolConfig, Tools, ) -from litellm.types.utils import GenericImageParsingChunk +from litellm.types.utils import GenericImageParsingChunk, LlmProviders from ..common_utils import ( _check_text_in_content, @@ -82,6 +81,7 @@ def _process_gemini_image( image_url: str, format: Optional[str] = None, media_resolution: Optional[Literal["low", "medium", "high"]] = None, + model: Optional[str] = None, ) -> PartType: """ Given an image URL, return the appropriate PartType for Gemini @@ -118,16 +118,19 @@ def _process_gemini_image( # https links for unsupported mime types and base64 images image = convert_to_anthropic_image_obj(image_url, format=format) _blob: BlobType = {"data": image["data"], "mime_type": image["media_type"]} - if media_resolution is not None: - _blob["media_resolution"] = media_resolution + # media_resolution on individual Part objects is exclusive to Gemini 3 models + if media_resolution is not None and model is not None: + from .vertex_and_google_ai_studio_gemini import VertexGeminiConfig + if VertexGeminiConfig._is_gemini_3_or_newer(model): + _blob["media_resolution"] = media_resolution # Convert snake_case keys to camelCase for JSON serialization # The TypedDict uses snake_case, but the API expects camelCase _blob_dict = dict(_blob) if "media_resolution" in _blob_dict: - _blob_dict["mediaResolution"] = _blob_dict.pop("media_resolution") + _blob_dict["media_resolution"] = _blob_dict.pop("media_resolution") if "mime_type" in _blob_dict: - _blob_dict["mimeType"] = _blob_dict.pop("mime_type") + _blob_dict["mime_type"] = _blob_dict.pop("mime_type") return PartType(inline_data=cast(BlobType, _blob_dict)) raise Exception("Invalid image received - {}".format(image_url)) @@ -247,6 +250,7 @@ def _gemini_convert_messages_with_history( # noqa: PLR0915 image_url=image_url, format=format, media_resolution=media_resolution, + model=model, ) _parts.append(_part) elif element["type"] == "input_audio": @@ -271,6 +275,7 @@ def _gemini_convert_messages_with_history( # noqa: PLR0915 _part = _process_gemini_image( image_url=openai_image_str, format=audio_format_modified, + model=model, ) _parts.append(_part) elif element["type"] == "file": @@ -287,6 +292,7 @@ def _gemini_convert_messages_with_history( # noqa: PLR0915 _part = _process_gemini_image( image_url=passed_file, format=format, + model=model, ) _parts.append(_part) except Exception: diff --git a/tests/test_litellm/llms/vertex_ai/gemini/test_vertex_and_google_ai_studio_gemini.py b/tests/test_litellm/llms/vertex_ai/gemini/test_vertex_and_google_ai_studio_gemini.py index 2b305dbade1..6bd0fb52f1d 100644 --- a/tests/test_litellm/llms/vertex_ai/gemini/test_vertex_and_google_ai_studio_gemini.py +++ b/tests/test_litellm/llms/vertex_ai/gemini/test_vertex_and_google_ai_studio_gemini.py @@ -1795,7 +1795,9 @@ def test_media_resolution_from_detail_parameter(): } ] - contents = _gemini_convert_messages_with_history(messages=messages) + contents = _gemini_convert_messages_with_history( + messages=messages, model="gemini-3-pro-preview" + ) # Verify media_resolution is set in the inline_data # Note: Gemini adds a blank text part when there's no text, so we expect 2 parts @@ -1837,7 +1839,9 @@ def test_media_resolution_low_detail(): } ] - contents = _gemini_convert_messages_with_history(messages=messages) + contents = _gemini_convert_messages_with_history( + messages=messages, model="gemini-3-pro-preview" + ) # Find the part with inline_data image_part = None @@ -1951,7 +1955,9 @@ def test_media_resolution_per_part(): } ] - contents = _gemini_convert_messages_with_history(messages=messages) + contents = _gemini_convert_messages_with_history( + messages=messages, model="gemini-3-pro-preview" + ) # Should have one content with multiple parts assert len(contents) == 1 @@ -1968,6 +1974,41 @@ def test_media_resolution_per_part(): assert image2_part["inline_data"]["mediaResolution"] == "high" +def test_media_resolution_only_for_gemini_3_models(): + """Ensure mediaResolution is not added for non-Gemini 3 models.""" + from litellm.llms.vertex_ai.gemini.transformation import ( + _gemini_convert_messages_with_history, + ) + + base64_image = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==" + messages = [ + { + "role": "user", + "content": [ + { + "type": "image_url", + "image_url": { + "url": base64_image, + "detail": "high", + }, + } + ], + } + ] + + contents = _gemini_convert_messages_with_history( + messages=messages, model="gemini-2.5-pro" + ) + image_part = None + for part in contents[0]["parts"]: + if "inline_data" in part: + image_part = part + break + assert image_part is not None + assert "inline_data" in image_part + assert "mediaResolution" not in image_part["inline_data"] + + def test_gemini_3_image_models_no_thinking_config(): """ Test that Gemini 3 image models do NOT receive automatic thinkingConfig. From dba9946b989a6d76f563878219f3246009e1a1a0 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Wed, 26 Nov 2025 21:04:15 +0530 Subject: [PATCH 021/370] Update new feats as reviewed --- litellm/llms/anthropic/chat/transformation.py | 17 +++- litellm/llms/anthropic/common_utils.py | 84 ++++++++++++++++++- .../bedrock/chat/converse_transformation.py | 16 +++- .../anthropic_claude3_transformation.py | 52 +++++++++++- .../anthropic_claude3_transformation.py | 42 +++++++++- .../anthropic/transformation.py | 20 +++++ 6 files changed, 213 insertions(+), 18 deletions(-) diff --git a/litellm/llms/anthropic/chat/transformation.py b/litellm/llms/anthropic/chat/transformation.py index ac1c9b1e000..4221dfacf34 100644 --- a/litellm/llms/anthropic/chat/transformation.py +++ b/litellm/llms/anthropic/chat/transformation.py @@ -119,6 +119,10 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): def get_config(cls): return super().get_config() + def _is_claude_opus_4_5(self, model: str) -> bool: + """Check if the model is Claude Opus 4.5.""" + return "opus-4-5" in model.lower() or "opus_4_5" in model.lower() + def get_supported_openai_params(self, model: str): params = [ "stream", @@ -626,7 +630,7 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): return hosted_web_search_tool - def map_openai_params( + def map_openai_params( # noqa: PLR0915 self, non_default_params: dict, optional_params: dict, @@ -712,9 +716,14 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): if param == "thinking": optional_params["thinking"] = value elif param == "reasoning_effort" and isinstance(value, str): - optional_params["thinking"] = AnthropicConfig._map_reasoning_effort( - value - ) + # For Claude Opus 4.5, map reasoning_effort to output_config + if self._is_claude_opus_4_5(model): + optional_params["output_config"] = {"effort": value} + else: + # For other models, map to thinking parameter + optional_params["thinking"] = AnthropicConfig._map_reasoning_effort( + value + ) elif param == "web_search_options" and isinstance(value, dict): hosted_web_search_tool = self.map_web_search_tool( cast(OpenAIWebSearchOptions, value) diff --git a/litellm/llms/anthropic/common_utils.py b/litellm/llms/anthropic/common_utils.py index 9f5688f9e01..6b339f169cd 100644 --- a/litellm/llms/anthropic/common_utils.py +++ b/litellm/llms/anthropic/common_utils.py @@ -151,15 +151,22 @@ class AnthropicModelInfo(BaseLLMModelInfo): return False - def is_effort_used(self, optional_params: Optional[dict]) -> bool: + def is_effort_used(self, optional_params: Optional[dict], model: Optional[str] = None) -> bool: """ - Check if effort parameter is being used via output_config. + Check if effort parameter is being used. - Returns True if output_config with effort field is present. + Returns True if effort-related parameters are present. """ if not optional_params: return False + # Check if reasoning_effort is provided for Claude Opus 4.5 + if model and ("opus-4-5" in model.lower() or "opus_4_5" in model.lower()): + reasoning_effort = optional_params.get("reasoning_effort") + if reasoning_effort and isinstance(reasoning_effort, str): + return True + + # Check if output_config is directly provided output_config = optional_params.get("output_config") if output_config and isinstance(output_config, dict): effort = output_config.get("effort") @@ -193,6 +200,75 @@ class AnthropicModelInfo(BaseLLMModelInfo): computer_tool_version, "computer-use-2024-10-22" # Default fallback ) + def get_anthropic_beta_list( + self, + model: str, + custom_llm_provider: str, + tools: Optional[List] = None, + optional_params: Optional[dict] = None, + computer_tool_used: Optional[str] = None, + prompt_caching_set: bool = False, + file_id_used: bool = False, + mcp_server_used: bool = False, + ) -> List[str]: + """ + Get list of beta headers based on provider and features used. + + This method provides provider-specific beta header values for different Anthropic features. + Different providers (Anthropic API, Bedrock, VertexAI, Microsoft Foundry) may require + different beta header values for the same feature. + + Returns: + List of beta header strings + """ + from litellm.types.llms.anthropic import ( + ANTHROPIC_EFFORT_BETA_HEADER, + ANTHROPIC_TOOL_SEARCH_BETA_HEADER, + ) + + betas = [] + + # Detect features + tool_search_used = self.is_tool_search_used(tools) + programmatic_tool_calling_used = self.is_programmatic_tool_calling_used(tools) + input_examples_used = self.is_input_examples_used(tools) + effort_used = self.is_effort_used(optional_params, model) + + # Add beta headers based on provider + if custom_llm_provider in ["vertex_ai", "vertex_ai_beta"]: + if tool_search_used: + betas.append("tool-search-tool-2025-10-19") + # VertexAI doesn't support programmatic tool calling or input_examples yet + elif custom_llm_provider == "bedrock": + # Bedrock: tool-search only for Opus 4.5, advanced-tool-use for programmatic/input_examples + if tool_search_used and ("opus-4" in model.lower() or "opus_4" in model.lower()): + betas.append("tool-search-tool-2025-10-19") + if programmatic_tool_calling_used or input_examples_used: + betas.append(ANTHROPIC_TOOL_SEARCH_BETA_HEADER) # advanced-tool-use-2025-11-20 + else: # anthropic, azure (Microsoft Foundry), and others + # Direct API and Microsoft Foundry use advanced-tool-use for all + if tool_search_used or programmatic_tool_calling_used or input_examples_used: + betas.append(ANTHROPIC_TOOL_SEARCH_BETA_HEADER) # advanced-tool-use-2025-11-20 + + if effort_used: + betas.append(ANTHROPIC_EFFORT_BETA_HEADER) # effort-2025-11-24 + + if computer_tool_used: + beta_header = self.get_computer_tool_beta_header(computer_tool_used) + betas.append(beta_header) + + if prompt_caching_set: + betas.append("prompt-caching-2024-07-31") + + if file_id_used: + betas.append("files-api-2025-04-14") + betas.append("code-execution-2025-05-22") + + if mcp_server_used: + betas.append("mcp-client-2025-04-04") + + return list(set(betas)) + def get_anthropic_headers( self, api_key: str, @@ -278,7 +354,7 @@ class AnthropicModelInfo(BaseLLMModelInfo): tool_search_used = self.is_tool_search_used(tools=tools) programmatic_tool_calling_used = self.is_programmatic_tool_calling_used(tools=tools) input_examples_used = self.is_input_examples_used(tools=tools) - effort_used = self.is_effort_used(optional_params=optional_params) + effort_used = self.is_effort_used(optional_params=optional_params, model=model) user_anthropic_beta_headers = self._get_user_anthropic_beta_headers( anthropic_beta_header=headers.get("anthropic-beta") ) diff --git a/litellm/llms/bedrock/chat/converse_transformation.py b/litellm/llms/bedrock/chat/converse_transformation.py index d76a3c31b51..759311c38fe 100644 --- a/litellm/llms/bedrock/chat/converse_transformation.py +++ b/litellm/llms/bedrock/chat/converse_transformation.py @@ -815,11 +815,21 @@ class AmazonConverseConfig(BaseConfig): user_betas = get_anthropic_beta_from_headers(headers) anthropic_beta_list.extend(user_betas) + # Filter out tool search tools - Bedrock Converse API doesn't support them + filtered_tools = [] + if original_tools: + for tool in original_tools: + tool_type = tool.get("type", "") + if tool_type in ("tool_search_tool_regex_20251119", "tool_search_tool_bm25_20251119"): + # Tool search not supported in Converse API - skip it + continue + filtered_tools.append(tool) + # Only separate tools if computer use tools are actually present - if original_tools and self.is_computer_use_tool_used(original_tools, model): + if filtered_tools and self.is_computer_use_tool_used(filtered_tools, model): # Separate computer use tools from regular function tools computer_use_tools, regular_tools = self._separate_computer_use_tools( - original_tools, model + filtered_tools, model ) # Process regular function tools using existing logic @@ -835,7 +845,7 @@ class AmazonConverseConfig(BaseConfig): additional_request_params["tools"] = transformed_computer_tools else: # No computer use tools, process all tools as regular tools - bedrock_tools = _bedrock_tools_pt(original_tools) + bedrock_tools = _bedrock_tools_pt(filtered_tools) # Set anthropic_beta in additional_request_params if we have any beta features if anthropic_beta_list: diff --git a/litellm/llms/bedrock/chat/invoke_transformations/anthropic_claude3_transformation.py b/litellm/llms/bedrock/chat/invoke_transformations/anthropic_claude3_transformation.py index 02b8fd57115..d618451f73e 100644 --- a/litellm/llms/bedrock/chat/invoke_transformations/anthropic_claude3_transformation.py +++ b/litellm/llms/bedrock/chat/invoke_transformations/anthropic_claude3_transformation.py @@ -76,6 +76,7 @@ class AmazonAnthropicClaudeConfig(AmazonInvokeConfig, AnthropicConfig): for k, v in optional_params.items() if k not in self.aws_authentication_params } + filtered_params = self._normalize_bedrock_tool_search_tools(filtered_params) _anthropic_request = AnthropicConfig.transform_request( self, @@ -91,13 +92,58 @@ class AmazonAnthropicClaudeConfig(AmazonInvokeConfig, AnthropicConfig): if "anthropic_version" not in _anthropic_request: _anthropic_request["anthropic_version"] = self.anthropic_version - # Handle anthropic_beta from user headers - anthropic_beta_list = get_anthropic_beta_from_headers(headers) + anthropic_beta_list = [] + + user_betas = get_anthropic_beta_from_headers(headers) + if user_betas: + anthropic_beta_list.extend(user_betas) + + # Auto-detect and add beta headers using the new method + tools = optional_params.get("tools") + auto_betas = self.get_anthropic_beta_list( + model=model, + custom_llm_provider=self.custom_llm_provider or "bedrock", + tools=tools, + optional_params=optional_params, + computer_tool_used=self.is_computer_tool_used(tools), + prompt_caching_set=self.is_cache_control_set(messages), + file_id_used=self.is_file_id_used(messages), + mcp_server_used=self.is_mcp_server_used(optional_params.get("mcp_servers")), + ) + anthropic_beta_list.extend(auto_betas) + if anthropic_beta_list: - _anthropic_request["anthropic_beta"] = anthropic_beta_list + _anthropic_request["anthropic_beta"] = list(set(anthropic_beta_list)) return _anthropic_request + def _normalize_bedrock_tool_search_tools(self, optional_params: dict) -> dict: + """ + Convert tool search entries to the format supported by the Bedrock Invoke API. + """ + tools = optional_params.get("tools") + if not tools or not isinstance(tools, list): + return optional_params + + normalized_tools = [] + for tool in tools: + tool_type = tool.get("type") + if tool_type == "tool_search_tool_bm25_20251119": + # Bedrock Invoke does not support the BM25 variant, so skip it. + continue + if tool_type == "tool_search_tool_regex_20251119": + normalized_tool = tool.copy() + normalized_tool["type"] = "tool_search_tool_regex" + normalized_tool["name"] = normalized_tool.get( + "name", "tool_search_tool_regex" + ) + normalized_tools.append(normalized_tool) + continue + normalized_tools.append(tool) + + optional_params["tools"] = normalized_tools + return optional_params + def transform_response( self, model: str, diff --git a/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py b/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py index be782d35766..6f5165241ca 100644 --- a/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py +++ b/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py @@ -1,7 +1,18 @@ -from typing import TYPE_CHECKING, Any, AsyncIterator, Dict, List, Optional, Tuple, Union +from typing import ( + TYPE_CHECKING, + Any, + AsyncIterator, + Dict, + List, + Optional, + Tuple, + Union, + cast, +) import httpx +from litellm.llms.anthropic.common_utils import AnthropicModelInfo from litellm.llms.anthropic.experimental_pass_through.messages.transformation import ( AnthropicMessagesConfig, ) @@ -13,6 +24,7 @@ from litellm.llms.bedrock.chat.invoke_transformations.base_invoke_transformation AmazonInvokeConfig, ) from litellm.llms.bedrock.common_utils import get_anthropic_beta_from_headers +from litellm.types.llms.openai import AllMessageValues from litellm.types.router import GenericLiteLLMParams from litellm.types.utils import GenericStreamingChunk from litellm.types.utils import GenericStreamingChunk as GChunk @@ -129,10 +141,32 @@ class AmazonAnthropicClaudeMessagesConfig( if "model" in anthropic_messages_request: anthropic_messages_request.pop("model", None) - # 4. Handle anthropic_beta from user headers - anthropic_beta_list = get_anthropic_beta_from_headers(headers) + # 4. AUTO-INJECT beta headers based on features used + anthropic_beta_list = [] + + # Get user-provided beta headers first + user_betas = get_anthropic_beta_from_headers(headers) + if user_betas: + anthropic_beta_list.extend(user_betas) + + anthropic_model_info = AnthropicModelInfo() + tools = anthropic_messages_optional_request_params.get("tools") + messages_typed = cast(List[AllMessageValues], messages) + auto_betas = anthropic_model_info.get_anthropic_beta_list( + model=model, + custom_llm_provider="bedrock", + tools=tools, + optional_params=anthropic_messages_optional_request_params, + computer_tool_used=anthropic_model_info.is_computer_tool_used(tools), + prompt_caching_set=anthropic_model_info.is_cache_control_set(messages_typed), + file_id_used=anthropic_model_info.is_file_id_used(messages_typed), + mcp_server_used=anthropic_model_info.is_mcp_server_used(anthropic_messages_optional_request_params.get("mcp_servers")), + ) + anthropic_beta_list.extend(auto_betas) + + # Remove duplicates and set in request body if any beta headers exist if anthropic_beta_list: - anthropic_messages_request["anthropic_beta"] = anthropic_beta_list + anthropic_messages_request["anthropic_beta"] = list(set(anthropic_beta_list)) return anthropic_messages_request diff --git a/litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/transformation.py b/litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/transformation.py index 7ba788e335c..69651ca4358 100644 --- a/litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/transformation.py +++ b/litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/transformation.py @@ -68,6 +68,26 @@ class VertexAIAnthropicConfig(AnthropicConfig): ) data.pop("model", None) # vertex anthropic doesn't accept 'model' parameter + + tools = optional_params.get("tools") + anthropic_beta_list = [] + + auto_betas = self.get_anthropic_beta_list( + model=model, + custom_llm_provider=self.custom_llm_provider or "vertex_ai", + tools=tools, + optional_params=optional_params, + computer_tool_used=self.is_computer_tool_used(tools), + prompt_caching_set=self.is_cache_control_set(messages), + file_id_used=self.is_file_id_used(messages), + mcp_server_used=self.is_mcp_server_used(optional_params.get("mcp_servers")), + ) + anthropic_beta_list.extend(auto_betas) + + # Note: VertexAI uses tool-search-tool-2025-10-19 for tool search (different from direct API) + if anthropic_beta_list: + data["anthropic_beta"] = list(set(anthropic_beta_list)) + return data def transform_response( From c7ef668d783870b485fd3b0c9211c3e844944d42 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Wed, 26 Nov 2025 21:18:47 +0530 Subject: [PATCH 022/370] Update documentation for azure 4 feats --- .../index.md | 301 +----------------- docs/my-website/docs/providers/anthropic.md | 4 +- .../docs/providers/anthropic_effort.md | 27 +- .../anthropic_programmatic_tool_calling.md | 17 +- .../anthropic_tool_input_examples.md | 19 +- .../docs/providers/anthropic_tool_search.md | 17 +- 6 files changed, 67 insertions(+), 318 deletions(-) diff --git a/docs/my-website/blog/anthropic_opus_4_5_and_advanced_features/index.md b/docs/my-website/blog/anthropic_opus_4_5_and_advanced_features/index.md index b545e936186..6df4823b188 100644 --- a/docs/my-website/blog/anthropic_opus_4_5_and_advanced_features/index.md +++ b/docs/my-website/blog/anthropic_opus_4_5_and_advanced_features/index.md @@ -897,14 +897,13 @@ curl --location 'http://0.0.0.0:4000/chat/completions' \ ## Effort Parameter: Control Token Usage {#effort-parameter} -Controls aspects like how much effort the model puts into its response, via `output_config={"effort": ..}`. +Control how much effort Claude puts into its response using the `reasoning_effort` parameter. This allows you to trade off between response thoroughness and token efficiency. :::info - -Soon, we will map OpenAI's `reasoning_effort` parameter to this. +LiteLLM automatically maps `reasoning_effort` to Anthropic's `output_config` format and adds the required `effort-2025-11-24` beta header for Claude Opus 4.5. ::: -Potential Values for `effort` parameter: `"high"`, `"medium"`, `"low"`. +Potential values for `reasoning_effort` parameter: `"high"`, `"medium"`, `"low"`. ### Usage Example @@ -920,7 +919,7 @@ message = "Analyze the trade-offs between microservices and monolithic architect response_high = litellm.completion( model="anthropic/claude-opus-4-5-20251101", messages=[{"role": "user", "content": message}], - output_config={"effort": "high"} + reasoning_effort="high" ) print("High effort response:") @@ -931,7 +930,7 @@ print(f"Tokens used: {response_high.usage.completion_tokens}\n") response_medium = litellm.completion( model="anthropic/claude-opus-4-5-20251101", messages=[{"role": "user", "content": message}], - output_config={"effort": "medium"} + reasoning_effort="medium" ) print("Medium effort response:") @@ -942,7 +941,7 @@ print(f"Tokens used: {response_medium.usage.completion_tokens}\n") response_low = litellm.completion( model="anthropic/claude-opus-4-5-20251101", messages=[{"role": "user", "content": message}], - output_config={"effort": "low"} + reasoning_effort="low" ) print("Low effort response:") @@ -987,295 +986,9 @@ curl --location 'http://0.0.0.0:4000/chat/completions' \ "role": "user", "content": "Analyze the trade-offs between microservices and monolithic architectures" }], - "output_config": { - "effort": "high" - } + "reasoning_effort": "high" } ' ``` - - -## Cost Tracking: Monitor Tool Search Usage {#cost-tracking} - -### Understanding Tool Search Costs - -Tool search operations are tracked separately in the usage object, allowing you to monitor and optimize costs. - -It is available in the `usage` object, under `server_tool_use.tool_search_requests`. - -Anthropic charges $0.0001 per tool search request. - -### Tracking Example - - - - -```python -import litellm - -tools = [ - { - "type": "tool_search_tool_regex_20251119", - "name": "tool_search_tool_regex" - }, - # ... 100 deferred tools -] - -response = litellm.completion( - model="anthropic/claude-sonnet-4-5-20250929", - messages=[{ - "role": "user", - "content": "Find and use the weather tool for San Francisco" - }], - tools=tools -) - -# Standard token usage -print("Token Usage:") -print(f" Input tokens: {response.usage.prompt_tokens}") -print(f" Output tokens: {response.usage.completion_tokens}") -print(f" Total tokens: {response.usage.total_tokens}") - -# Tool search specific usage -if hasattr(response.usage, 'server_tool_use') and response.usage.server_tool_use: - print(f"\nTool Search Usage:") - print(f" Search requests: {response.usage.server_tool_use.tool_search_requests}") - - # Calculate cost (example pricing) - input_cost = response.usage.prompt_tokens * 0.000003 # $3 per 1M tokens - output_cost = response.usage.completion_tokens * 0.000015 # $15 per 1M tokens - search_cost = response.usage.server_tool_use.tool_search_requests * 0.0001 # Example - - total_cost = input_cost + output_cost + search_cost - - print(f"\nCost Breakdown:") - print(f" Input tokens: ${input_cost:.6f}") - print(f" Output tokens: ${output_cost:.6f}") - print(f" Tool searches: ${search_cost:.6f}") - print(f" Total: ${total_cost:.6f}") -``` - - - - -1. Setup config.yaml - -```yaml -model_list: - - model_name: claude-4 - litellm_params: - model: anthropic/claude-opus-4-5-20251101 - api_key: os.environ/ANTHROPIC_API_KEY -``` - -2. Start the proxy - -```bash -litellm --config /path/to/config.yaml -``` - -3. Test it! - -```bash -curl --location 'http://0.0.0.0:4000/chat/completions' \ ---header 'Content-Type: application/json' \ ---header 'Authorization: Bearer $LITELLM_KEY' \ ---data ' { - "model": "claude-4", - "messages": [{ - "role": "user", - "content": "Find and use the weather tool for San Francisco" - }], - "tools": [ - { - "type": "tool_search_tool_regex_20251119", - "name": "tool_search_tool_regex" - }, - # ... 100 deferred tools - ] - } -' -``` - -Expected Response: - -```json -{ - ..., - "usage": { - ..., - "server_tool_use": { - "tool_search_requests": 1 - } - } -} -``` - - - - -### Cost Optimization Tips - -1. **Keep frequently used tools non-deferred** (3-5 tools) -2. **Use tool search for large catalogs** (10+ tools) -3. **Monitor search requests** to identify optimization opportunities -4. **Combine with effort parameter** for maximum efficiency - - ---- - -## Combining Features {#combining-features} - -### The Power of Integration - -These features work together seamlessly. Here's a real-world example combining all of them: - - - - -```python -import litellm -import json - -# Large tool catalog with search, programmatic calling, and examples -tools = [ - # Enable tool search - { - "type": "tool_search_tool_regex_20251119", - "name": "tool_search_tool_regex" - }, - # Enable programmatic calling - { - "type": "code_execution_20250825", - "name": "code_execution" - }, - # Database tool with all features - { - "type": "function", - "function": { - "name": "query_database", - "description": "Execute SQL queries against the analytics database. Returns JSON array of results.", - "parameters": { - "type": "object", - "properties": { - "sql": { - "type": "string", - "description": "SQL SELECT statement" - }, - "limit": { - "type": "integer", - "description": "Maximum rows to return" - } - }, - "required": ["sql"] - } - }, - "defer_loading": True, # Tool search - "allowed_callers": ["code_execution_20250825"], # Programmatic calling - "input_examples": [ # Input examples - { - "sql": "SELECT region, SUM(revenue) as total FROM sales GROUP BY region", - "limit": 100 - } - ] - }, - # ... 50 more tools with defer_loading -] - -# Make request with effort control -response = litellm.completion( - model="anthropic/claude-opus-4-5-20251101", - messages=[{ - "role": "user", - "content": "Analyze sales by region for the last quarter and identify top performers" - }], - tools=tools, - output_config={"effort": "medium"} # Balanced efficiency -) - -# Track comprehensive usage -print("Complete Usage Metrics:") -print(f" Input tokens: {response.usage.prompt_tokens}") -print(f" Output tokens: {response.usage.completion_tokens}") -print(f" Total tokens: {response.usage.total_tokens}") - -if hasattr(response.usage, 'server_tool_use') and response.usage.server_tool_use: - print(f" Tool searches: {response.usage.server_tool_use.tool_search_requests}") - -print(f"\nResponse: {response.choices[0].message.content}") -``` - - - - -1. Setup config.yaml - -```yaml -model_list: - - model_name: claude-4 - litellm_params: - model: anthropic/claude-opus-4-5-20251101 - api_key: os.environ/ANTHROPIC_API_KEY -``` - -2. Start the proxy - -```bash -litellm --config /path/to/config.yaml -``` - -3. Test it! - -```bash -curl --location 'http://0.0.0.0:4000/chat/completions' \ ---header 'Content-Type: application/json' \ ---header 'Authorization: Bearer $LITELLM_KEY' \ ---data ' { - "model": "claude-4", - "messages": [{ - "role": "user", - "content": "Analyze sales by region for the last quarter and identify top performers" - }], - "tools": [ - { - "type": "tool_search_tool_regex_20251119", - "name": "tool_search_tool_regex" - }, - # ... 100 deferred tools - ], - "output_config": { - "effort": "medium" - } - } -' -``` - -Expected Response: - -```json -{ - ..., - "usage": { - ..., - "server_tool_use": { - "tool_search_requests": 1 - } - } -} -``` - - - - -### Real-World Benefits - -This combination enables: - -1. **Massive scale** - Handle 1000+ tools efficiently -2. **Low latency** - Programmatic calling reduces round trips -3. **High accuracy** - Input examples ensure correct tool usage -4. **Cost control** - Effort parameter optimizes token spend -5. **Full visibility** - Track all usage metrics - diff --git a/docs/my-website/docs/providers/anthropic.md b/docs/my-website/docs/providers/anthropic.md index 24365f0cc47..d84c1c23048 100644 --- a/docs/my-website/docs/providers/anthropic.md +++ b/docs/my-website/docs/providers/anthropic.md @@ -41,7 +41,8 @@ Check this in code, [here](../completion/input.md#translated-openai-params) "extra_headers", "parallel_tool_calls", "response_format", -"user" +"user", +"reasoning_effort", ``` :::info @@ -49,6 +50,7 @@ Check this in code, [here](../completion/input.md#translated-openai-params) **Notes:** - Anthropic API fails requests when `max_tokens` are not passed. Due to this litellm passes `max_tokens=4096` when no `max_tokens` are passed. - `response_format` is fully supported for Claude Sonnet 4.5 and Opus 4.1 models (see [Structured Outputs](#structured-outputs) section) +- `reasoning_effort` is automatically mapped to `output_config={"effort": ...}` for Claude Opus 4.5 models (see [Effort Parameter](./anthropic_effort.md)) ::: diff --git a/docs/my-website/docs/providers/anthropic_effort.md b/docs/my-website/docs/providers/anthropic_effort.md index 0015162a95b..e4bfd50e6c2 100644 --- a/docs/my-website/docs/providers/anthropic_effort.md +++ b/docs/my-website/docs/providers/anthropic_effort.md @@ -9,7 +9,10 @@ Control how many tokens Claude uses when responding with the `effort` parameter, The `effort` parameter allows you to control how eager Claude is about spending tokens when responding to requests. This gives you the ability to trade off between response thoroughness and token efficiency, all with a single model. -**Note**: The effort parameter is currently in beta and only supported by Claude Opus 4.5. You must include the beta header `effort-2025-11-24` when using this feature (LiteLLM automatically adds this header when `output_config` with `effort` is detected). +**Note**: The effort parameter is currently in beta and only supported by Claude Opus 4.5. LiteLLM automatically adds the `effort-2025-11-24` beta header when: +- `reasoning_effort` parameter is provided (for Claude Opus 4.5 only) + +For Claude Opus 4.5, `reasoning_effort="medium"`—both are automatically mapped to the correct format. ## How Effort Works @@ -52,9 +55,7 @@ response = litellm.completion( "role": "user", "content": "Analyze the trade-offs between microservices and monolithic architectures" }], - output_config={ - "effort": "medium" - } + reasoning_effort="medium" # Automatically mapped to output_config for Opus 4.5 ) print(response.choices[0].message.content) @@ -217,11 +218,14 @@ response = litellm.completion( The effort parameter is supported across all Anthropic-compatible providers: -- **Standard Anthropic**: āœ… Supported (Claude Opus 4.5) -- **Azure Anthropic**: āœ… Supported (Claude Opus 4.5) -- **Vertex AI Anthropic**: āœ… Supported (Claude Opus 4.5) +- **Standard Anthropic API**: āœ… Supported (Claude Opus 4.5) +- **Azure Anthropic / Microsoft Foundry**: āœ… Supported (Claude Opus 4.5) +- **Amazon Bedrock**: āœ… Supported (Claude Opus 4.5) +- **Google Cloud Vertex AI**: āœ… Supported (Claude Opus 4.5) -LiteLLM automatically handles the beta header injection for all providers. +LiteLLM automatically handles: +- Beta header injection (`effort-2025-11-24`) for all providers +- Parameter mapping: `reasoning_effort` → `output_config={"effort": ...}` for Claude Opus 4.5 ## Usage and Pricing @@ -242,9 +246,12 @@ print(f"Total tokens: {response.usage.total_tokens}") ### Beta header not being added -LiteLLM automatically adds the `effort-2025-11-24` beta header when `output_config` with `effort` is detected. If you're not seeing the header: +LiteLLM automatically adds the `effort-2025-11-24` beta header when: +- `reasoning_effort` parameter is provided (for Claude Opus 4.5 only) -1. Ensure you're using `output_config` with an `effort` field +If you're not seeing the header: + +1. Ensure you're using `reasoning_effort` parameter 2. Verify the model is Claude Opus 4.5 3. Check that LiteLLM version supports this feature diff --git a/docs/my-website/docs/providers/anthropic_programmatic_tool_calling.md b/docs/my-website/docs/providers/anthropic_programmatic_tool_calling.md index 6d3e15785e5..574dd7b0935 100644 --- a/docs/my-website/docs/providers/anthropic_programmatic_tool_calling.md +++ b/docs/my-website/docs/providers/anthropic_programmatic_tool_calling.md @@ -3,7 +3,11 @@ Programmatic tool calling allows Claude to write code that calls your tools programmatically within a code execution container, rather than requiring round trips through the model for each tool invocation. This reduces latency for multi-tool workflows and decreases token consumption by allowing Claude to filter or process data before it reaches the model's context window. :::info -Programmatic tool calling is currently in public beta. LiteLLM automatically adds the required `advanced-tool-use-2025-11-20` beta header when it detects tools with the `allowed_callers` field. +Programmatic tool calling is currently in public beta. LiteLLM automatically detects tools with the `allowed_callers` field and adds the appropriate beta header based on your provider: + +- **Anthropic API & Microsoft Foundry**: `advanced-tool-use-2025-11-20` +- **Amazon Bedrock**: `advanced-tool-use-2025-11-20` +- **Google Cloud Vertex AI**: Not supported This feature requires the code execution tool to be enabled. ::: @@ -380,13 +384,14 @@ For example, calling 10 tools directly uses ~10x the tokens of calling them prog ## Provider Support -LiteLLM supports programmatic tool calling across all Anthropic-compatible providers: +LiteLLM supports programmatic tool calling across the following Anthropic-compatible providers: -- **Standard Anthropic API** (`anthropic/claude-sonnet-4-5-20250929`) -- **Azure Anthropic** (`azure/claude-sonnet-4-5-20250929`) -- **Vertex AI Anthropic** (`vertex_ai/claude-sonnet-4-5-20250929`) +- **Standard Anthropic API** (`anthropic/claude-sonnet-4-5-20250929`) āœ… +- **Azure Anthropic / Microsoft Foundry** (`azure/claude-sonnet-4-5-20250929`) āœ… +- **Amazon Bedrock** (`bedrock/invoke/anthropic.claude-sonnet-4-5-20250929-v1:0`) āœ… +- **Google Cloud Vertex AI** (`vertex_ai/claude-sonnet-4-5-20250929`) āŒ Not supported -The beta header is automatically added when LiteLLM detects tools with `allowed_callers` field. +The beta header (`advanced-tool-use-2025-11-20`) is automatically added when LiteLLM detects tools with the `allowed_callers` field. ## Limitations diff --git a/docs/my-website/docs/providers/anthropic_tool_input_examples.md b/docs/my-website/docs/providers/anthropic_tool_input_examples.md index d0b7cc1762c..39f4d8555f4 100644 --- a/docs/my-website/docs/providers/anthropic_tool_input_examples.md +++ b/docs/my-website/docs/providers/anthropic_tool_input_examples.md @@ -3,7 +3,13 @@ Provide concrete examples of valid tool inputs to help Claude understand how to use your tools more effectively. This is particularly useful for complex tools with nested objects, optional parameters, or format-sensitive inputs. :::info -Tool input examples is a beta feature. LiteLLM automatically adds the required `advanced-tool-use-2025-11-20` beta header when it detects tools with the `input_examples` field. +Tool input examples is a beta feature. LiteLLM automatically detects tools with the `input_examples` field and adds the appropriate beta header based on your provider: + +- **Anthropic API & Microsoft Foundry**: `advanced-tool-use-2025-11-20` +- **Amazon Bedrock**: `advanced-tool-use-2025-11-20` (Claude Opus 4.5 only) +- **Google Cloud Vertex AI**: Not supported + +You don't need to manually specify beta headers—LiteLLM handles this automatically. ::: ## When to Use Input Examples @@ -378,13 +384,14 @@ Input examples work seamlessly with other Anthropic tool features: ## Provider Support -LiteLLM supports input examples across all Anthropic-compatible providers: +LiteLLM supports input examples across the following Anthropic-compatible providers: -- **Standard Anthropic API** (`anthropic/claude-sonnet-4-5-20250929`) -- **Azure Anthropic** (`azure/claude-sonnet-4-5-20250929`) -- **Vertex AI Anthropic** (`vertex_ai/claude-sonnet-4-5-20250929`) +- **Standard Anthropic API** (`anthropic/claude-sonnet-4-5-20250929`) āœ… +- **Azure Anthropic / Microsoft Foundry** (`azure/claude-sonnet-4-5-20250929`) āœ… +- **Amazon Bedrock** (`bedrock/invoke/anthropic.claude-opus-4-5-20251101-v1:0`) āœ… (Opus 4.5 only) +- **Google Cloud Vertex AI** (`vertex_ai/claude-sonnet-4-5-20250929`) āŒ Not supported -The beta header is automatically added when LiteLLM detects tools with `input_examples` field. +The beta header (`advanced-tool-use-2025-11-20`) is automatically added when LiteLLM detects tools with the `input_examples` field. ## Troubleshooting diff --git a/docs/my-website/docs/providers/anthropic_tool_search.md b/docs/my-website/docs/providers/anthropic_tool_search.md index 7b9e7cfaa72..28ce5688eeb 100644 --- a/docs/my-website/docs/providers/anthropic_tool_search.md +++ b/docs/my-website/docs/providers/anthropic_tool_search.md @@ -290,7 +290,13 @@ response = client.chat.completions.create( ### Beta Header -LiteLLM automatically adds the `advanced-tool-use-2025-11-20` beta header when tool search tools are detected. You don't need to manually specify it. +LiteLLM automatically detects tool search tools and adds the appropriate beta header based on your provider: + +- **Anthropic API & Microsoft Foundry**: `advanced-tool-use-2025-11-20` +- **Google Cloud Vertex AI**: `tool-search-tool-2025-10-19` +- **Amazon Bedrock** (Invoke API, Opus 4.5 only): `tool-search-tool-2025-10-19` + +You don't need to manually specify beta headers—LiteLLM handles this automatically. ### Deferred Loading @@ -387,9 +393,18 @@ If Claude references a tool that isn't in your deferred tools list, you'll get a - Not compatible with tool use examples - Requires Claude Opus 4.5 or Sonnet 4.5 - On Bedrock, only available via invoke API (not converse API) +- On Bedrock, only supported for Claude Opus 4.5 (not Sonnet 4.5) +- BM25 variant (`tool_search_tool_bm25_20251119`) is not supported on Bedrock - Maximum 10,000 tools in catalog - Returns 3-5 most relevant tools per search +### Bedrock-Specific Notes + +When using Bedrock's Invoke API: +- The regex variant (`tool_search_tool_regex_20251119`) is automatically normalized to `tool_search_tool_regex` +- The BM25 variant (`tool_search_tool_bm25_20251119`) is automatically filtered out as it's not supported +- Tool search is only available for Claude Opus 4.5 models + ## Additional Resources - [Anthropic Tool Search Documentation](https://docs.anthropic.com/en/docs/build-with-claude/tool-use/tool-search) From 9a85ffceffa528b561170f804435359fdb02b4ce Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Wed, 26 Nov 2025 21:45:50 +0530 Subject: [PATCH 023/370] Fix tests related to mediaResolution --- tests/llm_translation/test_prompt_factory.py | 4 ++-- ...test_vertex_and_google_ai_studio_gemini.py | 20 +++++++++---------- .../llms/vertex_ai/test_vertex.py | 2 +- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/llm_translation/test_prompt_factory.py b/tests/llm_translation/test_prompt_factory.py index 1ed1e327ec2..abd8ae52157 100644 --- a/tests/llm_translation/test_prompt_factory.py +++ b/tests/llm_translation/test_prompt_factory.py @@ -567,7 +567,7 @@ def test_vertex_only_image_user_message(): }, ] - response = _gemini_convert_messages_with_history(messages=messages) + response = _gemini_convert_messages_with_history(messages=messages, model="gemini-1.5-pro") expected_response = [ { @@ -576,7 +576,7 @@ def test_vertex_only_image_user_message(): { "inline_data": { "data": "/9j/2wCEAAgGBgcGBQ", - "mimeType": "image/jpeg", + "mime_type": "image/jpeg", } }, {"text": " "}, diff --git a/tests/test_litellm/llms/vertex_ai/gemini/test_vertex_and_google_ai_studio_gemini.py b/tests/test_litellm/llms/vertex_ai/gemini/test_vertex_and_google_ai_studio_gemini.py index 6bd0fb52f1d..92be385c04f 100644 --- a/tests/test_litellm/llms/vertex_ai/gemini/test_vertex_and_google_ai_studio_gemini.py +++ b/tests/test_litellm/llms/vertex_ai/gemini/test_vertex_and_google_ai_studio_gemini.py @@ -1811,9 +1811,9 @@ def test_media_resolution_from_detail_parameter(): break assert image_part is not None assert "inline_data" in image_part - # The TypedDict uses snake_case internally, but mediaResolution is camelCase in the dict - assert "mediaResolution" in image_part["inline_data"] - assert image_part["inline_data"]["mediaResolution"] == "high" + # The TypedDict uses snake_case internally, and we keep it as snake_case + assert "media_resolution" in image_part["inline_data"] + assert image_part["inline_data"]["media_resolution"] == "high" def test_media_resolution_low_detail(): @@ -1851,7 +1851,7 @@ def test_media_resolution_low_detail(): break assert image_part is not None assert "inline_data" in image_part - assert image_part["inline_data"]["mediaResolution"] == "low" + assert image_part["inline_data"]["media_resolution"] == "low" def test_media_resolution_auto_detail(): @@ -1888,8 +1888,8 @@ def test_media_resolution_auto_detail(): break assert image_part is not None assert "inline_data" in image_part - # mediaResolution should not be set for auto - assert "mediaResolution" not in image_part["inline_data"] or image_part["inline_data"].get("mediaResolution") is None + # media_resolution should not be set for auto + assert "media_resolution" not in image_part["inline_data"] or image_part["inline_data"].get("media_resolution") is None # Test with None messages_none = [ @@ -1915,8 +1915,8 @@ def test_media_resolution_auto_detail(): break assert image_part is not None assert "inline_data" in image_part - # mediaResolution should not be set - assert "mediaResolution" not in image_part["inline_data"] or image_part["inline_data"].get("mediaResolution") is None + # media_resolution should not be set + assert "media_resolution" not in image_part["inline_data"] or image_part["inline_data"].get("media_resolution") is None def test_media_resolution_per_part(): @@ -1966,12 +1966,12 @@ def test_media_resolution_per_part(): # First image should have low resolution (first part is the image) image1_part = contents[0]["parts"][0] assert "inline_data" in image1_part - assert image1_part["inline_data"]["mediaResolution"] == "low" + assert image1_part["inline_data"]["media_resolution"] == "low" # Second image should have high resolution (third part is the second image) image2_part = contents[0]["parts"][2] assert "inline_data" in image2_part - assert image2_part["inline_data"]["mediaResolution"] == "high" + assert image2_part["inline_data"]["media_resolution"] == "high" def test_media_resolution_only_for_gemini_3_models(): diff --git a/tests/test_litellm/llms/vertex_ai/test_vertex.py b/tests/test_litellm/llms/vertex_ai/test_vertex.py index 394cd2978bd..39ed09f81be 100644 --- a/tests/test_litellm/llms/vertex_ai/test_vertex.py +++ b/tests/test_litellm/llms/vertex_ai/test_vertex.py @@ -1241,7 +1241,7 @@ def test_process_gemini_image(): base64_image = "data:image/jpeg;base64,/9j/4AAQSkZJRg..." base64_result = _process_gemini_image(base64_image) print("base64_result", base64_result) - assert base64_result["inline_data"]["mimeType"] == "image/jpeg" + assert base64_result["inline_data"]["mime_type"] == "image/jpeg" assert base64_result["inline_data"]["data"] == "/9j/4AAQSkZJRg..." From 210560e1e76f5d2f1ad4c94dcb3e41303c710ae2 Mon Sep 17 00:00:00 2001 From: AlexsanderHamir Date: Wed, 26 Nov 2025 15:48:43 -0800 Subject: [PATCH 024/370] Add paginated /spend/logs/v2 endpoint - Add /spend/logs/v2 endpoint that shares implementation with /spend/logs/ui - Provides paginated access to spend logs with comprehensive filtering - Replaces non-paginated /spend/logs endpoint to prevent performance issues - Both v2 and ui endpoints share the same function for consistency --- .../spend_management_endpoints.py | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/litellm/proxy/spend_tracking/spend_management_endpoints.py b/litellm/proxy/spend_tracking/spend_management_endpoints.py index 5dfedcc0b87..2c9dc3fc09a 100644 --- a/litellm/proxy/spend_tracking/spend_management_endpoints.py +++ b/litellm/proxy/spend_tracking/spend_management_endpoints.py @@ -1,5 +1,6 @@ #### SPEND MANAGEMENT ##### import collections +import json import os from datetime import datetime, timedelta, timezone from functools import lru_cache @@ -1609,6 +1610,14 @@ async def calculate_spend(request: SpendCalculateRequest): ) +@router.get( + "/spend/logs/v2", + tags=["Budget & Spend Tracking"], + dependencies=[Depends(user_api_key_auth)], + responses={ + 200: {"model": Dict[str, Any]}, + }, +) @router.get( "/spend/logs/ui", tags=["Budget & Spend Tracking"], @@ -1672,16 +1681,16 @@ async def ui_view_spend_logs( # noqa: PLR0915 ), ): """ - View spend logs for UI with pagination support + View spend logs with pagination support. + Available at both `/spend/logs/v2` (public API) and `/spend/logs/ui` (internal UI). - Returns: - { - "data": List[LiteLLM_SpendLogs], # Paginated spend logs - "total": int, # Total number of records - "page": int, # Current page number - "page_size": int, # Number of items per page - "total_pages": int # Total number of pages - } + Returns paginated response with data, total, page, page_size, and total_pages. + + Example: + ``` + curl -X GET "http://0.0.0.0:8000/spend/logs/v2?start_date=2025-11-25%2000:00:00&end_date=2025-11-26%2023:59:59&page=1&page_size=50" \ +-H "Authorization: Bearer sk-1234" + ``` """ from litellm.proxy.proxy_server import prisma_client From 1ad9e014abfea81174bb2bcf4112af1f6d3e658f Mon Sep 17 00:00:00 2001 From: AlexsanderHamir Date: Wed, 26 Nov 2025 16:11:53 -0800 Subject: [PATCH 025/370] Add endpoint-based date parsing for /spend/logs/v2 - Add flexible date parsing for v2 endpoint (supports both YYYY-MM-DD and YYYY-MM-DD HH:MM:SS) - Keep strict timestamp format for /spend/logs/ui endpoint for backward compatibility - Parse dates based on which endpoint was called using request path --- .../spend_management_endpoints.py | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/litellm/proxy/spend_tracking/spend_management_endpoints.py b/litellm/proxy/spend_tracking/spend_management_endpoints.py index 2c9dc3fc09a..608db6af9a6 100644 --- a/litellm/proxy/spend_tracking/spend_management_endpoints.py +++ b/litellm/proxy/spend_tracking/spend_management_endpoints.py @@ -7,7 +7,7 @@ from functools import lru_cache from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional import fastapi -from fastapi import APIRouter, Depends, HTTPException, status +from fastapi import APIRouter, Depends, HTTPException, Request, status import litellm from litellm._logging import verbose_proxy_logger @@ -1628,6 +1628,7 @@ async def calculate_spend(request: SpendCalculateRequest): }, ) async def ui_view_spend_logs( # noqa: PLR0915 + request: Request, api_key: Optional[str] = fastapi.Query( default=None, description="Get spend logs based on api key", @@ -1711,13 +1712,24 @@ async def ui_view_spend_logs( # noqa: PLR0915 ) try: - # Convert the date strings to datetime objects - start_date_obj = datetime.strptime(start_date, "%Y-%m-%d %H:%M:%S").replace( - tzinfo=timezone.utc - ) - end_date_obj = datetime.strptime(end_date, "%Y-%m-%d %H:%M:%S").replace( - tzinfo=timezone.utc - ) + is_v2 = "/spend/logs/v2" in request.url.path + formats = ["%Y-%m-%d %H:%M:%S", "%Y-%m-%d"] if is_v2 else ["%Y-%m-%d %H:%M:%S"] + + def parse_date(date_str: str) -> datetime: + date_str = date_str.strip() + for fmt in formats: + try: + return datetime.strptime(date_str, fmt).replace(tzinfo=timezone.utc) + except ValueError: + continue + expected = "'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'" if is_v2 else "'YYYY-MM-DD HH:MM:SS'" + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Invalid date format: {date_str}. Expected: {expected}", + ) + + start_date_obj = parse_date(start_date) + end_date_obj = parse_date(end_date) # Convert to ISO format strings for Prisma start_date_iso = start_date_obj.isoformat() # Already in UTC, no need to add Z From df190d25b87ced96cce2d2940e0f2b770256856c Mon Sep 17 00:00:00 2001 From: AlexsanderHamir Date: Wed, 26 Nov 2025 16:16:13 -0800 Subject: [PATCH 026/370] Add deprecation notice to /spend/logs endpoint - Mark /spend/logs as deprecated in docstring - Direct users to use /spend/logs/v2 for paginated access - Warns about performance issues with non-paginated endpoint --- litellm/proxy/spend_tracking/spend_management_endpoints.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/litellm/proxy/spend_tracking/spend_management_endpoints.py b/litellm/proxy/spend_tracking/spend_management_endpoints.py index 608db6af9a6..2d3fc023a39 100644 --- a/litellm/proxy/spend_tracking/spend_management_endpoints.py +++ b/litellm/proxy/spend_tracking/spend_management_endpoints.py @@ -1917,6 +1917,9 @@ async def view_spend_logs( # noqa: PLR0915 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), ): """ + [DEPRECATED] This endpoint is not paginated and can cause performance issues. + Please use `/spend/logs/v2` instead for paginated access to spend logs. + View all spend logs, if request_id is provided, only logs for that request_id will be returned When start_date and end_date are provided: From 247160277e11b28c59db05feef76c1adc105c886 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Thu, 27 Nov 2025 15:46:04 +0530 Subject: [PATCH 027/370] Added support for twelvelabs pegasus --- litellm/__init__.py | 3 + litellm/constants.py | 1 + ...mazon_twelvelabs_pegasus_transformation.py | 133 ++++++++++++++++++ litellm/llms/bedrock/common_utils.py | 2 + .../test_twelvelabs_pegasus_transformation.py | 85 +++++++++++ 5 files changed, 224 insertions(+) create mode 100644 litellm/llms/bedrock/chat/invoke_transformations/amazon_twelvelabs_pegasus_transformation.py create mode 100644 tests/test_litellm/llms/bedrock/chat/invoke_transformations/test_twelvelabs_pegasus_transformation.py diff --git a/litellm/__init__.py b/litellm/__init__.py index aebf1404196..e6af8a21ff5 100644 --- a/litellm/__init__.py +++ b/litellm/__init__.py @@ -1222,6 +1222,9 @@ from .llms.bedrock.chat.invoke_transformations.amazon_mistral_transformation imp from .llms.bedrock.chat.invoke_transformations.amazon_titan_transformation import ( AmazonTitanConfig, ) +from .llms.bedrock.chat.invoke_transformations.amazon_twelvelabs_pegasus_transformation import ( + AmazonTwelveLabsPegasusConfig, +) from .llms.bedrock.chat.invoke_transformations.base_invoke_transformation import ( AmazonInvokeConfig, ) diff --git a/litellm/constants.py b/litellm/constants.py index cf3d4c6e742..00e17788466 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -851,6 +851,7 @@ BEDROCK_INVOKE_PROVIDERS_LITERAL = Literal[ "nova", "deepseek_r1", "qwen3", + "twelvelabs", ] BEDROCK_EMBEDDING_PROVIDERS_LITERAL = Literal[ diff --git a/litellm/llms/bedrock/chat/invoke_transformations/amazon_twelvelabs_pegasus_transformation.py b/litellm/llms/bedrock/chat/invoke_transformations/amazon_twelvelabs_pegasus_transformation.py new file mode 100644 index 00000000000..7b72968ea3d --- /dev/null +++ b/litellm/llms/bedrock/chat/invoke_transformations/amazon_twelvelabs_pegasus_transformation.py @@ -0,0 +1,133 @@ +""" +Transforms OpenAI-style requests into TwelveLabs Pegasus 1.2 requests for Bedrock. + +Reference: +https://docs.twelvelabs.io/docs/models/pegasus +""" + +from typing import Any, Dict, List, Optional + +from litellm.llms.base_llm.base_utils import type_to_response_format_param +from litellm.llms.base_llm.chat.transformation import BaseConfig +from litellm.llms.bedrock.chat.invoke_transformations.base_invoke_transformation import ( + AmazonInvokeConfig, +) +from litellm.types.llms.openai import AllMessageValues +from litellm.utils import get_base64_str + + +class AmazonTwelveLabsPegasusConfig(AmazonInvokeConfig, BaseConfig): + """ + Handles transforming OpenAI-style requests into Bedrock InvokeModel requests for + `twelvelabs.pegasus-1-2-v1:0`. + + Pegasus 1.2 requires an `inputPrompt` and a `mediaSource` that either references + an S3 object or a base64-encoded clip. Optional OpenAI params (temperature, + response_format, max_tokens) are translated to the TwelveLabs schema. + """ + + def get_supported_openai_params(self, model: str) -> List[str]: + return [ + "max_tokens", + "max_completion_tokens", + "temperature", + "response_format", + ] + + def map_openai_params( + self, + non_default_params: dict, + optional_params: dict, + model: str, + drop_params: bool, + ) -> dict: + for param, value in non_default_params.items(): + if param in {"max_tokens", "max_completion_tokens"}: + optional_params["maxOutputTokens"] = value + if param == "temperature": + optional_params["temperature"] = value + if param == "response_format": + optional_params["responseFormat"] = self._normalize_response_format( + value + ) + return optional_params + + def _normalize_response_format(self, value: Any) -> Any: + if isinstance(value, dict): + return value + return type_to_response_format_param(response_format=value) or value + + def transform_request( + self, + model: str, + messages: List[AllMessageValues], + optional_params: dict, + litellm_params: dict, + headers: dict, + ) -> dict: + input_prompt = self._convert_messages_to_prompt(messages=messages) + request_data: Dict[str, Any] = {"inputPrompt": input_prompt} + + media_source = self._build_media_source(optional_params) + if media_source is not None: + request_data["mediaSource"] = media_source + + for key in ("temperature", "maxOutputTokens", "responseFormat"): + if key in optional_params: + request_data[key] = optional_params.get(key) + return request_data + + def _build_media_source(self, optional_params: dict) -> Optional[dict]: + direct_source = optional_params.get("mediaSource") or optional_params.get( + "media_source" + ) + if isinstance(direct_source, dict): + return direct_source + + base64_input = optional_params.get("video_base64") or optional_params.get( + "base64_string" + ) + if base64_input: + return {"base64String": get_base64_str(base64_input)} + + s3_uri = ( + optional_params.get("video_s3_uri") + or optional_params.get("s3_uri") + or optional_params.get("media_source_s3_uri") + ) + if s3_uri: + s3_location = {"uri": s3_uri} + bucket_owner = ( + optional_params.get("video_s3_bucket_owner") + or optional_params.get("s3_bucket_owner") + or optional_params.get("media_source_bucket_owner") + ) + if bucket_owner: + s3_location["bucketOwner"] = bucket_owner + return {"s3Location": s3_location} + return None + + def _convert_messages_to_prompt(self, messages: List[AllMessageValues]) -> str: + prompt_parts: List[str] = [] + for message in messages: + role = message.get("role", "user") + content = message.get("content", "") + if isinstance(content, list): + text_fragments = [] + for item in content: + if isinstance(item, dict): + item_type = item.get("type") + if item_type == "text": + text_fragments.append(item.get("text", "")) + elif item_type == "image_url": + text_fragments.append("") + elif item_type == "video_url": + text_fragments.append("
- - {/* NEW: Feature flag label + toggle below the email field */} -
- Refactored UI - setRefactoredUIFlag(checked)} - aria-label="Toggle refactored UI feature flag" - /> -
), diff --git a/ui/litellm-dashboard/src/components/public_model_hub.test.tsx b/ui/litellm-dashboard/src/components/public_model_hub.test.tsx index 8ff57b49e08..47a44fa5147 100644 --- a/ui/litellm-dashboard/src/components/public_model_hub.test.tsx +++ b/ui/litellm-dashboard/src/components/public_model_hub.test.tsx @@ -1,7 +1,6 @@ import { describe, it, expect, vi, beforeAll, beforeEach } from "vitest"; import { render } from "@testing-library/react"; import PublicModelHub from "./public_model_hub"; -import { FeatureFlagsProvider } from "@/hooks/useFeatureFlags"; vi.mock("next/navigation", () => ({ useRouter: vi.fn(() => ({ @@ -58,11 +57,7 @@ beforeEach(() => { describe("PublicModelHub", () => { it("renders", () => { - const { container } = render( - - - , - ); + const { container } = render(); expect(container).toBeInTheDocument(); }); }); diff --git a/ui/litellm-dashboard/src/hooks/useFeatureFlags.test.tsx b/ui/litellm-dashboard/src/hooks/useFeatureFlags.test.tsx deleted file mode 100644 index ca0529b0f28..00000000000 --- a/ui/litellm-dashboard/src/hooks/useFeatureFlags.test.tsx +++ /dev/null @@ -1,296 +0,0 @@ -import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; -import { renderHook, waitFor } from "@testing-library/react"; -import { useRouter } from "next/navigation"; -import useFeatureFlags, { FeatureFlagsProvider } from "./useFeatureFlags"; - -// Mock next/navigation -vi.mock("next/navigation", () => ({ - useRouter: vi.fn(), -})); - -// Mock the networking module to control serverRootPath -vi.mock("@/components/networking", () => ({ - serverRootPath: "/", -})); - -describe("useFeatureFlags", () => { - let mockReplace: ReturnType; - let originalLocation: Location; - - beforeEach(() => { - // Mock router - mockReplace = vi.fn(); - (useRouter as ReturnType).mockReturnValue({ - replace: mockReplace, - }); - - // Store original location - originalLocation = window.location; - - // Mock localStorage - Storage.prototype.getItem = vi.fn(() => null); - Storage.prototype.setItem = vi.fn(); - Storage.prototype.removeItem = vi.fn(); - }); - - afterEach(() => { - vi.clearAllMocks(); - // Restore location - Object.defineProperty(window, "location", { - writable: true, - value: originalLocation, - }); - }); - - describe("FeatureFlagsProvider - redirect logic", () => { - it("should not redirect when refactoredUIFlag is true", async () => { - // Set flag to true - Storage.prototype.getItem = vi.fn(() => "true"); - - const { result } = renderHook(() => useFeatureFlags(), { - wrapper: FeatureFlagsProvider, - }); - - expect(result.current.refactoredUIFlag).toBe(true); - - // Wait for any effects - await waitFor(() => { - expect(mockReplace).not.toHaveBeenCalled(); - }); - }); - - it("should not redirect when already on a /ui path (race condition protection)", async () => { - // Set flag to false to trigger redirect logic - Storage.prototype.getItem = vi.fn(() => "false"); - - // Mock window.location to be on a custom UI path - delete (window as any).location; - window.location = { - pathname: "/my-custom-path/ui/", - } as Location; - - renderHook(() => useFeatureFlags(), { - wrapper: FeatureFlagsProvider, - }); - - // Wait for timeout and check redirect was NOT called - await new Promise((resolve) => setTimeout(resolve, 150)); - - expect(mockReplace).not.toHaveBeenCalled(); - }); - - it("should not redirect when on /ui path without custom root", async () => { - // Set flag to false - Storage.prototype.getItem = vi.fn(() => "false"); - - // Mock window.location to be on standard UI path - delete (window as any).location; - window.location = { - pathname: "/ui/", - } as Location; - - renderHook(() => useFeatureFlags(), { - wrapper: FeatureFlagsProvider, - }); - - // Wait for timeout and check redirect was NOT called - await new Promise((resolve) => setTimeout(resolve, 150)); - - expect(mockReplace).not.toHaveBeenCalled(); - }); - - it("should redirect when flag is false and not on a /ui path", async () => { - // Set flag to false - Storage.prototype.getItem = vi.fn(() => "false"); - - // Mock window.location to be on a non-UI path - delete (window as any).location; - window.location = { - pathname: "/some-other-path/", - } as Location; - - renderHook(() => useFeatureFlags(), { - wrapper: FeatureFlagsProvider, - }); - - // Wait for timeout plus a bit more - await new Promise((resolve) => setTimeout(resolve, 150)); - - // Should have called replace to redirect to base path - expect(mockReplace).toHaveBeenCalledWith("/"); - }); - - it("should not redirect if already at base path", async () => { - // Set flag to false - Storage.prototype.getItem = vi.fn(() => "false"); - - // Mock window.location to be at root - delete (window as any).location; - window.location = { - pathname: "/", - } as Location; - - renderHook(() => useFeatureFlags(), { - wrapper: FeatureFlagsProvider, - }); - - // Wait for timeout - await new Promise((resolve) => setTimeout(resolve, 150)); - - expect(mockReplace).not.toHaveBeenCalled(); - }); - }); - - describe("useFeatureFlags - flag management", () => { - it("should initialize with false when no value in localStorage", () => { - Storage.prototype.getItem = vi.fn(() => null); - - const { result } = renderHook(() => useFeatureFlags(), { - wrapper: FeatureFlagsProvider, - }); - - expect(result.current.refactoredUIFlag).toBe(false); - }); - - it("should initialize with true when localStorage has true", () => { - Storage.prototype.getItem = vi.fn(() => "true"); - - const { result } = renderHook(() => useFeatureFlags(), { - wrapper: FeatureFlagsProvider, - }); - - expect(result.current.refactoredUIFlag).toBe(true); - }); - - it("should update localStorage when setRefactoredUIFlag is called", () => { - const setItemMock = vi.fn(); - Storage.prototype.setItem = setItemMock; - Storage.prototype.getItem = vi.fn(() => "false"); - - const { result } = renderHook(() => useFeatureFlags(), { - wrapper: FeatureFlagsProvider, - }); - - result.current.setRefactoredUIFlag(true); - - expect(setItemMock).toHaveBeenCalledWith( - "feature.refactoredUIFlag", - "true" - ); - }); - - it("should handle malformed localStorage values gracefully", () => { - Storage.prototype.getItem = vi.fn(() => "invalid-value"); - - const { result } = renderHook(() => useFeatureFlags(), { - wrapper: FeatureFlagsProvider, - }); - - // Should default to false for malformed values - expect(result.current.refactoredUIFlag).toBe(false); - }); - }); - - describe("getBasePath logic with serverRootPath", () => { - it("should handle serverRootPath being set to custom path", async () => { - // Mock the networking module with custom serverRootPath - vi.doMock("@/components/networking", () => ({ - serverRootPath: "/my-custom-path", - })); - - // Set flag to false to trigger redirect - Storage.prototype.getItem = vi.fn(() => "false"); - - // Mock location to be on wrong path - delete (window as any).location; - window.location = { - pathname: "/wrong-path/", - } as Location; - - renderHook(() => useFeatureFlags(), { - wrapper: FeatureFlagsProvider, - }); - - // Wait for timeout - await new Promise((resolve) => setTimeout(resolve, 150)); - - // With default NEXT_PUBLIC_BASE_URL being empty, should redirect to "/" - // (In reality, with serverRootPath="/my-custom-path", it would be "/my-custom-path/") - expect(mockReplace).toHaveBeenCalled(); - }); - }); - - describe("storage event synchronization", () => { - it("should update flag when storage event is fired", async () => { - Storage.prototype.getItem = vi.fn(() => "false"); - - const { result } = renderHook(() => useFeatureFlags(), { - wrapper: FeatureFlagsProvider, - }); - - expect(result.current.refactoredUIFlag).toBe(false); - - // Simulate storage event from another tab - const storageEvent = new StorageEvent("storage", { - key: "feature.refactoredUIFlag", - newValue: "true", - }); - - window.dispatchEvent(storageEvent); - - await waitFor(() => { - expect(result.current.refactoredUIFlag).toBe(true); - }); - }); - - it("should self-heal when storage key is cleared", async () => { - const setItemMock = vi.fn(); - Storage.prototype.setItem = setItemMock; - Storage.prototype.getItem = vi.fn(() => "true"); - - renderHook(() => useFeatureFlags(), { - wrapper: FeatureFlagsProvider, - }); - - // Simulate storage event where key was cleared - const storageEvent = new StorageEvent("storage", { - key: "feature.refactoredUIFlag", - newValue: null, - }); - - window.dispatchEvent(storageEvent); - - await waitFor(() => { - expect(setItemMock).toHaveBeenCalledWith( - "feature.refactoredUIFlag", - "false" - ); - }); - }); - }); - - describe("timeout cleanup", () => { - it("should cleanup timeout on unmount", async () => { - Storage.prototype.getItem = vi.fn(() => "false"); - - delete (window as any).location; - window.location = { - pathname: "/some-path/", - } as Location; - - const { unmount } = renderHook(() => useFeatureFlags(), { - wrapper: FeatureFlagsProvider, - }); - - // Unmount immediately before timeout fires - unmount(); - - // Wait past the timeout - await new Promise((resolve) => setTimeout(resolve, 150)); - - // Should not have called replace since component unmounted - expect(mockReplace).not.toHaveBeenCalled(); - }); - }); -}); - diff --git a/ui/litellm-dashboard/src/hooks/useFeatureFlags.tsx b/ui/litellm-dashboard/src/hooks/useFeatureFlags.tsx deleted file mode 100644 index 03b4465b096..00000000000 --- a/ui/litellm-dashboard/src/hooks/useFeatureFlags.tsx +++ /dev/null @@ -1,137 +0,0 @@ -"use client"; - -import React, { createContext, useContext, useEffect, useState } from "react"; -import { useRouter } from "next/navigation"; -import { serverRootPath } from "@/components/networking"; - -const getBasePath = () => { - const raw = process.env.NEXT_PUBLIC_BASE_URL ?? ""; - const trimmed = raw.replace(/^\/+|\/+$/g, ""); // strip leading/trailing slashes - const uiPath = trimmed ? `/${trimmed}/` : "/"; - - // If serverRootPath is set and not "/", prepend it to the UI path - if (serverRootPath && serverRootPath !== "/") { - // Remove trailing slash from serverRootPath and ensure uiPath has no leading slash for proper joining - const cleanServerRoot = serverRootPath.replace(/\/+$/, ""); - const cleanUiPath = uiPath.replace(/^\/+/, ""); - return `${cleanServerRoot}/${cleanUiPath}`; - } - - return uiPath; -} - -type Flags = { - refactoredUIFlag: boolean; - setRefactoredUIFlag: (v: boolean) => void; -}; - -const STORAGE_KEY = "feature.refactoredUIFlag"; - -const FeatureFlagsCtx = createContext(null); - -/** Safely read the flag from localStorage. If anything goes wrong, reset to false. */ -function readFlagSafely(): boolean { - try { - const raw = localStorage.getItem(STORAGE_KEY); - if (raw === null) { - localStorage.setItem(STORAGE_KEY, "false"); - return false; - } - - const v = raw.trim().toLowerCase(); - if (v === "true" || v === "1") return true; - if (v === "false" || v === "0") return false; - - // Last chance: try JSON.parse in case something odd was stored. - const parsed = JSON.parse(raw); - if (typeof parsed === "boolean") return parsed; - - // Malformed → reset to false - localStorage.setItem(STORAGE_KEY, "false"); - return false; - } catch { - // If even accessing localStorage throws, best effort reset then default to false - try { - localStorage.setItem(STORAGE_KEY, "false"); - } catch {} - return false; - } -} - -function writeFlagSafely(v: boolean) { - try { - localStorage.setItem(STORAGE_KEY, String(v)); - } catch { - // Ignore write errors; state will still reflect the intended value. - } -} - -export const FeatureFlagsProvider = ({ children }: { children: React.ReactNode }) => { - const router = useRouter(); // ⟵ add this - - // Lazy init reads from localStorage only on the client - const [refactoredUIFlag, setRefactoredUIFlagState] = useState(() => readFlagSafely()); - - const setRefactoredUIFlag = (v: boolean) => { - setRefactoredUIFlagState(v); - writeFlagSafely(v); - }; - - // Keep this flag in sync across tabs/windows. - useEffect(() => { - const onStorage = (e: StorageEvent) => { - if (e.key === STORAGE_KEY && e.newValue != null) { - const next = e.newValue.trim().toLowerCase(); - setRefactoredUIFlagState(next === "true" || next === "1"); - } - // If the key was cleared elsewhere, self-heal to false. - if (e.key === STORAGE_KEY && e.newValue === null) { - writeFlagSafely(false); - setRefactoredUIFlagState(false); - } - }; - window.addEventListener("storage", onStorage); - return () => window.removeEventListener("storage", onStorage); - }, []); - - // Redirect to base path the moment the flag is OFF. - useEffect(() => { - if (refactoredUIFlag) return; // only act when turned off - - // Wait a moment for serverRootPath to be initialized from getUiConfig() - // This prevents a race condition where we redirect before knowing the correct path - const checkAndRedirect = () => { - const base = getBasePath(); - const normalize = (p: string) => (p.endsWith("/") ? p : p + "/"); - const current = normalize(window.location.pathname); - - // Don't redirect if we're already on a UI path (even if serverRootPath hasn't loaded yet) - // This handles the case where the page is mounted at a custom server root path - if (current.includes("/ui")) { - return; - } - - // Avoid a redirect loop if we're already at the base path. - if (current !== base) { - // Replace so the "off" redirect doesn't pollute history. - router.replace(base); - } - }; - - // Small delay to allow serverRootPath to be set by getUiConfig() - const timeoutId = setTimeout(checkAndRedirect, 100); - return () => clearTimeout(timeoutId); - }, [refactoredUIFlag, router]); - - return ( - {children} - ); -}; - -const useFeatureFlags = () => { - const ctx = useContext(FeatureFlagsCtx); - if (!ctx) throw new Error("useFeatureFlags must be used within FeatureFlagsProvider"); - return ctx; -}; - -export default useFeatureFlags; From 0d329826f10da90a6b8205ac58ce0344ef82ee9c Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Thu, 27 Nov 2025 17:40:38 -0800 Subject: [PATCH 037/370] Fix flaky tests --- .../src/components/entity_usage.test.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/ui/litellm-dashboard/src/components/entity_usage.test.tsx b/ui/litellm-dashboard/src/components/entity_usage.test.tsx index d5cc503fd10..17016b6479d 100644 --- a/ui/litellm-dashboard/src/components/entity_usage.test.tsx +++ b/ui/litellm-dashboard/src/components/entity_usage.test.tsx @@ -168,16 +168,18 @@ describe("EntityUsage", () => { }); it("should render with organization entity type and call organization API", async () => { - const { getByText, getAllByText } = render(); + render(); await waitFor(() => { expect(mockOrganizationDailyActivityCall).toHaveBeenCalled(); }); - expect(getByText("Organization Spend Overview")).toBeInTheDocument(); + expect(screen.getByText("Organization Spend Overview")).toBeInTheDocument(); - const spendElements = getAllByText("$100.50"); - expect(spendElements.length).toBeGreaterThan(0); + await waitFor(() => { + const spendElements = screen.getAllByText("$100.50"); + expect(spendElements.length).toBeGreaterThan(0); + }); }); it("should switch between tabs", async () => { From a33a2cb5b54d98dc061406cbec62840274e7a811 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Thu, 27 Nov 2025 17:53:09 -0800 Subject: [PATCH 038/370] Adding timeout to flaky test --- .../e2e_ui_tests/view_user_info.spec.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/proxy_admin_ui_tests/e2e_ui_tests/view_user_info.spec.ts b/tests/proxy_admin_ui_tests/e2e_ui_tests/view_user_info.spec.ts index 5b9a9ab133c..01eadc9ad1a 100644 --- a/tests/proxy_admin_ui_tests/e2e_ui_tests/view_user_info.spec.ts +++ b/tests/proxy_admin_ui_tests/e2e_ui_tests/view_user_info.spec.ts @@ -29,8 +29,12 @@ test.describe("User Info View", () => { await firstUserIdCell.click(); // Check for tabs - await expect(page.locator('button:has-text("Overview")')).toBeVisible(); - await expect(page.locator('button:has-text("Details")')).toBeVisible(); + await expect(page.locator('button:has-text("Overview")')).toBeVisible({ + timeout: 10000, + }); + await expect(page.locator('button:has-text("Details")')).toBeVisible({ + timeout: 10000, + }); // Switch to details tab await page.locator('button:has-text("Details")').click(); From d43c0776534251213c47323e5abfcc9b2a645a60 Mon Sep 17 00:00:00 2001 From: Wei-Chiet Ku Date: Fri, 28 Nov 2025 13:24:04 +0800 Subject: [PATCH 039/370] Fix/issue 16759 streaming error validation (#17242) * Enhance error handling in OpenAIResponsesAPIConfig to coalesce null error codes into a default string, preventing validation errors and improving stability during streaming iterations. * Add test for coalescing null error codes in streaming responses This test ensures that when a streaming error event has error.code set to None, the system correctly transforms it to 'unknown_error' and returns an ErrorEvent instance without raising a ValidationError. --------- Co-authored-by: Ku Wei Chiet --- .../llms/openai/responses/transformation.py | 18 +++++++++++++ .../test_openai_responses_transformation.py | 27 +++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/litellm/llms/openai/responses/transformation.py b/litellm/llms/openai/responses/transformation.py index f75213b0688..4c9d3828383 100644 --- a/litellm/llms/openai/responses/transformation.py +++ b/litellm/llms/openai/responses/transformation.py @@ -238,6 +238,24 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig): event_pydantic_model = OpenAIResponsesAPIConfig.get_event_model_class( event_type=event_type ) + # Defensive: Some OpenAI-compatible providers may send `error.code: null`. + # Pydantic will raise a ValidationError when it expects a string but gets None. + # Coalesce a None `error.code` to a stable default string so streaming + # iteration does not crash (see issue report). This keeps behavior similar + # to previous fixes (coalesce before validation) and lets higher-level + # handlers still receive an `ErrorEvent` object. + try: + error_obj = parsed_chunk.get("error") + if isinstance(error_obj, dict) and error_obj.get("code") is None: + # Preserve other fields, but ensure `code` is a non-null string + parsed_chunk = dict(parsed_chunk) + parsed_chunk["error"] = dict(error_obj) + parsed_chunk["error"]["code"] = "unknown_error" + except Exception: + # If anything unexpected happens here, fall back to attempting + # instantiation and let higher-level handlers manage errors. + verbose_logger.debug("Failed to coalesce error.code in parsed_chunk") + return event_pydantic_model(**parsed_chunk) @staticmethod diff --git a/tests/test_litellm/llms/openai/responses/test_openai_responses_transformation.py b/tests/test_litellm/llms/openai/responses/test_openai_responses_transformation.py index fa5231a2a2f..074378fd562 100644 --- a/tests/test_litellm/llms/openai/responses/test_openai_responses_transformation.py +++ b/tests/test_litellm/llms/openai/responses/test_openai_responses_transformation.py @@ -390,6 +390,33 @@ class TestOpenAIResponsesAPIConfig: assert result["partial_images"] == partial_images_value assert result["stream"] is True + def test_transform_streaming_response_coalesces_null_error_code(self): + """Ensure that when a streaming error event contains error.code=None, + transform_streaming_response coalesces it to 'unknown_error' and returns + an ErrorEvent instance without raising a ValidationError. + """ + from litellm.types.llms.openai import ErrorEvent + + parsed_chunk = { + "type": "error", + "sequence_number": 1, + "error": { + "type": "invalid_request_error", + "code": None, + "message": "Something went wrong", + "param": None, + }, + } + + event = self.config.transform_streaming_response( + model=self.model, parsed_chunk=parsed_chunk, logging_obj=self.logging_obj + ) + + # Validate returned type and coalesced code + assert isinstance(event, ErrorEvent) + assert event.error.code == "unknown_error" + assert event.error.message == "Something went wrong" + class TestAzureResponsesAPIConfig: def setup_method(self): From 334d09b3b21c728e6f7152d1804cb8b5aac643ef Mon Sep 17 00:00:00 2001 From: YutaSaito <36355491+uc4w6c@users.noreply.github.com> Date: Fri, 28 Nov 2025 14:26:27 +0900 Subject: [PATCH 040/370] feat: add regex-based tool_name/tool_type matching for tool-permission (#17164) * feat: add regex-based tool_name/tool_type matching for tool-permission * docs: update tool permission quick start for UI workflow --- .../docs/proxy/guardrails/tool_permission.md | 45 +++++- .../guardrail_hooks/tool_permission.py | 139 +++++++++++------ .../guardrail_hooks/tool_permission.py | 33 +++- .../guardrail_hooks/test_tool_permission.py | 144 ++++++++++++------ .../ToolPermissionRulesEditor.tsx | 36 ++++- 5 files changed, 286 insertions(+), 111 deletions(-) diff --git a/docs/my-website/docs/proxy/guardrails/tool_permission.md b/docs/my-website/docs/proxy/guardrails/tool_permission.md index 19b674c9e55..897c31d9dab 100644 --- a/docs/my-website/docs/proxy/guardrails/tool_permission.md +++ b/docs/my-website/docs/proxy/guardrails/tool_permission.md @@ -7,9 +7,38 @@ import TabItem from '@theme/TabItem'; LiteLLM provides the LiteLLM Tool Permission Guardrail that lets you control which **tool calls** a model is allowed to invoke, using configurable allow/deny rules. This offers fine-grained, provider-agnostic control over tool execution (e.g., OpenAI Chat Completions `tool_calls`, Anthropic Messages `tool_use`, MCP tools). ## Quick Start -### 1. Define Guardrails on your LiteLLM config.yaml -Define your guardrails under the `guardrails` section +### LiteLLM UI + +#### Step 1: Select Tool Permission Guardrail + +Open the LiteLLM Dashboard, click **Add New Guardrail**, and choose **LiteLLM Tool Permission Guardrail**. This loads the rule builder UI. + +Configure tool permission guardrail in LiteLLM UI + +#### Step 2: Define Regex Rules + +1. Click **Add Rule**. +2. Enter a unique Rule ID. +3. Provide a regex for the tool name (e.g., `^mcp__github_.*$`). +4. Optionally add a regex for tool type (e.g., `^function$`). +5. Pick **Allow** or **Deny**. + +Configure tool permission guardrail in LiteLLM UI + +#### Step 3: Restrict Tool Arguments (Optional) + +Select **+ Restrict tool arguments** to attach regex validations to nested paths (dot + `[]` notation). This enforces that sensitive parameters (such as `arguments.to[]`) conform to pre-approved formats. + +#### Step 4: Choose Defaults & Actions + +- Set the fallback decision (`default_action`) for tools that do not hit any rule. +- Decide how disallowed tools behave: **Block** halts the request, **Rewrite** strips forbidden tools and returns an error message inside the response. +- Customize `violation_message_template` if you want branded error copy. +- Save the guardrail. + +### LiteLLM Config.yaml Setup + ```yaml guardrails: - guardrail_name: "tool-permission-guardrail" @@ -21,16 +50,17 @@ guardrails: tool_name: "Bash" decision: "allow" - id: "allow_github_mcp" - tool_name: "mcp__github_*" + tool_name: "^mcp__github_.*$" decision: "allow" - id: "allow_aws_documentation" - tool_name: "mcp__aws-documentation_*_documentation" + tool_name: "^mcp__aws-documentation_.*_documentation$" decision: "allow" - id: "deny_read_commands" tool_name: "Read" - decision: "Deny" + decision: "deny" - id: "mail-domain" - tool_name: "send_email" + tool_name: "^send_email$" + tool_type: "^function$" decision: "allow" allowed_param_patterns: "to[]": "^.+@berri\\.ai$" @@ -44,7 +74,8 @@ guardrails: ```yaml - id: "unique_rule_id" # Unique identifier for the rule - tool_name: "pattern" # Tool name or pattern to match + tool_name: "^regex$" # Regex for tool name (optional, at least one of name/type required) + tool_type: "^function$" # Regex for tool type (optional) decision: "allow" # "allow" or "deny" allowed_param_patterns: # Optional - regex map for argument paths (dot + [] notation) "path.to[].field": "^regex$" diff --git a/litellm/proxy/guardrails/guardrail_hooks/tool_permission.py b/litellm/proxy/guardrails/guardrail_hooks/tool_permission.py index 02e06acbda4..64753d9fa85 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/tool_permission.py +++ b/litellm/proxy/guardrails/guardrail_hooks/tool_permission.py @@ -62,6 +62,7 @@ class ToolPermissionGuardrail(CustomGuardrail): self.rules: List[ToolPermissionRule] = [] self._compiled_rule_patterns: Dict[str, Dict[str, re.Pattern]] = {} + self._compiled_rule_targets: Dict[str, Dict[str, Optional[re.Pattern]]] = {} if rules: for rule_item in rules: if isinstance(rule_item, ToolPermissionRule): @@ -70,6 +71,30 @@ class ToolPermissionGuardrail(CustomGuardrail): rule = ToolPermissionRule(**rule_item) self.rules.append(rule) + compiled_target_patterns: Dict[str, Optional[re.Pattern]] = { + "tool_name": None, + "tool_type": None, + } + if rule.tool_name is not None: + try: + compiled_target_patterns["tool_name"] = re.compile( + rule.tool_name + ) + except re.error as exc: + raise ValueError( + f"Invalid regex for tool_name in rule '{rule.id}': {exc}" + ) from exc + if rule.tool_type is not None: + try: + compiled_target_patterns["tool_type"] = re.compile( + rule.tool_type + ) + except re.error as exc: + raise ValueError( + f"Invalid regex for tool_type in rule '{rule.id}': {exc}" + ) from exc + self._compiled_rule_targets[rule.id] = compiled_target_patterns + if rule.allowed_param_patterns: compiled_patterns: Dict[str, re.Pattern] = {} for path, pattern in rule.allowed_param_patterns.items(): @@ -100,59 +125,75 @@ class ToolPermissionGuardrail(CustomGuardrail): return ToolPermissionGuardrailConfigModel - def _matches_pattern(self, tool_name: str, pattern: str) -> bool: - """ - Check if a tool name matches a pattern - - Supports patterns like: - - "Bash" - exact match - - "mcp__*" - prefix pattern (matches names starting wich "mcp__") - - "*_read" - suffix wildcard (matches names ending with "_read") - - "mcp__github_*_read" - infix wildcard (matches names like "mcp__github_mark_all_notifications_read") - - Args: - tool_name: Name of the tool to check - pattern: Pattern to match against - - Returns: - True if the tool name matches the pattern - """ - # Handle exact matches - if tool_name == pattern: + def _matches_regex( + self, pattern: Optional[re.Pattern], value: Optional[str] + ) -> bool: + if pattern is None: return True + if value is None: + return False + return bool(pattern.fullmatch(value)) - if "*" in pattern: - # Escape regex special chars except '*' - escaped_pattern = re.escape(pattern) - # Turn \* into .* - regex_pattern = escaped_pattern.replace(r"\*", ".*") - return bool(re.fullmatch(regex_pattern, tool_name)) + def _rule_matches_tool( + self, + rule: ToolPermissionRule, + *, + tool_name: Optional[str], + tool_type: Optional[str] = None, + ) -> tuple[bool, bool]: + target_patterns = self._compiled_rule_targets.get(rule.id, {}) + name_pattern = target_patterns.get("tool_name") + type_pattern = target_patterns.get("tool_type") - return False + name_required = rule.tool_name is not None + type_required = rule.tool_type is not None + + name_matched = ( + self._matches_regex(name_pattern, tool_name) if name_required else True + ) + type_matched = ( + self._matches_regex(type_pattern, tool_type) if type_required else True + ) + + overall_match = name_matched and type_matched + should_check_params = name_required and name_matched + + return overall_match, should_check_params def _check_tool_permission( - self, tool_name: str + self, + tool_name: Optional[str], + tool_type: Optional[str] = None, ) -> tuple[bool, Optional[str], Optional[str]]: """ Check if a tool is allowed based on the configured rules Args: tool_name: Name of the tool to check + tool_type: Type of the tool to check Returns: Tuple of (is_allowed, rule_id, message) """ - verbose_proxy_logger.debug(f"Checking permission for tool: {tool_name}") + verbose_proxy_logger.debug( + f"Checking permission for tool: {tool_name or tool_type}" + ) # Check each rule in order for rule in self.rules: - if self._matches_pattern(tool_name, rule.tool_name): + matches, _ = self._rule_matches_tool( + rule, + tool_name=tool_name, + tool_type=tool_type, + ) + if matches: is_allowed = rule.decision == "allow" - default_message = f"Tool '{tool_name}' {'allowed' if is_allowed else 'denied'} by rule '{rule.id}'" + tool_identifier = tool_name or tool_type or "unknown_tool" + default_message = f"Tool '{tool_identifier}' {'allowed' if is_allowed else 'denied'} by rule '{rule.id}'" message = self.render_violation_message( default=default_message, context={ - "tool_name": tool_name, + "tool_name": tool_name or tool_identifier, "rule_id": rule.id, }, ) @@ -161,11 +202,12 @@ class ToolPermissionGuardrail(CustomGuardrail): # No rule matched, use default action is_allowed = self.default_action == "allow" - default_message = f"Tool '{tool_name}' {'allowed' if is_allowed else 'denied'} by default action" + tool_identifier = tool_name or tool_type or "unknown_tool" + default_message = f"Tool '{tool_identifier}' {'allowed' if is_allowed else 'denied'} by default action" message = self.render_violation_message( default=default_message, context={ - "tool_name": tool_name, + "tool_name": tool_name or tool_identifier, "rule_id": None, }, ) @@ -228,7 +270,7 @@ class ToolPermissionGuardrail(CustomGuardrail): *, arguments: Dict[str, Any], rule: ToolPermissionRule, - tool_name: str, + tool_name: Optional[str], ) -> tuple[bool, Optional[str]]: compiled_patterns = self._compiled_rule_patterns.get(rule.id) if not compiled_patterns: @@ -249,7 +291,7 @@ class ToolPermissionGuardrail(CustomGuardrail): return ( False, f"Value '{raw_value}' for path '{path}' does not match allowed pattern" - f" '{compiled_pattern.pattern}' for tool '{tool_name}'", + f" '{compiled_pattern.pattern}' for tool '{tool_name or 'unknown_tool'}'", ) return True, None @@ -258,19 +300,27 @@ class ToolPermissionGuardrail(CustomGuardrail): self, tool_call: ChatCompletionMessageToolCall ) -> tuple[bool, Optional[str], Optional[str]]: tool_name = tool_call.function.name if tool_call.function else None - if not tool_name: + tool_type = getattr(tool_call, "type", None) + if not tool_name and not tool_type: return self.default_action == "allow", None, None + tool_identifier = tool_name or tool_type or "unknown_tool" + last_pattern_failure_msg: Optional[str] = None for rule in self.rules: - if not self._matches_pattern(tool_name, rule.tool_name): + matches, should_check_params = self._rule_matches_tool( + rule, + tool_name=tool_name, + tool_type=tool_type, + ) + if not matches: continue - if rule.allowed_param_patterns: + if rule.allowed_param_patterns and should_check_params: arguments = self._parse_tool_call_arguments(tool_call) if not arguments: - last_pattern_failure_msg = f"Tool '{tool_name}' is missing arguments required by rule '{rule.id}'" + last_pattern_failure_msg = f"Tool '{tool_identifier}' is missing arguments required by rule '{rule.id}'" continue patterns_match, failure_message = self._patterns_match_for_rule( @@ -283,10 +333,10 @@ class ToolPermissionGuardrail(CustomGuardrail): continue is_allowed = rule.decision == "allow" - default_message = f"Tool '{tool_name}' {'allowed' if is_allowed else 'denied'} by rule '{rule.id}'" + default_message = f"Tool '{tool_identifier}' {'allowed' if is_allowed else 'denied'} by rule '{rule.id}'" message = self.render_violation_message( default=default_message, - context={"tool_name": tool_name, "rule_id": rule.id}, + context={"tool_name": tool_identifier, "rule_id": rule.id}, ) return is_allowed, rule.id, message @@ -294,11 +344,11 @@ class ToolPermissionGuardrail(CustomGuardrail): default_message = ( last_pattern_failure_msg if (last_pattern_failure_msg and not is_allowed) - else f"Tool '{tool_name}' {'allowed' if is_allowed else 'denied'} by default action" + else f"Tool '{tool_identifier}' {'allowed' if is_allowed else 'denied'} by default action" ) message = self.render_violation_message( default=default_message, - context={"tool_name": tool_name, "rule_id": None}, + context={"tool_name": tool_identifier, "rule_id": None}, ) return is_allowed, None, message @@ -469,8 +519,9 @@ class ToolPermissionGuardrail(CustomGuardrail): if tool["type"] != "function": continue tool_name: str = tool["function"]["name"] + tool_type: Optional[str] = tool.get("type") - is_allowed, _, message = self._check_tool_permission(tool_name) + is_allowed, _, message = self._check_tool_permission(tool_name, tool_type) if not is_allowed and message is not None: verbose_proxy_logger.warning(f"Tool Permission Guardrail: {message}") diff --git a/litellm/types/proxy/guardrails/guardrail_hooks/tool_permission.py b/litellm/types/proxy/guardrails/guardrail_hooks/tool_permission.py index e78cfad8bdb..2ed1f3d2e3a 100644 --- a/litellm/types/proxy/guardrails/guardrail_hooks/tool_permission.py +++ b/litellm/types/proxy/guardrails/guardrail_hooks/tool_permission.py @@ -1,7 +1,7 @@ # Tool Permission Guardrail Type Definitions from typing import Dict, List, Literal, Optional -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, field_validator, model_validator from .base import GuardrailConfigModel @@ -12,8 +12,13 @@ class ToolPermissionRule(BaseModel): """ id: str = Field(description="Unique identifier for the rule") - tool_name: str = Field( - description="Tool name or pattern (e.g., 'Bash', 'mcp__github_*', 'mcp__github_*_read', '*_read')" + tool_name: Optional[str] = Field( + default=None, + description="Regex pattern applied to the tool's function name", + ) + tool_type: Optional[str] = Field( + default=None, + description="Regex pattern applied to the tool type (e.g., function)", ) decision: Literal["allow", "deny"] = Field( description="Whether to allow or deny this tool usage" @@ -23,6 +28,26 @@ class ToolPermissionRule(BaseModel): description="Optional regex map enforcing nested parameter values using dot/[] paths", ) + @field_validator("tool_name", "tool_type", mode="before") + @classmethod + def _blank_to_none(cls, value: Optional[str]) -> Optional[str]: + if value is None: + return None + if isinstance(value, str): + stripped = value.strip() + if not stripped: + return None + return stripped + return value + + @model_validator(mode="after") + def _ensure_target_present(self): + if self.tool_name is None and self.tool_type is None: + raise ValueError( + "Each rule must specify at least a tool_name or tool_type regex" + ) + return self + class ToolResult(BaseModel): """ @@ -52,7 +77,7 @@ class ToolPermissionGuardrailConfigModel(GuardrailConfigModel): rules: Optional[List[ToolPermissionRule]] = Field( default=None, - description="Ordered allow/deny rules. Patterns support * wildcards and optional regex constraints on tool arguments.", + description="Ordered allow/deny rules. Patterns use regex for tool names/types and optional regex constraints on tool arguments.", ) default_action: Literal["allow", "deny"] = Field( default="deny", description="Fallback decision when no rule matches" diff --git a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_tool_permission.py b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_tool_permission.py index 5468dcf9491..a7fd1c64955 100644 --- a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_tool_permission.py +++ b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_tool_permission.py @@ -3,6 +3,7 @@ Unit tests for Tool Permission Guardrail (OpenAI tool_calls semantics) """ import os +import re import sys from unittest.mock import patch @@ -36,15 +37,19 @@ class TestToolPermissionGuardrail: def setup_method(self): """Set up test fixtures""" self.test_rules = [ - {"id": "allow_bash", "tool_name": "Bash", "decision": "allow"}, - {"id": "allow_github", "tool_name": "mcp__github_*", "decision": "allow"}, + {"id": "allow_bash", "tool_name": r"^Bash$", "decision": "allow"}, { - "id": "allow_documentation", - "tool_name": "mcp__aws-documentation_*_documentation", + "id": "allow_github", + "tool_name": r"^mcp__github_.*$", "decision": "allow", }, - {"id": "deny_read", "tool_name": "Read", "decision": "deny"}, - {"id": "deny_get", "tool_name": "*_get", "decision": "deny"}, + { + "id": "allow_documentation", + "tool_name": r"^mcp__aws-documentation_.*_documentation$", + "decision": "allow", + }, + {"id": "deny_read", "tool_name": r"^Read$", "decision": "deny"}, + {"id": "deny_get", "tool_name": r".*_get$", "decision": "deny"}, ] self.guardrail = ToolPermissionGuardrail( @@ -64,50 +69,91 @@ class TestToolPermissionGuardrail: self.guardrail.supported_event_hooks or [] ) - def test_pattern_matching_exact(self): - """Test exact pattern matching""" - assert self.guardrail._matches_pattern("Read", "Read") is True - assert self.guardrail._matches_pattern("Write", "Read") is False + def test_matches_regex_helper(self): + pattern = re.compile(r"^Read$") + assert self.guardrail._matches_regex(pattern, "Read") is True + assert self.guardrail._matches_regex(pattern, "Write") is False + assert self.guardrail._matches_regex(None, "Any") is True + assert self.guardrail._matches_regex(pattern, None) is False - def test_pattern_matching_wildcards(self): - """Test wildcard pattern matching""" - assert ( - self.guardrail._matches_pattern( - "mcp__github_add_issue_comment", "mcp__github_*" - ) - is True + def test_rule_matches_tool_with_type_only(self): + guardrail = ToolPermissionGuardrail( + guardrail_name="type-only", + rules=[ + { + "id": "allow_functions", + "tool_type": r"^function$", + "decision": "allow", + } + ], + default_action="deny", + on_disallowed_action="block", ) - assert ( - self.guardrail._matches_pattern( - "mcp__github_add_issue_comment", "mcp__github_*_comment" - ) - is True + + is_allowed, rule_id, _ = guardrail._check_tool_permission("AnyTool", "function") + assert is_allowed is True + assert rule_id == "allow_functions" + + is_allowed, rule_id, _ = guardrail._check_tool_permission("AnyTool", "custom") + assert is_allowed is False + assert rule_id is None + + def test_rule_matches_tool_with_name_and_type(self): + guardrail = ToolPermissionGuardrail( + guardrail_name="name-type", + rules=[ + { + "id": "allow_specific", + "tool_name": r"^Bash$", + "tool_type": r"^function$", + "decision": "allow", + } + ], + default_action="deny", + on_disallowed_action="block", ) - assert ( - self.guardrail._matches_pattern( - "mcp__github_add_issue_comment", "*_comment" + + is_allowed, rule_id, _ = guardrail._check_tool_permission("Bash", "function") + assert is_allowed is True + assert rule_id == "allow_specific" + + is_allowed, rule_id, _ = guardrail._check_tool_permission("Bash", "custom") + assert is_allowed is False + assert rule_id is None + + def test_rule_requires_name_or_type(self): + with pytest.raises(ValueError): + ToolPermissionGuardrail( + guardrail_name="invalid-rule", + rules=[{"id": "no_target", "decision": "allow"}], + default_action="deny", + on_disallowed_action="block", ) - is True + + def test_type_only_rule_skips_param_patterns(self): + guardrail = ToolPermissionGuardrail( + guardrail_name="type-param", + rules=[ + { + "id": "allow_type_only", + "tool_type": r"^function$", + "decision": "allow", + "allowed_param_patterns": {"foo": r"^bar$"}, + } + ], + default_action="deny", + on_disallowed_action="block", ) - assert ( - self.guardrail._matches_pattern( - "mcp__git_add_issue_comment", "mcp__github_*" - ) - is False - ) - assert ( - self.guardrail._matches_pattern( - "mcp__github_assign_copilot_to_issue", "mcp__github_*_comment" - ) - is False - ) - assert ( - self.guardrail._matches_pattern( - "mcp__github_assign_copilot_to_issue", "*_comment" - ) - is False + + tool_call = ChatCompletionMessageToolCall( + function={"name": "AnyTool", "arguments": "{}"}, + type="function", ) + is_allowed, rule_id, _ = guardrail._get_permission_for_tool_call(tool_call) + assert is_allowed is True + assert rule_id == "allow_type_only" + def test_check_tool_permission_allow(self): is_allowed, rule_id, msg = self.guardrail._check_tool_permission("Bash") assert is_allowed is True @@ -232,7 +278,7 @@ class TestToolPermissionGuardrail: rules=[ { "id": "allow_mail", - "tool_name": "mail_mcp-send_email", + "tool_name": r"^mail_mcp-send_email$", "decision": "allow", "allowed_param_patterns": { "to[]": r"^.+@berri\.ai$", @@ -267,7 +313,7 @@ class TestToolPermissionGuardrail: rules=[ { "id": "allow_mail", - "tool_name": "mail_mcp-send_email", + "tool_name": r"^mail_mcp-send_email$", "decision": "allow", "allowed_param_patterns": {"to[]": r"^.+@berri\.ai$"}, } @@ -300,7 +346,7 @@ class TestToolPermissionGuardrail: rules=[ { "id": "allow_mail", - "tool_name": "mail_mcp-send_email", + "tool_name": r"^mail_mcp-send_email$", "decision": "allow", "allowed_param_patterns": {"to[]": r"^.+@berri\.ai$"}, } @@ -339,7 +385,7 @@ class TestToolPermissionGuardrail: rules=[ { "id": "deny_gmail", - "tool_name": "mail_mcp-send_email", + "tool_name": r"^mail_mcp-send_email$", "decision": "deny", "allowed_param_patterns": {"to[]": r"^.+@gmail\.com$"}, } @@ -486,7 +532,9 @@ class TestToolPermissionGuardrailIntegration: def test_default_action_allow(self): guardrail = ToolPermissionGuardrail( guardrail_name="test-allow-default", - rules=[{"id": "deny_read", "tool_name": "Read", "decision": "deny"}], + rules=[ + {"id": "deny_read", "tool_name": r"^Read$", "decision": "deny"} + ], default_action="allow", ) diff --git a/ui/litellm-dashboard/src/components/guardrails/tool_permission/ToolPermissionRulesEditor.tsx b/ui/litellm-dashboard/src/components/guardrails/tool_permission/ToolPermissionRulesEditor.tsx index 790876ed3f0..68bbc055f45 100644 --- a/ui/litellm-dashboard/src/components/guardrails/tool_permission/ToolPermissionRulesEditor.tsx +++ b/ui/litellm-dashboard/src/components/guardrails/tool_permission/ToolPermissionRulesEditor.tsx @@ -9,7 +9,8 @@ export type ToolPermissionOnDisallowedAction = "block" | "rewrite"; export interface ToolPermissionRuleConfig { id: string; - tool_name: string; + tool_name?: string; + tool_type?: string; decision: ToolPermissionDecision; allowed_param_patterns?: Record; } @@ -67,7 +68,6 @@ const ToolPermissionRulesEditor: React.FC = ({ ...config.rules, { id: `rule_${Math.random().toString(36).slice(2, 8)}`, - tool_name: "", decision: "allow" as ToolPermissionDecision, allowed_param_patterns: undefined, }, @@ -195,8 +195,8 @@ const ToolPermissionRulesEditor: React.FC = ({
LiteLLM Tool Permission Guardrail - Use wildcards (e.g., mcp__github_*) to scope which tools can run and optionally constrain - payload fields. + Provide regex patterns (e.g., ^mcp__github_.*$) for tool names or types and optionally + constrain payload fields.
{!disabled && ( @@ -242,12 +242,32 @@ const ToolPermissionRulesEditor: React.FC = ({ />
- Tool Name / Pattern + Tool Name (optional) updateRule(index, { tool_name: e.target.value })} + placeholder="^mcp__github_.*$" + value={rule.tool_name ?? ""} + onChange={(e) => + updateRule(index, { + tool_name: e.target.value.trim() === "" ? undefined : e.target.value, + }) + } + /> +
+ + +
+
+ Tool Type (optional) + + updateRule(index, { + tool_type: e.target.value.trim() === "" ? undefined : e.target.value, + }) + } />
From 87050c6a022053147fde198d418312615a08981b Mon Sep 17 00:00:00 2001 From: Saar wintrov Date: Fri, 28 Nov 2025 07:27:23 +0200 Subject: [PATCH 041/370] SSO: fix the generic SSO provider (#17227) * SSO: fix the generic SSO provider * adding tests --- litellm/proxy/management_endpoints/ui_sso.py | 12 +- .../proxy/management_endpoints/test_ui_sso.py | 221 ++++++++++++++++++ 2 files changed, 225 insertions(+), 8 deletions(-) diff --git a/litellm/proxy/management_endpoints/ui_sso.py b/litellm/proxy/management_endpoints/ui_sso.py index 44b593efea4..a033e2cf5f4 100644 --- a/litellm/proxy/management_endpoints/ui_sso.py +++ b/litellm/proxy/management_endpoints/ui_sso.py @@ -1253,7 +1253,8 @@ class SSOAuthenticationHandler: Priority order: 1. CLI state (if provided) 2. GENERIC_CLIENT_STATE environment variable - 3. Generated UUID for Okta (if Okta endpoint detected) + 3. Generated UUID (required by Okta and most OAuth providers) + Args: state: Optional state parameter (e.g., CLI state) @@ -1275,13 +1276,8 @@ class SSOAuthenticationHandler: generic_client_state = os.getenv("GENERIC_CLIENT_STATE", None) if generic_client_state: redirect_params["state"] = generic_client_state - elif ( - generic_authorization_endpoint - and "okta" in generic_authorization_endpoint - ): - redirect_params["state"] = ( - uuid.uuid4().hex - ) # set state param for okta - required + else: + redirect_params["state"] = uuid.uuid4().hex # Handle PKCE (Proof Key for Code Exchange) if enabled # Set GENERIC_CLIENT_USE_PKCE=true to enable PKCE for enhanced OAuth security diff --git a/tests/test_litellm/proxy/management_endpoints/test_ui_sso.py b/tests/test_litellm/proxy/management_endpoints/test_ui_sso.py index 4f5f4e0d858..f01813fa587 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_ui_sso.py +++ b/tests/test_litellm/proxy/management_endpoints/test_ui_sso.py @@ -2239,6 +2239,227 @@ class TestGenericResponseConvertorNestedAttributes: assert result.display_name == "user-sub-123" # Top-level attribute works +class TestGetGenericSSORedirectParams: + """Test _get_generic_sso_redirect_params state parameter priority handling""" + + def test_state_priority_cli_state_provided(self): + """ + Test that CLI state takes highest priority when provided + """ + from litellm.proxy.management_endpoints.ui_sso import SSOAuthenticationHandler + + # Arrange + cli_state = "litellm-session-token:sk-test123" + + with patch.dict(os.environ, {"GENERIC_CLIENT_STATE": "env_state_value"}): + # Act + redirect_params, code_verifier = ( + SSOAuthenticationHandler._get_generic_sso_redirect_params( + state=cli_state, + generic_authorization_endpoint="https://auth.example.com/authorize", + ) + ) + + # Assert + assert redirect_params["state"] == cli_state + assert code_verifier is None # PKCE not enabled by default + + def test_state_priority_env_variable_when_no_cli_state(self): + """ + Test that GENERIC_CLIENT_STATE environment variable is used when CLI state is not provided + """ + from litellm.proxy.management_endpoints.ui_sso import SSOAuthenticationHandler + + # Arrange + env_state = "custom_env_state_value" + + with patch.dict(os.environ, {"GENERIC_CLIENT_STATE": env_state}): + # Act + redirect_params, code_verifier = ( + SSOAuthenticationHandler._get_generic_sso_redirect_params( + state=None, + generic_authorization_endpoint="https://auth.example.com/authorize", + ) + ) + + # Assert + assert redirect_params["state"] == env_state + assert code_verifier is None + + def test_state_priority_generated_uuid_fallback(self): + """ + Test that a UUID is generated when neither CLI state nor env variable is provided + """ + from litellm.proxy.management_endpoints.ui_sso import SSOAuthenticationHandler + + # Arrange - no CLI state and no env variable + with patch.dict(os.environ, {}, clear=False): + # Remove GENERIC_CLIENT_STATE if it exists + os.environ.pop("GENERIC_CLIENT_STATE", None) + + # Act + redirect_params, code_verifier = ( + SSOAuthenticationHandler._get_generic_sso_redirect_params( + state=None, + generic_authorization_endpoint="https://auth.example.com/authorize", + ) + ) + + # Assert + assert "state" in redirect_params + assert redirect_params["state"] is not None + assert len(redirect_params["state"]) == 32 # UUID hex is 32 chars + assert code_verifier is None + + def test_state_with_pkce_enabled(self): + """ + Test that PKCE parameters are generated when GENERIC_CLIENT_USE_PKCE is enabled + """ + import base64 + import hashlib + + from litellm.proxy.management_endpoints.ui_sso import SSOAuthenticationHandler + + # Arrange + test_state = "test_state_123" + + with patch.dict(os.environ, {"GENERIC_CLIENT_USE_PKCE": "true"}): + # Act + redirect_params, code_verifier = ( + SSOAuthenticationHandler._get_generic_sso_redirect_params( + state=test_state, + generic_authorization_endpoint="https://auth.example.com/authorize", + ) + ) + + # Assert state + assert redirect_params["state"] == test_state + + # Assert PKCE parameters + assert code_verifier is not None + assert len(code_verifier) == 43 # Standard PKCE verifier length + assert "code_challenge" in redirect_params + assert "code_challenge_method" in redirect_params + assert redirect_params["code_challenge_method"] == "S256" + + # Verify code_challenge is correctly derived from code_verifier + expected_challenge_bytes = hashlib.sha256( + code_verifier.encode("utf-8") + ).digest() + expected_challenge = ( + base64.urlsafe_b64encode(expected_challenge_bytes) + .decode("utf-8") + .rstrip("=") + ) + assert redirect_params["code_challenge"] == expected_challenge + + def test_state_with_pkce_disabled(self): + """ + Test that PKCE parameters are NOT generated when GENERIC_CLIENT_USE_PKCE is false + """ + from litellm.proxy.management_endpoints.ui_sso import SSOAuthenticationHandler + + # Arrange + test_state = "test_state_456" + + with patch.dict(os.environ, {"GENERIC_CLIENT_USE_PKCE": "false"}): + # Act + redirect_params, code_verifier = ( + SSOAuthenticationHandler._get_generic_sso_redirect_params( + state=test_state, + generic_authorization_endpoint="https://auth.example.com/authorize", + ) + ) + + # Assert + assert redirect_params["state"] == test_state + assert code_verifier is None + assert "code_challenge" not in redirect_params + assert "code_challenge_method" not in redirect_params + + def test_state_priority_cli_state_overrides_env_with_pkce(self): + """ + Test that CLI state takes priority over env variable even when PKCE is enabled + """ + from litellm.proxy.management_endpoints.ui_sso import SSOAuthenticationHandler + + # Arrange + cli_state = "cli_state_priority" + env_state = "env_state_should_not_be_used" + + with patch.dict( + os.environ, + { + "GENERIC_CLIENT_STATE": env_state, + "GENERIC_CLIENT_USE_PKCE": "true", + }, + ): + # Act + redirect_params, code_verifier = ( + SSOAuthenticationHandler._get_generic_sso_redirect_params( + state=cli_state, + generic_authorization_endpoint="https://auth.example.com/authorize", + ) + ) + + # Assert + assert redirect_params["state"] == cli_state # CLI state takes priority + assert redirect_params["state"] != env_state + + # PKCE should still be generated + assert code_verifier is not None + assert "code_challenge" in redirect_params + assert "code_challenge_method" in redirect_params + + def test_empty_string_state_uses_env_variable(self): + """ + Test that empty string state is treated as None and uses env variable + """ + from litellm.proxy.management_endpoints.ui_sso import SSOAuthenticationHandler + + # Arrange + env_state = "env_state_for_empty_cli" + + with patch.dict(os.environ, {"GENERIC_CLIENT_STATE": env_state}): + # Act + redirect_params, code_verifier = ( + SSOAuthenticationHandler._get_generic_sso_redirect_params( + state="", # Empty string + generic_authorization_endpoint="https://auth.example.com/authorize", + ) + ) + + # Assert - empty string is falsy, so env variable should be used + # Note: This tests current implementation behavior + # If empty string should be treated differently, implementation needs update + assert redirect_params["state"] == env_state + + def test_multiple_calls_generate_different_uuids(self): + """ + Test that multiple calls without state generate different UUIDs + """ + from litellm.proxy.management_endpoints.ui_sso import SSOAuthenticationHandler + + # Arrange - no state provided + with patch.dict(os.environ, {}, clear=False): + os.environ.pop("GENERIC_CLIENT_STATE", None) + + # Act + params1, _ = SSOAuthenticationHandler._get_generic_sso_redirect_params( + state=None, + generic_authorization_endpoint="https://auth.example.com/authorize", + ) + params2, _ = SSOAuthenticationHandler._get_generic_sso_redirect_params( + state=None, + generic_authorization_endpoint="https://auth.example.com/authorize", + ) + + # Assert + assert params1["state"] != params2["state"] + assert len(params1["state"]) == 32 + assert len(params2["state"]) == 32 + + class TestPKCEFunctionality: """Test PKCE (Proof Key for Code Exchange) functionality""" From 8aa4f3d476f373bd8c0520dfdb15147353641118 Mon Sep 17 00:00:00 2001 From: Andy Forest Date: Fri, 28 Nov 2025 00:50:35 -0500 Subject: [PATCH 042/370] fix(bedrock): handle cohere v4 embed response dictionary format (#17220) --- .../llms/cohere/embed/v1_transformation.py | 10 +++- .../bedrock/embed/test_bedrock_embedding.py | 55 ++++++++++++++++++- 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/litellm/llms/cohere/embed/v1_transformation.py b/litellm/llms/cohere/embed/v1_transformation.py index 1a4bc393e84..feca9cb5b88 100644 --- a/litellm/llms/cohere/embed/v1_transformation.py +++ b/litellm/llms/cohere/embed/v1_transformation.py @@ -1,5 +1,5 @@ """ -Legacy /v1/embedding transformation logic for Bedrock Cohere. +Legacy /v1/embedding transformation logic for Bedrock Cohere. """ from typing import Any, List, Optional, Union @@ -123,7 +123,13 @@ class CohereEmbeddingConfig: """ embeddings = response_json["embeddings"] output_data = [] - is_embeddings_by_type = response_json.get("response_type") == "embeddings_by_type" + is_embeddings_by_type = ( + response_json.get("response_type") == "embeddings_by_type" + ) + + if isinstance(embeddings, dict): + is_embeddings_by_type = True + if is_embeddings_by_type: for embedding_type in embeddings: for idx, embedding in enumerate(embeddings[embedding_type]): diff --git a/tests/test_litellm/llms/bedrock/embed/test_bedrock_embedding.py b/tests/test_litellm/llms/bedrock/embed/test_bedrock_embedding.py index a266bea3513..d6253e59488 100644 --- a/tests/test_litellm/llms/bedrock/embed/test_bedrock_embedding.py +++ b/tests/test_litellm/llms/bedrock/embed/test_bedrock_embedding.py @@ -554,4 +554,57 @@ def test_bedrock_embedding_extra_headers_and_headers_merge(): print(f" Final headers: {list(headers.keys())}") except Exception as e: - pytest.fail(f"Failed to merge and forward headers: {str(e)}") \ No newline at end of file + pytest.fail(f"Failed to merge and forward headers: {str(e)}") + + +def test_bedrock_cohere_v4_embedding_response_parsing(): + """ + Test parsing of Bedrock Cohere v4 embedding response which returns a dictionary of embeddings + keyed by type (e.g. 'float', 'int8') instead of a direct list. + """ + litellm.set_verbose = True + client = HTTPHandler() + test_api_key = "test-bearer-token-12345" + model = "bedrock/cohere.embed-v4:0" + + # Mock response for Cohere v4 with multiple embedding types + cohere_v4_response = { + "embeddings": { + "float": [[0.1, 0.2, 0.3]], + "int8": [[1, 2, 3]] + }, + "response_type": "embeddings_by_type", + "id": "test-id", + "texts": ["test input"] + } + + with patch.object(client, "post") as mock_post: + mock_response = Mock() + mock_response.status_code = 200 + mock_response.text = json.dumps(cohere_v4_response) + mock_response.json = lambda: json.loads(mock_response.text) + mock_post.return_value = mock_response + + response = litellm.embedding( + model=model, + input=["test input"], + client=client, + aws_region_name="us-east-1", + aws_bedrock_runtime_endpoint="https://bedrock-runtime.us-east-1.amazonaws.com", + api_key=test_api_key + ) + + assert isinstance(response, litellm.EmbeddingResponse) + + # Verify we get two embedding objects back (one for float, one for int8) + assert len(response.data) == 2 + + # Check first embedding (float) + assert response.data[0]['object'] == 'embedding' + assert response.data[0]['embedding'] == [0.1, 0.2, 0.3] + assert response.data[0]['type'] == 'float' + + # Check second embedding (int8) + assert response.data[1]['object'] == 'embedding' + assert response.data[1]['embedding'] == [1, 2, 3] + assert response.data[1]['type'] == 'int8' From bbea83fd9366bf6cd2592eed133b00bda0421015 Mon Sep 17 00:00:00 2001 From: Omkar Malpure <77787482+omkar806@users.noreply.github.com> Date: Fri, 28 Nov 2025 11:29:24 +0530 Subject: [PATCH 043/370] Fix : acompletion throws error with SambaNova models (#17217) Co-authored-by: Omkar Malpure --- litellm/llms/sambanova/chat.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/litellm/llms/sambanova/chat.py b/litellm/llms/sambanova/chat.py index b0534347c9a..2218c808721 100644 --- a/litellm/llms/sambanova/chat.py +++ b/litellm/llms/sambanova/chat.py @@ -121,5 +121,10 @@ class SambanovaConfig(OpenAIGPTConfig): SambaNova API doesn't support content as a list - only string content. This converts content lists like [{"type": "text", "text": "..."}] to strings. """ + async def _async_transform(): + return handle_messages_with_content_list_to_str_conversion(messages) + + if is_async: + return _async_transform() messages = handle_messages_with_content_list_to_str_conversion(messages) return messages From 205a563b65d179063ea90128081a0676f75235c2 Mon Sep 17 00:00:00 2001 From: v0rtex20k <55466324+v0rtex20k@users.noreply.github.com> Date: Fri, 28 Nov 2025 01:10:19 -0500 Subject: [PATCH 044/370] Allow wildcard routes for nonproxy admin (SCIM) (#17178) * checked for wildcards in nonproxy * ready --- litellm/proxy/auth/route_checks.py | 6 ++++ .../proxy/auth/test_route_checks.py | 28 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/litellm/proxy/auth/route_checks.py b/litellm/proxy/auth/route_checks.py index 664b8a9ddce..76621e95cd3 100644 --- a/litellm/proxy/auth/route_checks.py +++ b/litellm/proxy/auth/route_checks.py @@ -241,6 +241,12 @@ class RouteChecks: route_allowed = True break + if RouteChecks._route_matches_wildcard_pattern( + route=route, pattern=allowed_route + ): + route_allowed = True + break + if not route_allowed: RouteChecks._raise_admin_only_route_exception( user_obj=user_obj, route=route diff --git a/tests/test_litellm/proxy/auth/test_route_checks.py b/tests/test_litellm/proxy/auth/test_route_checks.py index b2a51de3d67..de2aa2427ca 100644 --- a/tests/test_litellm/proxy/auth/test_route_checks.py +++ b/tests/test_litellm/proxy/auth/test_route_checks.py @@ -732,3 +732,31 @@ def test_videos_route_with_virtual_key_llm_api_routes(): assert ( result is True ), f"Virtual key with llm_api_routes should be able to access {route}" + +def test_non_proxy_admin_wildcard_allowed_routes(): + """Test that nonproxy admin users can still use wildcard routes""" + + user_obj = LiteLLM_UserTable( + user_id="test_user", + user_email="test@example.com", + user_role=LitellmUserRoles.INTERNAL_USER.value, + ) + + valid_token = UserAPIKeyAuth( + user_id="test_user", + user_role=LitellmUserRoles.INTERNAL_USER.value, + allowed_routes=["/scim/*"], + ) + + request = MagicMock(spec=Request) + request.query_params = {} + + RouteChecks.non_proxy_admin_allowed_routes_check( + user_obj=user_obj, + _user_role=LitellmUserRoles.INTERNAL_USER.value, + route="/scim/v2/Users", + request=request, + valid_token=valid_token, + request_data={}, + ) + From 8700c5ced665728e51624e7c95dc0d2894115a9f Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Fri, 28 Nov 2025 14:56:46 +0530 Subject: [PATCH 045/370] Add nova embedding support --- .../docs/embedding/supported_embedding.md | 2 + .../docs/providers/bedrock_embedding.md | 51 +- litellm/__init__.py | 3 + litellm/constants.py | 2 + litellm/llms/bedrock/base_aws_llm.py | 11 +- .../embed/amazon_nova_transformation.py | 258 ++++++++++ litellm/llms/bedrock/embed/embedding.py | 22 + litellm/types/llms/bedrock.py | 127 +++++ litellm/utils.py | 2 + .../test_bedrock_nova_embedding.py | 469 ++++++++++++++++++ 10 files changed, 939 insertions(+), 8 deletions(-) create mode 100644 litellm/llms/bedrock/embed/amazon_nova_transformation.py create mode 100644 tests/llm_translation/test_bedrock_nova_embedding.py diff --git a/docs/my-website/docs/embedding/supported_embedding.md b/docs/my-website/docs/embedding/supported_embedding.md index e63d9403665..0e8252b409b 100644 --- a/docs/my-website/docs/embedding/supported_embedding.md +++ b/docs/my-website/docs/embedding/supported_embedding.md @@ -263,6 +263,8 @@ print(response) | Model Name | Function Call | |----------------------|---------------------------------------------| +| Amazon Nova Multimodal Embeddings | `embedding(model="bedrock/amazon.nova-2-multimodal-embeddings-v1:0", input=input)` | [Nova Docs](../providers/bedrock_embedding#amazon-nova-multimodal-embeddings) | +| Amazon Nova (Async) | `embedding(model="bedrock/async_invoke/amazon.nova-2-multimodal-embeddings-v1:0", input=input, input_type="text", output_s3_uri="s3://bucket/")` | [Nova Async Docs](../providers/bedrock_embedding#asynchronous-embeddings-with-segmentation) | | Titan Embeddings - G1 | `embedding(model="amazon.titan-embed-text-v1", input=input)` | | Cohere Embeddings - English | `embedding(model="cohere.embed-english-v3", input=input)` | | Cohere Embeddings - Multilingual | `embedding(model="cohere.embed-multilingual-v3", input=input)` | diff --git a/docs/my-website/docs/providers/bedrock_embedding.md b/docs/my-website/docs/providers/bedrock_embedding.md index 76c9606533e..e2e7c0dcedd 100644 --- a/docs/my-website/docs/providers/bedrock_embedding.md +++ b/docs/my-website/docs/providers/bedrock_embedding.md @@ -4,7 +4,8 @@ | Provider | LiteLLM Route | AWS Documentation | Cost Tracking | |----------|---------------|-------------------|---------------| -| Amazon Titan | `bedrock/amazon.*` | [Amazon Titan Embeddings](https://docs.aws.amazon.com/bedrock/latest/userguide/titan-embedding-models.html) | āœ… | +| Amazon Titan | `bedrock/amazon.titan-*` | [Amazon Titan Embeddings](https://docs.aws.amazon.com/bedrock/latest/userguide/titan-embedding-models.html) | āœ… | +| Amazon Nova | `bedrock/amazon.nova-*` | [Amazon Nova Embeddings](https://docs.aws.amazon.com/bedrock/latest/userguide/nova-embed.html) | āœ… | | Cohere | `bedrock/cohere.*` | [Cohere Embeddings](https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-cohere-embed.html) | āœ… | | TwelveLabs | `bedrock/us.twelvelabs.*` | [TwelveLabs](https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-twelvelabs.html) | āœ… | @@ -16,6 +17,7 @@ LiteLLM supports AWS Bedrock's async-invoke feature for embedding models that re | Provider | Async Invoke Route | Use Case | |----------|-------------------|----------| +| Amazon Nova | `bedrock/async_invoke/amazon.nova-2-multimodal-embeddings-v1:0` | Multimodal embeddings with segmentation for long text, video, and audio | | TwelveLabs Marengo | `bedrock/async_invoke/us.twelvelabs.marengo-embed-2-7-v1:0` | Video, audio, image, and text embeddings | ### Required Parameters @@ -116,7 +118,7 @@ def check_async_job_status(invocation_arn, aws_region_name="us-east-1"): """Check the status of an async invoke job using LiteLLM batch API""" try: response = retrieve_batch( - batch_id=invocation_arn, + batch_id=invocation_arn, # Pass the invocation ARN here custom_llm_provider="bedrock", aws_region_name=aws_region_name ) @@ -128,11 +130,47 @@ def check_async_job_status(invocation_arn, aws_region_name="us-east-1"): # Check status status = check_async_job_status(invocation_arn, "us-east-1") if status: - print(f"Job Status: {status.status}") - print(f"Output Location: {status.output_file_id}") + print(f"Job Status: {status.status}") # "in_progress", "completed", or "failed" + print(f"Output Location: {status.metadata['output_file_id']}") # S3 URI where results are stored ``` -**Note:** The actual embedding results are stored in S3. The `output_file_id` from the batch status can be used to locate the results file in your S3 bucket. +#### Polling Until Complete + +Here's a complete example of polling for job completion: + +```python +def wait_for_async_job(invocation_arn, aws_region_name="us-east-1", max_wait=3600): + """Poll job status until completion""" + start_time = time.time() + + while True: + status = retrieve_batch( + batch_id=invocation_arn, + custom_llm_provider="bedrock", + aws_region_name=aws_region_name, + ) + + if status.status == "completed": + print("āœ… Job completed!") + return status + elif status.status == "failed": + error_msg = status.metadata.get('failure_message', 'Unknown error') + raise Exception(f"āŒ Job failed: {error_msg}") + else: + elapsed = time.time() - start_time + if elapsed > max_wait: + raise TimeoutError(f"Job timed out after {max_wait} seconds") + + print(f"ā³ Job still processing... (elapsed: {elapsed:.0f}s)") + time.sleep(10) # Wait 10 seconds before checking again + +# Wait for completion +completed_status = wait_for_async_job(invocation_arn) +output_s3_uri = completed_status.metadata['output_file_id'] +print(f"Results available at: {output_s3_uri}") +``` + +**Note:** The actual embedding results are stored in S3. When the job is completed, download the results from the S3 location specified in `status.metadata['output_file_id']`. The results will be in JSON/JSONL format containing the embedding vectors. ### Error Handling @@ -179,7 +217,7 @@ except Exception as e: ### Limitations -- Async-invoke is currently only supported for TwelveLabs Marengo models +- Async-invoke is supported for TwelveLabs Marengo and Amazon Nova models - Results are stored in S3 and must be retrieved separately using the output file ID - Job status checking requires using LiteLLM's `retrieve_batch()` function - No built-in polling mechanism in LiteLLM (must implement your own status checking loop) @@ -259,6 +297,7 @@ print(response) | Model Name | Usage | Supported Additional OpenAI params | |----------------------|---------------------------------------------|-----| +| **Amazon Nova Multimodal Embeddings** | `embedding(model="bedrock/amazon.nova-2-multimodal-embeddings-v1:0", input=input)` | Supports multimodal input (text, image, video, audio), multiple purposes, dimensions (256, 384, 1024, 3072) | | Titan Embeddings V2 | `embedding(model="bedrock/amazon.titan-embed-text-v2:0", input=input)` | [here](https://github.com/BerriAI/litellm/blob/f5905e100068e7a4d61441d7453d7cf5609c2121/litellm/llms/bedrock/embed/amazon_titan_v2_transformation.py#L59) | | Titan Embeddings - V1 | `embedding(model="bedrock/amazon.titan-embed-text-v1", input=input)` | [here](https://github.com/BerriAI/litellm/blob/f5905e100068e7a4d61441d7453d7cf5609c2121/litellm/llms/bedrock/embed/amazon_titan_g1_transformation.py#L53) | Titan Multimodal Embeddings | `embedding(model="bedrock/amazon.titan-embed-image-v1", input=input)` | [here](https://github.com/BerriAI/litellm/blob/f5905e100068e7a4d61441d7453d7cf5609c2121/litellm/llms/bedrock/embed/amazon_titan_multimodal_transformation.py#L28) | diff --git a/litellm/__init__.py b/litellm/__init__.py index 71be5113e2d..6a9ced0f4b9 100644 --- a/litellm/__init__.py +++ b/litellm/__init__.py @@ -1252,6 +1252,9 @@ from .llms.bedrock.embed.cohere_transformation import BedrockCohereEmbeddingConf from .llms.bedrock.embed.twelvelabs_marengo_transformation import ( TwelveLabsMarengoEmbeddingConfig, ) +from .llms.bedrock.embed.amazon_nova_transformation import ( + AmazonNovaEmbeddingConfig, +) from .llms.openai.openai import OpenAIConfig, MistralEmbeddingConfig from .llms.openai.image_variations.transformation import OpenAIImageVariationConfig from .llms.deepinfra.chat.transformation import DeepInfraConfig diff --git a/litellm/constants.py b/litellm/constants.py index 9235916dd43..fd3858ae2f8 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -858,6 +858,7 @@ BEDROCK_EMBEDDING_PROVIDERS_LITERAL = Literal[ "cohere", "amazon", "twelvelabs", + "nova", ] BEDROCK_CONVERSE_MODELS = [ @@ -918,6 +919,7 @@ cohere_embedding_models: set = set( bedrock_embedding_models: set = set( [ "amazon.titan-embed-text-v1", + "amazon.nova-2-multimodal-embeddings-v1:0", "cohere.embed-english-v3", "cohere.embed-multilingual-v3", "cohere.embed-v4:0", diff --git a/litellm/llms/bedrock/base_aws_llm.py b/litellm/llms/bedrock/base_aws_llm.py index 72e270428ac..ed658c793af 100644 --- a/litellm/llms/bedrock/base_aws_llm.py +++ b/litellm/llms/bedrock/base_aws_llm.py @@ -387,9 +387,16 @@ class BaseAWSLLM: Handles scenarios like: 1. model=cohere.embed-english-v3:0 -> Returns `cohere` 2. model=amazon.titan-embed-text-v1 -> Returns `amazon` - 3. model=us.twelvelabs.marengo-embed-2-7-v1:0 -> Returns `twelvelabs` - 4. model=twelvelabs.marengo-embed-2-7-v1:0 -> Returns `twelvelabs` + 3. model=amazon.nova-2-multimodal-embeddings-v1:0 -> Returns `nova` + 4. model=us.twelvelabs.marengo-embed-2-7-v1:0 -> Returns `twelvelabs` + 5. model=twelvelabs.marengo-embed-2-7-v1:0 -> Returns `twelvelabs` """ + # Special case: Check for "nova" in model name first (before "amazon") + # This handles amazon.nova-* models + if "nova" in model.lower(): + if "nova" in get_args(BEDROCK_EMBEDDING_PROVIDERS_LITERAL): + return cast(BEDROCK_EMBEDDING_PROVIDERS_LITERAL, "nova") + # Handle regional models like us.twelvelabs.marengo-embed-2-7-v1:0 if "." in model: parts = model.split(".") diff --git a/litellm/llms/bedrock/embed/amazon_nova_transformation.py b/litellm/llms/bedrock/embed/amazon_nova_transformation.py new file mode 100644 index 00000000000..97652175a94 --- /dev/null +++ b/litellm/llms/bedrock/embed/amazon_nova_transformation.py @@ -0,0 +1,258 @@ +""" +Transformation logic from OpenAI /v1/embeddings format to Bedrock Amazon Nova /invoke and /async-invoke format. + +Why separate file? Make it easy to see how transformation works + +Supports: +- Synchronous embeddings (SINGLE_EMBEDDING) +- Asynchronous embeddings with segmentation (SEGMENTED_EMBEDDING) +- Multimodal inputs: text, image, video, audio +- Multiple embedding purposes and dimensions + +Docs - https://docs.aws.amazon.com/bedrock/latest/userguide/nova-embed.html +""" + +from typing import List, Optional + +from litellm.types.utils import Embedding, EmbeddingResponse, Usage + + +class AmazonNovaEmbeddingConfig: + """ + Reference: https://docs.aws.amazon.com/bedrock/latest/userguide/nova-embed.html + + Amazon Nova Multimodal Embeddings supports: + - Text, image, video, and audio inputs + - Synchronous (InvokeModel) and asynchronous (StartAsyncInvoke) APIs + - Multiple embedding purposes and dimensions + """ + + def __init__(self) -> None: + pass + + def get_supported_openai_params(self) -> List[str]: + return [ + "dimensions", + ] + + def map_openai_params( + self, non_default_params: dict, optional_params: dict + ) -> dict: + """Map OpenAI-style parameters to Nova parameters.""" + for k, v in non_default_params.items(): + if k == "dimensions": + # Map OpenAI dimensions to Nova embedding_dimension + optional_params["embedding_dimension"] = v + elif k in self.get_supported_openai_params(): + optional_params[k] = v + return optional_params + + def _transform_request( + self, + input: str, + inference_params: dict, + async_invoke_route: bool = False, + model_id: Optional[str] = None, + output_s3_uri: Optional[str] = None, + ) -> dict: + """ + Transform OpenAI-style input to Nova format. + + Only handles OpenAI params (dimensions). All other Nova-specific params + should be passed via inference_params and will be passed through as-is. + + Args: + input: The input text or media reference + inference_params: Additional parameters (will be passed through) + async_invoke_route: Whether this is for async invoke + model_id: Model ID (for async invoke) + output_s3_uri: S3 URI for output (for async invoke) + + Returns: + dict: Nova embedding request + """ + # Determine task type + task_type = "SEGMENTED_EMBEDDING" if async_invoke_route else "SINGLE_EMBEDDING" + + # Build the base request structure + request: dict = { + "schemaVersion": "nova-multimodal-embed-v1", + "taskType": task_type, + } + + # Start with inference_params (user-provided params) + embedding_params = inference_params.copy() + + # Map OpenAI dimensions to embeddingDimension if provided + if "dimensions" in embedding_params: + embedding_params["embeddingDimension"] = embedding_params.pop("dimensions") + elif "embedding_dimension" in embedding_params: + embedding_params["embeddingDimension"] = embedding_params.pop("embedding_dimension") + + # Add required embeddingPurpose if not provided (required by Nova API) + if "embeddingPurpose" not in embedding_params: + embedding_params["embeddingPurpose"] = "GENERIC_INDEX" + + # Add required embeddingDimension if not provided (required by Nova API) + if "embeddingDimension" not in embedding_params: + embedding_params["embeddingDimension"] = 3072 + + # For text input, add basic text structure if user hasn't provided text/image/video/audio + if "text" not in embedding_params and "image" not in embedding_params and "video" not in embedding_params and "audio" not in embedding_params: + # Default to text if no modality specified + if input.startswith("s3://"): + embedding_params["text"] = { + "source": {"s3Location": {"uri": input}}, + "truncationMode": "END" # Required by Nova API + } + else: + embedding_params["text"] = { + "value": input, + "truncationMode": "END" # Required by Nova API + } + + # Set the embedding params in the request + if task_type == "SINGLE_EMBEDDING": + request["singleEmbeddingParams"] = embedding_params + else: + request["segmentedEmbeddingParams"] = embedding_params + + # For async invoke, wrap in the async invoke format + if async_invoke_route and model_id: + return self._wrap_async_invoke_request( + model_input=request, + model_id=model_id, + output_s3_uri=output_s3_uri, + ) + + return request + + def _wrap_async_invoke_request( + self, + model_input: dict, + model_id: str, + output_s3_uri: Optional[str] = None, + ) -> dict: + """ + Wrap the transformed request in the AWS Bedrock async invoke format. + + Args: + model_input: The transformed Nova embedding request + model_id: The model identifier (without async_invoke prefix) + output_s3_uri: S3 URI for output data config + + Returns: + dict: The wrapped async invoke request + """ + import urllib.parse + + # Clean the model ID + unquoted_model_id = urllib.parse.unquote(model_id) + if unquoted_model_id.startswith("async_invoke/"): + unquoted_model_id = unquoted_model_id.replace("async_invoke/", "") + + # Validate that the S3 URI is not empty + if not output_s3_uri or output_s3_uri.strip() == "": + raise ValueError("output_s3_uri is required for async invoke requests") + + return { + "modelId": unquoted_model_id, + "modelInput": model_input, + "outputDataConfig": { + "s3OutputDataConfig": { + "s3Uri": output_s3_uri + } + }, + } + + def _transform_response( + self, response_list: List[dict], model: str + ) -> EmbeddingResponse: + """ + Transform Nova response to OpenAI format. + + Nova response format: + { + "embeddings": [ + { + "embeddingType": "TEXT" | "IMAGE" | "VIDEO" | "AUDIO" | "AUDIO_VIDEO_COMBINED", + "embedding": [0.1, 0.2, ...], + "truncatedCharLength": 100 # Optional, only for text + } + ] + } + """ + embeddings: List[Embedding] = [] + total_tokens = 0 + + for response in response_list: + # Nova response has an "embeddings" array + if "embeddings" in response and isinstance(response["embeddings"], list): + for item in response["embeddings"]: + if "embedding" in item: + embedding = Embedding( + embedding=item["embedding"], + index=len(embeddings), + object="embedding", + ) + embeddings.append(embedding) + + # Estimate token count + # For text, use truncatedCharLength if available + if "truncatedCharLength" in item: + total_tokens += item["truncatedCharLength"] // 4 + else: + # Rough estimate based on embedding dimension + total_tokens += len(item["embedding"]) // 4 + elif "embedding" in response: + # Direct embedding response (fallback) + embedding = Embedding( + embedding=response["embedding"], + index=len(embeddings), + object="embedding", + ) + embeddings.append(embedding) + total_tokens += len(response["embedding"]) // 4 + + usage = Usage(prompt_tokens=total_tokens, total_tokens=total_tokens) + + return EmbeddingResponse(data=embeddings, model=model, usage=usage) + + def _transform_async_invoke_response( + self, response: dict, model: str + ) -> EmbeddingResponse: + """ + Transform async invoke response (invocation ARN) to OpenAI format. + + AWS async invoke returns: + { + "invocationArn": "arn:aws:bedrock:us-east-1:123456789012:async-invoke/abc123" + } + + We transform this to a job-like embedding response with the ARN in hidden params. + """ + invocation_arn = response.get("invocationArn", "") + + # Create a placeholder embedding object for the job + embedding = Embedding( + embedding=[], # Empty embedding for async jobs + index=0, + object="embedding", + ) + + # Create usage object (empty for async jobs) + usage = Usage(prompt_tokens=0, total_tokens=0) + + # Create hidden params with job ID + from litellm.types.llms.base import HiddenParams + + hidden_params = HiddenParams() + setattr(hidden_params, "_invocation_arn", invocation_arn) + + return EmbeddingResponse( + data=[embedding], + model=model, + usage=usage, + hidden_params=hidden_params, + ) + diff --git a/litellm/llms/bedrock/embed/embedding.py b/litellm/llms/bedrock/embed/embedding.py index fea29935975..be2bfcd70ec 100644 --- a/litellm/llms/bedrock/embed/embedding.py +++ b/litellm/llms/bedrock/embed/embedding.py @@ -27,6 +27,7 @@ from litellm.types.utils import EmbeddingResponse, LlmProviders from ..base_aws_llm import BaseAWSLLM from ..common_utils import BedrockError +from .amazon_nova_transformation import AmazonNovaEmbeddingConfig from .amazon_titan_g1_transformation import AmazonTitanG1Config from .amazon_titan_multimodal_transformation import ( AmazonTitanMultimodalEmbeddingG1Config, @@ -175,6 +176,12 @@ class BedrockEmbedding(BaseAWSLLM): response=response_list[0], model=model ) ) + elif provider == "nova": + returned_response = ( + AmazonNovaEmbeddingConfig()._transform_async_invoke_response( + response=response_list[0], model=model + ) + ) else: # For other providers, create a generic async response invocation_arn = response_list[0].get("invocationArn", "") @@ -222,6 +229,10 @@ class BedrockEmbedding(BaseAWSLLM): response_list=response_list, model=model ) ) + elif provider == "nova": + returned_response = AmazonNovaEmbeddingConfig()._transform_response( + response_list=response_list, model=model + ) ########################################################## # Validate returned response @@ -467,6 +478,17 @@ class BedrockEmbedding(BaseAWSLLM): ) ) batch_data.append(twelvelabs_request) + elif provider == "nova": + batch_data = [] + for i in input: + nova_request = AmazonNovaEmbeddingConfig()._transform_request( + input=i, + inference_params=inference_params, + async_invoke_route=has_async_invoke, + model_id=modelId, + output_s3_uri=inference_params.get("output_s3_uri"), + ) + batch_data.append(nova_request) ### SET RUNTIME ENDPOINT ### endpoint_url, proxy_endpoint_url = self.get_runtime_endpoint( diff --git a/litellm/types/llms/bedrock.py b/litellm/types/llms/bedrock.py index 330308e179c..3696f679640 100644 --- a/litellm/types/llms/bedrock.py +++ b/litellm/types/llms/bedrock.py @@ -427,6 +427,133 @@ class TwelveLabsAsyncInvokeStatusResponse(TypedDict): failureMessage: Optional[str] +# Amazon Nova Multimodal Embeddings types +NOVA_EMBEDDING_PURPOSES = Literal[ + "GENERIC_INDEX", + "GENERIC_RETRIEVAL", + "TEXT_RETRIEVAL", + "IMAGE_RETRIEVAL", + "VIDEO_RETRIEVAL", + "DOCUMENT_RETRIEVAL", + "AUDIO_RETRIEVAL", + "CLASSIFICATION", + "CLUSTERING", +] + +NOVA_EMBEDDING_DIMENSIONS = Literal[256, 384, 1024, 3072] + +NOVA_TRUNCATION_MODES = Literal["START", "END", "NONE"] + +NOVA_DETAIL_LEVELS = Literal["STANDARD_IMAGE", "DOCUMENT_IMAGE"] + +NOVA_EMBEDDING_MODES = Literal["AUDIO_VIDEO_COMBINED", "AUDIO_VIDEO_SEPARATE"] + +NOVA_EMBEDDING_TYPES = Literal[ + "TEXT", "IMAGE", "VIDEO", "AUDIO", "AUDIO_VIDEO_COMBINED" +] + + +class NovaSourceS3Location(TypedDict): + uri: str + + +class NovaSourceObject(TypedDict, total=False): + bytes: str # base64 encoded + s3Location: NovaSourceS3Location + + +class NovaTextParams(TypedDict, total=False): + truncationMode: NOVA_TRUNCATION_MODES + value: str + source: NovaSourceObject + + +class NovaImageParams(TypedDict, total=False): + format: str # png, jpeg, gif, webp + source: Required[NovaSourceObject] + detailLevel: NOVA_DETAIL_LEVELS + + +class NovaVideoParams(TypedDict, total=False): + format: str # mp4, mov, mkv, webm, flv, mpeg, mpg, wmv, 3gp + source: Required[NovaSourceObject] + embeddingMode: Required[NOVA_EMBEDDING_MODES] + + +class NovaAudioParams(TypedDict, total=False): + format: str # mp3, wav, ogg + source: Required[NovaSourceObject] + + +class NovaTextSegmentationConfig(TypedDict, total=False): + maxLengthChars: int # 800-50,000, default 32,000 + + +class NovaMediaSegmentationConfig(TypedDict, total=False): + durationSeconds: int # 1-30, default 5 + + +class NovaTextParamsWithSegmentation(NovaTextParams, total=False): + segmentationConfig: NovaTextSegmentationConfig + + +class NovaVideoParamsWithSegmentation(NovaVideoParams, total=False): + segmentationConfig: NovaMediaSegmentationConfig + + +class NovaAudioParamsWithSegmentation(NovaAudioParams, total=False): + segmentationConfig: NovaMediaSegmentationConfig + + +class NovaSingleEmbeddingParams(TypedDict, total=False): + embeddingPurpose: Required[NOVA_EMBEDDING_PURPOSES] + embeddingDimension: NOVA_EMBEDDING_DIMENSIONS + text: NovaTextParams + image: NovaImageParams + video: NovaVideoParams + audio: NovaAudioParams + + +class NovaSegmentedEmbeddingParams(TypedDict, total=False): + embeddingPurpose: Required[NOVA_EMBEDDING_PURPOSES] + embeddingDimension: NOVA_EMBEDDING_DIMENSIONS + text: NovaTextParamsWithSegmentation + image: NovaImageParams + video: NovaVideoParamsWithSegmentation + audio: NovaAudioParamsWithSegmentation + + +class NovaEmbeddingRequest(TypedDict, total=False): + schemaVersion: str # "nova-multimodal-embed-v1" + taskType: Literal["SINGLE_EMBEDDING", "SEGMENTED_EMBEDDING"] + singleEmbeddingParams: NovaSingleEmbeddingParams + segmentedEmbeddingParams: NovaSegmentedEmbeddingParams + + +class NovaEmbeddingItem(TypedDict, total=False): + embeddingType: NOVA_EMBEDDING_TYPES + embedding: Required[List[float]] + truncatedCharLength: int # Only for text + + +class NovaEmbeddingResponse(TypedDict): + embeddings: List[NovaEmbeddingItem] + + +class NovaS3OutputDataConfig(TypedDict): + s3Uri: str + + +class NovaOutputDataConfig(TypedDict): + s3OutputDataConfig: NovaS3OutputDataConfig + + +class NovaAsyncInvokeRequest(TypedDict): + modelId: str + modelInput: NovaEmbeddingRequest + outputDataConfig: NovaOutputDataConfig + + AmazonEmbeddingRequest = Union[ AmazonTitanMultimodalEmbeddingRequest, AmazonTitanV2EmbeddingRequest, diff --git a/litellm/utils.py b/litellm/utils.py index 053368a3e0a..15e068a4681 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -2827,6 +2827,8 @@ def get_optional_params_embeddings( # noqa: PLR0915 object = litellm.BedrockCohereEmbeddingConfig() elif "twelvelabs" in model or "marengo" in model: object = litellm.TwelveLabsMarengoEmbeddingConfig() + elif "nova" in model.lower(): + object = litellm.AmazonNovaEmbeddingConfig() else: # unmapped model supported_params = [] _check_valid_arg(supported_params=supported_params) diff --git a/tests/llm_translation/test_bedrock_nova_embedding.py b/tests/llm_translation/test_bedrock_nova_embedding.py new file mode 100644 index 00000000000..8cc77b3c3cb --- /dev/null +++ b/tests/llm_translation/test_bedrock_nova_embedding.py @@ -0,0 +1,469 @@ +""" +Test suite for Amazon Nova Multimodal Embeddings integration with LiteLLM. + +Tests cover: +- Synchronous text embeddings +- Synchronous image embeddings +- Synchronous video/audio embeddings +- Asynchronous embeddings with segmentation +- Different embedding purposes and dimensions +- Error handling +""" + +import json +import os +import sys +from unittest.mock import MagicMock, Mock, patch + +import pytest + +sys.path.insert( + 0, os.path.abspath("../..") +) # Adds the parent directory to the system path + +import litellm +from litellm.llms.bedrock.embed.amazon_nova_transformation import ( + AmazonNovaEmbeddingConfig, +) + + +class TestNovaTransformationRequest: + """Test request transformation for Nova embeddings.""" + + def test_text_embedding_sync_request(self): + """Test synchronous text embedding request transformation.""" + config = AmazonNovaEmbeddingConfig() + + inference_params = { + "embeddingPurpose": "GENERIC_INDEX", + "embedding_dimension": 1024, + "truncation_mode": "END", + } + + request = config._transform_request( + input="Hello, world!", + inference_params=inference_params, + async_invoke_route=False, + ) + + assert request["schemaVersion"] == "nova-multimodal-embed-v1" + assert request["taskType"] == "SINGLE_EMBEDDING" + assert "singleEmbeddingParams" in request + + params = request["singleEmbeddingParams"] + assert params["embeddingPurpose"] == "GENERIC_INDEX" + assert params["embeddingDimension"] == 1024 + assert params["text"]["truncationMode"] == "END" + assert params["text"]["value"] == "Hello, world!" + + def test_text_embedding_async_request(self): + """Test asynchronous text embedding request transformation.""" + config = AmazonNovaEmbeddingConfig() + + inference_params = { + "embeddingPurpose": "TEXT_RETRIEVAL", + "embeddingDimension": 3072, + "text": { + "value": "Long text content...", + "segmentationConfig": {"maxLengthChars": 10000} + }, + "output_s3_uri": "s3://my-bucket/output/", + } + + request = config._transform_request( + input="Long text content...", + inference_params=inference_params, + async_invoke_route=True, + model_id="amazon.nova-2-multimodal-embeddings-v1:0", + output_s3_uri="s3://my-bucket/output/", + ) + + assert "modelId" in request + assert "modelInput" in request + assert "outputDataConfig" in request + + model_input = request["modelInput"] + assert model_input["taskType"] == "SEGMENTED_EMBEDDING" + assert "segmentedEmbeddingParams" in model_input + + params = model_input["segmentedEmbeddingParams"] + assert params["embeddingPurpose"] == "TEXT_RETRIEVAL" + assert params["embeddingDimension"] == 3072 + assert params["text"]["segmentationConfig"]["maxLengthChars"] == 10000 + + def test_image_embedding_request(self): + """Test image embedding request transformation.""" + config = AmazonNovaEmbeddingConfig() + + # Mock base64 image data + image_data = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==" + + inference_params = { + "embeddingPurpose": "IMAGE_RETRIEVAL", + "embeddingDimension": 1024, + "image": { + "format": "png", + "source": {"bytes": image_data}, + "detailLevel": "STANDARD_IMAGE" + }, + } + + request = config._transform_request( + input=image_data, + inference_params=inference_params, + async_invoke_route=False, + ) + + params = request["singleEmbeddingParams"] + assert params["embeddingPurpose"] == "IMAGE_RETRIEVAL" + assert params["embeddingDimension"] == 1024 + assert params["image"]["format"] == "png" + assert params["image"]["detailLevel"] == "STANDARD_IMAGE" + assert "source" in params["image"] + assert "bytes" in params["image"]["source"] + + def test_video_embedding_request(self): + """Test video embedding request transformation.""" + config = AmazonNovaEmbeddingConfig() + + inference_params = { + "embeddingPurpose": "VIDEO_RETRIEVAL", + "embeddingDimension": 3072, + "video": { + "format": "mp4", + "source": {"s3Location": {"uri": "s3://my-bucket/video.mp4"}}, + "embeddingMode": "AUDIO_VIDEO_COMBINED" + }, + } + + request = config._transform_request( + input="s3://my-bucket/video.mp4", + inference_params=inference_params, + async_invoke_route=False, + ) + + params = request["singleEmbeddingParams"] + assert params["embeddingPurpose"] == "VIDEO_RETRIEVAL" + assert params["embeddingDimension"] == 3072 + assert params["video"]["format"] == "mp4" + assert params["video"]["embeddingMode"] == "AUDIO_VIDEO_COMBINED" + assert params["video"]["source"]["s3Location"]["uri"] == "s3://my-bucket/video.mp4" + + def test_audio_embedding_request(self): + """Test audio embedding request transformation.""" + config = AmazonNovaEmbeddingConfig() + + inference_params = { + "embeddingPurpose": "AUDIO_RETRIEVAL", + "embeddingDimension": 1024, + "audio": { + "format": "mp3", + "source": {"s3Location": {"uri": "s3://my-bucket/audio.mp3"}} + }, + } + + request = config._transform_request( + input="s3://my-bucket/audio.mp3", + inference_params=inference_params, + async_invoke_route=False, + ) + + params = request["singleEmbeddingParams"] + assert params["embeddingPurpose"] == "AUDIO_RETRIEVAL" + assert params["embeddingDimension"] == 1024 + assert params["audio"]["format"] == "mp3" + assert params["audio"]["source"]["s3Location"]["uri"] == "s3://my-bucket/audio.mp3" + + def test_async_invoke_requires_output_s3_uri(self): + """Test that async invoke requires output_s3_uri.""" + config = AmazonNovaEmbeddingConfig() + + inference_params = { + "embedding_purpose": "GENERIC_INDEX", + } + + with pytest.raises(ValueError, match="output_s3_uri is required"): + config._transform_request( + input="Test text", + inference_params=inference_params, + async_invoke_route=True, + model_id="amazon.nova-2-multimodal-embeddings-v1:0", + output_s3_uri=None, + ) + + def test_default_embedding_purpose(self): + """Test default embedding purpose is GENERIC_INDEX.""" + config = AmazonNovaEmbeddingConfig() + + request = config._transform_request( + input="Test text", + inference_params={}, + async_invoke_route=False, + ) + + params = request["singleEmbeddingParams"] + assert params["embeddingPurpose"] == "GENERIC_INDEX" + + def test_default_embedding_dimension(self): + """Test default embedding dimension is 3072.""" + config = AmazonNovaEmbeddingConfig() + + request = config._transform_request( + input="Test text", + inference_params={}, + async_invoke_route=False, + ) + + params = request["singleEmbeddingParams"] + assert params["embeddingDimension"] == 3072 + + +class TestNovaTransformationResponse: + """Test response transformation for Nova embeddings.""" + + def test_text_embedding_response(self): + """Test text embedding response transformation.""" + config = AmazonNovaEmbeddingConfig() + + response_list = [ + { + "embeddings": [ + { + "embeddingType": "TEXT", + "embedding": [0.1, 0.2, 0.3, 0.4, 0.5], + } + ] + } + ] + + result = config._transform_response(response_list, model="amazon.nova-2-multimodal-embeddings-v1:0") + + assert result.model == "amazon.nova-2-multimodal-embeddings-v1:0" + assert len(result.data) == 1 + assert result.data[0].embedding == [0.1, 0.2, 0.3, 0.4, 0.5] + assert result.data[0].index == 0 + assert result.data[0].object == "embedding" + assert result.usage.total_tokens > 0 + + def test_multiple_embeddings_response(self): + """Test response with multiple embeddings.""" + config = AmazonNovaEmbeddingConfig() + + response_list = [ + { + "embeddings": [ + { + "embeddingType": "TEXT", + "embedding": [0.1, 0.2, 0.3], + } + ] + }, + { + "embeddings": [ + { + "embeddingType": "TEXT", + "embedding": [0.4, 0.5, 0.6], + } + ] + }, + ] + + result = config._transform_response(response_list, model="amazon.nova-2-multimodal-embeddings-v1:0") + + assert len(result.data) == 2 + assert result.data[0].embedding == [0.1, 0.2, 0.3] + assert result.data[1].embedding == [0.4, 0.5, 0.6] + assert result.data[0].index == 0 + assert result.data[1].index == 1 + + def test_video_embedding_response_separate_mode(self): + """Test video embedding response with separate audio/video.""" + config = AmazonNovaEmbeddingConfig() + + response_list = [ + { + "embeddings": [ + { + "embeddingType": "VIDEO", + "embedding": [0.1, 0.2, 0.3], + }, + { + "embeddingType": "AUDIO", + "embedding": [0.4, 0.5, 0.6], + } + ] + } + ] + + result = config._transform_response(response_list, model="amazon.nova-2-multimodal-embeddings-v1:0") + + assert len(result.data) == 2 + assert result.data[0].embedding == [0.1, 0.2, 0.3] + assert result.data[1].embedding == [0.4, 0.5, 0.6] + + def test_async_invoke_response(self): + """Test async invoke response transformation.""" + config = AmazonNovaEmbeddingConfig() + + response = { + "invocationArn": "arn:aws:bedrock:us-east-1:123456789012:async-invoke/abc123" + } + + result = config._transform_async_invoke_response(response, model="amazon.nova-2-multimodal-embeddings-v1:0") + + assert result.model == "amazon.nova-2-multimodal-embeddings-v1:0" + assert len(result.data) == 1 + assert result.data[0].embedding == [] # Empty for async jobs + assert result.usage.total_tokens == 0 + assert hasattr(result, "_hidden_params") + assert hasattr(result._hidden_params, "_invocation_arn") + assert result._hidden_params._invocation_arn == "arn:aws:bedrock:us-east-1:123456789012:async-invoke/abc123" + + +class TestNovaEmbeddingIntegration: + """Integration tests for Nova embeddings through LiteLLM.""" + + @pytest.mark.skip(reason="Requires AWS credentials and actual API calls") + def test_sync_text_embedding_e2e(self): + """End-to-end test for synchronous text embedding.""" + response = litellm.embedding( + model="bedrock/amazon.nova-2-multimodal-embeddings-v1:0", + input=["Hello, world!"], + aws_region_name="us-east-1", + ) + + assert response is not None + assert len(response.data) == 1 + assert len(response.data[0].embedding) > 0 + + @pytest.mark.skip(reason="Requires AWS credentials and actual API calls") + def test_async_text_embedding_e2e(self): + """End-to-end test for asynchronous text embedding.""" + response = litellm.embedding( + model="bedrock/async_invoke/amazon.nova-2-multimodal-embeddings-v1:0", + input=["Long text content for segmentation..."], + aws_region_name="us-east-1", + output_s3_uri="s3://my-bucket/output/", + segmentation_config={"maxLengthChars": 10000}, + ) + + assert response is not None + assert hasattr(response, "_hidden_params") + assert hasattr(response._hidden_params, "_invocation_arn") + + @pytest.mark.skip(reason="Requires AWS credentials and actual API calls") + def test_image_embedding_e2e(self): + """End-to-end test for image embedding.""" + response = litellm.embedding( + model="bedrock/amazon.nova-2-multimodal-embeddings-v1:0", + input=["s3://my-bucket/image.png"], + aws_region_name="us-east-1", + input_type="image", + format="png", + embedding_purpose="IMAGE_RETRIEVAL", + ) + + assert response is not None + assert len(response.data) == 1 + + @pytest.mark.skip(reason="Requires AWS credentials and actual API calls") + def test_video_embedding_e2e(self): + """End-to-end test for video embedding.""" + response = litellm.embedding( + model="bedrock/amazon.nova-2-multimodal-embeddings-v1:0", + input=["s3://my-bucket/video.mp4"], + aws_region_name="us-east-1", + input_type="video", + format="mp4", + embedding_mode="AUDIO_VIDEO_COMBINED", + embedding_purpose="VIDEO_RETRIEVAL", + ) + + assert response is not None + assert len(response.data) == 1 + + @pytest.mark.skip(reason="Requires AWS credentials and actual API calls") + def test_different_dimensions(self): + """Test different embedding dimensions.""" + for dimension in [256, 384, 1024, 3072]: + response = litellm.embedding( + model="bedrock/amazon.nova-2-multimodal-embeddings-v1:0", + input=["Test text"], + aws_region_name="us-east-1", + dimensions=dimension, + ) + + assert response is not None + assert len(response.data[0].embedding) == dimension + + @pytest.mark.skip(reason="Requires AWS credentials and actual API calls") + def test_different_embedding_purposes(self): + """Test different embedding purposes.""" + purposes = [ + "GENERIC_INDEX", + "GENERIC_RETRIEVAL", + "TEXT_RETRIEVAL", + "CLASSIFICATION", + "CLUSTERING", + ] + + for purpose in purposes: + response = litellm.embedding( + model="bedrock/amazon.nova-2-multimodal-embeddings-v1:0", + input=["Test text"], + aws_region_name="us-east-1", + embedding_purpose=purpose, + ) + + assert response is not None + assert len(response.data) == 1 + + +class TestNovaProviderDetection: + """Test provider detection for Nova models.""" + + def test_nova_provider_detection(self): + """Test that Nova provider is correctly detected.""" + from litellm.llms.bedrock.base_aws_llm import BaseAWSLLM + + provider = BaseAWSLLM.get_bedrock_embedding_provider( + "amazon.nova-2-multimodal-embeddings-v1:0" + ) + + # Should detect "amazon" as provider since "nova" is in the model name + # but the provider detection looks at the first part before the dot + assert provider in ["amazon", "nova"] + + def test_nova_in_model_name(self): + """Test that models with 'nova' in the name are detected.""" + from litellm.llms.bedrock.base_aws_llm import BaseAWSLLM + + # Test various Nova model name formats + test_models = [ + "amazon.nova-2-multimodal-embeddings-v1:0", + "us.amazon.nova-2-multimodal-embeddings-v1:0", + ] + + for model in test_models: + provider = BaseAWSLLM.get_bedrock_embedding_provider(model) + assert provider is not None + + +if __name__ == "__main__": + # Run basic transformation tests + print("Running Nova Embedding Transformation Tests...") + + test_request = TestNovaTransformationRequest() + test_request.test_text_embedding_sync_request() + test_request.test_text_embedding_async_request() + test_request.test_image_embedding_request() + test_request.test_video_embedding_request() + test_request.test_audio_embedding_request() + + test_response = TestNovaTransformationResponse() + test_response.test_text_embedding_response() + test_response.test_multiple_embeddings_response() + test_response.test_async_invoke_response() + + print("All transformation tests passed!") + From f0d3c96a8d5a42763798d4f76afa71464d87d97f Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Fri, 28 Nov 2025 17:23:15 +0530 Subject: [PATCH 046/370] Add tags and other field in UI logs and add responses api cost tracking --- .../proxy/hooks/proxy_track_cost_callback.py | 17 +- .../openai_passthrough_logging_handler.py | 84 ++++++-- .../pass_through_endpoints.py | 26 ++- ...test_openai_passthrough_logging_handler.py | 185 ++++++++++++++++++ 4 files changed, 292 insertions(+), 20 deletions(-) diff --git a/litellm/proxy/hooks/proxy_track_cost_callback.py b/litellm/proxy/hooks/proxy_track_cost_callback.py index e165f96b663..dab5fb1bfd5 100644 --- a/litellm/proxy/hooks/proxy_track_cost_callback.py +++ b/litellm/proxy/hooks/proxy_track_cost_callback.py @@ -80,10 +80,25 @@ class _ProxyDBLogger(CustomLogger): if "litellm_params" not in request_data: request_data["litellm_params"] = {} + + existing_litellm_params = request_data.get("litellm_params", {}) + existing_litellm_metadata = existing_litellm_params.get("metadata", {}) or {} + + # Preserve tags from existing metadata + if existing_litellm_metadata.get("tags"): + existing_metadata["tags"] = existing_litellm_metadata.get("tags") + request_data["litellm_params"]["proxy_server_request"] = ( - request_data.get("proxy_server_request") or {} + request_data.get("proxy_server_request") or existing_litellm_params.get("proxy_server_request") or {} ) request_data["litellm_params"]["metadata"] = existing_metadata + + # Preserve model name and custom_llm_provider + if "model" not in request_data: + request_data["model"] = existing_litellm_params.get("model") or request_data.get("model", "") + if "custom_llm_provider" not in request_data: + request_data["custom_llm_provider"] = existing_litellm_params.get("custom_llm_provider") or request_data.get("custom_llm_provider", "") + await proxy_logging_obj.db_spend_update_writer.update_database( token=user_api_key_dict.api_key, response_cost=0.0, diff --git a/litellm/proxy/pass_through_endpoints/llm_provider_handlers/openai_passthrough_logging_handler.py b/litellm/proxy/pass_through_endpoints/llm_provider_handlers/openai_passthrough_logging_handler.py index d6ab121096e..6745c559cd2 100644 --- a/litellm/proxy/pass_through_endpoints/llm_provider_handlers/openai_passthrough_logging_handler.py +++ b/litellm/proxy/pass_through_endpoints/llm_provider_handlers/openai_passthrough_logging_handler.py @@ -91,6 +91,21 @@ class OpenAIPassthroughLoggingHandler(BasePassthroughLoggingHandler): and "/v1/images/edits" in parsed_url.path ) + @staticmethod + def is_openai_responses_route(url_route: str) -> bool: + """Check if the URL route is an OpenAI responses API endpoint.""" + if not url_route: + return False + parsed_url = urlparse(url_route) + return bool( + parsed_url.hostname + and ( + "api.openai.com" in parsed_url.hostname + or "openai.azure.com" in parsed_url.hostname + ) + and ("/v1/responses" in parsed_url.path or "/responses" in parsed_url.path) + ) + def _get_user_from_metadata( self, passthrough_logging_payload: PassthroughStandardLoggingPayload, @@ -187,7 +202,7 @@ class OpenAIPassthroughLoggingHandler(BasePassthroughLoggingHandler): **kwargs, ) -> PassThroughEndpointLoggingTypedDict: """ - Handle OpenAI passthrough logging with cost tracking for chat completions, image generation, and image editing. + Handle OpenAI passthrough logging with cost tracking for chat completions, image generation, image editing, and responses API. """ # Check if this is a supported endpoint for cost tracking is_chat_completions = ( @@ -199,8 +214,11 @@ class OpenAIPassthroughLoggingHandler(BasePassthroughLoggingHandler): is_image_editing = ( OpenAIPassthroughLoggingHandler.is_openai_image_editing_route(url_route) ) + is_responses = ( + OpenAIPassthroughLoggingHandler.is_openai_responses_route(url_route) + ) - if not (is_chat_completions or is_image_generation or is_image_editing): + if not (is_chat_completions or is_image_generation or is_image_editing or is_responses): # For unsupported endpoints, return None to let the system fall back to generic behavior return { "result": None, @@ -232,9 +250,13 @@ class OpenAIPassthroughLoggingHandler(BasePassthroughLoggingHandler): litellm_model_response: Optional[Union[ModelResponse, TextCompletionResponse, ImageResponse]] = None handler_instance = OpenAIPassthroughLoggingHandler() + custom_llm_provider = kwargs.get("custom_llm_provider", "openai") + if is_chat_completions: # Handle chat completions with existing logic provider_config = handler_instance.get_provider_config(model=model) + # Preserve existing litellm_params to maintain metadata tags + existing_litellm_params = kwargs.get("litellm_params", {}) or {} litellm_model_response = provider_config.transform_response( raw_response=httpx_response, model_response=litellm.ModelResponse(), @@ -247,14 +269,14 @@ class OpenAIPassthroughLoggingHandler(BasePassthroughLoggingHandler): encoding=litellm.encoding, json_mode=request_body.get("response_format", {}).get("type") == "json_object", - litellm_params={}, + litellm_params=existing_litellm_params, ) # Calculate cost using LiteLLM's cost calculator response_cost = litellm.completion_cost( completion_response=litellm_model_response, model=model, - custom_llm_provider="openai", + custom_llm_provider=custom_llm_provider, ) elif is_image_generation: # Handle image generation cost calculation @@ -306,11 +328,36 @@ class OpenAIPassthroughLoggingHandler(BasePassthroughLoggingHandler): if not hasattr(litellm_model_response, "_hidden_params"): litellm_model_response._hidden_params = {} litellm_model_response._hidden_params["response_cost"] = response_cost + elif is_responses: + # Handle responses API cost calculation + provider_config = handler_instance.get_provider_config(model=model) + existing_litellm_params = kwargs.get("litellm_params", {}) or {} + litellm_model_response = provider_config.transform_response( + raw_response=httpx_response, + model_response=litellm.ModelResponse(), + model=model, + messages=request_body.get("messages", []), + logging_obj=logging_obj, + optional_params=request_body.get("optional_params", {}), + api_key="", + request_data=request_body, + encoding=litellm.encoding, + json_mode=False, + litellm_params=existing_litellm_params, + ) + + # Calculate cost using LiteLLM's cost calculator with responses call type + response_cost = litellm.completion_cost( + completion_response=litellm_model_response, + model=model, + custom_llm_provider=custom_llm_provider, + call_type="responses", + ) # Update kwargs with cost information kwargs["response_cost"] = response_cost kwargs["model"] = model - kwargs["custom_llm_provider"] = "openai" + kwargs["custom_llm_provider"] = custom_llm_provider # Extract user information for tracking passthrough_logging_payload: Optional[ @@ -321,10 +368,7 @@ class OpenAIPassthroughLoggingHandler(BasePassthroughLoggingHandler): passthrough_logging_payload=passthrough_logging_payload, ) if user: - kwargs.setdefault("litellm_params", {}) - kwargs["litellm_params"].update( - {"proxy_server_request": {"body": {"user": user}}} - ) + kwargs["litellm_params"].setdefault("proxy_server_request", {}).setdefault("body", {})["user"] = user # Create standard logging object if litellm_model_response is not None: @@ -339,7 +383,7 @@ class OpenAIPassthroughLoggingHandler(BasePassthroughLoggingHandler): # Update logging object with cost information logging_obj.model_call_details["model"] = model - logging_obj.model_call_details["custom_llm_provider"] = "openai" + logging_obj.model_call_details["custom_llm_provider"] = custom_llm_provider logging_obj.model_call_details["response_cost"] = response_cost endpoint_type = ( @@ -481,18 +525,27 @@ class OpenAIPassthroughLoggingHandler(BasePassthroughLoggingHandler): "kwargs": {}, } + custom_llm_provider = litellm_logging_obj.model_call_details.get( + "custom_llm_provider", "openai" + ) # Calculate cost using LiteLLM's cost calculator response_cost = litellm.completion_cost( completion_response=complete_response, model=model, - custom_llm_provider="openai", + custom_llm_provider=custom_llm_provider, ) + # Preserve existing litellm_params to maintain metadata tags + existing_litellm_params = litellm_logging_obj.model_call_details.get( + "litellm_params", {} + ) or {} + # Prepare kwargs for logging kwargs = { "response_cost": response_cost, "model": model, - "custom_llm_provider": "openai", + "custom_llm_provider": custom_llm_provider, + "litellm_params": existing_litellm_params.copy(), } # Extract user information for tracking @@ -506,10 +559,7 @@ class OpenAIPassthroughLoggingHandler(BasePassthroughLoggingHandler): passthrough_logging_payload=passthrough_logging_payload, ) if user: - kwargs.setdefault("litellm_params", {}) - kwargs["litellm_params"].update( - {"proxy_server_request": {"body": {"user": user}}} - ) + kwargs["litellm_params"].setdefault("proxy_server_request", {}).setdefault("body", {})["user"] = user # Create standard logging object get_standard_logging_object_payload( @@ -523,7 +573,7 @@ class OpenAIPassthroughLoggingHandler(BasePassthroughLoggingHandler): # Update logging object with cost information litellm_logging_obj.model_call_details["model"] = model - litellm_logging_obj.model_call_details["custom_llm_provider"] = "openai" + litellm_logging_obj.model_call_details["custom_llm_provider"] = custom_llm_provider litellm_logging_obj.model_call_details["response_cost"] = response_cost verbose_proxy_logger.debug( diff --git a/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py b/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py index 780a3d6dcc6..5b47a8af7a5 100644 --- a/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py +++ b/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py @@ -735,6 +735,12 @@ async def pass_through_request( # noqa: PLR0915 logging_obj=logging_obj, ) + # Store custom_llm_provider in kwargs and logging object if provided + if custom_llm_provider: + kwargs["custom_llm_provider"] = custom_llm_provider + logging_obj.model_call_details["custom_llm_provider"] = custom_llm_provider + logging_obj.model_call_details["litellm_params"] = kwargs.get("litellm_params", {}) + # done for supporting 'parallel_request_limiter.py' with pass-through endpoints logging_obj.update_environment_variables( model="unknown", @@ -923,6 +929,12 @@ async def pass_through_request( # noqa: PLR0915 if kwargs: for key, value in kwargs.items(): request_payload[key] = value + + if "model" not in request_payload and _parsed_body and isinstance(_parsed_body, dict): + request_payload["model"] = _parsed_body.get("model", "") + if "custom_llm_provider" not in request_payload and custom_llm_provider: + request_payload["custom_llm_provider"] = custom_llm_provider + await proxy_logging_obj.post_call_failure_hook( user_api_key_dict=user_api_key_dict, original_exception=e, @@ -957,11 +969,21 @@ def _update_metadata_with_tags_in_header(request: Request, metadata: dict) -> di """ If tags are in the request headers, add them to the metadata - Used for google and vertex JS SDKs + Used for google and vertex JS SDKs, and Azure passthrough + Checks both 'tags' and 'x-litellm-tags' headers """ + # Initialize tags list if it doesn't exist + if "tags" not in metadata: + metadata["tags"] = [] + + # Check for 'tags' header first _tags = request.headers.get("tags") if _tags: - metadata["tags"] = _tags.split(",") + metadata["tags"].extend([tag.strip() for tag in _tags.split(",")]) + + _tags = request.headers.get("x-litellm-tags") + if _tags: + metadata["tags"].extend([tag.strip() for tag in _tags.split(",")]) return metadata diff --git a/tests/test_litellm/proxy/pass_through_endpoints/llm_provider_handlers/test_openai_passthrough_logging_handler.py b/tests/test_litellm/proxy/pass_through_endpoints/llm_provider_handlers/test_openai_passthrough_logging_handler.py index 789b16f9515..becb34409b1 100644 --- a/tests/test_litellm/proxy/pass_through_endpoints/llm_provider_handlers/test_openai_passthrough_logging_handler.py +++ b/tests/test_litellm/proxy/pass_through_endpoints/llm_provider_handlers/test_openai_passthrough_logging_handler.py @@ -130,6 +130,19 @@ class TestOpenAIPassthroughLoggingHandler: assert OpenAIPassthroughLoggingHandler.is_openai_image_editing_route("http://localhost:4000/openai/v1/images/edits") == False assert OpenAIPassthroughLoggingHandler.is_openai_image_editing_route("") == False + def test_is_openai_responses_route(self): + """Test OpenAI responses API route detection""" + # Positive cases + assert OpenAIPassthroughLoggingHandler.is_openai_responses_route("https://api.openai.com/v1/responses") == True + assert OpenAIPassthroughLoggingHandler.is_openai_responses_route("https://openai.azure.com/v1/responses") == True + assert OpenAIPassthroughLoggingHandler.is_openai_responses_route("https://api.openai.com/responses") == True + + # Negative cases + assert OpenAIPassthroughLoggingHandler.is_openai_responses_route("https://api.openai.com/v1/chat/completions") == False + assert OpenAIPassthroughLoggingHandler.is_openai_responses_route("https://api.openai.com/v1/images/generations") == False + assert OpenAIPassthroughLoggingHandler.is_openai_responses_route("http://localhost:4000/openai/v1/responses") == False + assert OpenAIPassthroughLoggingHandler.is_openai_responses_route("") == False + @patch('litellm.completion_cost') @patch('litellm.litellm_core_utils.litellm_logging.get_standard_logging_object_payload') def test_openai_passthrough_handler_success(self, mock_get_standard_logging, mock_completion_cost): @@ -369,6 +382,178 @@ class TestOpenAIPassthroughLoggingHandler: handler = OpenAIPassthroughLoggingHandler() assert handler.get_provider_config("gpt-4o") is not None + @patch('litellm.completion_cost') + @patch('litellm.litellm_core_utils.litellm_logging.get_standard_logging_object_payload') + def test_azure_passthrough_tags_metadata_model_provider(self, mock_get_standard_logging, mock_completion_cost): + """Test that tags, metadata, model, and custom_llm_provider are preserved for Azure passthrough in UI""" + # Arrange + mock_completion_cost.return_value = 0.000045 + mock_get_standard_logging.return_value = {"test": "logging_payload"} + + mock_httpx_response = self._create_mock_httpx_response() + mock_logging_obj = self._create_mock_logging_obj() + + # Create payload with metadata tags + passthrough_payload = PassthroughStandardLoggingPayload( + url="https://openai.azure.com/v1/chat/completions", + request_body={ + "model": "gpt-4o", + "messages": [{"role": "user", "content": "Hello"}] + }, + request_method="POST", + ) + + # Set up kwargs with existing litellm_params containing metadata tags + kwargs = { + "passthrough_logging_payload": passthrough_payload, + "model": "gpt-4o", + "custom_llm_provider": "azure", # Azure passthrough + "litellm_params": { + "metadata": { + "tags": ["production", "azure-deployment"], + "user_id": "user_123" + }, + "proxy_server_request": { + "body": { + "user": "test_user" + } + } + } + } + + # Act + result = OpenAIPassthroughLoggingHandler.openai_passthrough_handler( + httpx_response=mock_httpx_response, + response_body=self.mock_openai_response, + logging_obj=mock_logging_obj, + url_route="https://openai.azure.com/v1/chat/completions", + result="", + start_time=self.start_time, + end_time=self.end_time, + cache_hit=False, + request_body={"model": "gpt-4o", "messages": [{"role": "user", "content": "Hello"}]}, + **kwargs + ) + + # Assert - Verify tags, model, and custom_llm_provider are preserved + assert result is not None + assert "kwargs" in result + + # Verify model and custom_llm_provider are set correctly + assert result["kwargs"]["model"] == "gpt-4o" + assert result["kwargs"]["custom_llm_provider"] == "azure" # Should preserve Azure, not default to "openai" + assert result["kwargs"]["response_cost"] == 0.000045 + + # Verify metadata tags are preserved in litellm_params + assert "litellm_params" in result["kwargs"] + assert "metadata" in result["kwargs"]["litellm_params"] + assert "tags" in result["kwargs"]["litellm_params"]["metadata"] + assert result["kwargs"]["litellm_params"]["metadata"]["tags"] == ["production", "azure-deployment"] + assert result["kwargs"]["litellm_params"]["metadata"]["user_id"] == "user_123" + + # Verify logging object has correct values for UI display + assert mock_logging_obj.model_call_details["model"] == "gpt-4o" + assert mock_logging_obj.model_call_details["custom_llm_provider"] == "azure" + assert mock_logging_obj.model_call_details["response_cost"] == 0.000045 + + # Verify cost calculation was called with correct custom_llm_provider + mock_completion_cost.assert_called_once() + call_args = mock_completion_cost.call_args + assert call_args[1]["custom_llm_provider"] == "azure" + + @patch('litellm.completion_cost') + @patch('litellm.litellm_core_utils.litellm_logging.get_standard_logging_object_payload') + @patch('litellm.proxy.pass_through_endpoints.llm_provider_handlers.openai_passthrough_logging_handler.OpenAIPassthroughLoggingHandler.get_provider_config') + def test_responses_api_cost_tracking(self, mock_get_provider_config, mock_get_standard_logging, mock_completion_cost): + """Test cost tracking for responses API route""" + # Arrange + mock_completion_cost.return_value = 0.000050 + mock_get_standard_logging.return_value = {"test": "logging_payload"} + + # Mock the provider config's transform_response to return a valid ModelResponse + from litellm import ModelResponse + mock_model_response = ModelResponse( + id="resp_abc123", + model="gpt-4o-2024-08-06", + choices=[{ + "message": { + "role": "assistant", + "content": "Hello! How can I help you today?" + } + }], + usage={ + "prompt_tokens": 20, + "completion_tokens": 15, + "total_tokens": 35 + } + ) + + mock_provider_config = MagicMock() + mock_provider_config.transform_response.return_value = mock_model_response + mock_get_provider_config.return_value = mock_provider_config + + # Mock responses API response + mock_responses_response = { + "id": "resp_abc123", + "object": "response", + "created": 1677652288, + "model": "gpt-4o-2024-08-06", + "output": [ + { + "type": "text", + "text": "Hello! How can I help you today?" + } + ], + "usage": { + "input_tokens": 20, + "output_tokens": 15 + } + } + + mock_httpx_response = self._create_mock_httpx_response(mock_responses_response) + mock_logging_obj = self._create_mock_logging_obj() + passthrough_payload = self._create_passthrough_logging_payload() + + kwargs = { + "passthrough_logging_payload": passthrough_payload, + "model": "gpt-4o", + "custom_llm_provider": "openai", + } + + # Act + result = OpenAIPassthroughLoggingHandler.openai_passthrough_handler( + httpx_response=mock_httpx_response, + response_body=mock_responses_response, + logging_obj=mock_logging_obj, + url_route="https://api.openai.com/v1/responses", + result="", + start_time=self.start_time, + end_time=self.end_time, + cache_hit=False, + request_body={"model": "gpt-4o", "input": "Tell me about AI"}, + **kwargs + ) + + # Assert + assert result is not None + assert "result" in result + assert "kwargs" in result + assert result["kwargs"]["response_cost"] == 0.000050 + assert result["kwargs"]["model"] == "gpt-4o" + assert result["kwargs"]["custom_llm_provider"] == "openai" + + # Verify cost calculation was called with responses call type + mock_completion_cost.assert_called_once() + call_args = mock_completion_cost.call_args + assert call_args[1]["call_type"] == "responses" + assert call_args[1]["model"] == "gpt-4o" + assert call_args[1]["custom_llm_provider"] == "openai" + + # Verify logging object was updated + assert mock_logging_obj.model_call_details["response_cost"] == 0.000050 + assert mock_logging_obj.model_call_details["model"] == "gpt-4o" + assert mock_logging_obj.model_call_details["custom_llm_provider"] == "openai" + class TestOpenAIPassthroughIntegration: """Integration tests for OpenAI passthrough cost tracking""" From eab0ec95f01a61f14c3d441fdcd7960a49a003fd Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Fri, 28 Nov 2025 18:06:53 +0530 Subject: [PATCH 047/370] Fix async get request --- litellm/batches/main.py | 44 +++++++++++++------ .../embed/amazon_nova_transformation.py | 2 + litellm/llms/bedrock/embed/embedding.py | 39 +++++++++++----- 3 files changed, 60 insertions(+), 25 deletions(-) diff --git a/litellm/batches/main.py b/litellm/batches/main.py index 995c45b925a..57a9857dd6a 100644 --- a/litellm/batches/main.py +++ b/litellm/batches/main.py @@ -1045,28 +1045,44 @@ def _handle_async_invoke_status( # Transform response to a LiteLLMBatch object from litellm.types.utils import LiteLLMBatch + # Normalize status to lowercase (AWS returns 'Completed', 'Failed', etc.) + aws_status_raw = status_response.get("status", "") + aws_status_lower = aws_status_raw.lower() + # Map AWS status values to LiteLLM expected values + status_mapping = { + "completed": "completed", + "failed": "failed", + "inprogress": "in_progress", + "in_progress": "in_progress", + } + normalized_status = status_mapping.get(aws_status_lower, aws_status_lower) + + # Get output S3 URI safely + output_s3_uri = "" + try: + output_s3_uri = status_response["outputDataConfig"]["s3OutputDataConfig"]["s3Uri"] + except (KeyError, TypeError): + pass + + # Use BedrockBatchesConfig's timestamp parsing method (expects raw AWS status string) + from litellm.llms.bedrock.batches.transformation import BedrockBatchesConfig + created_at, in_progress_at, completed_at, failed_at, _, _ = BedrockBatchesConfig()._parse_timestamps_and_status(status_response, aws_status_raw) result = LiteLLMBatch( id=status_response["invocationArn"], object="batch", - status=status_response["status"], - created_at=status_response["submitTime"], - in_progress_at=status_response["lastModifiedTime"], - completed_at=status_response.get("endTime"), - failed_at=( - status_response.get("endTime") - if status_response["status"] == "failed" - else None - ), + status=normalized_status, + created_at=created_at, + in_progress_at=in_progress_at, + completed_at=completed_at, + failed_at=failed_at, request_counts=BatchRequestCounts( total=1, - completed=1 if status_response["status"] == "completed" else 0, - failed=1 if status_response["status"] == "failed" else 0, + completed=1 if normalized_status == "completed" else 0, + failed=1 if normalized_status == "failed" else 0, ), metadata=dict( **{ - "output_file_id": status_response["outputDataConfig"][ - "s3OutputDataConfig" - ]["s3Uri"], + "output_file_id": output_s3_uri, "failure_message": status_response.get("failureMessage") or "", "model_arn": status_response["modelArn"], } diff --git a/litellm/llms/bedrock/embed/amazon_nova_transformation.py b/litellm/llms/bedrock/embed/amazon_nova_transformation.py index 97652175a94..ada49d0ff21 100644 --- a/litellm/llms/bedrock/embed/amazon_nova_transformation.py +++ b/litellm/llms/bedrock/embed/amazon_nova_transformation.py @@ -83,6 +83,8 @@ class AmazonNovaEmbeddingConfig: # Start with inference_params (user-provided params) embedding_params = inference_params.copy() + embedding_params.pop("output_s3_uri", None) + # Map OpenAI dimensions to embeddingDimension if provided if "dimensions" in embedding_params: embedding_params["embeddingDimension"] = embedding_params.pop("dimensions") diff --git a/litellm/llms/bedrock/embed/embedding.py b/litellm/llms/bedrock/embed/embedding.py index be2bfcd70ec..c9eea516eb0 100644 --- a/litellm/llms/bedrock/embed/embedding.py +++ b/litellm/llms/bedrock/embed/embedding.py @@ -603,22 +603,39 @@ class BedrockEmbedding(BaseAWSLLM): aws_region_name=aws_region_name, ) - # Construct the status check URL - status_url = f"{endpoint_url}/async-invoke/{invocation_arn}" - # Prepare headers + from urllib.parse import quote + + # Encode the ARN for use in URL path + encoded_arn = quote(invocation_arn, safe="") + status_url = f"{endpoint_url.rstrip('/')}/async-invoke/{encoded_arn}" + + # Prepare headers for GET request headers = {"Content-Type": "application/json"} - # Get AWS signed headers - prepped = self.get_request_headers( # type: ignore - credentials=credentials, - aws_region_name=aws_region_name, - extra_headers=None, - endpoint_url=status_url, - data="", # GET request, no body + # Use AWSRequest directly for GET requests (get_request_headers hardcodes POST) + try: + from botocore.auth import SigV4Auth + from botocore.awsrequest import AWSRequest + except ImportError: + raise ImportError( + "Missing boto3 to call bedrock. Run 'pip install boto3'." + ) + + # Create AWSRequest with GET method and encoded URL + request = AWSRequest( + method="GET", + url=status_url, + data=None, # GET request, no body headers=headers, - api_key=None, ) + + # Sign the request - SigV4Auth will create canonical string from request URL + sigv4 = SigV4Auth(credentials, "bedrock", aws_region_name) + sigv4.add_auth(request) + + # Prepare the request + prepped = request.prepare() # LOGGING if logging_obj is not None: From 4cf7a74e6092b7f73d4da12f1bdd3d57e3857a86 Mon Sep 17 00:00:00 2001 From: abi_jey Date: Fri, 28 Nov 2025 14:27:57 +0000 Subject: [PATCH 048/370] fix: Azure OpenAI GA path relies soley on model paramter as deployment --- litellm/llms/azure/realtime/handler.py | 10 ++++++---- .../azure/realtime/test_azure_realtime_handler.py | 13 +++++++++---- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/litellm/llms/azure/realtime/handler.py b/litellm/llms/azure/realtime/handler.py index 0dc42dad43e..217a05c83a4 100644 --- a/litellm/llms/azure/realtime/handler.py +++ b/litellm/llms/azure/realtime/handler.py @@ -12,6 +12,7 @@ from ....litellm_core_utils.litellm_logging import Logging as LiteLLMLogging from ....litellm_core_utils.realtime_streaming import RealTimeStreaming from ....llms.custom_httpx.http_handler import get_shared_realtime_ssl_context from ..azure import AzureChatCompletion +from litellm._logging import verbose_proxy_logger # BACKEND_WS_URL = "ws://localhost:8080/v1/realtime?model=gpt-4o-realtime-preview-2024-10-01" @@ -51,18 +52,18 @@ class AzureOpenAIRealtime(AzureChatCompletion): Examples: beta/default: "wss://.../openai/realtime?api-version=2024-10-01-preview&deployment=gpt-4o-realtime-preview" - GA/v1: "wss://.../openai/v1/realtime?api-version=2024-10-01-preview&deployment=gpt-4o-realtime-preview" + GA/v1: "wss://.../openai/v1/realtime?model=gpt-realtime-deployment" """ api_base = api_base.replace("https://", "wss://") # Determine path based on realtime_protocol if realtime_protocol in ("GA", "v1"): - path = "/openai/v1/realtime" + path = "/openai/v1/realtime" + return f"{api_base}{path}?model={model}" else: # Default to beta path for backwards compatibility path = "/openai/realtime" - - return f"{api_base}{path}?api-version={api_version}&deployment={model}" + return f"{api_base}{path}?api-version={api_version}&deployment={model}" async def async_realtime( self, @@ -107,4 +108,5 @@ class AzureOpenAIRealtime(AzureChatCompletion): except websockets.exceptions.InvalidStatusCode as e: # type: ignore await websocket.close(code=e.status_code, reason=str(e)) except Exception: + verbose_proxy_logger.exception("Error in AzureOpenAIRealtime.async_realtime") pass diff --git a/tests/test_litellm/llms/azure/realtime/test_azure_realtime_handler.py b/tests/test_litellm/llms/azure/realtime/test_azure_realtime_handler.py index ca8d01e158f..2a110c8f9a7 100644 --- a/tests/test_litellm/llms/azure/realtime/test_azure_realtime_handler.py +++ b/tests/test_litellm/llms/azure/realtime/test_azure_realtime_handler.py @@ -117,6 +117,7 @@ async def test_construct_url_beta_protocol_explicit(): async def test_construct_url_ga_protocol(): """ Test that realtime_protocol='GA' uses /openai/v1/realtime (GA path). + GA path uses ?model= instead of ?api-version=&deployment= format. """ from litellm.llms.azure.realtime.handler import AzureOpenAIRealtime @@ -132,8 +133,10 @@ async def test_construct_url_ga_protocol(): assert "/openai/v1/realtime?" in url # Ensure it doesn't have both paths assert url.count("/realtime") == 1 - assert "api-version=2024-10-01-preview" in url - assert "deployment=gpt-4o-realtime-preview" in url + # GA path uses model= query param, not api-version and deployment + assert "model=gpt-4o-realtime-preview" in url + assert "api-version" not in url + assert "deployment" not in url @pytest.mark.asyncio @@ -203,8 +206,10 @@ async def test_async_realtime_uses_ga_protocol_end_to_end(): called_url = mock_ws_connect.call_args[0][0] assert "/openai/v1/realtime" in called_url assert called_url.startswith("wss://") - assert "api-version=2024-10-01-preview" in called_url - assert "deployment=gpt-4o-realtime-preview" in called_url + # GA path uses model= query param, not api-version and deployment + assert "model=gpt-4o-realtime-preview" in called_url + assert "api-version" not in called_url + assert "deployment" not in called_url @pytest.mark.asyncio From af8f1475bfd709e7126847e15831abe17a72df2b Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Fri, 28 Nov 2025 21:12:42 +0530 Subject: [PATCH 049/370] fix PLR0915 --- litellm/llms/bedrock/embed/embedding.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/llms/bedrock/embed/embedding.py b/litellm/llms/bedrock/embed/embedding.py index c9eea516eb0..7152d7ce15c 100644 --- a/litellm/llms/bedrock/embed/embedding.py +++ b/litellm/llms/bedrock/embed/embedding.py @@ -377,7 +377,7 @@ class BedrockEmbedding(BaseAWSLLM): is_async_invoke=is_async_invoke, ) - def embeddings( + def embeddings( # noqa: PLR0915 self, model: str, input: List[str], From 9d058398dfdacf65791cea3cd3ed34ee0427d8e1 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Fri, 28 Nov 2025 21:41:25 +0530 Subject: [PATCH 050/370] Fix pegasus response and add doc --- docs/my-website/docs/providers/bedrock.md | 127 +++++++++++++++ ...mazon_twelvelabs_pegasus_transformation.py | 151 +++++++++++++++++- .../base_invoke_transformation.py | 22 +++ .../test_twelvelabs_pegasus_transformation.py | 4 +- 4 files changed, 301 insertions(+), 3 deletions(-) diff --git a/docs/my-website/docs/providers/bedrock.md b/docs/my-website/docs/providers/bedrock.md index 9e22f67527e..a9ac85a7571 100644 --- a/docs/my-website/docs/providers/bedrock.md +++ b/docs/my-website/docs/providers/bedrock.md @@ -1683,6 +1683,131 @@ curl --location 'http://0.0.0.0:4000/chat/completions' \ +## TwelveLabs Pegasus - Video Understanding + +TwelveLabs Pegasus 1.2 is a video understanding model that can analyze and describe video content. LiteLLM supports this model through Bedrock's `/invoke` endpoint. + +| Property | Details | +|----------|---------| +| Provider Route | `bedrock/us.twelvelabs.pegasus-1-2-v1:0`, `bedrock/eu.twelvelabs.pegasus-1-2-v1:0` | +| Provider Documentation | [TwelveLabs Pegasus Docs ↗](https://docs.twelvelabs.io/docs/models/pegasus) | +| Supported Parameters | `max_tokens`, `temperature`, `response_format` | +| Media Input | S3 URI or base64-encoded video | + +### Supported Features + +- **Video Analysis**: Analyze video content from S3 or base64 input +- **Structured Output**: Support for JSON schema response format +- **S3 Integration**: Support for S3 video URLs with bucket owner specification + +### Usage with S3 Video + + + + +```python title="TwelveLabs Pegasus SDK Usage" showLineNumbers +from litellm import completion +import os + +# Set AWS credentials +os.environ["AWS_ACCESS_KEY_ID"] = "your-aws-access-key" +os.environ["AWS_SECRET_ACCESS_KEY"] = "your-aws-secret-key" +os.environ["AWS_REGION_NAME"] = "us-east-1" + +response = completion( + model="bedrock/us.twelvelabs.pegasus-1-2-v1:0", + messages=[{"role": "user", "content": "Describe what happens in this video."}], + mediaSource={ + "s3Location": { + "uri": "s3://your-bucket/video.mp4", + "bucketOwner": "123456789012", # 12-digit AWS account ID + } + }, + temperature=0.2 +) + +print(response.choices[0].message.content) +``` + + + + + +**1. Add to config** + +```yaml title="config.yaml" showLineNumbers +model_list: + - model_name: pegasus-video + litellm_params: + model: bedrock/us.twelvelabs.pegasus-1-2-v1:0 + aws_access_key_id: os.environ/AWS_ACCESS_KEY_ID + aws_secret_access_key: os.environ/AWS_SECRET_ACCESS_KEY + aws_region_name: os.environ/AWS_REGION_NAME +``` + +**2. Start proxy** + +```bash title="Start LiteLLM Proxy" showLineNumbers +litellm --config /path/to/config.yaml + +# RUNNING at http://0.0.0.0:4000 +``` + +**3. Test it!** + +```bash title="Test Pegasus via Proxy" showLineNumbers +curl --location 'http://0.0.0.0:4000/chat/completions' \ + --header 'Authorization: Bearer sk-1234' \ + --header 'Content-Type: application/json' \ + --data '{ + "model": "pegasus-video", + "messages": [ + { + "role": "user", + "content": "Describe what happens in this video." + } + ], + "mediaSource": { + "s3Location": { + "uri": "s3://your-bucket/video.mp4", + "bucketOwner": "123456789012" + } + }, + "temperature": 0.2 + }' +``` + + + + +### Usage with Base64 Video + +You can also pass video content directly as base64: + +```python title="Base64 Video Input" showLineNumbers +from litellm import completion +import base64 + +# Read video file and encode to base64 +with open("video.mp4", "rb") as video_file: + video_base64 = base64.b64encode(video_file.read()).decode("utf-8") + +response = completion( + model="bedrock/us.twelvelabs.pegasus-1-2-v1:0", + messages=[{"role": "user", "content": "What is happening in this video?"}], + mediaSource={ + "base64String": video_base64 + }, + temperature=0.2, +) + +print(response.choices[0].message.content) +``` + +### Important Notes + +- **Response Format**: The model supports structured output via `response_format` with JSON schema + ## Provisioned throughput models To use provisioned throughput Bedrock models pass - `model=bedrock/`, example `model=bedrock/anthropic.claude-v2`. Set `model` to any of the [Supported AWS models](#supported-aws-bedrock-models) @@ -1743,6 +1868,8 @@ Here's an example of using a bedrock model with LiteLLM. For a complete list, re | Meta Llama 2 Chat 70b | `completion(model='bedrock/meta.llama2-70b-chat-v1', messages=messages)` | `os.environ['AWS_ACCESS_KEY_ID']`, `os.environ['AWS_SECRET_ACCESS_KEY']`, `os.environ['AWS_REGION_NAME']` | | Mistral 7B Instruct | `completion(model='bedrock/mistral.mistral-7b-instruct-v0:2', messages=messages)` | `os.environ['AWS_ACCESS_KEY_ID']`, `os.environ['AWS_SECRET_ACCESS_KEY']`, `os.environ['AWS_REGION_NAME']` | | Mixtral 8x7B Instruct | `completion(model='bedrock/mistral.mixtral-8x7b-instruct-v0:1', messages=messages)` | `os.environ['AWS_ACCESS_KEY_ID']`, `os.environ['AWS_SECRET_ACCESS_KEY']`, `os.environ['AWS_REGION_NAME']` | +| TwelveLabs Pegasus 1.2 (US) | `completion(model='bedrock/us.twelvelabs.pegasus-1-2-v1:0', messages=messages, mediaSource={...})` | `os.environ['AWS_ACCESS_KEY_ID']`, `os.environ['AWS_SECRET_ACCESS_KEY']`, `os.environ['AWS_REGION_NAME']` | +| TwelveLabs Pegasus 1.2 (EU) | `completion(model='bedrock/eu.twelvelabs.pegasus-1-2-v1:0', messages=messages, mediaSource={...})` | `os.environ['AWS_ACCESS_KEY_ID']`, `os.environ['AWS_SECRET_ACCESS_KEY']`, `os.environ['AWS_REGION_NAME']` | ## Bedrock Embedding diff --git a/litellm/llms/bedrock/chat/invoke_transformations/amazon_twelvelabs_pegasus_transformation.py b/litellm/llms/bedrock/chat/invoke_transformations/amazon_twelvelabs_pegasus_transformation.py index 7b72968ea3d..62e98f7472f 100644 --- a/litellm/llms/bedrock/chat/invoke_transformations/amazon_twelvelabs_pegasus_transformation.py +++ b/litellm/llms/bedrock/chat/invoke_transformations/amazon_twelvelabs_pegasus_transformation.py @@ -5,16 +5,32 @@ Reference: https://docs.twelvelabs.io/docs/models/pegasus """ -from typing import Any, Dict, List, Optional +import json +import time +from typing import TYPE_CHECKING, Any, Dict, List, Optional +import httpx + +import litellm +from litellm._logging import verbose_logger +from litellm.litellm_core_utils.core_helpers import map_finish_reason from litellm.llms.base_llm.base_utils import type_to_response_format_param from litellm.llms.base_llm.chat.transformation import BaseConfig from litellm.llms.bedrock.chat.invoke_transformations.base_invoke_transformation import ( AmazonInvokeConfig, ) +from litellm.llms.bedrock.common_utils import BedrockError from litellm.types.llms.openai import AllMessageValues +from litellm.types.utils import ModelResponse, Usage from litellm.utils import get_base64_str +if TYPE_CHECKING: + from litellm.litellm_core_utils.litellm_logging import Logging as _LiteLLMLoggingObj + + LiteLLMLoggingObj = _LiteLLMLoggingObj +else: + LiteLLMLoggingObj = Any + class AmazonTwelveLabsPegasusConfig(AmazonInvokeConfig, BaseConfig): """ @@ -53,7 +69,35 @@ class AmazonTwelveLabsPegasusConfig(AmazonInvokeConfig, BaseConfig): return optional_params def _normalize_response_format(self, value: Any) -> Any: + """Normalize response_format to TwelveLabs format. + + TwelveLabs expects: + { + "jsonSchema": {...} + } + + But OpenAI format is: + { + "type": "json_schema", + "json_schema": { + "name": "...", + "schema": {...} + } + } + """ if isinstance(value, dict): + # If it has json_schema field, extract and transform it + if "json_schema" in value: + json_schema = value["json_schema"] + # Extract the schema if nested + if isinstance(json_schema, dict) and "schema" in json_schema: + return {"jsonSchema": json_schema["schema"]} + # Otherwise use json_schema directly + return {"jsonSchema": json_schema} + # If it already has jsonSchema, return as is + if "jsonSchema" in value: + return value + # Otherwise return the dict as is return value return type_to_response_format_param(response_format=value) or value @@ -72,9 +116,18 @@ class AmazonTwelveLabsPegasusConfig(AmazonInvokeConfig, BaseConfig): if media_source is not None: request_data["mediaSource"] = media_source - for key in ("temperature", "maxOutputTokens", "responseFormat"): + # Handle temperature and maxOutputTokens + for key in ("temperature", "maxOutputTokens"): if key in optional_params: request_data[key] = optional_params.get(key) + + # Handle responseFormat - transform to TwelveLabs format + if "responseFormat" in optional_params: + response_format = optional_params["responseFormat"] + transformed_format = self._normalize_response_format(response_format) + if transformed_format: + request_data["responseFormat"] = transformed_format + return request_data def _build_media_source(self, optional_params: dict) -> Optional[dict]: @@ -131,3 +184,97 @@ class AmazonTwelveLabsPegasusConfig(AmazonInvokeConfig, BaseConfig): prompt_parts.append(f"{role}: {content}") return "\n".join(part for part in prompt_parts if part).strip() + def transform_response( + self, + model: str, + raw_response: httpx.Response, + model_response: ModelResponse, + logging_obj: LiteLLMLoggingObj, + request_data: dict, + messages: List[AllMessageValues], + optional_params: dict, + litellm_params: dict, + encoding: Any, + api_key: Optional[str] = None, + json_mode: Optional[bool] = None, + ) -> ModelResponse: + """ + Transform TwelveLabs Pegasus response to LiteLLM format. + + TwelveLabs response format: + { + "message": "...", + "finishReason": "stop" | "length" + } + + LiteLLM format: + ModelResponse with choices[0].message.content and finish_reason + """ + try: + completion_response = raw_response.json() + except Exception as e: + raise BedrockError( + message=f"Error parsing response: {raw_response.text}, error: {str(e)}", + status_code=raw_response.status_code, + ) + + verbose_logger.debug( + "twelvelabs pegasus response: %s", + json.dumps(completion_response, indent=4, default=str), + ) + + # Extract message content + message_content = completion_response.get("message", "") + + # Extract finish reason and map to LiteLLM format + finish_reason_raw = completion_response.get("finishReason", "stop") + finish_reason = map_finish_reason(finish_reason_raw) + + # Set the response content + try: + if ( + message_content + and hasattr(model_response.choices[0], "message") + and getattr(model_response.choices[0].message, "tool_calls", None) is None + ): + model_response.choices[0].message.content = message_content # type: ignore + model_response.choices[0].finish_reason = finish_reason + else: + raise Exception("Unable to set message content") + except Exception as e: + raise BedrockError( + message=f"Error setting response content: {str(e)}. Response: {completion_response}", + status_code=raw_response.status_code, + ) + + # Calculate usage from headers + bedrock_input_tokens = raw_response.headers.get( + "x-amzn-bedrock-input-token-count", None + ) + bedrock_output_tokens = raw_response.headers.get( + "x-amzn-bedrock-output-token-count", None + ) + + prompt_tokens = int( + bedrock_input_tokens or litellm.token_counter(messages=messages) + ) + + completion_tokens = int( + bedrock_output_tokens + or litellm.token_counter( + text=model_response.choices[0].message.content, # type: ignore + count_response_tokens=True, + ) + ) + + model_response.created = int(time.time()) + model_response.model = model + usage = Usage( + prompt_tokens=prompt_tokens, + completion_tokens=completion_tokens, + total_tokens=prompt_tokens + completion_tokens, + ) + setattr(model_response, "usage", usage) + + return model_response + diff --git a/litellm/llms/bedrock/chat/invoke_transformations/base_invoke_transformation.py b/litellm/llms/bedrock/chat/invoke_transformations/base_invoke_transformation.py index e6146f1064e..6c389ff3b7d 100644 --- a/litellm/llms/bedrock/chat/invoke_transformations/base_invoke_transformation.py +++ b/litellm/llms/bedrock/chat/invoke_transformations/base_invoke_transformation.py @@ -250,6 +250,14 @@ class AmazonInvokeConfig(BaseConfig, BaseAWSLLM): ): # completion(top_k=3) > anthropic_config(top_k=3) <- allows for dynamic variables to be passed in inference_params[k] = v request_data = {"prompt": prompt, **inference_params} + elif provider == "twelvelabs": + return litellm.AmazonTwelveLabsPegasusConfig().transform_request( + model=model, + messages=messages, + optional_params=optional_params, + litellm_params=litellm_params, + headers=headers, + ) else: raise BedrockError( status_code=404, @@ -321,6 +329,20 @@ class AmazonInvokeConfig(BaseConfig, BaseAWSLLM): litellm_params=litellm_params, encoding=encoding, ) + elif provider == "twelvelabs": + return litellm.AmazonTwelveLabsPegasusConfig().transform_response( + model=model, + raw_response=raw_response, + model_response=model_response, + logging_obj=logging_obj, + request_data=request_data, + messages=messages, + optional_params=optional_params, + litellm_params=litellm_params, + encoding=encoding, + api_key=api_key, + json_mode=json_mode, + ) elif provider == "ai21": outputText = ( completion_response.get("completions")[0].get("data").get("text") diff --git a/tests/test_litellm/llms/bedrock/chat/invoke_transformations/test_twelvelabs_pegasus_transformation.py b/tests/test_litellm/llms/bedrock/chat/invoke_transformations/test_twelvelabs_pegasus_transformation.py index 9063c0f1c94..f2cf6f9857c 100644 --- a/tests/test_litellm/llms/bedrock/chat/invoke_transformations/test_twelvelabs_pegasus_transformation.py +++ b/tests/test_litellm/llms/bedrock/chat/invoke_transformations/test_twelvelabs_pegasus_transformation.py @@ -38,7 +38,9 @@ def test_map_openai_params_translates_fields(): assert optional_params["maxOutputTokens"] == 20 assert optional_params["temperature"] == 0.6 assert "responseFormat" in optional_params - assert optional_params["responseFormat"]["json_schema"]["name"] == "video_schema" + # TwelveLabs format: responseFormat contains jsonSchema directly (not json_schema) + assert "jsonSchema" in optional_params["responseFormat"] + assert optional_params["responseFormat"]["jsonSchema"]["type"] == "object" def test_transform_request_includes_base64_media(): From b85df0b1fb38abfc01255db50f49f066bf61ae14 Mon Sep 17 00:00:00 2001 From: hxomer <164746029+hxomer@users.noreply.github.com> Date: Fri, 28 Nov 2025 18:22:19 +0200 Subject: [PATCH 051/370] Better handle anonymization (#17207) * Better handle anonymization * Fix tests --- .../guardrails/guardrail_hooks/aim/aim.py | 37 +++++-------------- tests/local_testing/test_aim_guardrails.py | 24 ++++++------ 2 files changed, 22 insertions(+), 39 deletions(-) diff --git a/litellm/proxy/guardrails/guardrail_hooks/aim/aim.py b/litellm/proxy/guardrails/guardrail_hooks/aim/aim.py index 3a0b0c3202b..7711a934998 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/aim/aim.py +++ b/litellm/proxy/guardrails/guardrail_hooks/aim/aim.py @@ -117,7 +117,7 @@ class AimGuardrail(CustomGuardrail): self._handle_block_action(res["analysis_result"], required_action) elif action_type == "anonymize_action": return self._anonymize_request( - res["analysis_result"], required_action, data + res, data ) else: verbose_proxy_logger.error(f"Aim: {action_type} action") @@ -133,27 +133,18 @@ class AimGuardrail(CustomGuardrail): raise HTTPException(status_code=400, detail=detection_message) def _anonymize_request( - self, analysis_result: Any, required_action: Any, data: dict + self, res: Any, data: dict ) -> dict: verbose_proxy_logger.info("Aim: anonymize action") - redaction_result = required_action and required_action.get( - "chat_redaction_result" - ) - if not redaction_result: + redacted_chat = res.get("redacted_chat") + if not redacted_chat: return data - if analysis_result and analysis_result.get("session_entities"): - self._set_dlp_entities(analysis_result.get("session_entities")) data["messages"] = [ - { - "role": redaction_result["redacted_new_message"]["role"], - "content": redaction_result["redacted_new_message"]["content"], - } - ] + [ { "role": message["role"], "content": message["content"], } - for message in redaction_result["all_redacted_messages"] + for message in redacted_chat["all_redacted_messages"] ] return data @@ -185,7 +176,11 @@ class AimGuardrail(CustomGuardrail): return self._handle_block_action_on_output( res["analysis_result"], required_action ) - return self._deanonymize_output(output) + redacted_chat = res.get("redacted_chat", None) + + if action_type and action_type == "anonymize_action" and redacted_chat: + return {"redacted_output": redacted_chat["all_redacted_messages"][-1]["content"]} + return {"redacted_output": output} def _handle_block_action_on_output( self, analysis_result: Any, required_action: Any @@ -199,15 +194,6 @@ class AimGuardrail(CustomGuardrail): ) return {"detection_message": detection_message} - def _deanonymize_output(self, output: str) -> dict | None: - try: - for entity in self.dlp_entities: - output = output.replace(f"[{entity['name']}]", entity["content"]) - return {"redacted_output": output} - except Exception as e: - verbose_proxy_logger.error(f"Aim: Error while redacting output: {e}") - return None - def _build_aim_headers( self, *, @@ -323,9 +309,6 @@ class AimGuardrail(CustomGuardrail): await websocket.send(chunk) await websocket.send(json.dumps({"done": True})) - def _set_dlp_entities(self, entities: list[dict]) -> None: - self.dlp_entities = entities[: self._max_dlp_entities] - @staticmethod def get_config_model() -> Optional[Type["GuardrailConfigModel"]]: from litellm.types.proxy.guardrails.guardrail_hooks.aim import ( diff --git a/tests/local_testing/test_aim_guardrails.py b/tests/local_testing/test_aim_guardrails.py index b271875c2ec..f24b74e5110 100644 --- a/tests/local_testing/test_aim_guardrails.py +++ b/tests/local_testing/test_aim_guardrails.py @@ -443,23 +443,23 @@ response_with_detections = Response( "required_action": { "action_type": "anonymize_action", "policy_name": "PII", - "chat_redaction_result": { - "all_redacted_messages": [ - { - "content": "Hi my name is [NAME_1]", - "role": "user", - "additional_contents": [], - "received_message_id": "0", - "extra_fields": {}, - } - ], - "redacted_new_message": { + }, + "redacted_chat": { + "all_redacted_messages": [ + { "content": "Hi my name is [NAME_1]", "role": "user", "additional_contents": [], "received_message_id": "0", "extra_fields": {}, - }, + } + ], + "redacted_new_message": { + "content": "Hi my name is [NAME_1]", + "role": "user", + "additional_contents": [], + "received_message_id": "0", + "extra_fields": {}, }, }, }, From f7380a51de3aa1823194a8b23ed1a60f2818f190 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Mon, 1 Dec 2025 08:36:29 +0530 Subject: [PATCH 052/370] Respect custom llm provider in header --- litellm/proxy/batches_endpoints/endpoints.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/litellm/proxy/batches_endpoints/endpoints.py b/litellm/proxy/batches_endpoints/endpoints.py index 98492bcc2d6..03b9ac3deaa 100644 --- a/litellm/proxy/batches_endpoints/endpoints.py +++ b/litellm/proxy/batches_endpoints/endpoints.py @@ -31,7 +31,6 @@ from litellm.proxy.openai_files_endpoints.common_utils import ( get_original_file_id, prepare_data_with_credentials, ) - from litellm.proxy.utils import handle_exception_on_proxy, is_known_model from litellm.types.llms.openai import LiteLLMBatchCreateRequest @@ -112,7 +111,10 @@ async def create_batch( # noqa: PLR0915 is_router_model = is_known_model(model=router_model, llm_router=llm_router) custom_llm_provider = ( - provider or data.pop("custom_llm_provider", None) or "openai" + provider + or data.pop("custom_llm_provider", None) + or get_custom_llm_provider_from_request_headers(request=request) + or "openai" ) _create_batch_data = LiteLLMBatchCreateRequest(**data) input_file_id = _create_batch_data.get("input_file_id", None) From 9edc50efbd117b38e54c038cbc1af1dd32572206 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Mon, 1 Dec 2025 10:21:44 +0530 Subject: [PATCH 053/370] Fix 500 error for malformed request --- litellm/proxy/common_request_processing.py | 19 ++++++++- tests/proxy_unit_tests/test_proxy_server.py | 44 +++++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/litellm/proxy/common_request_processing.py b/litellm/proxy/common_request_processing.py index d2b04410026..1c6c9b97173 100644 --- a/litellm/proxy/common_request_processing.py +++ b/litellm/proxy/common_request_processing.py @@ -809,7 +809,24 @@ class ProxyBaseLLMRequestProcessing: status_code=e.response.status_code, detail={"error": error_text}, ) - error_msg = f"{str(e)}" + error_msg = f"{str(e)}" + # Check for AttributeError in various places: + # 1. Direct AttributeError (already handled above) + # 2. In underlying exception (__cause__, __context__, original_exception) + has_attribute_error = ( + (isinstance(e, Exception) and isinstance(getattr(e, "__cause__", None), AttributeError)) + or (isinstance(e, Exception) and isinstance(getattr(e, "__context__", None), AttributeError)) + or (isinstance(e, Exception) and isinstance(getattr(e, "original_exception", None), AttributeError)) + ) + + if has_attribute_error: + raise ProxyException( + message=f"Invalid request format: {error_msg}", + type="invalid_request_error", + param=None, + code=status.HTTP_400_BAD_REQUEST, + headers=headers, + ) raise ProxyException( message=getattr(e, "message", error_msg), type=getattr(e, "type", "None"), diff --git a/tests/proxy_unit_tests/test_proxy_server.py b/tests/proxy_unit_tests/test_proxy_server.py index 6dad7cb08d0..dc34a50f87e 100644 --- a/tests/proxy_unit_tests/test_proxy_server.py +++ b/tests/proxy_unit_tests/test_proxy_server.py @@ -175,6 +175,50 @@ def test_chat_completion(mock_acompletion, client_no_auth): pytest.fail(f"LiteLLM Proxy test failed. Exception - {str(e)}") +def test_chat_completion_malformed_messages_returns_400(client_no_auth): + """ + Test that malformed messages (strings instead of dicts) return 400 instead of 500. + + This test verifies that when a client sends messages as raw strings instead of + {role, content} objects, LiteLLM returns a 400 invalid_request_error instead + of a 500 Internal Server Error. + """ + global headers + try: + # Test data with malformed messages (string instead of dict) + test_data = { + "model": "gpt-3.5-turbo", + "messages": ["hi how are you"], # Invalid: should be [{"role": "user", "content": "hi how are you"}] + } + + print("testing proxy server with malformed messages") + response = client_no_auth.post("/v1/chat/completions", json=test_data, headers=headers) + + print(f"response status: {response.status_code}") + print(f"response text: {response.text}") + + # Should return 400, not 500 + assert response.status_code == 400, f"Expected 400, got {response.status_code}. Response: {response.text}" + + # Verify error format + result = response.json() + assert "error" in result, "Response should contain 'error' key" + error = result["error"] + + # Verify error type and message + assert error.get("type") == "invalid_request_error" or error.get("type") is None, \ + f"Expected invalid_request_error or None, got {error.get('type')}" + assert error.get("code") == "400" or error.get("code") == 400, \ + f"Expected code 400, got {error.get('code')}" + + # Error message should indicate invalid request format + error_message = error.get("message", "") + assert len(error_message) > 0, "Error message should not be empty" + + except Exception as e: + pytest.fail(f"LiteLLM Proxy test failed. Exception - {str(e)}") + + def test_get_settings_request_timeout(client_no_auth): """ When no timeout is set, it should use the litellm.request_timeout value From 02510a908f389055ccf55db0a59a141a49bc222c Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Mon, 1 Dec 2025 10:42:38 +0530 Subject: [PATCH 054/370] Add better handling image generation for gemini models --- .../llms/gemini/image_generation/transformation.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/litellm/llms/gemini/image_generation/transformation.py b/litellm/llms/gemini/image_generation/transformation.py index e79414394fa..2d8d82e6ad8 100644 --- a/litellm/llms/gemini/image_generation/transformation.py +++ b/litellm/llms/gemini/image_generation/transformation.py @@ -21,12 +21,6 @@ else: LiteLLMLoggingObj = Any -FLASH_IMAGE_PREVIEW_MODEL_IDENTIFIERS = ( - "2.0-flash-preview-image", - "2.0-flash-preview-image-generation", - "2.5-flash-image-preview", - "3-pro-image-preview", -) class GoogleImageGenConfig(BaseImageGenerationConfig): DEFAULT_BASE_URL: str = "https://generativelanguage.googleapis.com/v1beta" @@ -104,7 +98,7 @@ class GoogleImageGenConfig(BaseImageGenerationConfig): complete_url = complete_url.rstrip("/") # Gemini Flash Image Preview models use generateContent endpoint - if any(identifier in model for identifier in FLASH_IMAGE_PREVIEW_MODEL_IDENTIFIERS): + if "gemini" in model: complete_url = f"{complete_url}/models/{model}:generateContent" else: # All other Imagen models use predict endpoint @@ -159,7 +153,7 @@ class GoogleImageGenConfig(BaseImageGenerationConfig): } """ # For Gemini Flash Image Preview models, use standard Gemini format - if any(identifier in model for identifier in FLASH_IMAGE_PREVIEW_MODEL_IDENTIFIERS): + if "gemini" in model: request_body: dict = { "contents": [ { @@ -218,7 +212,7 @@ class GoogleImageGenConfig(BaseImageGenerationConfig): model_response.data = [] # Handle different response formats based on model - if any(identifier in model for identifier in FLASH_IMAGE_PREVIEW_MODEL_IDENTIFIERS): + if "gemini" in model: # Gemini Flash Image Preview models return in candidates format candidates = response_data.get("candidates", []) for candidate in candidates: From 7dac498efbaf9142c44ba560e736baa28f1223c2 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Mon, 1 Dec 2025 14:33:03 +0530 Subject: [PATCH 055/370] Add passthrough cost tracking for veo --- litellm/proxy/_types.py | 2 + .../llm_passthrough_endpoints.py | 9 +-- .../gemini_passthrough_logging_handler.py | 38 +++++++++ .../vertex_passthrough_logging_handler.py | 43 +++++++++- .../pass_through_endpoints.py | 26 +++++- .../pass_through_endpoints/success_handler.py | 4 +- ...test_gemini_passthrough_logging_handler.py | 79 ++++++++++++++++++- 7 files changed, 191 insertions(+), 10 deletions(-) diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 9e915d4bc5a..fe87a70b244 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -47,6 +47,7 @@ from litellm.types.utils import ( StandardPassThroughResponseObject, TextCompletionResponse, ) +from litellm.types.videos.main import VideoObject from .types_utils.utils import get_instance_fn, validate_custom_validate_return_type @@ -3275,6 +3276,7 @@ PassThroughEndpointLoggingResultValues = Union[ TextCompletionResponse, ImageResponse, EmbeddingResponse, + VideoObject, StandardPassThroughResponseObject, ] diff --git a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py index 7afb6868c73..d1294f996ec 100644 --- a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py +++ b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py @@ -187,13 +187,10 @@ async def gemini_proxy_route( """ [Docs](https://docs.litellm.ai/docs/pass_through/google_ai_studio) """ - ## CHECK FOR LITELLM API KEY IN THE QUERY PARAMS - ?..key=LITELLM_API_KEY - google_ai_studio_api_key = request.query_params.get("key") or request.headers.get( - "x-goog-api-key" - ) - + # Get LiteLLM API key from Authorization header for authentication + api_key_to_use = get_litellm_virtual_key(request=request) user_api_key_dict = await user_api_key_auth( - request=request, api_key=f"Bearer {google_ai_studio_api_key}" + request=request, api_key=api_key_to_use ) base_target_url = ( diff --git a/litellm/proxy/pass_through_endpoints/llm_provider_handlers/gemini_passthrough_logging_handler.py b/litellm/proxy/pass_through_endpoints/llm_provider_handlers/gemini_passthrough_logging_handler.py index 16e8d5b4349..2bda9ba4856 100644 --- a/litellm/proxy/pass_through_endpoints/llm_provider_handlers/gemini_passthrough_logging_handler.py +++ b/litellm/proxy/pass_through_endpoints/llm_provider_handlers/gemini_passthrough_logging_handler.py @@ -7,6 +7,7 @@ import httpx import litellm from litellm._logging import verbose_proxy_logger from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj +from litellm.llms.gemini.videos.transformation import GeminiVideoConfig from litellm.llms.vertex_ai.gemini.vertex_and_google_ai_studio_gemini import ( ModelResponseIterator as GeminiModelResponseIterator, ) @@ -39,6 +40,43 @@ class GeminiPassthroughLoggingHandler: request_body: dict, **kwargs, ) -> PassThroughEndpointLoggingTypedDict: + if "predictLongRunning" in url_route: + model = GeminiPassthroughLoggingHandler.extract_model_from_url(url_route) + + gemini_video_config = GeminiVideoConfig() + litellm_video_response = gemini_video_config.transform_video_create_response( + model=model, + raw_response=httpx_response, + logging_obj=logging_obj, + custom_llm_provider="gemini", + request_data=request_body, + ) + logging_obj.model = model + logging_obj.model_call_details["model"] = model + logging_obj.model_call_details["custom_llm_provider"] = "gemini" + logging_obj.custom_llm_provider = "gemini" + + response_cost = litellm.completion_cost( + completion_response=litellm_video_response, + model=model, + custom_llm_provider="gemini", + call_type="create_video", + ) + + # Set response_cost in _hidden_params to prevent recalculation + if not hasattr(litellm_video_response, "_hidden_params"): + litellm_video_response._hidden_params = {} + litellm_video_response._hidden_params["response_cost"] = response_cost + + kwargs["response_cost"] = response_cost + kwargs["model"] = model + kwargs["custom_llm_provider"] = "gemini" + logging_obj.model_call_details["response_cost"] = response_cost + return { + "result": litellm_video_response, + "kwargs": kwargs, + } + if "generateContent" in url_route: model = GeminiPassthroughLoggingHandler.extract_model_from_url(url_route) diff --git a/litellm/proxy/pass_through_endpoints/llm_provider_handlers/vertex_passthrough_logging_handler.py b/litellm/proxy/pass_through_endpoints/llm_provider_handlers/vertex_passthrough_logging_handler.py index b34a6f455c3..0962fafe3f6 100644 --- a/litellm/proxy/pass_through_endpoints/llm_provider_handlers/vertex_passthrough_logging_handler.py +++ b/litellm/proxy/pass_through_endpoints/llm_provider_handlers/vertex_passthrough_logging_handler.py @@ -14,6 +14,7 @@ from litellm.llms.vertex_ai.gemini.vertex_and_google_ai_studio_gemini import ( from litellm.llms.vertex_ai.vector_stores.search_api.transformation import ( VertexSearchAPIVectorStoreConfig, ) +from litellm.llms.vertex_ai.videos.transformation import VertexAIVideoConfig from litellm.proxy._types import PassThroughEndpointLoggingTypedDict from litellm.types.utils import ( Choices, @@ -49,9 +50,49 @@ class VertexPassthroughLoggingHandler: start_time: datetime, end_time: datetime, cache_hit: bool, + request_body: Optional[dict] = None, **kwargs, ) -> PassThroughEndpointLoggingTypedDict: - if "generateContent" in url_route: + if "predictLongRunning" in url_route: + model = VertexPassthroughLoggingHandler.extract_model_from_url(url_route) + + vertex_video_config = VertexAIVideoConfig() + litellm_video_response = vertex_video_config.transform_video_create_response( + model=model, + raw_response=httpx_response, + logging_obj=logging_obj, + custom_llm_provider="vertex_ai", + request_data=request_body, + ) + + logging_obj.model = model + logging_obj.model_call_details["model"] = model + logging_obj.model_call_details["custom_llm_provider"] = "vertex_ai" + logging_obj.custom_llm_provider = "vertex_ai" + + response_cost = litellm.completion_cost( + completion_response=litellm_video_response, + model=model, + custom_llm_provider="vertex_ai", + call_type="create_video", + ) + + # Set response_cost in _hidden_params to prevent recalculation + if not hasattr(litellm_video_response, "_hidden_params"): + litellm_video_response._hidden_params = {} + litellm_video_response._hidden_params["response_cost"] = response_cost + + kwargs["response_cost"] = response_cost + kwargs["model"] = model + kwargs["custom_llm_provider"] = "vertex_ai" + logging_obj.model_call_details["response_cost"] = response_cost + + return { + "result": litellm_video_response, + "kwargs": kwargs, + } + + elif "generateContent" in url_route: model = VertexPassthroughLoggingHandler.extract_model_from_url(url_route) instance_of_vertex_llm = litellm.VertexGeminiConfig() diff --git a/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py b/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py index 5b47a8af7a5..8e297f645c6 100644 --- a/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py +++ b/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py @@ -412,6 +412,31 @@ class HttpPassThroughEndpointHelpers(BasePassthroughUtils): params=requested_query_params, json=_parsed_body, ) + # Mock httpx response emulating a Google AI video generation operation status + # Attach a dummy request with headers set, so response.request.headers is always present + dummy_request = httpx.Request( + method=request.method, + url=str(url), + headers=headers or {}, + params=requested_query_params, + json=_parsed_body, + ) + # Ensure the .headers attribute exists and is a dict (httpx will normalize it) + mock_headers = httpx.Headers({"content-type": "application/json"}) + response = httpx.Response( + status_code=200, + headers=mock_headers, + json={ + "name": "operations/1234567890123456789", + "metadata": { + "@type": "type.googleapis.com/google.ai.generativelanguage.v1beta.GenerateVideoMetadata", + "state": "RUNNING", + "createTime": "2025-01-01T12:00:00Z" + }, + "done": False + }, + request=dummy_request + ) return response @staticmethod @@ -737,7 +762,6 @@ async def pass_through_request( # noqa: PLR0915 # Store custom_llm_provider in kwargs and logging object if provided if custom_llm_provider: - kwargs["custom_llm_provider"] = custom_llm_provider logging_obj.model_call_details["custom_llm_provider"] = custom_llm_provider logging_obj.model_call_details["litellm_params"] = kwargs.get("litellm_params", {}) diff --git a/litellm/proxy/pass_through_endpoints/success_handler.py b/litellm/proxy/pass_through_endpoints/success_handler.py index cc50d2c2d8e..6d93ef68dfd 100644 --- a/litellm/proxy/pass_through_endpoints/success_handler.py +++ b/litellm/proxy/pass_through_endpoints/success_handler.py @@ -42,6 +42,7 @@ class PassThroughEndpointLogging: "streamRawPredict", "search", "batchPredictionJobs", + "predictLongRunning", ] # Anthropic @@ -57,7 +58,7 @@ class PassThroughEndpointLogging: self.TRACKED_LANGFUSE_ROUTES = ["/langfuse/"] # Gemini - self.TRACKED_GEMINI_ROUTES = ["generateContent", "streamGenerateContent"] + self.TRACKED_GEMINI_ROUTES = ["generateContent", "streamGenerateContent", "predictLongRunning"] # Vertex AI Live API WebSocket self.TRACKED_VERTEX_AI_LIVE_ROUTES = ["/vertex_ai/live"] @@ -149,6 +150,7 @@ class PassThroughEndpointLogging: start_time=start_time, end_time=end_time, cache_hit=cache_hit, + request_body=request_body, **kwargs, ) ) diff --git a/tests/test_litellm/proxy/pass_through_endpoints/llm_provider_handlers/test_gemini_passthrough_logging_handler.py b/tests/test_litellm/proxy/pass_through_endpoints/llm_provider_handlers/test_gemini_passthrough_logging_handler.py index 6f87d8f6ab5..2c3bbc0e6ed 100644 --- a/tests/test_litellm/proxy/pass_through_endpoints/llm_provider_handlers/test_gemini_passthrough_logging_handler.py +++ b/tests/test_litellm/proxy/pass_through_endpoints/llm_provider_handlers/test_gemini_passthrough_logging_handler.py @@ -75,7 +75,9 @@ class TestGeminiPassthroughLoggingHandler: def test_is_gemini_route(self): """Test that Gemini routes are correctly identified""" - from litellm.proxy.pass_through_endpoints.success_handler import PassThroughEndpointLogging + from litellm.proxy.pass_through_endpoints.success_handler import ( + PassThroughEndpointLogging, + ) handler = PassThroughEndpointLogging() @@ -285,3 +287,78 @@ class TestGeminiPassthroughLoggingHandler: assert call_kwargs["response_cost"] is not None assert call_kwargs["model"] == "gemini-1.5-flash" assert call_kwargs["custom_llm_provider"] == "gemini" + + @patch("litellm.completion_cost") + def test_veo3_passthrough_cost_tracking(self, mock_completion_cost): + """Test Veo3 video generation cost tracking for passthrough requests""" + # Mock the completion_cost to return the expected video generation cost + # For veo-2.0-generate-001 with 8 seconds: 0.35 * 8 = 2.8 + expected_cost = 0.35 * 8.0 # $2.80 + mock_completion_cost.return_value = expected_cost + + # Mock Veo3 predictLongRunning response + mock_veo_response = { + "name": "operations/1234567890123456789" + } + + mock_httpx_response = MagicMock(spec=httpx.Response) + mock_httpx_response.status_code = 200 + mock_httpx_response.json.return_value = mock_veo_response + mock_httpx_response.headers = {"content-type": "application/json"} + + mock_logging_obj = self._create_mock_logging_obj() + + # Request body with durationSeconds + request_body = { + "instances": [{"prompt": "A close up of two people staring at a cryptic drawing on a wall,"}], + "parameters": {"durationSeconds": 8} + } + + kwargs = { + "passthrough_logging_payload": PassthroughStandardLoggingPayload( + url="https://generativelanguage.googleapis.com/v1beta/models/veo-2.0-generate-001:predictLongRunning", + request_body=request_body, + request_method="POST", + ), + } + + # Act + result = GeminiPassthroughLoggingHandler.gemini_passthrough_handler( + httpx_response=mock_httpx_response, + response_body=mock_veo_response, + logging_obj=mock_logging_obj, + url_route="https://generativelanguage.googleapis.com/v1beta/models/veo-2.0-generate-001:predictLongRunning", + result="", + start_time=self.start_time, + end_time=self.end_time, + cache_hit=False, + request_body=request_body, + **kwargs, + ) + + # Assert + assert result is not None + assert "result" in result + assert "kwargs" in result + + # Verify the cost is calculated correctly + assert result["kwargs"]["response_cost"] == expected_cost + assert result["kwargs"]["model"] == "veo-2.0-generate-001" + assert result["kwargs"]["custom_llm_provider"] == "gemini" + + # Verify completion_cost was called with create_video call_type + mock_completion_cost.assert_called_once() + call_args = mock_completion_cost.call_args + assert call_args.kwargs.get("call_type") == "create_video" + assert call_args.kwargs.get("custom_llm_provider") == "gemini" + assert call_args.kwargs.get("model") == "veo-2.0-generate-001" + + # Verify the response object has _hidden_params with response_cost + video_response = result["result"] + assert hasattr(video_response, "_hidden_params") + assert video_response._hidden_params.get("response_cost") == expected_cost + + # Verify logging object was updated + assert mock_logging_obj.model_call_details["response_cost"] == expected_cost + assert mock_logging_obj.model_call_details["model"] == "veo-2.0-generate-001" + assert mock_logging_obj.model_call_details["custom_llm_provider"] == "gemini" From ae132abff42cf7a3e65a28e40e7811bedc2b9a04 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Mon, 1 Dec 2025 14:36:30 +0530 Subject: [PATCH 056/370] Revert auth change --- .../llm_passthrough_endpoints.py | 9 ++++--- .../pass_through_endpoints.py | 25 ------------------- 2 files changed, 6 insertions(+), 28 deletions(-) diff --git a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py index d1294f996ec..7afb6868c73 100644 --- a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py +++ b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py @@ -187,10 +187,13 @@ async def gemini_proxy_route( """ [Docs](https://docs.litellm.ai/docs/pass_through/google_ai_studio) """ - # Get LiteLLM API key from Authorization header for authentication - api_key_to_use = get_litellm_virtual_key(request=request) + ## CHECK FOR LITELLM API KEY IN THE QUERY PARAMS - ?..key=LITELLM_API_KEY + google_ai_studio_api_key = request.query_params.get("key") or request.headers.get( + "x-goog-api-key" + ) + user_api_key_dict = await user_api_key_auth( - request=request, api_key=api_key_to_use + request=request, api_key=f"Bearer {google_ai_studio_api_key}" ) base_target_url = ( diff --git a/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py b/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py index 8e297f645c6..a447dad0b19 100644 --- a/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py +++ b/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py @@ -412,31 +412,6 @@ class HttpPassThroughEndpointHelpers(BasePassthroughUtils): params=requested_query_params, json=_parsed_body, ) - # Mock httpx response emulating a Google AI video generation operation status - # Attach a dummy request with headers set, so response.request.headers is always present - dummy_request = httpx.Request( - method=request.method, - url=str(url), - headers=headers or {}, - params=requested_query_params, - json=_parsed_body, - ) - # Ensure the .headers attribute exists and is a dict (httpx will normalize it) - mock_headers = httpx.Headers({"content-type": "application/json"}) - response = httpx.Response( - status_code=200, - headers=mock_headers, - json={ - "name": "operations/1234567890123456789", - "metadata": { - "@type": "type.googleapis.com/google.ai.generativelanguage.v1beta.GenerateVideoMetadata", - "state": "RUNNING", - "createTime": "2025-01-01T12:00:00Z" - }, - "done": False - }, - request=dummy_request - ) return response @staticmethod From 983ba7aa0f2e4e7f3e6ffe6082fcd93c366a76ce Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Mon, 1 Dec 2025 17:22:04 +0530 Subject: [PATCH 057/370] Remove not compatible beta header from claude code --- .../anthropic_claude3_transformation.py | 2 +- .../anthropic_claude3_transformation.py | 2 +- .../bedrock/test_anthropic_beta_support.py | 138 +++++++++++++++++- 3 files changed, 133 insertions(+), 9 deletions(-) diff --git a/litellm/llms/bedrock/chat/invoke_transformations/anthropic_claude3_transformation.py b/litellm/llms/bedrock/chat/invoke_transformations/anthropic_claude3_transformation.py index f003c0ed95f..53e08229799 100644 --- a/litellm/llms/bedrock/chat/invoke_transformations/anthropic_claude3_transformation.py +++ b/litellm/llms/bedrock/chat/invoke_transformations/anthropic_claude3_transformation.py @@ -103,7 +103,7 @@ class AmazonAnthropicClaudeConfig(AmazonInvokeConfig, AnthropicConfig): model=model, optional_params=optional_params, computer_tool_used=self.is_computer_tool_used(tools), - prompt_caching_set=self.is_cache_control_set(messages), + prompt_caching_set=False, file_id_used=self.is_file_id_used(messages), mcp_server_used=self.is_mcp_server_used(optional_params.get("mcp_servers")), ) diff --git a/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py b/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py index aea8a4b5a8f..32be1a780a3 100644 --- a/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py +++ b/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py @@ -157,7 +157,7 @@ class AmazonAnthropicClaudeMessagesConfig( model=model, optional_params=anthropic_messages_optional_request_params, computer_tool_used=anthropic_model_info.is_computer_tool_used(tools), - prompt_caching_set=anthropic_model_info.is_cache_control_set(messages_typed), + prompt_caching_set=False, file_id_used=anthropic_model_info.is_file_id_used(messages_typed), mcp_server_used=anthropic_model_info.is_mcp_server_used( anthropic_messages_optional_request_params.get("mcp_servers") diff --git a/tests/test_litellm/llms/bedrock/test_anthropic_beta_support.py b/tests/test_litellm/llms/bedrock/test_anthropic_beta_support.py index 1b9e1b5284c..bd64670517c 100644 --- a/tests/test_litellm/llms/bedrock/test_anthropic_beta_support.py +++ b/tests/test_litellm/llms/bedrock/test_anthropic_beta_support.py @@ -5,14 +5,19 @@ Tests that anthropic-beta headers are correctly processed and passed to AWS Bedr for enabling beta features like 1M context window, computer use tools, etc. """ -import pytest -from unittest.mock import patch, MagicMock import json +from unittest.mock import MagicMock, patch + +import pytest -from litellm.llms.bedrock.common_utils import get_anthropic_beta_from_headers from litellm.llms.bedrock.chat.converse_transformation import AmazonConverseConfig -from litellm.llms.bedrock.chat.invoke_transformations.anthropic_claude3_transformation import AmazonAnthropicClaudeConfig -from litellm.llms.bedrock.messages.invoke_transformations.anthropic_claude3_transformation import AmazonAnthropicClaudeMessagesConfig +from litellm.llms.bedrock.chat.invoke_transformations.anthropic_claude3_transformation import ( + AmazonAnthropicClaudeConfig, +) +from litellm.llms.bedrock.common_utils import get_anthropic_beta_from_headers +from litellm.llms.bedrock.messages.invoke_transformations.anthropic_claude3_transformation import ( + AmazonAnthropicClaudeMessagesConfig, +) class TestAnthropicBetaHeaderSupport: @@ -56,7 +61,8 @@ class TestAnthropicBetaHeaderSupport: ) assert "anthropic_beta" in result - assert result["anthropic_beta"] == ["context-1m-2025-08-07", "computer-use-2024-10-22"] + # Beta flags are stored as sets, so order may vary + assert set(result["anthropic_beta"]) == {"context-1m-2025-08-07", "computer-use-2024-10-22"} def test_converse_transformation_anthropic_beta(self): """Test that Converse API transformation includes anthropic_beta in additionalModelRequestFields.""" @@ -163,4 +169,122 @@ class TestAnthropicBetaHeaderSupport: ) assert "anthropic_beta" in result - assert result["anthropic_beta"] == supported_features \ No newline at end of file + # Beta flags are stored as sets, so order may vary + assert set(result["anthropic_beta"]) == set(supported_features) + + def test_prompt_caching_no_beta_header_messages_api(self): + """Test that prompt caching (cache_control) does NOT add prompt-caching-2024-07-31 beta header for Bedrock. + + Bedrock recognizes prompt caching via the request body (cache_control field), + not through beta headers. This test verifies the fix. + """ + config = AmazonAnthropicClaudeMessagesConfig() + headers = {} + + # Messages with cache_control set (prompt caching enabled) + messages = [ + { + "role": "user", + "content": [ + { + "type": "text", + "text": "Hello", + "cache_control": {"type": "ephemeral"} + } + ] + } + ] + + result = config.transform_anthropic_messages_request( + model="us.anthropic.claude-sonnet-4-5-20250929-v1:0", + messages=messages, + anthropic_messages_optional_request_params={"max_tokens": 100}, + litellm_params={}, + headers=headers + ) + + # Verify prompt-caching-2024-07-31 is NOT in anthropic_beta + if "anthropic_beta" in result: + assert "prompt-caching-2024-07-31" not in result["anthropic_beta"], ( + "prompt-caching-2024-07-31 should not be added as a beta header for Bedrock. " + "Bedrock recognizes prompt caching via cache_control in the request body, not beta headers." + ) + else: + # It's also valid if anthropic_beta is not present at all + assert True + + def test_prompt_caching_no_beta_header_chat_api(self): + """Test that prompt caching (cache_control) does NOT add prompt-caching-2024-07-31 beta header for Bedrock Chat API. + + Bedrock recognizes prompt caching via the request body (cache_control field), + not through beta headers. This test verifies the fix. + """ + config = AmazonAnthropicClaudeConfig() + headers = {} + + # Messages with cache_control set (prompt caching enabled) + messages = [ + { + "role": "user", + "content": [ + { + "type": "text", + "text": "Hello", + "cache_control": {"type": "ephemeral"} + } + ] + } + ] + + result = config.transform_request( + model="us.anthropic.claude-sonnet-4-5-20250929-v1:0", + messages=messages, + optional_params={}, + litellm_params={}, + headers=headers + ) + + # Verify prompt-caching-2024-07-31 is NOT in anthropic_beta + if "anthropic_beta" in result: + assert "prompt-caching-2024-07-31" not in result["anthropic_beta"], ( + "prompt-caching-2024-07-31 should not be added as a beta header for Bedrock. " + "Bedrock recognizes prompt caching via cache_control in the request body, not beta headers." + ) + else: + # It's also valid if anthropic_beta is not present at all + assert True + + def test_prompt_caching_with_other_beta_headers(self): + """Test that prompt caching doesn't interfere with other valid beta headers.""" + config = AmazonAnthropicClaudeMessagesConfig() + headers = {"anthropic-beta": "context-1m-2025-08-07"} + + # Messages with cache_control set + messages = [ + { + "role": "user", + "content": [ + { + "type": "text", + "text": "Hello", + "cache_control": {"type": "ephemeral"} + } + ] + } + ] + + result = config.transform_anthropic_messages_request( + model="us.anthropic.claude-sonnet-4-5-20250929-v1:0", + messages=messages, + anthropic_messages_optional_request_params={"max_tokens": 100}, + litellm_params={}, + headers=headers + ) + + # Should have the user-provided beta header but NOT prompt-caching + if "anthropic_beta" in result: + assert "context-1m-2025-08-07" in result["anthropic_beta"] + assert "prompt-caching-2024-07-31" not in result["anthropic_beta"] + else: + # If no beta headers, that's also fine + assert True \ No newline at end of file From 8f6822a64264474d2643c12a2f5b1d8857d169d9 Mon Sep 17 00:00:00 2001 From: Chesars Date: Mon, 1 Dec 2025 12:55:01 -0300 Subject: [PATCH 058/370] Fix: Allow reasoning_effort='none' for Azure gpt-5.1 models PR #17071 drops or errors on reasoning_effort='none' for all GPT models. It doesn't actually allow 'none' to be sent to Azure for gpt-5.1 which supports it according to Azure documentation. See: https://learn.microsoft.com/en-us/azure/ai-foundry/openai/how-to/reasoning "none is only supported for gpt-5.1" --- .../llms/azure/chat/gpt_5_transformation.py | 12 +++- .../chat/test_azure_gpt5_transformation.py | 59 +++++++++++++------ 2 files changed, 50 insertions(+), 21 deletions(-) diff --git a/litellm/llms/azure/chat/gpt_5_transformation.py b/litellm/llms/azure/chat/gpt_5_transformation.py index 209475730f8..2d0ce8b5bce 100644 --- a/litellm/llms/azure/chat/gpt_5_transformation.py +++ b/litellm/llms/azure/chat/gpt_5_transformation.py @@ -40,7 +40,11 @@ class AzureOpenAIGPT5Config(AzureOpenAIConfig, OpenAIGPT5Config): or optional_params.get("reasoning_effort") ) - if reasoning_effort_value == "none": + # gpt-5.1 supports reasoning_effort='none', but other gpt-5 models don't + # See: https://learn.microsoft.com/en-us/azure/ai-foundry/openai/how-to/reasoning + is_gpt_5_1 = self.is_model_gpt_5_1_model(model) + + if reasoning_effort_value == "none" and not is_gpt_5_1: if litellm.drop_params is True or ( drop_params is not None and drop_params is True ): @@ -54,8 +58,9 @@ class AzureOpenAIGPT5Config(AzureOpenAIConfig, OpenAIGPT5Config): raise UnsupportedParamsError( status_code=400, message=( - "Azure OpenAI does not support reasoning_effort='none'. " + "Azure OpenAI does not support reasoning_effort='none' for this model. " "Supported values are: 'low', 'medium', and 'high'. " + "Note: gpt-5.1 does support reasoning_effort='none'. " "To drop this parameter, set `litellm.drop_params=True` or for proxy:\n\n" "`litellm_settings:\n drop_params: true`\n" "Issue: https://github.com/BerriAI/litellm/issues/16704" @@ -70,7 +75,8 @@ class AzureOpenAIGPT5Config(AzureOpenAIConfig, OpenAIGPT5Config): drop_params=drop_params, ) - if result.get("reasoning_effort") == "none": + # Only drop reasoning_effort='none' for non-gpt-5.1 models + if result.get("reasoning_effort") == "none" and not is_gpt_5_1: result.pop("reasoning_effort") return result diff --git a/tests/test_litellm/llms/azure/chat/test_azure_gpt5_transformation.py b/tests/test_litellm/llms/azure/chat/test_azure_gpt5_transformation.py index 3095ff87f5a..91d664c3216 100644 --- a/tests/test_litellm/llms/azure/chat/test_azure_gpt5_transformation.py +++ b/tests/test_litellm/llms/azure/chat/test_azure_gpt5_transformation.py @@ -104,34 +104,33 @@ def test_azure_gpt5_codex_series_transform_request(config: AzureOpenAIGPT5Config # GPT-5.1 temperature handling tests for Azure def test_azure_gpt5_1_temperature_with_reasoning_effort_none(config: AzureOpenAIGPT5Config): - """Test that Azure GPT-5.1 supports any temperature when reasoning_effort='none' and drop_params=True. - - Note: Azure OpenAI doesn't support reasoning_effort='none', so it's dropped from the params - when drop_params=True. The temperature logic still works correctly because the parent treats - missing reasoning_effort the same as 'none' for gpt-5.1. + """Test that Azure GPT-5.1 supports any temperature when reasoning_effort='none'. + + Azure OpenAI supports reasoning_effort='none' for gpt-5.1 models. + See: https://learn.microsoft.com/en-us/azure/ai-foundry/openai/how-to/reasoning """ params = config.map_openai_params( non_default_params={"temperature": 0.5, "reasoning_effort": "none"}, optional_params={}, model="azure/gpt-5.1", - drop_params=True, + drop_params=False, api_version="2024-05-01-preview", ) assert params["temperature"] == 0.5 - # Azure doesn't support reasoning_effort="none", so it should be dropped - assert "reasoning_effort" not in params or params.get("reasoning_effort") != "none" + # Azure supports reasoning_effort="none" for gpt-5.1 + assert params.get("reasoning_effort") == "none" -def test_azure_gpt5_1_reasoning_effort_none_error_when_drop_params_false(config: AzureOpenAIGPT5Config): - """Test that Azure GPT-5.1 raises error for reasoning_effort='none' when drop_params=False.""" - with pytest.raises(litellm.utils.UnsupportedParamsError): - config.map_openai_params( - non_default_params={"reasoning_effort": "none"}, - optional_params={}, - model="azure/gpt-5.1", - drop_params=False, - api_version="2024-05-01-preview", - ) +def test_azure_gpt5_1_reasoning_effort_none_supported(config: AzureOpenAIGPT5Config): + """Test that Azure GPT-5.1 supports reasoning_effort='none' without error.""" + params = config.map_openai_params( + non_default_params={"reasoning_effort": "none"}, + optional_params={}, + model="azure/gpt-5.1", + drop_params=False, + api_version="2024-05-01-preview", + ) + assert params.get("reasoning_effort") == "none" def test_azure_gpt5_1_temperature_without_reasoning_effort(config: AzureOpenAIGPT5Config): @@ -181,3 +180,27 @@ def test_azure_gpt5_1_series_temperature_handling(config: AzureOpenAIGPT5Config) ) assert params["temperature"] == 0.6 + +def test_azure_gpt5_reasoning_effort_none_error(config: AzureOpenAIGPT5Config): + """Test that Azure GPT-5 (non-5.1) raises error for reasoning_effort='none' when drop_params=False.""" + with pytest.raises(litellm.utils.UnsupportedParamsError): + config.map_openai_params( + non_default_params={"reasoning_effort": "none"}, + optional_params={}, + model="azure/gpt-5", + drop_params=False, + api_version="2024-05-01-preview", + ) + + +def test_azure_gpt5_reasoning_effort_none_dropped(config: AzureOpenAIGPT5Config): + """Test that Azure GPT-5 (non-5.1) drops reasoning_effort='none' when drop_params=True.""" + params = config.map_openai_params( + non_default_params={"reasoning_effort": "none"}, + optional_params={}, + model="azure/gpt-5", + drop_params=True, + api_version="2024-05-01-preview", + ) + assert "reasoning_effort" not in params or params.get("reasoning_effort") != "none" + From 6de610767340cadd6df1c5508325128045c8fae5 Mon Sep 17 00:00:00 2001 From: YutaSaito <36355491+uc4w6c@users.noreply.github.com> Date: Tue, 2 Dec 2025 02:59:01 +0900 Subject: [PATCH 059/370] fix: respect guardrail mock_response during during_call to return blocked output (#17247) --- litellm/proxy/common_request_processing.py | 23 +++-- .../proxy/test_common_request_processing.py | 99 ++++++++++++++++++- 2 files changed, 111 insertions(+), 11 deletions(-) diff --git a/litellm/proxy/common_request_processing.py b/litellm/proxy/common_request_processing.py index d2b04410026..ed4c451f8d3 100644 --- a/litellm/proxy/common_request_processing.py +++ b/litellm/proxy/common_request_processing.py @@ -536,7 +536,11 @@ class ProxyBaseLLMRequestProcessing: responses = await llm_responses - response = responses[1] + # Guardrails (pre/during-call) can inject a mock response to short-circuit the LLM call. + # Prefer it when present so blocked/filtered output is returned instead of the model response. + response = self.data.get("mock_response") + if response is None: + response = responses[1] hidden_params = getattr(response, "_hidden_params", {}) or {} model_id = hidden_params.get("model_id", None) or "" @@ -804,7 +808,7 @@ class ProxyBaseLLMRequestProcessing: # This matches the original behavior before the refactor in commit 511d435f6f error_body = await e.response.aread() error_text = error_body.decode("utf-8") - + raise HTTPException( status_code=e.response.status_code, detail={"error": error_text}, @@ -1072,9 +1076,9 @@ class ProxyBaseLLMRequestProcessing: # Add cache-related fields to **params (handled by Usage.__init__) if cache_creation_input_tokens is not None: - usage_kwargs["cache_creation_input_tokens"] = ( - cache_creation_input_tokens - ) + usage_kwargs[ + "cache_creation_input_tokens" + ] = cache_creation_input_tokens if cache_read_input_tokens is not None: usage_kwargs["cache_read_input_tokens"] = cache_read_input_tokens @@ -1093,7 +1097,9 @@ class ProxyBaseLLMRequestProcessing: return obj return None - def maybe_get_model_id(self, _logging_obj: Optional[LiteLLMLoggingObj]) -> Optional[str]: + def maybe_get_model_id( + self, _logging_obj: Optional[LiteLLMLoggingObj] + ) -> Optional[str]: """ Get model_id from logging object or request metadata. @@ -1103,10 +1109,7 @@ class ProxyBaseLLMRequestProcessing: model_id = None if _logging_obj: # 1. Try getting from litellm_params (updated during call) - if ( - hasattr(_logging_obj, "litellm_params") - and _logging_obj.litellm_params - ): + if hasattr(_logging_obj, "litellm_params") and _logging_obj.litellm_params: # First check direct model_info path (set by router.py with selected deployment) model_info = _logging_obj.litellm_params.get("model_info") or {} model_id = model_info.get("id", None) diff --git a/tests/test_litellm/proxy/test_common_request_processing.py b/tests/test_litellm/proxy/test_common_request_processing.py index 4768ec42ff6..8f5f182f429 100644 --- a/tests/test_litellm/proxy/test_common_request_processing.py +++ b/tests/test_litellm/proxy/test_common_request_processing.py @@ -1,11 +1,13 @@ import copy +from types import SimpleNamespace from unittest.mock import AsyncMock, MagicMock import pytest -from fastapi import Request, status +from fastapi import Request, Response, status from fastapi.responses import StreamingResponse import litellm +import litellm.proxy.common_request_processing as common_request_processing from litellm._uuid import uuid from litellm.integrations.opentelemetry import UserAPIKeyAuth from litellm.proxy.common_request_processing import ( @@ -75,6 +77,101 @@ class TestProxyBaseLLMRequestProcessing: pytest.fail("litellm_call_id is not a valid UUID") assert data_passed["litellm_call_id"] == returned_data["litellm_call_id"] + @pytest.mark.asyncio + async def test_base_process_llm_request_prefers_guardrail_mock_response( + self, monkeypatch + ): + processing_obj = ProxyBaseLLMRequestProcessing( + data={ + "messages": [], + "metadata": {}, + "litellm_metadata": {"model_info": {"id": "fallback-model"}}, + } + ) + + guardrail_response = litellm.ModelResponse( + model="bedrock-guardrail", + hidden_params={"model_id": "guardrail-model"}, + ) + llm_response = litellm.ModelResponse( + model="real-model", + hidden_params={"model_id": "real-model"}, + ) + + async def mock_common_processing(self, *args, **kwargs): + logging_obj = SimpleNamespace(litellm_call_id="test-call-id") + self.data["litellm_call_id"] = "test-call-id" + self.data["litellm_logging_obj"] = logging_obj + return self.data, logging_obj + + monkeypatch.setattr( + ProxyBaseLLMRequestProcessing, + "common_processing_pre_call_logic", + mock_common_processing, + ) + + async def mock_route_request(*args, **kwargs): + async def _inner(): + return llm_response + + return _inner() + + monkeypatch.setattr( + common_request_processing, + "route_request", + mock_route_request, + ) + + check_response_size_is_safe_mock = AsyncMock() + monkeypatch.setattr( + common_request_processing, + "check_response_size_is_safe", + check_response_size_is_safe_mock, + ) + + async def mock_during_call_hook(*args, **kwargs): + kwargs["data"]["mock_response"] = guardrail_response + + proxy_logging_obj = MagicMock(spec=ProxyLogging) + proxy_logging_obj.during_call_hook = AsyncMock( + side_effect=mock_during_call_hook + ) + proxy_logging_obj.update_request_status = AsyncMock(return_value=None) + proxy_logging_obj.post_call_success_hook = AsyncMock( + return_value=guardrail_response + ) + + user_api_key_dict = MagicMock(spec=UserAPIKeyAuth) + user_api_key_dict.tpm_limit = None + user_api_key_dict.rpm_limit = None + user_api_key_dict.max_budget = None + user_api_key_dict.spend = 0 + user_api_key_dict.allowed_model_region = None + + fastapi_response = Response() + proxy_config = MagicMock(spec=ProxyConfig) + + result = await processing_obj.base_process_llm_request( + request=MagicMock(spec=Request), + fastapi_response=fastapi_response, + user_api_key_dict=user_api_key_dict, + route_type="acompletion", + proxy_logging_obj=proxy_logging_obj, + general_settings={}, + proxy_config=proxy_config, + select_data_generator=lambda **kwargs: None, + ) + + assert result is guardrail_response + assert ( + proxy_logging_obj.post_call_success_hook.await_args.kwargs["response"] + is guardrail_response + ) + assert ( + check_response_size_is_safe_mock.await_args.kwargs["response"] + is guardrail_response + ) + @pytest.mark.asyncio async def test_stream_timeout_header_processing(self): """ From 7808a610f8a95ddb4449eae1b0af67d4e5b2e50d Mon Sep 17 00:00:00 2001 From: orgersh92 Date: Mon, 1 Dec 2025 20:03:51 +0200 Subject: [PATCH 060/370] Fix session consistency, move Lasso API version away from source code (#17316) * store and fetch lasso-conversation id from cache * include gateway/v# in the baseUrl to allow simpler version migrations in the future * add tests for cached conversation ID --- .../docs/proxy/guardrails/lasso_security.md | 4 +- .../guardrails/guardrail_hooks/lasso/lasso.py | 68 +++++++------------ .../guardrails/guardrail_hooks/test_lasso.py | 29 +++++--- 3 files changed, 47 insertions(+), 54 deletions(-) diff --git a/docs/my-website/docs/proxy/guardrails/lasso_security.md b/docs/my-website/docs/proxy/guardrails/lasso_security.md index 21528790afe..113e3f8974a 100644 --- a/docs/my-website/docs/proxy/guardrails/lasso_security.md +++ b/docs/my-website/docs/proxy/guardrails/lasso_security.md @@ -35,7 +35,7 @@ guardrails: guardrail: lasso mode: "pre_call" api_key: os.environ/LASSO_API_KEY - api_base: "https://server.lasso.security" + api_base: "https://server.lasso.security/gateway/v3" - guardrail_name: "lasso-post-guard" litellm_params: guardrail: lasso @@ -228,7 +228,7 @@ Expected response: ## PII Masking with Lasso -Lasso supports automatic PII detection and masking using the `/gateway/v1/classifix` endpoint. When enabled, sensitive information like emails, phone numbers, and other PII will be automatically masked with appropriate placeholders. +Lasso supports automatic PII detection and masking using the `/classifix` endpoint. When enabled, sensitive information like emails, phone numbers, and other PII will be automatically masked with appropriate placeholders. ### Enabling PII Masking diff --git a/litellm/proxy/guardrails/guardrail_hooks/lasso/lasso.py b/litellm/proxy/guardrails/guardrail_hooks/lasso/lasso.py index 99d2b82400f..ea8f1b0a97f 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/lasso/lasso.py +++ b/litellm/proxy/guardrails/guardrail_hooks/lasso/lasso.py @@ -33,6 +33,8 @@ from litellm.integrations.custom_guardrail import ( CustomGuardrail, log_guardrail_information, ) +from litellm.integrations.custom_guardrail import dc as global_cache + from litellm.llms.custom_httpx.http_handler import ( get_async_httpx_client, httpxSpecialProvider, @@ -100,7 +102,7 @@ class LassoGuardrail(CustomGuardrail): ) self.api_base = ( - api_base or os.getenv("LASSO_API_BASE") or "https://server.lasso.security" + api_base or os.getenv("LASSO_API_BASE") or "https://server.lasso.security/gateway/v3" ) verbose_proxy_logger.debug( @@ -125,7 +127,7 @@ class LassoGuardrail(CustomGuardrail): async def async_pre_call_hook( self, user_api_key_dict: UserAPIKeyAuth, - cache: DualCache, + cache: DualCache, # Deprecated, use global_cache instead (kept to align with CustomGuardrail interface) data: dict, call_type: Literal[ "completion", @@ -150,10 +152,10 @@ class LassoGuardrail(CustomGuardrail): return data # Get or generate conversation_id and store it in data for post-call consistency - conversation_id = self._get_or_generate_conversation_id(data, cache) - data.setdefault("_lasso_internal", {})["conversation_id"] = conversation_id + # The conversation_id is being stored in the cache so it can be used by the post_call hook + self._get_or_generate_conversation_id(data, global_cache) - return await self._run_lasso_guardrail(data, cache, message_type="PROMPT") + return await self._run_lasso_guardrail(data, global_cache, message_type="PROMPT") @log_guardrail_information async def async_moderation_hook( @@ -213,17 +215,12 @@ class LassoGuardrail(CustomGuardrail): "litellm_call_id": data.get("litellm_call_id"), } - # Copy stored conversation_id from pre-call hook - if data.get("_lasso_internal", {}).get("conversation_id") and isinstance(response_data, dict): - response_data.setdefault("_lasso_internal", {})["conversation_id"] = data["_lasso_internal"][ - "conversation_id" - ] # Handle masking for post-call if self.mask: - headers = self._prepare_headers(response_data) - payload = self._prepare_payload(response_messages, "COMPLETION", response_data) - api_url = f"{self.api_base}/gateway/v3/classifix" + headers = self._prepare_headers(response_data, global_cache) + payload = self._prepare_payload(response_messages, response_data, global_cache, "COMPLETION") + api_url = f"{self.api_base}/classifix" try: lasso_response = await self._call_lasso_api(headers=headers, payload=payload, api_url=api_url) @@ -241,7 +238,7 @@ class LassoGuardrail(CustomGuardrail): raise LassoGuardrailAPIError(f"Failed to apply post-call masking: {str(e)}") else: # Use the same data for conversation_id consistency (no cache access needed) - await self._run_lasso_guardrail(response_data, cache=None, message_type="COMPLETION") + await self._run_lasso_guardrail(response_data, cache=global_cache, message_type="COMPLETION") verbose_proxy_logger.debug("Post-call Lasso validation completed") else: verbose_proxy_logger.warning("No response messages found to validate") @@ -306,7 +303,7 @@ class LassoGuardrail(CustomGuardrail): async def _run_lasso_guardrail( self, data: dict, - cache: Optional[DualCache], + cache: DualCache, message_type: Literal["PROMPT", "COMPLETION"] = "PROMPT", ): """ @@ -345,14 +342,14 @@ class LassoGuardrail(CustomGuardrail): async def _handle_classification( self, data: dict, - cache: Optional[DualCache], + cache: DualCache, message_type: Literal["PROMPT", "COMPLETION"], messages: List[Dict[str, str]], ) -> dict: """Handle classification without masking.""" try: headers = self._prepare_headers(data, cache) - payload = self._prepare_payload(messages, message_type, data, cache) + payload = self._prepare_payload(messages, data, cache, message_type) response = await self._call_lasso_api(headers=headers, payload=payload) self._process_lasso_response(response) return data @@ -363,15 +360,15 @@ class LassoGuardrail(CustomGuardrail): async def _handle_masking( self, data: dict, - cache: Optional[DualCache], + cache: DualCache, message_type: Literal["PROMPT", "COMPLETION"], messages: List[Dict[str, str]], ) -> dict: """Handle masking with classifix endpoint.""" try: headers = self._prepare_headers(data, cache) - payload = self._prepare_payload(messages, message_type, data, cache) - api_url = f"{self.api_base}/gateway/v3/classifix" + payload = self._prepare_payload(messages, data, cache, message_type) + api_url = f"{self.api_base}/classifix" response = await self._call_lasso_api(headers=headers, payload=payload, api_url=api_url) self._process_lasso_response(response) @@ -437,7 +434,7 @@ class LassoGuardrail(CustomGuardrail): }, ) - def _prepare_headers(self, data: dict, cache: Optional[DualCache] = None) -> Dict[str, str]: + def _prepare_headers(self, data: dict, cache: DualCache) -> Dict[str, str]: """Prepare headers for the Lasso API request.""" if not self.lasso_api_key: raise LassoGuardrailMissingSecrets( @@ -455,13 +452,7 @@ class LassoGuardrail(CustomGuardrail): headers["lasso-user-id"] = self.user_id # Always include conversation_id (generated or provided) - if cache is not None: - conversation_id = self._get_or_generate_conversation_id(data, cache) - else: - # For post-call hook, use stored conversation_id or generate a new one - conversation_id = ( - data.get("_lasso_internal", {}).get("conversation_id") or self.conversation_id or self._generate_ulid() - ) + conversation_id = self._get_or_generate_conversation_id(data, cache) headers["lasso-conversation-id"] = conversation_id @@ -470,9 +461,9 @@ class LassoGuardrail(CustomGuardrail): def _prepare_payload( self, messages: List[Dict[str, str]], + data: dict, + cache: DualCache, message_type: Literal["PROMPT", "COMPLETION"] = "PROMPT", - data: Optional[dict] = None, - cache: Optional[DualCache] = None, ) -> Dict[str, Any]: """ Prepare the payload for the Lasso API request. @@ -490,20 +481,9 @@ class LassoGuardrail(CustomGuardrail): payload["userId"] = self.user_id # Always include sessionId (conversation_id - generated or provided) - if data is not None: - if cache is not None: - conversation_id = self._get_or_generate_conversation_id(data, cache) - else: - # For post-call hook, use stored conversation_id or fallback - conversation_id = ( - data.get("_lasso_internal", {}).get("conversation_id") - or self.conversation_id - or self._generate_ulid() - ) + conversation_id = self._get_or_generate_conversation_id(data, cache) - payload["sessionId"] = conversation_id - elif self.conversation_id: - payload["sessionId"] = self.conversation_id + payload["sessionId"] = conversation_id return payload @@ -514,7 +494,7 @@ class LassoGuardrail(CustomGuardrail): api_url: Optional[str] = None, ) -> LassoResponse: """Call the Lasso API and return the response.""" - url = api_url or f"{self.api_base}/gateway/v3/classify" + url = api_url or f"{self.api_base}/classify" verbose_proxy_logger.debug(f"Calling Lasso API with messageType: {payload.get('messageType')}") response = await self.async_handler.post( url=url, diff --git a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_lasso.py b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_lasso.py index c63974ac3f2..87542c974a5 100644 --- a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_lasso.py +++ b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_lasso.py @@ -1,6 +1,7 @@ import os import sys import pytest +import uuid from unittest.mock import patch, MagicMock from httpx import Response, Request from fastapi import HTTPException @@ -77,10 +78,11 @@ class TestLassoGuardrail: assert guardrail.lasso_api_key == "test-api-key" assert guardrail.user_id == "test-user" assert guardrail.conversation_id == "test-conversation" - assert guardrail.api_base == "https://server.lasso.security" + assert guardrail.api_base == "https://server.lasso.security/gateway/v3" @pytest.mark.asyncio async def test_pre_call_no_violations(self): + from litellm.integrations.custom_guardrail import dc as global_cache """Test pre-call hook with no violations detected.""" # Setup guardrail guardrail = LassoGuardrail( @@ -90,12 +92,16 @@ class TestLassoGuardrail: default_on=True ) + test_call_id = str(uuid.uuid4()) + assert global_cache.get_cache(f"lasso_conversation_id:{test_call_id}") is None + # Test data data = { "messages": [ {"role": "user", "content": "Hello, how are you?"} ], - "metadata": {} + "metadata": {}, + "litellm_call_id": test_call_id } # Mock successful API response with no violations @@ -118,13 +124,14 @@ class TestLassoGuardrail: request=Request(method="POST", url="https://server.lasso.security/gateway/v3/classify"), ) + local_cache = DualCache() with patch( "litellm.llms.custom_httpx.http_handler.AsyncHTTPHandler.post", return_value=mock_response ): result = await guardrail.async_pre_call_hook( user_api_key_dict=UserAPIKeyAuth(), - cache=DualCache(), + cache=local_cache, data=data, call_type="completion" ) @@ -132,6 +139,11 @@ class TestLassoGuardrail: # Should return original data when no violations detected assert result == data + # Verify that the conversation_id is stored in the global cache but not the local cache + cache_key = f"lasso_conversation_id:{test_call_id}" + assert global_cache.get_cache(cache_key) is not None + assert local_cache.get_cache(cache_key) is None + @pytest.mark.asyncio async def test_pre_call_with_violations(self): """Test pre-call hook with violations detected.""" @@ -466,9 +478,10 @@ class TestLassoGuardrail: ) messages = [{"role": "user", "content": "Test message"}] + cache = DualCache() # Test PROMPT payload - prompt_payload = guardrail._prepare_payload(messages, "PROMPT") + prompt_payload = guardrail._prepare_payload(messages, {}, cache, "PROMPT") assert prompt_payload["messageType"] == "PROMPT" assert prompt_payload["messages"] == messages assert prompt_payload["userId"] == "test-user" @@ -476,7 +489,7 @@ class TestLassoGuardrail: # Test COMPLETION payload completion_messages = [{"role": "assistant", "content": "Test response"}] - completion_payload = guardrail._prepare_payload(completion_messages, "COMPLETION") + completion_payload = guardrail._prepare_payload(completion_messages, {}, cache, "COMPLETION") assert completion_payload["messageType"] == "COMPLETION" assert completion_payload["messages"] == completion_messages assert completion_payload["userId"] == "test-user" @@ -489,9 +502,9 @@ class TestLassoGuardrail: user_id="test-user", conversation_id="test-conversation" ) - + cache = DualCache() data = {"litellm_call_id": "test-call-id"} - headers = guardrail._prepare_headers(data) + headers = guardrail._prepare_headers(data, cache) assert headers["lasso-api-key"] == "test-api-key" assert headers["Content-Type"] == "application/json" assert headers["lasso-user-id"] == "test-user" @@ -499,7 +512,7 @@ class TestLassoGuardrail: # Test without optional fields guardrail_minimal = LassoGuardrail(lasso_api_key="test-api-key") - headers_minimal = guardrail_minimal._prepare_headers(data) + headers_minimal = guardrail_minimal._prepare_headers(data, cache) assert headers_minimal["lasso-api-key"] == "test-api-key" assert headers_minimal["Content-Type"] == "application/json" assert "lasso-user-id" not in headers_minimal From 625b2fd54930a46d4647b906157ea94566fe5d0e Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Mon, 1 Dec 2025 10:18:38 -0800 Subject: [PATCH 061/370] Await cred delete refresh + migrate to reusable delete modal --- .../components/model_add/credentials.test.tsx | 46 ++++++++-- .../src/components/model_add/credentials.tsx | 92 +++++++++++-------- 2 files changed, 92 insertions(+), 46 deletions(-) diff --git a/ui/litellm-dashboard/src/components/model_add/credentials.test.tsx b/ui/litellm-dashboard/src/components/model_add/credentials.test.tsx index 8e356baa12b..fab74a39d2e 100644 --- a/ui/litellm-dashboard/src/components/model_add/credentials.test.tsx +++ b/ui/litellm-dashboard/src/components/model_add/credentials.test.tsx @@ -1,5 +1,5 @@ import { CredentialItem } from "@/components/networking"; -import { render, waitFor } from "@testing-library/react"; +import { render, screen, waitFor } from "@testing-library/react"; import { UploadProps } from "antd/es/upload"; import { describe, expect, it, vi } from "vitest"; import CredentialsPanel from "./credentials"; @@ -7,10 +7,25 @@ import CredentialsPanel from "./credentials"; const DEFAULT_UPLOAD_PROPS = {} as UploadProps; describe("CredentialsPanel", () => { - it("renders without crashing and fetches credentials when token exists", async () => { + it("should render", () => { const fetchCredentials = vi.fn(() => Promise.resolve()); - const { getByRole, getByText } = render( + render( + , + ); + + expect(screen.getByRole("button", { name: /add credential/i })).toBeInTheDocument(); + }); + + it("should call fetchCredentials when accessToken exists", async () => { + const fetchCredentials = vi.fn(() => Promise.resolve()); + + render( { ); await waitFor(() => { - expect(getByRole("button", { name: /add credential/i })).toBeInTheDocument(); - expect(getByText("Credential Name")).toBeInTheDocument(); - expect(getByText("Provider")).toBeInTheDocument(); + expect(fetchCredentials).toHaveBeenCalledWith("test-token"); }); }); - it("displays provided credentials and still calls the fetch helper", async () => { + it("should display provided credentials", () => { const fetchCredentials = vi.fn(() => Promise.resolve()); const credentials: CredentialItem[] = [ { @@ -36,7 +49,7 @@ describe("CredentialsPanel", () => { }, ]; - const { getByText } = render( + render( { />, ); - await waitFor(() => expect(getByText("openai-key")).toBeInTheDocument()); + expect(screen.getByText("openai-key")).toBeInTheDocument(); + }); + + it("should display empty state when no credentials are provided", () => { + const fetchCredentials = vi.fn(() => Promise.resolve()); + + render( + , + ); + + expect(screen.getByText("No credentials configured")).toBeInTheDocument(); }); }); diff --git a/ui/litellm-dashboard/src/components/model_add/credentials.tsx b/ui/litellm-dashboard/src/components/model_add/credentials.tsx index e36a759294b..cf6a5cb9eef 100644 --- a/ui/litellm-dashboard/src/components/model_add/credentials.tsx +++ b/ui/litellm-dashboard/src/components/model_add/credentials.tsx @@ -1,28 +1,28 @@ -import React, { useState, useEffect } from "react"; import { + credentialCreateCall, + credentialDeleteCall, + CredentialItem, + credentialUpdateCall, +} from "@/components/networking"; // Assume this is your networking function +import { PencilAltIcon, TrashIcon } from "@heroicons/react/outline"; +import { + Badge, + Button, + Card, Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow, - Card, Text, - Badge, - Button, } from "@tremor/react"; -import { PencilAltIcon, TrashIcon } from "@heroicons/react/outline"; -import { UploadProps } from "antd/es/upload"; -import { - credentialCreateCall, - credentialDeleteCall, - credentialUpdateCall, - CredentialItem, -} from "@/components/networking"; // Assume this is your networking function -import AddCredentialsTab from "./add_credentials_tab"; -import CredentialDeleteModal from "./CredentialDeleteModal"; import { Form } from "antd"; +import { UploadProps } from "antd/es/upload"; +import React, { useEffect, useState } from "react"; +import DeleteResourceModal from "../common_components/DeleteResourceModal"; import NotificationsManager from "../molecules/notifications_manager"; +import AddCredentialsTab from "./add_credentials_tab"; interface CredentialsPanelProps { accessToken: string | null; uploadProps: UploadProps; @@ -39,7 +39,9 @@ const CredentialsPanel: React.FC = ({ const [isAddModalOpen, setIsAddModalOpen] = useState(false); const [isUpdateModalOpen, setIsUpdateModalOpen] = useState(false); const [selectedCredential, setSelectedCredential] = useState(null); - const [credentialToDelete, setCredentialToDelete] = useState(null); + const [credentialToDelete, setCredentialToDelete] = useState(null); + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const [isCredentialDeleting, setIsCredentialDeleting] = useState(false); const [form] = Form.useForm(); const restrictedFields = ["credential_name", "custom_llm_provider"]; @@ -113,27 +115,38 @@ const CredentialsPanel: React.FC = ({ ); }; - const handleDeleteCredential = async (credentialName: string) => { - if (!accessToken) { + const handleDeleteCredential = async () => { + if (!accessToken || !credentialToDelete) { return; } - const response = await credentialDeleteCall(accessToken, credentialName); - NotificationsManager.success("Credential deleted successfully"); - setCredentialToDelete(null); - fetchCredentials(accessToken); + setIsCredentialDeleting(true); + try { + await credentialDeleteCall(accessToken, credentialToDelete.credential_name); + NotificationsManager.success("Credential deleted successfully"); + await fetchCredentials(accessToken); + } catch (error) { + NotificationsManager.error("Failed to delete credential"); + } finally { + setCredentialToDelete(null); + setIsDeleteModalOpen(false); + setIsCredentialDeleting(false); + } }; - const openDeleteModal = (credentialName: string) => { - setCredentialToDelete(credentialName); + const openDeleteModal = (credential: CredentialItem) => { + setCredentialToDelete(credential); + setIsDeleteModalOpen(true); }; const closeDeleteModal = () => { setCredentialToDelete(null); + setIsDeleteModalOpen(false); }; return ( -
-
+
+ +
Configured credentials for different AI providers. Add and manage your API credentials.
@@ -143,6 +156,7 @@ const CredentialsPanel: React.FC = ({ Credential Name Provider + Actions @@ -173,7 +187,8 @@ const CredentialsPanel: React.FC = ({ icon={TrashIcon} variant="light" size="sm" - onClick={() => openDeleteModal(credential.credential_name)} + onClick={() => openDeleteModal(credential)} + className="ml-2" /> @@ -182,9 +197,6 @@ const CredentialsPanel: React.FC = ({ - {isAddModalOpen && ( = ({ /> )} - {credentialToDelete && ( - handleDeleteCredential(credentialToDelete)} - credentialName={credentialToDelete} - /> - )} +
); }; From c588e7854d7a8d03363b1b203af8650d37ab9b57 Mon Sep 17 00:00:00 2001 From: Colin Lin Date: Thu, 27 Nov 2025 16:36:04 -0500 Subject: [PATCH 062/370] use kwargs --- litellm/llms/custom_httpx/llm_http_handler.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/litellm/llms/custom_httpx/llm_http_handler.py b/litellm/llms/custom_httpx/llm_http_handler.py index fdd504e2f57..10353c68b97 100644 --- a/litellm/llms/custom_httpx/llm_http_handler.py +++ b/litellm/llms/custom_httpx/llm_http_handler.py @@ -1804,15 +1804,23 @@ class BaseLLMHTTPHandler: Optional[litellm.types.utils.ProviderSpecificHeader], kwargs.get("provider_specific_header", None), ) - extra_headers = ProviderSpecificHeaderUtils.get_provider_specific_headers( + provider_specific_headers = ProviderSpecificHeaderUtils.get_provider_specific_headers( provider_specific_header=provider_specific_header, custom_llm_provider=custom_llm_provider, ) forwarded_headers = kwargs.get("headers", None) - if forwarded_headers and extra_headers: - merged_headers = {**forwarded_headers, **extra_headers} - else: - merged_headers = forwarded_headers or extra_headers + # Also check for extra_headers in kwargs (from config or direct calls) + extra_headers_from_kwargs = kwargs.get("extra_headers", None) + print("extra_headers_from_kwargs", extra_headers_from_kwargs) + print("provider_specific_headers", provider_specific_headers) + # Merge all header sources: forwarded < extra_headers < provider_specific + merged_headers = {} + if forwarded_headers: + merged_headers.update(forwarded_headers) + if extra_headers_from_kwargs: + merged_headers.update(extra_headers_from_kwargs) + if provider_specific_headers: + merged_headers.update(provider_specific_headers) ( headers, api_base, From 2a5082e6cf2e00c64faea984e587c314fac2731d Mon Sep 17 00:00:00 2001 From: Colin Lin Date: Thu, 27 Nov 2025 16:58:32 -0500 Subject: [PATCH 063/370] remove logs --- litellm/llms/custom_httpx/llm_http_handler.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/litellm/llms/custom_httpx/llm_http_handler.py b/litellm/llms/custom_httpx/llm_http_handler.py index 10353c68b97..701cefb771e 100644 --- a/litellm/llms/custom_httpx/llm_http_handler.py +++ b/litellm/llms/custom_httpx/llm_http_handler.py @@ -1811,8 +1811,6 @@ class BaseLLMHTTPHandler: forwarded_headers = kwargs.get("headers", None) # Also check for extra_headers in kwargs (from config or direct calls) extra_headers_from_kwargs = kwargs.get("extra_headers", None) - print("extra_headers_from_kwargs", extra_headers_from_kwargs) - print("provider_specific_headers", provider_specific_headers) # Merge all header sources: forwarded < extra_headers < provider_specific merged_headers = {} if forwarded_headers: From e420b633a1ac8eaaf33fc2024d4ad70e7b8d688a Mon Sep 17 00:00:00 2001 From: Colin Lin Date: Fri, 28 Nov 2025 17:06:10 -0500 Subject: [PATCH 064/370] add tests --- .../custom_httpx/test_llm_http_handler.py | 150 +++++++++++++++++- 1 file changed, 145 insertions(+), 5 deletions(-) diff --git a/tests/test_litellm/llms/custom_httpx/test_llm_http_handler.py b/tests/test_litellm/llms/custom_httpx/test_llm_http_handler.py index 26fc18de16d..17b4243da1d 100644 --- a/tests/test_litellm/llms/custom_httpx/test_llm_http_handler.py +++ b/tests/test_litellm/llms/custom_httpx/test_llm_http_handler.py @@ -1,17 +1,14 @@ -import io import os -import pathlib -import ssl import sys -from unittest.mock import MagicMock +from unittest.mock import AsyncMock, Mock, patch import pytest sys.path.insert( 0, os.path.abspath("../../../..") ) # Adds the parent directory to the system path -import litellm from litellm.llms.custom_httpx.llm_http_handler import BaseLLMHTTPHandler +from litellm.types.router import GenericLiteLLMParams def test_prepare_fake_stream_request(): @@ -75,3 +72,146 @@ def test_prepare_fake_stream_request(): assert "stream" not in result_data assert result_data["model"] == "gpt-4" assert result_data["messages"] == [{"role": "user", "content": "Hello"}] + + +@pytest.mark.asyncio +async def test_async_anthropic_messages_handler_extra_headers(): + """ + Test that async_anthropic_messages_handler correctly extracts and merges + extra_headers from kwargs with proper priority. + """ + handler = BaseLLMHTTPHandler() + + # Mock the config + mock_config = Mock() + mock_config.validate_anthropic_messages_environment = Mock( + return_value=({"x-api-key": "test-key"}, "https://api.anthropic.com") + ) + mock_config.transform_anthropic_messages_request = Mock( + return_value={"model": "claude-3-opus-20240229", "messages": []} + ) + + # Mock the client + mock_client = AsyncMock() + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = { + "id": "msg_123", + "type": "message", + "role": "assistant", + "content": [{"type": "text", "text": "Hello!"}], + "model": "claude-3-opus-20240229", + "stop_reason": "end_turn", + } + mock_client.post = AsyncMock(return_value=mock_response) + + # Mock logging object + mock_logging_obj = Mock() + mock_logging_obj.update_environment_variables = Mock() + mock_logging_obj.model_call_details = {} + mock_logging_obj.stream = False + + # Test case 1: Only extra_headers in kwargs + kwargs = { + "extra_headers": { + "X-Custom-Header": "from-kwargs", + "X-Auth-Token": "token123", + } + } + + with patch( + "litellm.litellm_core_utils.get_provider_specific_headers.ProviderSpecificHeaderUtils.get_provider_specific_headers" + ) as mock_provider_headers: + mock_provider_headers.return_value = None + + # Capture what headers are passed to validate_anthropic_messages_environment + captured_headers = {} + def capture_validate(*args, **kwargs): + captured_headers.update(kwargs.get("headers", {})) + return ({"x-api-key": "test-key"}, "https://api.anthropic.com") + + mock_config.validate_anthropic_messages_environment = capture_validate + + try: + await handler.async_anthropic_messages_handler( + model="claude-3-opus-20240229", + messages=[{"role": "user", "content": "Hello"}], + anthropic_messages_provider_config=mock_config, + anthropic_messages_optional_request_params={}, + custom_llm_provider="anthropic", + litellm_params=GenericLiteLLMParams(), + logging_obj=mock_logging_obj, + client=mock_client, + kwargs=kwargs, + ) + except Exception: + pass # We're testing header extraction, not the full flow + + # Verify extra_headers were extracted and merged + assert "X-Custom-Header" in captured_headers + assert captured_headers["X-Custom-Header"] == "from-kwargs" + assert "X-Auth-Token" in captured_headers + assert captured_headers["X-Auth-Token"] == "token123" + + +@pytest.mark.asyncio +async def test_async_anthropic_messages_handler_header_priority(): + """ + Test that async_anthropic_messages_handler respects header priority: + forwarded < extra_headers < provider_specific + """ + handler = BaseLLMHTTPHandler() + + # Mock the config + mock_config = Mock() + mock_client = AsyncMock() + mock_logging_obj = Mock() + mock_logging_obj.update_environment_variables = Mock() + mock_logging_obj.model_call_details = {} + mock_logging_obj.stream = False + + # Test with all three header sources + kwargs = { + "headers": {"X-Priority": "forwarded", "X-Forwarded-Only": "keep"}, + "extra_headers": {"X-Priority": "extra", "X-Extra-Only": "also-keep"}, + } + + with patch( + "litellm.litellm_core_utils.get_provider_specific_headers.ProviderSpecificHeaderUtils.get_provider_specific_headers" + ) as mock_provider_headers: + mock_provider_headers.return_value = { + "X-Priority": "provider", + "X-Provider-Only": "keep-this-too" + } + + captured_headers = {} + def capture_validate(*args, **kwargs): + captured_headers.update(kwargs.get("headers", {})) + return ({"x-api-key": "test-key"}, "https://api.anthropic.com") + + mock_config.validate_anthropic_messages_environment = capture_validate + mock_config.transform_anthropic_messages_request = Mock( + return_value={"model": "claude-3-opus-20240229", "messages": []} + ) + + try: + await handler.async_anthropic_messages_handler( + model="claude-3-opus-20240229", + messages=[{"role": "user", "content": "Hello"}], + anthropic_messages_provider_config=mock_config, + anthropic_messages_optional_request_params={}, + custom_llm_provider="anthropic", + litellm_params=GenericLiteLLMParams(), + logging_obj=mock_logging_obj, + client=mock_client, + kwargs=kwargs, + ) + except Exception: + pass + + # Verify priority: provider_specific should win + assert captured_headers["X-Priority"] == "provider" + # Verify all unique headers from different sources are present + assert captured_headers["X-Forwarded-Only"] == "keep" + assert captured_headers["X-Extra-Only"] == "also-keep" + assert captured_headers["X-Provider-Only"] == "keep-this-too" From 661bccbc3984396b13900ee9069754dca244e83d Mon Sep 17 00:00:00 2001 From: Colin Lin Date: Mon, 1 Dec 2025 14:15:54 -0500 Subject: [PATCH 065/370] fixed flaky test by sorting list --- .../llms/bedrock/test_anthropic_beta_support.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/test_litellm/llms/bedrock/test_anthropic_beta_support.py b/tests/test_litellm/llms/bedrock/test_anthropic_beta_support.py index bd64670517c..7de2294954c 100644 --- a/tests/test_litellm/llms/bedrock/test_anthropic_beta_support.py +++ b/tests/test_litellm/llms/bedrock/test_anthropic_beta_support.py @@ -80,7 +80,8 @@ class TestAnthropicBetaHeaderSupport: assert "additionalModelRequestFields" in result additional_fields = result["additionalModelRequestFields"] assert "anthropic_beta" in additional_fields - assert additional_fields["anthropic_beta"] == ["context-1m-2025-08-07", "interleaved-thinking-2025-05-14"] + # Sort both arrays before comparing to avoid flakiness from ordering differences + assert sorted(additional_fields["anthropic_beta"]) == sorted(["context-1m-2025-08-07", "interleaved-thinking-2025-05-14"]) def test_messages_transformation_anthropic_beta(self): """Test that Messages API transformation includes anthropic_beta in request.""" @@ -96,7 +97,8 @@ class TestAnthropicBetaHeaderSupport: ) assert "anthropic_beta" in result - assert result["anthropic_beta"] == ["output-128k-2025-02-19"] + # Sort both arrays before comparing to avoid flakiness from ordering differences + assert sorted(result["anthropic_beta"]) == sorted(["output-128k-2025-02-19"]) def test_converse_computer_use_compatibility(self): """Test that user anthropic_beta headers work with computer use tools.""" @@ -287,4 +289,4 @@ class TestAnthropicBetaHeaderSupport: assert "prompt-caching-2024-07-31" not in result["anthropic_beta"] else: # If no beta headers, that's also fine - assert True \ No newline at end of file + assert True From 69a6c25a5ff0c04799b597c602e2853f011d7819 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Mon, 1 Dec 2025 12:28:53 -0800 Subject: [PATCH 066/370] Add user alias to user table --- .../src/components/view_users/columns.tsx | 6 + .../src/components/view_users/table.test.tsx | 187 ++++++------------ .../src/components/view_users/types.ts | 1 + 3 files changed, 68 insertions(+), 126 deletions(-) diff --git a/ui/litellm-dashboard/src/components/view_users/columns.tsx b/ui/litellm-dashboard/src/components/view_users/columns.tsx index 20df4fc246e..48895c69def 100644 --- a/ui/litellm-dashboard/src/components/view_users/columns.tsx +++ b/ui/litellm-dashboard/src/components/view_users/columns.tsx @@ -46,6 +46,12 @@ export const columns = ( enableSorting: true, cell: ({ row }) => {possibleUIRoles?.[row.original.user_role]?.ui_label || "-"}, }, + { + header: "User Alias", + accessorKey: "user_alias", + enableSorting: false, + cell: ({ row }) => {row.original.user_alias || "-"}, + }, { header: "Spend (USD)", accessorKey: "spend", diff --git a/ui/litellm-dashboard/src/components/view_users/table.test.tsx b/ui/litellm-dashboard/src/components/view_users/table.test.tsx index 8ef887932cc..c688d5749d0 100644 --- a/ui/litellm-dashboard/src/components/view_users/table.test.tsx +++ b/ui/litellm-dashboard/src/components/view_users/table.test.tsx @@ -1,63 +1,52 @@ import { act, fireEvent, render, screen } from "@testing-library/react"; import { describe, expect, it, vi } from "vitest"; - import { UserDataTable } from "./table"; +const defaultFilters = { + email: "", + user_id: "", + user_role: "", + sso_user_id: "", + team: "", + model: "", + min_spend: null, + max_spend: null, + sort_by: "", + sort_order: "asc" as const, +}; + +const getDefaultProps = () => ({ + data: [] as any[], + columns: [] as any[], + accessToken: null, + userRole: "Admin", + possibleUIRoles: null as Record> | null, + filters: defaultFilters, + updateFilters: vi.fn(), + initialFilters: defaultFilters, + teams: [] as any[], + handleEdit: vi.fn(), + handleDelete: vi.fn(), + handleResetPassword: vi.fn(), + userListResponse: { users: [], total: 0, page: 1, page_size: 25, total_pages: 1 }, + currentPage: 1, + handlePageChange: vi.fn(), +}); + describe("UserDataTable", () => { it("should render the UserDataTable component", () => { - const filters = { - email: "", - user_id: "", - user_role: "", - sso_user_id: "", - team: "", - model: "", - min_spend: null, - max_spend: null, - sort_by: "", - sort_order: "asc" as const, - }; - - const updateFilters = vi.fn(); - - render( - , - ); + render(); expect(screen.getByText("Filters")).toBeInTheDocument(); }); it("should call onSortChange when clicking a sortable header", () => { const filters = { - email: "", - user_id: "", - user_role: "", - sso_user_id: "", - team: "", - model: "", - min_spend: null, - max_spend: null, + ...defaultFilters, sort_by: "created_at", sort_order: "desc" as const, }; - const updateFilters = vi.fn(); const onSortChange = vi.fn(); const possibleUIRoles = { @@ -67,21 +56,10 @@ describe("UserDataTable", () => { render( , @@ -96,41 +74,7 @@ describe("UserDataTable", () => { }); it("should show skeleton loaders when isLoading is true", () => { - const filters = { - email: "", - user_id: "", - user_role: "", - sso_user_id: "", - team: "", - model: "", - min_spend: null, - max_spend: null, - sort_by: "", - sort_order: "asc" as const, - }; - - const updateFilters = vi.fn(); - - render( - , - ); + render(); expect(screen.queryByText(/Showing/i)).not.toBeInTheDocument(); expect(screen.queryByRole("button", { name: /Previous/i })).not.toBeInTheDocument(); @@ -138,44 +82,35 @@ describe("UserDataTable", () => { }); it("should show actual content when isLoading is false", () => { - const filters = { - email: "", - user_id: "", - user_role: "", - sso_user_id: "", - team: "", - model: "", - min_spend: null, - max_spend: null, - sort_by: "", - sort_order: "asc" as const, - }; - - const updateFilters = vi.fn(); - - render( - , - ); + render(); expect(screen.getByText(/Showing/i)).toBeInTheDocument(); expect(screen.getByRole("button", { name: /Previous/i })).toBeInTheDocument(); expect(screen.getByRole("button", { name: /Next/i })).toBeInTheDocument(); }); + + it("should render all column headers", () => { + const possibleUIRoles = { + admin: { ui_label: "Admin" }, + user: { ui_label: "User" }, + }; + + render(); + + [ + "User ID", + "Email", + "Global Proxy Role", + "User Alias", + "Spend (USD)", + "Budget (USD)", + "SSO ID", + "API Keys", + "Created At", + "Updated At", + "Actions", + ].forEach((header) => { + expect(screen.getByRole("columnheader", { name: header })).toBeInTheDocument(); + }); + }); }); diff --git a/ui/litellm-dashboard/src/components/view_users/types.ts b/ui/litellm-dashboard/src/components/view_users/types.ts index d976d46ebc1..d674db5c7db 100644 --- a/ui/litellm-dashboard/src/components/view_users/types.ts +++ b/ui/litellm-dashboard/src/components/view_users/types.ts @@ -1,6 +1,7 @@ export interface UserInfo { user_id: string; user_email: string; + user_alias: string | null; user_role: string; spend: number; max_budget: number | null; From a73bd751fcc6895fa27b801d61b11acb03f91e65 Mon Sep 17 00:00:00 2001 From: YutaSaito <36355491+uc4w6c@users.noreply.github.com> Date: Tue, 2 Dec 2025 05:38:49 +0900 Subject: [PATCH 067/370] doc: add images for tool permission guardrail (#17322) --- .../img/create_guard_tool_permission.png | Bin 0 -> 51115 bytes .../img/create_rule_tool_permission.png | Bin 0 -> 76256 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/my-website/img/create_guard_tool_permission.png create mode 100644 docs/my-website/img/create_rule_tool_permission.png diff --git a/docs/my-website/img/create_guard_tool_permission.png b/docs/my-website/img/create_guard_tool_permission.png new file mode 100644 index 0000000000000000000000000000000000000000..f6e0e77b1aa8c64447170b1db78bbde4ee4714a7 GIT binary patch literal 51115 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⁢iQJIr|hfF_P-@E@2gTV8yAc)*w^!Iq_C&1j(fVx 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�Xi|ViYoZ*4fAou1UpoI<2L}v5U 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 Date: Mon, 1 Dec 2025 12:55:47 -0800 Subject: [PATCH 068/370] [Feat] WatsonX - allow passing zen_api_key dynamically (#16655) * test_watsonx_zen_api_key_from_client * zen api key * docs using zen api key --- .../docs/providers/watsonx/index.md | 53 ++++ litellm/llms/anthropic/skills/readme.md | 286 +++++++++++++++++- litellm/llms/watsonx/common_utils.py | 6 +- .../test_litellm/llms/watsonx/test_watsonx.py | 90 ++++++ 4 files changed, 422 insertions(+), 13 deletions(-) diff --git a/docs/my-website/docs/providers/watsonx/index.md b/docs/my-website/docs/providers/watsonx/index.md index 279d2d1024e..14e0c07c081 100644 --- a/docs/my-website/docs/providers/watsonx/index.md +++ b/docs/my-website/docs/providers/watsonx/index.md @@ -175,3 +175,56 @@ For all available models, see [watsonx.ai documentation](https://dataplatform.cl For all available embedding models, see [watsonx.ai embedding documentation](https://dataplatform.cloud.ibm.com/docs/content/wsj/analyze-data/fm-models-embed.html?context=wx). + +## Advanced + +### Using Zen API Key + +You can use a Zen API key for long-term authentication instead of generating IAM tokens. Pass it either as an environment variable or as a parameter: + +```python +import os +from litellm import completion + +# Option 1: Set as environment variable +os.environ["WATSONX_ZENAPIKEY"] = "your-zen-api-key" + +response = completion( + model="watsonx/ibm/granite-13b-chat-v2", + messages=[{"content": "What is your favorite color?", "role": "user"}], + project_id="your-project-id" +) + +# Option 2: Pass as parameter +response = completion( + model="watsonx/ibm/granite-13b-chat-v2", + messages=[{"content": "What is your favorite color?", "role": "user"}], + zen_api_key="your-zen-api-key", + project_id="your-project-id" +) +``` + +**Using with LiteLLM Proxy via OpenAI client:** + +```python +import openai + +client = openai.OpenAI( + api_key="sk-1234", # LiteLLM proxy key + base_url="http://0.0.0.0:4000" +) + +response = client.chat.completions.create( + model="watsonx/ibm/granite-3-3-8b-instruct", + messages=[{"role": "user", "content": "What is your favorite color?"}], + max_tokens=2048, + extra_body={ + "project_id": "your-project-id", + "zen_api_key": "your-zen-api-key" + } +) +``` + +See [IBM documentation](https://www.ibm.com/docs/en/watsonx/w-and-w/2.2.0?topic=keys-generating-zenapikey-authorization-tokens) for more information on generating Zen API keys. + + diff --git a/litellm/llms/anthropic/skills/readme.md b/litellm/llms/anthropic/skills/readme.md index 898639cd44b..0602272256c 100644 --- a/litellm/llms/anthropic/skills/readme.md +++ b/litellm/llms/anthropic/skills/readme.md @@ -1,17 +1,279 @@ -# Anthropic Skills API +# Anthropic Skills API Integration -This folder maintains the integration for the Anthropic Skills API. +This module provides comprehensive support for the Anthropic Skills API through LiteLLM. -You can do the following with the Anthropic Skills API: +## Features -1. Create a new skill -2. List all skills -3. Get a skill -4. Delete a skill +The Skills API allows you to: +- **Create skills**: Define reusable AI capabilities +- **List skills**: Browse all available skills +- **Get skills**: Retrieve detailed information about a specific skill +- **Delete skills**: Remove skills that are no longer needed +## Quick Start -Versions: - - Create Skill Version - - List Skill Versions - - Get Skill Version - - Delete Skill Version \ No newline at end of file +### Prerequisites + +Set your Anthropic API key: +```python +import os +os.environ["ANTHROPIC_API_KEY"] = "your-api-key-here" +``` + +### Basic Usage + +#### Create a Skill + +```python +import litellm + +# Create a skill with files +# Note: All files must be in the same top-level directory +# and must include a SKILL.md file at the root +skill = litellm.create_skill( + files=[ + # List of file objects to upload + # Must include SKILL.md + ], + display_title="Python Code Generator", + custom_llm_provider="anthropic" +) +print(f"Created skill: {skill.id}") + +# Asynchronous version +skill = await litellm.acreate_skill( + files=[...], # Your files here + display_title="Python Code Generator", + custom_llm_provider="anthropic" +) +``` + +#### List Skills + +```python +# List all skills +skills = litellm.list_skills( + custom_llm_provider="anthropic" +) + +for skill in skills.data: + print(f"{skill.display_title}: {skill.id}") + +# With pagination and filtering +skills = litellm.list_skills( + limit=20, + source="custom", # Filter by 'custom' or 'anthropic' + custom_llm_provider="anthropic" +) + +# Get next page if available +if skills.has_more: + next_page = litellm.list_skills( + page=skills.next_page, + custom_llm_provider="anthropic" + ) +``` + +#### Get a Skill + +```python +skill = litellm.get_skill( + skill_id="skill_abc123", + custom_llm_provider="anthropic" +) + +print(f"Skill: {skill.display_title}") +print(f"Created: {skill.created_at}") +print(f"Latest version: {skill.latest_version}") +print(f"Source: {skill.source}") +``` + +#### Delete a Skill + +```python +result = litellm.delete_skill( + skill_id="skill_abc123", + custom_llm_provider="anthropic" +) + +print(f"Deleted skill {result.id}, type: {result.type}") +``` + +## API Reference + +### `create_skill()` + +Create a new skill. + +**Parameters:** +- `files` (List[Any], optional): Files to upload for the skill. All files must be in the same top-level directory and must include a SKILL.md file at the root. +- `display_title` (str, optional): Display title for the skill +- `custom_llm_provider` (str, optional): Provider name (default: "anthropic") +- `extra_headers` (dict, optional): Additional HTTP headers +- `timeout` (float, optional): Request timeout + +**Returns:** +- `Skill`: The created skill object + +**Async version:** `acreate_skill()` + +### `list_skills()` + +List all skills. + +**Parameters:** +- `limit` (int, optional): Number of results to return per page (max 100, default 20) +- `page` (str, optional): Pagination token for fetching a specific page of results +- `source` (str, optional): Filter skills by source ('custom' or 'anthropic') +- `custom_llm_provider` (str, optional): Provider name (default: "anthropic") +- `extra_headers` (dict, optional): Additional HTTP headers +- `timeout` (float, optional): Request timeout + +**Returns:** +- `ListSkillsResponse`: Object containing a list of skills and pagination info + +**Async version:** `alist_skills()` + +### `get_skill()` + +Get a specific skill by ID. + +**Parameters:** +- `skill_id` (str, required): The skill ID +- `custom_llm_provider` (str, optional): Provider name (default: "anthropic") +- `extra_headers` (dict, optional): Additional HTTP headers +- `timeout` (float, optional): Request timeout + +**Returns:** +- `Skill`: The requested skill object + +**Async version:** `aget_skill()` + +### `delete_skill()` + +Delete a skill. + +**Parameters:** +- `skill_id` (str, required): The skill ID to delete +- `custom_llm_provider` (str, optional): Provider name (default: "anthropic") +- `extra_headers` (dict, optional): Additional HTTP headers +- `timeout` (float, optional): Request timeout + +**Returns:** +- `DeleteSkillResponse`: Object with `id` and `type` fields + +**Async version:** `adelete_skill()` + +## Response Types + +### `Skill` + +Represents a skill from the Anthropic Skills API. + +**Fields:** +- `id` (str): Unique identifier +- `created_at` (str): ISO 8601 timestamp +- `display_title` (str, optional): Display title +- `latest_version` (str, optional): Latest version identifier +- `source` (str): Source ("custom" or "anthropic") +- `type` (str): Object type (always "skill") +- `updated_at` (str): ISO 8601 timestamp + +### `ListSkillsResponse` + +Response from listing skills. + +**Fields:** +- `data` (List[Skill]): List of skills +- `next_page` (str, optional): Pagination token for the next page +- `has_more` (bool): Whether more skills are available + +### `DeleteSkillResponse` + +Response from deleting a skill. + +**Fields:** +- `id` (str): The deleted skill ID +- `type` (str): Deleted object type (always "skill_deleted") + +## Architecture + +The Skills API implementation follows LiteLLM's standard patterns: + +1. **Type Definitions** (`litellm/types/llms/anthropic_skills.py`) + - Pydantic models for request/response types + - TypedDict definitions for request parameters + +2. **Base Configuration** (`litellm/llms/base_llm/skills/transformation.py`) + - Abstract base class `BaseSkillsAPIConfig` + - Defines transformation interface for provider-specific implementations + +3. **Provider Implementation** (`litellm/llms/anthropic/skills/transformation.py`) + - `AnthropicSkillsConfig` - Anthropic-specific transformations + - Handles API authentication, URL construction, and response mapping + +4. **Main Handler** (`litellm/skills/main.py`) + - Public API functions (sync and async) + - Request validation and routing + - Error handling + +5. **HTTP Handlers** (`litellm/llms/custom_httpx/llm_http_handler.py`) + - Low-level HTTP request/response handling + - Connection pooling and retry logic + +## Beta API Support + +The Skills API is in beta. The beta header (`skills-2025-10-02`) is automatically added by the Anthropic provider configuration. You can customize it if needed: + +```python +skill = litellm.create_skill( + display_title="My Skill", + extra_headers={ + "anthropic-beta": "skills-2025-10-02" # Or any other beta version + }, + custom_llm_provider="anthropic" +) +``` + +The default beta version is configured in `litellm.constants.ANTHROPIC_SKILLS_API_BETA_VERSION`. + +## Error Handling + +All Skills API functions follow LiteLLM's standard error handling: + +```python +import litellm + +try: + skill = litellm.create_skill( + display_title="My Skill", + custom_llm_provider="anthropic" + ) +except litellm.exceptions.AuthenticationError as e: + print(f"Authentication failed: {e}") +except litellm.exceptions.RateLimitError as e: + print(f"Rate limit exceeded: {e}") +except litellm.exceptions.APIError as e: + print(f"API error: {e}") +``` + +## Contributing + +To add support for Skills API to a new provider: + +1. Create provider-specific configuration class inheriting from `BaseSkillsAPIConfig` +2. Implement all abstract methods for request/response transformations +3. Register the config in `ProviderConfigManager.get_provider_skills_api_config()` +4. Add appropriate tests + +## Related Documentation + +- [Anthropic Skills API Documentation](https://platform.claude.com/docs/en/api/beta/skills/create) +- [LiteLLM Responses API](../../../responses/) +- [Provider Configuration System](../../base_llm/) + +## Support + +For issues or questions: +- GitHub Issues: https://github.com/BerriAI/litellm/issues +- Discord: https://discord.gg/wuPM9dRgDw diff --git a/litellm/llms/watsonx/common_utils.py b/litellm/llms/watsonx/common_utils.py index 58b33097cbd..0207020534c 100644 --- a/litellm/llms/watsonx/common_utils.py +++ b/litellm/llms/watsonx/common_utils.py @@ -252,9 +252,13 @@ class IBMWatsonXMixin: Optional[str], optional_params.get("token") or get_secret_str("WATSONX_TOKEN"), ) + zen_api_key = cast( + Optional[str], + optional_params.pop("zen_api_key", None) or get_secret_str("WATSONX_ZENAPIKEY"), + ) if token: headers["Authorization"] = f"Bearer {token}" - elif zen_api_key := get_secret_str("WATSONX_ZENAPIKEY"): + elif zen_api_key: headers["Authorization"] = f"ZenApiKey {zen_api_key}" else: token = _generate_watsonx_token(api_key=api_key, token=token) diff --git a/tests/test_litellm/llms/watsonx/test_watsonx.py b/tests/test_litellm/llms/watsonx/test_watsonx.py index a41316bb47e..fc45a13c2c1 100644 --- a/tests/test_litellm/llms/watsonx/test_watsonx.py +++ b/tests/test_litellm/llms/watsonx/test_watsonx.py @@ -414,3 +414,93 @@ def test_watsonx_chat_completion_with_reasoning_effort(monkeypatch): assert ( json_data["reasoning_effort"] == "low" ), "The value of 'reasoning_effort' should be 'low'." + + +def test_watsonx_zen_api_key_from_client(monkeypatch, watsonx_chat_completion_call): + """ + Test that zen_api_key can be passed from client code and is used in Authorization header. + """ + monkeypatch.setenv("WATSONX_PROJECT_ID", "test-project-id") + monkeypatch.setenv("WATSONX_API_BASE", "https://test-api.watsonx.ai") + + model = "watsonx/ibm/granite-3-3-8b-instruct" + messages = [{"role": "user", "content": "What is your favorite color?"}] + + client = HTTPHandler() + + zen_api_key = "U1ZDLWQo=" + + # No need to patch token call since zen_api_key should skip token generation + with patch.object(client, "post") as mock_post: + try: + completion( + model=model, + messages=messages, + api_key="test_api_key", + client=client, + zen_api_key=zen_api_key, + ) + except Exception as e: + print(f"Caught expected exception: {e}") + + # Verify the request was made + assert mock_post.call_count == 1, "The completion endpoint should have been called once." + + # Get the headers sent in the POST request + request_kwargs = mock_post.call_args.kwargs + headers = request_kwargs["headers"] + + print("\nHeaders sent to WatsonX API:") + print(json.dumps(dict(headers), indent=2)) + + # Verify Authorization header uses ZenApiKey format + assert "Authorization" in headers, "Authorization header should be present." + assert headers["Authorization"] == f"ZenApiKey {zen_api_key}", ( + f"Authorization header should use ZenApiKey format. " + f"Expected: 'ZenApiKey {zen_api_key}', Got: '{headers['Authorization']}'" + ) + + +def test_watsonx_zen_api_key_from_env(monkeypatch, watsonx_chat_completion_call): + """ + Test that zen_api_key from environment variable is used in Authorization header. + """ + monkeypatch.setenv("WATSONX_PROJECT_ID", "test-project-id") + monkeypatch.setenv("WATSONX_API_BASE", "https://test-api.watsonx.ai") + + zen_api_key = "U1ZDLWxpdG--===" + monkeypatch.setenv("WATSONX_ZENAPIKEY", zen_api_key) + + model = "watsonx/ibm/granite-3-3-8b-instruct" + messages = [{"role": "user", "content": "What is your favorite color?"}] + + client = HTTPHandler() + + # No need to patch token call since zen_api_key should skip token generation + with patch.object(client, "post") as mock_post: + try: + completion( + model=model, + messages=messages, + api_key="test_api_key", + client=client, + ) + except Exception as e: + print(f"Caught expected exception: {e}") + + # Verify the request was made + assert mock_post.call_count == 1, "The completion endpoint should have been called once." + + # Get the headers sent in the POST request + request_kwargs = mock_post.call_args.kwargs + headers = request_kwargs["headers"] + + print("\nHeaders sent to WatsonX API:") + print(json.dumps(dict(headers), indent=2)) + + # Verify Authorization header uses ZenApiKey format + assert "Authorization" in headers, "Authorization header should be present." + assert headers["Authorization"] == f"ZenApiKey {zen_api_key}", ( + f"Authorization header should use ZenApiKey format. " + f"Expected: 'ZenApiKey {zen_api_key}', Got: '{headers['Authorization']}'" + ) From 21baa354cc17b6f706f3bf93c9c8ba024a72bdb0 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Mon, 1 Dec 2025 13:13:39 -0800 Subject: [PATCH 069/370] Standardize API Key vs Virtual Key in UI --- .../src/components/activity_metrics.tsx | 7 ++- .../components/bulk_create_users_button.tsx | 8 +-- .../src/components/cache_dashboard.tsx | 12 ++--- .../PassThroughSecuritySection.tsx | 16 ++---- .../src/components/dashboard_default_team.tsx | 3 +- .../src/components/entity_usage.tsx | 2 +- .../src/components/make_agent_public_form.tsx | 16 ++---- .../src/components/make_mcp_public_form.tsx | 49 ++++++++----------- .../src/components/make_model_public_form.tsx | 4 +- .../src/components/mcp_tools/mcp_connect.tsx | 10 ++-- .../src/components/new_usage.test.tsx | 2 +- .../src/components/new_usage.tsx | 2 +- .../organisms/create_key_button.tsx | 14 +++--- .../organisms/regenerate_key_modal.tsx | 10 ++-- .../components/playground/chat_ui/ChatUI.tsx | 6 +-- .../playground/compareUI/CompareUI.tsx | 6 +-- .../llm_calls/anthropic_messages.tsx | 2 +- .../playground/llm_calls/embeddings_api.tsx | 2 +- .../playground/llm_calls/responses_api.tsx | 2 +- .../KeyInfoView.handleKeyUpdate.test.tsx | 2 +- .../components/templates/key_info_view.tsx | 8 +-- .../components/templates/view_key_table.tsx | 20 ++++---- ui/litellm-dashboard/src/components/usage.tsx | 2 +- .../src/components/view_users/columns.tsx | 4 +- .../components/view_users/user_info_view.tsx | 10 ++-- 25 files changed, 99 insertions(+), 120 deletions(-) diff --git a/ui/litellm-dashboard/src/components/activity_metrics.tsx b/ui/litellm-dashboard/src/components/activity_metrics.tsx index 1878e1364d0..a791ece9bb7 100644 --- a/ui/litellm-dashboard/src/components/activity_metrics.tsx +++ b/ui/litellm-dashboard/src/components/activity_metrics.tsx @@ -38,10 +38,9 @@ const ModelSection = ({ modelName, metrics }: { modelName: string; metrics: Mode - {/* Top API Keys Section */} {metrics.top_api_keys && metrics.top_api_keys.length > 0 && ( - Top API Keys by Spend + Top Virtual Keys by Spend
{metrics.top_api_keys.map((keyData, index) => ( @@ -384,12 +383,12 @@ export const processActivityData = ( }); }); - // Process API key breakdowns for each metric (skip if key is 'api_keys' to avoid duplication) + // Process Virtual Key breakdowns for each metric (skip if key is 'api_keys' to avoid duplication) if (key !== "api_keys") { Object.entries(modelMetrics).forEach(([model, _]) => { const apiKeyBreakdown: Record = {}; - // Aggregate API key data across all days + // Aggregate Virtual Key data across all days dailyActivity.results.forEach((day) => { const modelData = day.breakdown[key]?.[model]; if (modelData && "api_key_breakdown" in modelData) { diff --git a/ui/litellm-dashboard/src/components/bulk_create_users_button.tsx b/ui/litellm-dashboard/src/components/bulk_create_users_button.tsx index d34b14ceabf..a8046d146a8 100644 --- a/ui/litellm-dashboard/src/components/bulk_create_users_button.tsx +++ b/ui/litellm-dashboard/src/components/bulk_create_users_button.tsx @@ -569,7 +569,7 @@ const BulkCreateUsersButton: React.FC = ({
  • Download our CSV template
  • Add your users' information to the spreadsheet
  • Save the file and upload it here
  • -
  • After creation, download the results file containing the API keys for each user
  • +
  • After creation, download the results file containing the Virtual Keys for each user
  • @@ -809,9 +809,9 @@ const BulkCreateUsersButton: React.FC = ({
    User creation complete - Next step: Download the credentials file containing API - keys and invitation links. Users will need these API keys to make LLM requests through - LiteLLM. + Next step: Download the credentials file containing + Virtual Keys and invitation links. Users will need these Virtual Keys to make LLM requests + through LiteLLM.
    diff --git a/ui/litellm-dashboard/src/components/cache_dashboard.tsx b/ui/litellm-dashboard/src/components/cache_dashboard.tsx index a1c0cb0a664..38c0f1a8f41 100644 --- a/ui/litellm-dashboard/src/components/cache_dashboard.tsx +++ b/ui/litellm-dashboard/src/components/cache_dashboard.tsx @@ -293,7 +293,11 @@ const CacheDashboard: React.FC = ({ accessToken, token, userRole - + {uniqueApiKeys.map((key) => ( {key} @@ -388,11 +392,7 @@ const CacheDashboard: React.FC = ({ accessToken, token, userRole /> - + diff --git a/ui/litellm-dashboard/src/components/common_components/PassThroughSecuritySection.tsx b/ui/litellm-dashboard/src/components/common_components/PassThroughSecuritySection.tsx index c42094abb55..c63770d3c85 100644 --- a/ui/litellm-dashboard/src/components/common_components/PassThroughSecuritySection.tsx +++ b/ui/litellm-dashboard/src/components/common_components/PassThroughSecuritySection.tsx @@ -21,7 +21,7 @@ const PassThroughSecuritySection: React.FC = ({ Security - When enabled, requests to this endpoint will require a valid LiteLLM API key + When enabled, requests to this endpoint will require a valid LiteLLM Virtual Key {premiumUser ? ( @@ -35,22 +35,13 @@ const PassThroughSecuritySection: React.FC = ({ ) : (
    - + Authentication (Premium)
    Setting authentication for pass-through endpoints is a LiteLLM Enterprise feature. Get a trial key{" "} - + here . @@ -63,4 +54,3 @@ const PassThroughSecuritySection: React.FC = ({ }; export default PassThroughSecuritySection; - diff --git a/ui/litellm-dashboard/src/components/dashboard_default_team.tsx b/ui/litellm-dashboard/src/components/dashboard_default_team.tsx index 36805b8912a..6506e1a60a5 100644 --- a/ui/litellm-dashboard/src/components/dashboard_default_team.tsx +++ b/ui/litellm-dashboard/src/components/dashboard_default_team.tsx @@ -69,7 +69,8 @@ const DashboardTeam: React.FC = ({ Select Team - If you belong to multiple teams, this setting controls which team is used by default when creating new API Keys. + If you belong to multiple teams, this setting controls which team is used by default when creating new Virtual + Keys. Default Team: If no team_id is set for a key, it will be grouped under here. diff --git a/ui/litellm-dashboard/src/components/entity_usage.tsx b/ui/litellm-dashboard/src/components/entity_usage.tsx index a5789b7dbac..501eac7124b 100644 --- a/ui/litellm-dashboard/src/components/entity_usage.tsx +++ b/ui/litellm-dashboard/src/components/entity_usage.tsx @@ -550,7 +550,7 @@ const EntityUsage: React.FC = ({ {/* Top API Keys */} - Top API Keys + Top Virtual Keys = ({ setLoading(true); try { const agentIdsToMakePublic = Array.from(selectedAgents); - + // Make batch API call for all agents await makeAgentsPublicCall(accessToken, agentIdsToMakePublic); @@ -127,8 +127,8 @@ const MakeAgentPublicForm: React.FC = ({
    - Select the agents you want to be visible on the public model hub. Users will still require a valid API key to - use these agents. + Select the agents you want to be visible on the public model hub. Users will still require a valid Virtual Key + to use these agents.
    @@ -141,10 +141,7 @@ const MakeAgentPublicForm: React.FC = ({ agentHubData.map((agent) => { const agentId = agent.agent_id || agent.name; return ( -
    +
    handleAgentSelection(agentId, e.target.checked)} @@ -217,9 +214,7 @@ const MakeAgentPublicForm: React.FC = ({ )}
    - {agent?.description && ( - {agent.description} - )} + {agent?.description && {agent.description}}
    ); @@ -296,4 +291,3 @@ const MakeAgentPublicForm: React.FC = ({ }; export default MakeAgentPublicForm; - diff --git a/ui/litellm-dashboard/src/components/make_mcp_public_form.tsx b/ui/litellm-dashboard/src/components/make_mcp_public_form.tsx index 29f866f8bc6..f7bba175800 100644 --- a/ui/litellm-dashboard/src/components/make_mcp_public_form.tsx +++ b/ui/litellm-dashboard/src/components/make_mcp_public_form.tsx @@ -76,7 +76,7 @@ const MakeMCPPublicForm: React.FC = ({ const publicServerIds = mcpHubData .filter((server) => server.mcp_info?.is_public === true) .map((server) => server.server_id); - + // Preselect servers that are already public setSelectedServers(new Set(publicServerIds)); } @@ -91,7 +91,7 @@ const MakeMCPPublicForm: React.FC = ({ setLoading(true); try { const serverIdsToMakePublic = Array.from(selectedServers); - + // Make batch API call for all servers await makeMCPPublicCall(accessToken, serverIdsToMakePublic); @@ -128,8 +128,8 @@ const MakeMCPPublicForm: React.FC = ({
    - Select the MCP servers you want to be visible on the public model hub. Users will still require a valid API key to - use these servers. + Select the MCP servers you want to be visible on the public model hub. Users will still require a valid + Virtual Key to use these servers.
    @@ -161,22 +161,20 @@ const MakeMCPPublicForm: React.FC = ({ {server.transport} - {server.status || "unknown"}
    - - {server.description || server.url} - + {server.description || server.url} {server.allowed_tools && server.allowed_tools.length > 0 && (
    {server.allowed_tools.slice(0, 3).map((tool, idx) => ( @@ -236,14 +234,14 @@ const MakeMCPPublicForm: React.FC = ({ {server.transport} - {server.status || "unknown"} @@ -251,12 +249,8 @@ const MakeMCPPublicForm: React.FC = ({ )}
    - {server?.description && ( - {server.description} - )} - {server?.url && ( - {server.url} - )} + {server?.description && {server.description}} + {server?.url && {server.url}}
    ); @@ -267,8 +261,8 @@ const MakeMCPPublicForm: React.FC = ({
    - Total: {selectedServers.size} MCP server{selectedServers.size !== 1 ? "s" : ""} will be made - public + Total: {selectedServers.size} MCP server{selectedServers.size !== 1 ? "s" : ""} will be + made public
    @@ -333,4 +327,3 @@ const MakeMCPPublicForm: React.FC = ({ }; export default MakeMCPPublicForm; - diff --git a/ui/litellm-dashboard/src/components/make_model_public_form.tsx b/ui/litellm-dashboard/src/components/make_model_public_form.tsx index e67d60fb33b..750bdc24eeb 100644 --- a/ui/litellm-dashboard/src/components/make_model_public_form.tsx +++ b/ui/litellm-dashboard/src/components/make_model_public_form.tsx @@ -152,8 +152,8 @@ const MakeModelPublicForm: React.FC = ({
    - Select the models you want to be visible on the public model hub. Users will still require a valid API key to - use these models. + Select the models you want to be visible on the public model hub. Users will still require a valid Virtual Key + to use these models. {/* Filters */} diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_connect.tsx b/ui/litellm-dashboard/src/components/mcp_tools/mcp_connect.tsx index 4b4f1ab676b..5a012c1fc5c 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/mcp_connect.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/mcp_connect.tsx @@ -220,12 +220,12 @@ const MCPConnect: React.FC = ({ currentServerAccessGroups = [] } - title="API Key Setup" - description="Configure your LiteLLM Proxy API key for authentication" + title="Virtual Key Setup" + description="Configure your LiteLLM Proxy Virtual Key for authentication" >
    - Get your API key from your LiteLLM Proxy dashboard or contact your administrator + Get your Virtual Key from your LiteLLM Proxy dashboard or contact your administrator
    @@ -249,7 +249,7 @@ const MCPConnect: React.FC = ({ currentServerAccessGroups = [] = ({ currentServerAccessGroups = [] "server_url": "${proxyBaseUrl}/mcp", "require_approval": "never", "headers": { - "x-litellm-api-key": "Bearer YOUR_LITELLM_API_KEY", + "x-litellm-api-key": "Bearer YOUR_LITELLM_VIRTUAL_KEY", "x-mcp-servers": ["Zapier_MCP,dev"] } } diff --git a/ui/litellm-dashboard/src/components/new_usage.test.tsx b/ui/litellm-dashboard/src/components/new_usage.test.tsx index a4969124f1f..a06045137d7 100644 --- a/ui/litellm-dashboard/src/components/new_usage.test.tsx +++ b/ui/litellm-dashboard/src/components/new_usage.test.tsx @@ -239,7 +239,7 @@ describe("NewUsage", () => { // Check for chart titles expect(screen.getByText("Daily Spend")).toBeInTheDocument(); - expect(screen.getByText("Top API Keys")).toBeInTheDocument(); + expect(screen.getByText("Top Virtual Keys")).toBeInTheDocument(); }); it("should switch between tabs correctly", async () => { diff --git a/ui/litellm-dashboard/src/components/new_usage.tsx b/ui/litellm-dashboard/src/components/new_usage.tsx index 4794a7f091d..a8d30885493 100644 --- a/ui/litellm-dashboard/src/components/new_usage.tsx +++ b/ui/litellm-dashboard/src/components/new_usage.tsx @@ -580,7 +580,7 @@ const NewUsagePage: React.FC = ({ {/* Top API Keys */} - Top API Keys + Top Virtual Keys = ({ setApiKey(response["key"]); setSoftBudget(response["soft_budget"]); - NotificationsManager.success("API Key Created"); + NotificationsManager.success("Virtual Key Created"); form.resetFields(); localStorage.removeItem("userData" + userID); } catch (error) { @@ -415,7 +415,7 @@ const CreateKey: React.FC = ({ }; const handleCopy = () => { - NotificationsManager.success("API Key copied to clipboard"); + NotificationsManager.success("Virtual Key copied to clipboard"); }; useEffect(() => { @@ -505,7 +505,7 @@ const CreateKey: React.FC = ({ label={ Owned By{" "} - + @@ -594,8 +594,8 @@ const CreateKey: React.FC = ({ {isFormDisabled && (
    - Please select a team to continue configuring your API key. If you do not see any teams, please contact - your Proxy Admin to either provide you with access to models or to add you to a team. + Please select a team to continue configuring your Virtual Key. If you do not see any teams, please + contact your Proxy Admin to either provide you with access to models or to add you to a team.
    )} @@ -1277,7 +1277,7 @@ const CreateKey: React.FC = ({ {apiKey != null ? (
    - API Key: + Virtual Key:
    = ({
    - + {/*
    - New API Key: + New Virtual Key:
    {regeneratedKey}
    NotificationManager.success("API Key copied to clipboard")} + onCopy={() => NotificationManager.success("Virtual Key copied to clipboard")} > - +
    diff --git a/ui/litellm-dashboard/src/components/playground/chat_ui/ChatUI.tsx b/ui/litellm-dashboard/src/components/playground/chat_ui/ChatUI.tsx index 90c43df0dc4..c2924c048c3 100644 --- a/ui/litellm-dashboard/src/components/playground/chat_ui/ChatUI.tsx +++ b/ui/litellm-dashboard/src/components/playground/chat_ui/ChatUI.tsx @@ -691,7 +691,7 @@ const ChatUI: React.FC = ({ const effectiveApiKey = apiKeySource === "session" ? accessToken : apiKey; if (!effectiveApiKey) { - NotificationsManager.fromBackend("Please provide an API key or select Current UI Session"); + NotificationsManager.fromBackend("Please provide a Virtual Key or select Current UI Session"); return; } @@ -1003,7 +1003,7 @@ const ChatUI: React.FC = ({
    - API Key Source + Virtual Key Source setApiKeySource(value as "session" | "custom")} @@ -567,7 +567,7 @@ export default function CompareUI({ accessToken, disabledPersonalKeyCreation }: setCustomApiKey(event.target.value)} - placeholder="Enter API key" + placeholder="Enter Virtual Key" className="w-56" /> )} diff --git a/ui/litellm-dashboard/src/components/playground/llm_calls/anthropic_messages.tsx b/ui/litellm-dashboard/src/components/playground/llm_calls/anthropic_messages.tsx index 47941bce2ca..3f8c90424c6 100644 --- a/ui/litellm-dashboard/src/components/playground/llm_calls/anthropic_messages.tsx +++ b/ui/litellm-dashboard/src/components/playground/llm_calls/anthropic_messages.tsx @@ -20,7 +20,7 @@ export async function makeAnthropicMessagesRequest( selectedMCPTools?: string[], ) { if (!accessToken) { - throw new Error("API key is required"); + throw new Error("Virtual Key is required"); } const isLocal = process.env.NODE_ENV === "development"; diff --git a/ui/litellm-dashboard/src/components/playground/llm_calls/embeddings_api.tsx b/ui/litellm-dashboard/src/components/playground/llm_calls/embeddings_api.tsx index 832d29bb852..d0939c00437 100644 --- a/ui/litellm-dashboard/src/components/playground/llm_calls/embeddings_api.tsx +++ b/ui/litellm-dashboard/src/components/playground/llm_calls/embeddings_api.tsx @@ -9,7 +9,7 @@ export async function makeOpenAIEmbeddingsRequest( tags?: string[], ) { if (!accessToken) { - throw new Error("API key is required"); + throw new Error("Virtual Key is required"); } // Base URL should be the current base_url diff --git a/ui/litellm-dashboard/src/components/playground/llm_calls/responses_api.tsx b/ui/litellm-dashboard/src/components/playground/llm_calls/responses_api.tsx index 8461f8e20a0..46b0621a0b1 100644 --- a/ui/litellm-dashboard/src/components/playground/llm_calls/responses_api.tsx +++ b/ui/litellm-dashboard/src/components/playground/llm_calls/responses_api.tsx @@ -24,7 +24,7 @@ export async function makeOpenAIResponsesRequest( onMCPEvent?: (event: MCPEvent) => void, ) { if (!accessToken) { - throw new Error("API key is required"); + throw new Error("Virtual Key is required"); } // Base URL should be the current base_url diff --git a/ui/litellm-dashboard/src/components/templates/KeyInfoView.handleKeyUpdate.test.tsx b/ui/litellm-dashboard/src/components/templates/KeyInfoView.handleKeyUpdate.test.tsx index f58e392a58f..9897bb4d47a 100644 --- a/ui/litellm-dashboard/src/components/templates/KeyInfoView.handleKeyUpdate.test.tsx +++ b/ui/litellm-dashboard/src/components/templates/KeyInfoView.handleKeyUpdate.test.tsx @@ -245,7 +245,7 @@ import KeyInfoView from "./key_info_view"; const baseKeyData = { token_id: "tok_123", token: "tok_123", - key_alias: "My API Key", + key_alias: "My Virtual Key", key_name: "sk-xxxx", created_at: new Date().toISOString(), updated_at: new Date().toISOString(), diff --git a/ui/litellm-dashboard/src/components/templates/key_info_view.tsx b/ui/litellm-dashboard/src/components/templates/key_info_view.tsx index ec2b294d9de..dbbb195a1f8 100644 --- a/ui/litellm-dashboard/src/components/templates/key_info_view.tsx +++ b/ui/litellm-dashboard/src/components/templates/key_info_view.tsx @@ -297,7 +297,7 @@ export default function KeyInfoView({ - {currentKeyData.key_alias || "API Key"} + {currentKeyData.key_alias || "Virtual Key"}
    @@ -381,7 +381,7 @@ export default function KeyInfoView({ {/* Delete Confirmation Modal */} {isDeleteModalOpen && (() => { - const keyName = currentKeyData?.key_alias || currentKeyData?.token_id || "API Key"; + const keyName = currentKeyData?.key_alias || currentKeyData?.token_id || "Virtual Key"; const isValid = deleteConfirmInput === keyName; return (
    @@ -415,7 +415,7 @@ export default function KeyInfoView({

    - Warning: You are about to delete this API key. + Warning: You are about to delete this Virtual Key.

    This action is irreversible and will immediately revoke access for any applications using this @@ -423,7 +423,7 @@ export default function KeyInfoView({

    -

    Are you sure you want to delete this API key?

    +

    Are you sure you want to delete this Virtual Key?

    - Warning: You are about to delete this API key. + Warning: You are about to delete this Virtual Key.

    This action is irreversible and will immediately revoke access for any applications using this @@ -374,7 +374,7 @@ const ViewKeyTable: React.FC = ({

    -

    Are you sure you want to delete this API key?

    +

    Are you sure you want to delete this Virtual Key?

    @@ -417,7 +417,7 @@ const ViewKeyTable: React.FC = ({ {/* Regenerate Key Form Modal */} { setRegenerateDialogVisible(false); @@ -516,7 +516,7 @@ const ViewKeyTable: React.FC = ({ {selectedToken?.key_alias || "No alias set"}
    - New API Key: + New Virtual Key:
    = ({
    NotificationManager.success({ description: "API Key copied to clipboard" })} + onCopy={() => NotificationManager.success({ description: "Virtual Key copied to clipboard" })} > - + diff --git a/ui/litellm-dashboard/src/components/usage.tsx b/ui/litellm-dashboard/src/components/usage.tsx index 88b1e3c3fb7..0900a0a9cc1 100644 --- a/ui/litellm-dashboard/src/components/usage.tsx +++ b/ui/litellm-dashboard/src/components/usage.tsx @@ -615,7 +615,7 @@ const UsagePage: React.FC = ({ accessToken, token, userRole, use - Top API Keys + Top Virtual Keys ( {row.original.key_count > 0 ? ( - {row.original.key_count} Keys + {row.original.key_count} {row.original.key_count === 1 ? "Key" : "Keys"} ) : ( diff --git a/ui/litellm-dashboard/src/components/view_users/user_info_view.tsx b/ui/litellm-dashboard/src/components/view_users/user_info_view.tsx index 456f07d1882..2caae7d861f 100644 --- a/ui/litellm-dashboard/src/components/view_users/user_info_view.tsx +++ b/ui/litellm-dashboard/src/components/view_users/user_info_view.tsx @@ -320,9 +320,11 @@ export default function UserInfoView({ - API Keys + Virtual Keys
    - {userData.keys?.length || 0} keys + + {userData.keys?.length || 0} {userData.keys?.length === 1 ? "Key" : "Keys"} +
    @@ -467,7 +469,7 @@ export default function UserInfoView({
    - API Keys + Virtual Keys
    {userData.keys?.length && userData.keys?.length > 0 ? ( userData.keys.map((key, index) => ( @@ -476,7 +478,7 @@ export default function UserInfoView({ )) ) : ( - No API keys + No Virtual Keys )}
    From 24f847b84c947e13b765edaf172f646fd1b06297 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Mon, 1 Dec 2025 13:59:00 -0800 Subject: [PATCH 070/370] [Feat] JWT Auth - AI Gateway, allow using regular OIDC flow with user info endpoints (#17324) * feat: allow fetching OIDC user info * test: use test_auth_builder_with_oidc_userinfo_enabled gets user info when enabled * fix tool permission doc * docs fix diagram --- .../docs/proxy/guardrails/tool_permission.md | 5 - docs/my-website/docs/proxy/token_auth.md | 66 +++++ litellm/proxy/_types.py | 18 ++ litellm/proxy/auth/handle_jwt.py | 76 +++++- .../proxy/auth/test_handle_jwt.py | 227 +++++++++++++++++- 5 files changed, 385 insertions(+), 7 deletions(-) diff --git a/docs/my-website/docs/proxy/guardrails/tool_permission.md b/docs/my-website/docs/proxy/guardrails/tool_permission.md index 897c31d9dab..1827333654f 100644 --- a/docs/my-website/docs/proxy/guardrails/tool_permission.md +++ b/docs/my-website/docs/proxy/guardrails/tool_permission.md @@ -1,4 +1,3 @@ -import Image from '@theme/IdealImage'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; @@ -14,8 +13,6 @@ LiteLLM provides the LiteLLM Tool Permission Guardrail that lets you control whi Open the LiteLLM Dashboard, click **Add New Guardrail**, and choose **LiteLLM Tool Permission Guardrail**. This loads the rule builder UI. -Configure tool permission guardrail in LiteLLM UI - #### Step 2: Define Regex Rules 1. Click **Add Rule**. @@ -24,8 +21,6 @@ Open the LiteLLM Dashboard, click **Add New Guardrail**, and choose **LiteLLM To 4. Optionally add a regex for tool type (e.g., `^function$`). 5. Pick **Allow** or **Deny**. -Configure tool permission guardrail in LiteLLM UI - #### Step 3: Restrict Tool Arguments (Optional) Select **+ Restrict tool arguments** to attach regex validations to nested paths (dot + `[]` notation). This enforces that sensitive parameters (such as `arguments.to[]`) conform to pre-approved formats. diff --git a/docs/my-website/docs/proxy/token_auth.md b/docs/my-website/docs/proxy/token_auth.md index c2a88010d79..c465c1022e4 100644 --- a/docs/my-website/docs/proxy/token_auth.md +++ b/docs/my-website/docs/proxy/token_auth.md @@ -407,6 +407,72 @@ general_settings: user_id_upsert: true # šŸ‘ˆ upserts the user to db, if valid email but not in db ``` +## OIDC UserInfo Endpoint + +Use this when your JWT/access token doesn't contain user-identifying information. LiteLLM will call your identity provider's UserInfo endpoint to fetch user details. + +### When to Use + +- Your JWT is opaque (not self-contained) or lacks user claims +- You need to fetch fresh user information from your identity provider +- Your access tokens don't include email, roles, or other identifying data + +### Configuration + +```yaml title="config.yaml" showLineNumbers +general_settings: + enable_jwt_auth: True + litellm_jwtauth: + # Enable OIDC UserInfo endpoint + oidc_userinfo_enabled: true + oidc_userinfo_endpoint: "https://your-idp.com/oauth2/userinfo" + oidc_userinfo_cache_ttl: 300 # Cache for 5 minutes (default: 300) + + # Map fields from UserInfo response + user_id_jwt_field: "sub" + user_email_jwt_field: "email" + user_roles_jwt_field: "roles" +``` + +### Flow Diagram + +```mermaid +sequenceDiagram + participant Client + participant LiteLLM + participant IdP as Identity Provider + + Client->>LiteLLM: Request with Bearer token + Note over LiteLLM: Check cache for UserInfo + + LiteLLM->>IdP: GET /userinfo (if not cached)
    Authorization: Bearer {token} + IdP-->>LiteLLM: User data (sub, email, roles) + + Note over LiteLLM: Cache response (TTL: 5min)
    Extract user_id, email, roles
    Perform RBAC checks + + LiteLLM-->>Client: Authorized/Denied +``` + +### Example: Azure AD + +```yaml title="config.yaml" showLineNumbers +litellm_jwtauth: + oidc_userinfo_enabled: true + oidc_userinfo_endpoint: "https://graph.microsoft.com/oidc/userinfo" + user_id_jwt_field: "sub" + user_email_jwt_field: "email" +``` + +### Example: Keycloak + +```yaml title="config.yaml" showLineNumbers +litellm_jwtauth: + oidc_userinfo_enabled: true + oidc_userinfo_endpoint: "https://keycloak.example.com/realms/your-realm/protocol/openid-connect/userinfo" + user_id_jwt_field: "sub" + user_roles_jwt_field: "resource_access.your-client.roles" +``` + ## [BETA] Control Access with OIDC Roles Allow JWT tokens with supported roles to access the proxy. diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index fe87a70b244..b5b0bd80602 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -3422,6 +3422,9 @@ class LiteLLM_JWTAuth(LiteLLMPydanticObjectBase): - public_allowed_routes: list of allowed routes for authenticated but unknown litellm role jwt tokens. - enforce_rbac: If true, enforce RBAC for all routes. - custom_validate: A custom function to validates the JWT token. + - oidc_userinfo_endpoint: OIDC UserInfo endpoint URL. When set along with oidc_userinfo_enabled, LiteLLM will call this endpoint with the access token to retrieve user identity information. + - oidc_userinfo_enabled: Enable fetching user info from OIDC UserInfo endpoint instead of just decoding JWT token. Default: False. + - oidc_userinfo_cache_ttl: TTL (in seconds) for caching UserInfo responses. Default: 300s (5 minutes). See `auth_checks.py` for the specific routes """ @@ -3472,6 +3475,21 @@ class LiteLLM_JWTAuth(LiteLLMPydanticObjectBase): jwt_litellm_role_map: Optional[List[JWTLiteLLMRoleMap]] = None sync_user_role_and_teams: bool = False ######################################################### + ######################################################### + # OIDC UserInfo Endpoint Configuration + oidc_userinfo_endpoint: Optional[str] = Field( + default=None, + description="OIDC UserInfo endpoint URL. If set, LiteLLM will call this endpoint with the access token to retrieve user identity information.", + ) + oidc_userinfo_enabled: bool = Field( + default=False, + description="Enable fetching user info from OIDC UserInfo endpoint instead of just decoding JWT token.", + ) + oidc_userinfo_cache_ttl: float = Field( + default=300, + description="TTL (in seconds) for caching UserInfo responses. Default: 300s (5 minutes).", + ) + ######################################################### def __init__(self, **kwargs: Any) -> None: # get the attribute names for this Pydantic model diff --git a/litellm/proxy/auth/handle_jwt.py b/litellm/proxy/auth/handle_jwt.py index 3e18db2d025..ed6877d1469 100644 --- a/litellm/proxy/auth/handle_jwt.py +++ b/litellm/proxy/auth/handle_jwt.py @@ -480,6 +480,71 @@ class JWTHandler: else: return False + async def get_oidc_userinfo(self, token: str) -> dict: + """ + Fetch user information from OIDC UserInfo endpoint. + + This follows the OpenID Connect protocol where an access token + is sent to the identity provider's UserInfo endpoint to retrieve + user identity information. + + Args: + token: The access token to use for authentication + + Returns: + dict: User information from the UserInfo endpoint + + Raises: + Exception: If UserInfo endpoint is not configured or request fails + """ + if not self.litellm_jwtauth.oidc_userinfo_endpoint: + raise Exception( + "OIDC UserInfo endpoint not configured. Set 'oidc_userinfo_endpoint' in JWT auth config." + ) + + # Check cache first + cache_key = f"oidc_userinfo_{token[:20]}" # Use first 20 chars of token as cache key + cached_userinfo = await self.user_api_key_cache.async_get_cache(cache_key) + + if cached_userinfo is not None: + verbose_proxy_logger.debug("Returning cached OIDC UserInfo") + return cached_userinfo + + verbose_proxy_logger.debug( + f"Calling OIDC UserInfo endpoint: {self.litellm_jwtauth.oidc_userinfo_endpoint}" + ) + + try: + # Call the UserInfo endpoint with the access token + response = await self.http_handler.get( + url=self.litellm_jwtauth.oidc_userinfo_endpoint, + headers={ + "Authorization": f"Bearer {token}", + "Accept": "application/json", + }, + ) + + if response.status_code != 200: + raise Exception( + f"OIDC UserInfo endpoint returned status {response.status_code}: {response.text}" + ) + + userinfo = response.json() + verbose_proxy_logger.debug(f"Received OIDC UserInfo: {userinfo}") + + # Cache the userinfo response + await self.user_api_key_cache.async_set_cache( + key=cache_key, + value=userinfo, + ttl=self.litellm_jwtauth.oidc_userinfo_cache_ttl, + ) + + return userinfo + + except Exception as e: + verbose_proxy_logger.error(f"Error fetching OIDC UserInfo: {str(e)}") + raise Exception(f"Failed to fetch OIDC UserInfo: {str(e)}") + async def auth_jwt(self, token: str) -> dict: # Supported algos: https://pyjwt.readthedocs.io/en/stable/algorithms.html # "Warning: Make sure not to mix symmetric and asymmetric algorithms that interpret @@ -1077,7 +1142,16 @@ class JWTAuthManager: proxy_logging_obj: ProxyLogging, ) -> JWTAuthBuilderResult: """Main authentication and authorization builder""" - jwt_valid_token: dict = await jwt_handler.auth_jwt(token=api_key) + # Check if OIDC UserInfo endpoint is enabled + if jwt_handler.litellm_jwtauth.oidc_userinfo_enabled: + verbose_proxy_logger.debug( + "OIDC UserInfo is enabled. Fetching user info from UserInfo endpoint." + ) + # Use the access token to fetch user info from OIDC UserInfo endpoint + jwt_valid_token: dict = await jwt_handler.get_oidc_userinfo(token=api_key) + else: + # Default behavior: decode and validate the JWT token + jwt_valid_token = await jwt_handler.auth_jwt(token=api_key) # Check custom validate if jwt_handler.litellm_jwtauth.custom_validate: diff --git a/tests/test_litellm/proxy/auth/test_handle_jwt.py b/tests/test_litellm/proxy/auth/test_handle_jwt.py index 8f8f3ced074..603a6928f88 100644 --- a/tests/test_litellm/proxy/auth/test_handle_jwt.py +++ b/tests/test_litellm/proxy/auth/test_handle_jwt.py @@ -846,4 +846,229 @@ async def test_auth_builder_returns_team_membership_object(): assert result["team_membership"].user_id == _user_id, "team_membership user_id should match" assert result["team_membership"].team_id == _team_id, "team_membership team_id should match" assert result["team_membership"].budget_id == "budget_123", "team_membership budget_id should match" - assert result["team_membership"].spend == 10.5, "team_membership spend should match" \ No newline at end of file + assert result["team_membership"].spend == 10.5, "team_membership spend should match" + + +@pytest.mark.asyncio +async def test_auth_builder_with_oidc_userinfo_enabled(): + """Test that auth_builder uses OIDC UserInfo endpoint when enabled""" + from unittest.mock import MagicMock + + from litellm.caching import DualCache + from litellm.proxy.utils import ProxyLogging + + # Setup test data + api_key = "test_access_token" + request_data = {"model": "gpt-4"} + general_settings = {"enforce_rbac": False} + route = "/chat/completions" + + user_object = LiteLLM_UserTable( + user_id="test_user_1", user_role=LitellmUserRoles.INTERNAL_USER + ) + + # Create JWT handler with OIDC UserInfo enabled + jwt_handler = JWTHandler() + user_api_key_cache = DualCache() + proxy_logging_obj = ProxyLogging(user_api_key_cache=user_api_key_cache) + + jwt_handler.update_environment( + prisma_client=None, + user_api_key_cache=user_api_key_cache, + litellm_jwtauth=LiteLLM_JWTAuth( + oidc_userinfo_enabled=True, + oidc_userinfo_endpoint="https://example.com/oauth2/userinfo", + user_id_jwt_field="sub", + user_email_jwt_field="email", + ), + ) + + # Mock OIDC UserInfo response + userinfo_response = { + "sub": "test_user_1", + "email": "test@example.com", + "scope": "", + } + + # Mock all the dependencies + with patch.object( + jwt_handler, "get_oidc_userinfo", new_callable=AsyncMock + ) as mock_get_userinfo, patch.object( + jwt_handler, "auth_jwt", new_callable=AsyncMock + ) as mock_auth_jwt, patch.object( + JWTAuthManager, "check_rbac_role", new_callable=AsyncMock + ) as mock_check_rbac, patch.object( + jwt_handler, "get_rbac_role", return_value=None + ) as mock_get_rbac, patch.object( + jwt_handler, "get_scopes", return_value=[] + ) as mock_get_scopes, patch.object( + jwt_handler, "get_object_id", return_value=None + ) as mock_get_object_id, patch.object( + JWTAuthManager, + "get_user_info", + new_callable=AsyncMock, + return_value=("test_user_1", "test@example.com", True), + ) as mock_get_user_info, patch.object( + jwt_handler, "get_org_id", return_value=None + ) as mock_get_org_id, patch.object( + jwt_handler, "get_end_user_id", return_value=None + ) as mock_get_end_user_id, patch.object( + JWTAuthManager, "check_admin_access", new_callable=AsyncMock, return_value=None + ) as mock_check_admin, patch.object( + JWTAuthManager, + "find_and_validate_specific_team_id", + new_callable=AsyncMock, + return_value=(None, None), + ) as mock_find_team, patch.object( + JWTAuthManager, "get_all_team_ids", return_value=set() + ) as mock_get_all_team_ids, patch.object( + JWTAuthManager, + "find_team_with_model_access", + new_callable=AsyncMock, + return_value=(None, None), + ) as mock_find_team_access, patch.object( + JWTAuthManager, + "get_objects", + new_callable=AsyncMock, + return_value=(user_object, None, None, None), + ) as mock_get_objects, patch.object( + JWTAuthManager, "map_user_to_teams", new_callable=AsyncMock + ) as mock_map_user, patch.object( + JWTAuthManager, "validate_object_id", return_value=True + ) as mock_validate_object, patch.object( + JWTAuthManager, "sync_user_role_and_teams", new_callable=AsyncMock + ) as mock_sync_user: + # Set up mock return values + mock_get_userinfo.return_value = userinfo_response + + # Call auth_builder + result = await JWTAuthManager.auth_builder( + api_key=api_key, + jwt_handler=jwt_handler, + request_data=request_data, + general_settings=general_settings, + route=route, + prisma_client=None, + user_api_key_cache=user_api_key_cache, + parent_otel_span=None, + proxy_logging_obj=proxy_logging_obj, + ) + + # Verify that get_oidc_userinfo was called instead of auth_jwt + mock_get_userinfo.assert_called_once_with(token=api_key) + mock_auth_jwt.assert_not_called() # Should not be called when OIDC is enabled + + # Verify the result + assert result["user_id"] == "test_user_1" + assert result["user_object"] == user_object + + +@pytest.mark.asyncio +async def test_auth_builder_with_oidc_userinfo_disabled(): + """Test that auth_builder uses JWT validation when OIDC UserInfo is disabled""" + from unittest.mock import MagicMock + + from litellm.caching import DualCache + from litellm.proxy.utils import ProxyLogging + + # Setup test data + api_key = "test_jwt_token" + request_data = {"model": "gpt-4"} + general_settings = {"enforce_rbac": False} + route = "/chat/completions" + + user_object = LiteLLM_UserTable( + user_id="test_user_1", user_role=LitellmUserRoles.INTERNAL_USER + ) + + # Create JWT handler with OIDC UserInfo disabled + jwt_handler = JWTHandler() + user_api_key_cache = DualCache() + proxy_logging_obj = ProxyLogging(user_api_key_cache=user_api_key_cache) + + jwt_handler.update_environment( + prisma_client=None, + user_api_key_cache=user_api_key_cache, + litellm_jwtauth=LiteLLM_JWTAuth( + oidc_userinfo_enabled=False, # Disabled + user_id_jwt_field="sub", + ), + ) + + # Mock JWT validation response + jwt_response = { + "sub": "test_user_1", + "scope": "", + } + + # Mock all the dependencies + with patch.object( + jwt_handler, "get_oidc_userinfo", new_callable=AsyncMock + ) as mock_get_userinfo, patch.object( + jwt_handler, "auth_jwt", new_callable=AsyncMock + ) as mock_auth_jwt, patch.object( + JWTAuthManager, "check_rbac_role", new_callable=AsyncMock + ) as mock_check_rbac, patch.object( + jwt_handler, "get_rbac_role", return_value=None + ) as mock_get_rbac, patch.object( + jwt_handler, "get_scopes", return_value=[] + ) as mock_get_scopes, patch.object( + jwt_handler, "get_object_id", return_value=None + ) as mock_get_object_id, patch.object( + JWTAuthManager, + "get_user_info", + new_callable=AsyncMock, + return_value=("test_user_1", None, None), + ) as mock_get_user_info, patch.object( + jwt_handler, "get_org_id", return_value=None + ) as mock_get_org_id, patch.object( + jwt_handler, "get_end_user_id", return_value=None + ) as mock_get_end_user_id, patch.object( + JWTAuthManager, "check_admin_access", new_callable=AsyncMock, return_value=None + ) as mock_check_admin, patch.object( + JWTAuthManager, + "find_and_validate_specific_team_id", + new_callable=AsyncMock, + return_value=(None, None), + ) as mock_find_team, patch.object( + JWTAuthManager, "get_all_team_ids", return_value=set() + ) as mock_get_all_team_ids, patch.object( + JWTAuthManager, + "find_team_with_model_access", + new_callable=AsyncMock, + return_value=(None, None), + ) as mock_find_team_access, patch.object( + JWTAuthManager, + "get_objects", + new_callable=AsyncMock, + return_value=(user_object, None, None, None), + ) as mock_get_objects, patch.object( + JWTAuthManager, "map_user_to_teams", new_callable=AsyncMock + ) as mock_map_user, patch.object( + JWTAuthManager, "validate_object_id", return_value=True + ) as mock_validate_object, patch.object( + JWTAuthManager, "sync_user_role_and_teams", new_callable=AsyncMock + ) as mock_sync_user: + # Set up mock return values + mock_auth_jwt.return_value = jwt_response + + # Call auth_builder + result = await JWTAuthManager.auth_builder( + api_key=api_key, + jwt_handler=jwt_handler, + request_data=request_data, + general_settings=general_settings, + route=route, + prisma_client=None, + user_api_key_cache=user_api_key_cache, + parent_otel_span=None, + proxy_logging_obj=proxy_logging_obj, + ) + + # Verify that auth_jwt was called instead of get_oidc_userinfo + mock_auth_jwt.assert_called_once_with(token=api_key) + mock_get_userinfo.assert_not_called() # Should not be called when OIDC is disabled + + # Verify the result + assert result["user_id"] == "test_user_1" + assert result["user_object"] == user_object \ No newline at end of file From 7a46f3a0830c1f8a5635a6635f02dc7556ce0d30 Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Mon, 1 Dec 2025 14:05:54 -0800 Subject: [PATCH 071/370] docs: document azure ai provider for anthropic --- .../blog/anthropic_opus_4_5_and_advanced_features/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/my-website/blog/anthropic_opus_4_5_and_advanced_features/index.md b/docs/my-website/blog/anthropic_opus_4_5_and_advanced_features/index.md index be2c0b5dc5d..1e5f968b2ca 100644 --- a/docs/my-website/blog/anthropic_opus_4_5_and_advanced_features/index.md +++ b/docs/my-website/blog/anthropic_opus_4_5_and_advanced_features/index.md @@ -33,7 +33,7 @@ This guide covers Anthropic's latest model (Claude Opus 4.5) and its advanced fe | Input Examples | Claude Opus 4.5, Sonnet 4.5 | | Effort Parameter | Claude Opus 4.5 only | -Supported Providers: [Anthropic](../../docs/providers/anthropic), [Bedrock](../../docs/providers/bedrock), [Vertex AI](../../docs/providers/vertex_partner#vertex-ai---anthropic-claude). +Supported Providers: [Anthropic](../../docs/providers/anthropic), [Bedrock](../../docs/providers/bedrock), [Vertex AI](../../docs/providers/vertex_partner#vertex-ai---anthropic-claude), [Azure AI](../../docs/providers/azure_ai). ## Usage From c9afb869940ae2de27846ed5263ab27a4461d2b7 Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Mon, 1 Dec 2025 14:06:31 -0800 Subject: [PATCH 072/370] docs(azure_ai.md): document anthropic model usage on azure ai --- docs/my-website/docs/providers/azure_ai.md | 79 +++++++++++++++++++++- 1 file changed, 78 insertions(+), 1 deletion(-) diff --git a/docs/my-website/docs/providers/azure_ai.md b/docs/my-website/docs/providers/azure_ai.md index b1b5de5bb34..68e2df676e6 100644 --- a/docs/my-website/docs/providers/azure_ai.md +++ b/docs/my-website/docs/providers/azure_ai.md @@ -312,6 +312,82 @@ LiteLLM supports **ALL** azure ai models. Here's a few examples: | mistral-large-latest | `completion(model="azure_ai/mistral-large-latest", messages)` | | AI21-Jamba-Instruct | `completion(model="azure_ai/ai21-jamba-instruct", messages)` | +## Usage - Azure Anthropic (Azure Foundry Claude) + +LiteLLM funnels Azure Claude deployments through the `azure_ai/` provider so Claude Opus models on Azure Foundry keep working with Tool Search, Effort, streaming, and the rest of the advanced feature set. Point `AZURE_AI_API_BASE` to `https://.services.ai.azure.com/anthropic` (LiteLLM appends `/v1/messages` automatically) and authenticate with `AZURE_AI_API_KEY` or an Azure AD token. + + + + +```python +import os +from litellm import completion + +# Configure Azure credentials +os.environ["AZURE_AI_API_KEY"] = "your-azure-ai-api-key" +os.environ["AZURE_AI_API_BASE"] = "https://my-resource.services.ai.azure.com/anthropic" + +response = completion( + model="azure_ai/claude-opus-4-1", + messages=[{"role": "user", "content": "Explain how Azure Anthropic hosts Claude Opus differently from the public Anthropic API."}], + max_tokens=1200, + temperature=0.7, + stream=True, +) + +for chunk in response: + if chunk.choices[0].delta.content: + print(chunk.choices[0].delta.content, end="", flush=True) +``` + + + + +**1. Set environment variables** + +```bash +export AZURE_AI_API_KEY="your-azure-ai-api-key" +export AZURE_AI_API_BASE="https://my-resource.services.ai.azure.com/anthropic" +``` + +**2. Configure the proxy** + +```yaml +model_list: + - model_name: claude-4-azure + litellm_params: + model: azure_ai/claude-opus-4-1 + api_key: os.environ/AZURE_AI_API_KEY + api_base: os.environ/AZURE_AI_API_BASE +``` + +**3. Start LiteLLM** + +```bash +litellm --config /path/to/config.yaml +``` + +**4. Test the Azure Claude route** + +```bash +curl --location 'http://0.0.0.0:4000/chat/completions' \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Bearer $LITELLM_KEY' \ + --data '{ + "model": "claude-4-azure", + "messages": [ + { + "role": "user", + "content": "How do I use Claude Opus 4 via Azure Anthropic in LiteLLM?" + } + ], + "max_tokens": 1024 + }' +``` + + + + ## Rerank Endpoint @@ -397,4 +473,5 @@ curl http://0.0.0.0:4000/rerank \ ``` - \ No newline at end of file + + From f434ca61ec0c637ba676fb90336ff154a992793b Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Mon, 1 Dec 2025 14:14:41 -0800 Subject: [PATCH 073/370] add kimi-k2-instruct-0905 (#17328) --- litellm/model_prices_and_context_window_backup.json | 13 +++++++++++++ model_prices_and_context_window.json | 13 +++++++++++++ 2 files changed, 26 insertions(+) diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index f4f6b94fd18..af63d1e2592 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -10198,6 +10198,19 @@ "supports_response_schema": true, "supports_tool_choice": true }, + "fireworks_ai/accounts/fireworks/models/kimi-k2-instruct-0905": { + "input_cost_per_token": 6e-07, + "litellm_provider": "fireworks_ai", + "max_input_tokens": 262144, + "max_output_tokens": 32768, + "max_tokens": 262144, + "mode": "chat", + "output_cost_per_token": 2.5e-06, + "source": "https://app.fireworks.ai/models/fireworks/kimi-k2-instruct-0905", + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, "fireworks_ai/accounts/fireworks/models/kimi-k2-thinking": { "input_cost_per_token": 6e-07, "litellm_provider": "fireworks_ai", diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index f4f6b94fd18..af63d1e2592 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -10198,6 +10198,19 @@ "supports_response_schema": true, "supports_tool_choice": true }, + "fireworks_ai/accounts/fireworks/models/kimi-k2-instruct-0905": { + "input_cost_per_token": 6e-07, + "litellm_provider": "fireworks_ai", + "max_input_tokens": 262144, + "max_output_tokens": 32768, + "max_tokens": 262144, + "mode": "chat", + "output_cost_per_token": 2.5e-06, + "source": "https://app.fireworks.ai/models/fireworks/kimi-k2-instruct-0905", + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, "fireworks_ai/accounts/fireworks/models/kimi-k2-thinking": { "input_cost_per_token": 6e-07, "litellm_provider": "fireworks_ai", From b6d6f834e059e1cb0d9062f99189c4f521fe3e1e Mon Sep 17 00:00:00 2001 From: Krish Dholakia Date: Mon, 1 Dec 2025 14:29:52 -0800 Subject: [PATCH 074/370] (feat) Generic Guardrail API - allows guardrail providers to add INSTANT support for LiteLLM w/out PR to repo (#17175) * feat(generic_guardrail_api.py): new generic api for guardrails Allows guardrail providers to work with litellm for guardrails without needing to make a PR to LiteLLM * docs(generic_guardrail_api.md): document new generic guardrail api * Fix: Improve PII detection and guardrail API integration Co-authored-by: krrishdholakia * feat: correctly extract raw request from guardrail api * docs(generic_guardrail_api.md): document this is a beta feature --------- Co-authored-by: Cursor Agent --- .../mock_bedrock_guardrail_server.py | 564 ++++++++++++++++++ .../adding_provider/generic_guardrail_api.md | 160 +++++ docs/my-website/sidebars.js | 1 + litellm/proxy/_new_secret_config.yaml | 2 +- .../generic_guardrail_api/__init__.py | 37 ++ .../generic_guardrail_api/example_config.yaml | 52 ++ .../generic_guardrail_api.py | 235 ++++++++ litellm/types/guardrails.py | 11 +- .../guardrail_hooks/generic_guardrail_api.py | 29 + 9 files changed, 1089 insertions(+), 2 deletions(-) create mode 100644 cookbook/mock_guardrail_server/mock_bedrock_guardrail_server.py create mode 100644 docs/my-website/docs/adding_provider/generic_guardrail_api.md create mode 100644 litellm/proxy/guardrails/guardrail_hooks/generic_guardrail_api/__init__.py create mode 100644 litellm/proxy/guardrails/guardrail_hooks/generic_guardrail_api/example_config.yaml create mode 100644 litellm/proxy/guardrails/guardrail_hooks/generic_guardrail_api/generic_guardrail_api.py create mode 100644 litellm/types/proxy/guardrails/guardrail_hooks/generic_guardrail_api.py diff --git a/cookbook/mock_guardrail_server/mock_bedrock_guardrail_server.py b/cookbook/mock_guardrail_server/mock_bedrock_guardrail_server.py new file mode 100644 index 00000000000..9cfbb11feb5 --- /dev/null +++ b/cookbook/mock_guardrail_server/mock_bedrock_guardrail_server.py @@ -0,0 +1,564 @@ +#!/usr/bin/env python3 +""" +Mock Bedrock Guardrail API Server + +This is a FastAPI server that mimics the AWS Bedrock Guardrail API for testing purposes. +It follows the same API spec as the real Bedrock guardrail endpoint. + +Usage: + python mock_bedrock_guardrail_server.py + +The server will start on http://localhost:8080 +""" + +import os +import re +from typing import Any, Dict, List, Literal, Optional + +from fastapi import Depends, FastAPI, Header, HTTPException, status +from fastapi.responses import JSONResponse +from pydantic import BaseModel, Field + +# ============================================================================ +# Request/Response Models (matching Bedrock API spec) +# ============================================================================ + + +class BedrockTextContent(BaseModel): + text: str + + +class BedrockContentItem(BaseModel): + text: BedrockTextContent + + +class BedrockRequest(BaseModel): + source: Literal["INPUT", "OUTPUT"] + content: List[BedrockContentItem] = Field(default_factory=list) + + +class BedrockGuardrailOutput(BaseModel): + text: Optional[str] = None + + +class TopicPolicyItem(BaseModel): + name: str + type: str + action: Literal["BLOCKED", "NONE"] + + +class TopicPolicy(BaseModel): + topics: List[TopicPolicyItem] = Field(default_factory=list) + + +class ContentFilterItem(BaseModel): + type: str + confidence: str + action: Literal["BLOCKED", "NONE"] + + +class ContentPolicy(BaseModel): + filters: List[ContentFilterItem] = Field(default_factory=list) + + +class CustomWord(BaseModel): + match: str + action: Literal["BLOCKED", "NONE"] + + +class WordPolicy(BaseModel): + customWords: List[CustomWord] = Field(default_factory=list) + managedWordLists: List[Dict[str, Any]] = Field(default_factory=list) + + +class PiiEntity(BaseModel): + type: str + match: str + action: Literal["BLOCKED", "ANONYMIZED", "NONE"] + + +class RegexMatch(BaseModel): + name: str + match: str + regex: str + action: Literal["BLOCKED", "ANONYMIZED", "NONE"] + + +class SensitiveInformationPolicy(BaseModel): + piiEntities: List[PiiEntity] = Field(default_factory=list) + regexes: List[RegexMatch] = Field(default_factory=list) + + +class ContextualGroundingFilter(BaseModel): + type: str + threshold: float + score: float + action: Literal["BLOCKED", "NONE"] + + +class ContextualGroundingPolicy(BaseModel): + filters: List[ContextualGroundingFilter] = Field(default_factory=list) + + +class Assessment(BaseModel): + topicPolicy: Optional[TopicPolicy] = None + contentPolicy: Optional[ContentPolicy] = None + wordPolicy: Optional[WordPolicy] = None + sensitiveInformationPolicy: Optional[SensitiveInformationPolicy] = None + contextualGroundingPolicy: Optional[ContextualGroundingPolicy] = None + + +class BedrockGuardrailResponse(BaseModel): + usage: Dict[str, int] = Field( + default_factory=lambda: {"topicPolicyUnits": 1, "contentPolicyUnits": 1} + ) + action: Literal["NONE", "GUARDRAIL_INTERVENED"] = "NONE" + outputs: List[BedrockGuardrailOutput] = Field(default_factory=list) + assessments: List[Assessment] = Field(default_factory=list) + + +# ============================================================================ +# Mock Guardrail Configuration +# ============================================================================ + + +class GuardrailConfig(BaseModel): + """Configuration for mock guardrail behavior""" + + blocked_words: List[str] = Field( + default_factory=lambda: ["offensive", "inappropriate", "badword"] + ) + blocked_topics: List[str] = Field(default_factory=lambda: ["violence", "illegal"]) + pii_patterns: Dict[str, str] = Field( + default_factory=lambda: { + "EMAIL": r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b", + "PHONE": r"\b\d{3}[-.]?\d{3}[-.]?\d{4}\b", + "SSN": r"\b\d{3}-\d{2}-\d{4}\b", + "CREDIT_CARD": r"\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b", + } + ) + anonymize_pii: bool = True # If True, ANONYMIZE PII; if False, BLOCK it + bearer_token: str = "mock-bedrock-token-12345" + + +# Global config +GUARDRAIL_CONFIG = GuardrailConfig() + +# ============================================================================ +# FastAPI App Setup +# ============================================================================ + +app = FastAPI( + title="Mock Bedrock Guardrail API", + description="Mock server mimicking AWS Bedrock Guardrail API", + version="1.0.0", +) + + +# ============================================================================ +# Authentication +# ============================================================================ + + +async def verify_bearer_token(authorization: Optional[str] = Header(None)) -> str: + """ + Verify the Bearer token from the Authorization header. + + Args: + authorization: The Authorization header value + + Returns: + The token if valid + + Raises: + HTTPException: If token is missing or invalid + """ + if authorization is None: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Missing Authorization header", + headers={"WWW-Authenticate": "Bearer"}, + ) + + # Check if it's a Bearer token + parts = authorization.split() + print(f"parts: {parts}") + if len(parts) != 2 or parts[0].lower() != "bearer": + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid Authorization header format. Expected: Bearer ", + headers={"WWW-Authenticate": "Bearer"}, + ) + + token = parts[1] + + # Verify token + if token != GUARDRAIL_CONFIG.bearer_token: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Invalid bearer token", + ) + + return token + + +# ============================================================================ +# Guardrail Logic +# ============================================================================ + + +def check_blocked_words(text: str) -> Optional[WordPolicy]: + """Check if text contains blocked words""" + found_words = [] + text_lower = text.lower() + + for word in GUARDRAIL_CONFIG.blocked_words: + if word.lower() in text_lower: + found_words.append(CustomWord(match=word, action="BLOCKED")) + + if found_words: + return WordPolicy(customWords=found_words) + return None + + +def check_blocked_topics(text: str) -> Optional[TopicPolicy]: + """Check if text contains blocked topics""" + found_topics = [] + text_lower = text.lower() + + for topic in GUARDRAIL_CONFIG.blocked_topics: + if topic.lower() in text_lower: + found_topics.append( + TopicPolicyItem(name=topic, type=topic.upper(), action="BLOCKED") + ) + + if found_topics: + return TopicPolicy(topics=found_topics) + return None + + +def check_pii(text: str) -> tuple[Optional[SensitiveInformationPolicy], str]: + """ + Check for PII in text and return policy + anonymized text + + Returns: + Tuple of (SensitiveInformationPolicy or None, anonymized_text) + """ + pii_entities = [] + anonymized_text = text + action = "ANONYMIZED" if GUARDRAIL_CONFIG.anonymize_pii else "BLOCKED" + + for pii_type, pattern in GUARDRAIL_CONFIG.pii_patterns.items(): + try: + # Compile the regex pattern with a timeout to prevent ReDoS attacks + compiled_pattern = re.compile(pattern) + matches = compiled_pattern.finditer(text) + for match in matches: + matched_text = match.group() + pii_entities.append( + PiiEntity(type=pii_type, match=matched_text, action=action) + ) + + # Anonymize the text if configured + if GUARDRAIL_CONFIG.anonymize_pii: + anonymized_text = anonymized_text.replace( + matched_text, f"[{pii_type}_REDACTED]" + ) + except re.error: + # Invalid regex pattern - skip it and log a warning + print(f"Warning: Invalid regex pattern for PII type {pii_type}: {pattern}") + continue + + if pii_entities: + return SensitiveInformationPolicy(piiEntities=pii_entities), anonymized_text + + return None, text + + +def process_guardrail_request( + request: BedrockRequest, +) -> tuple[BedrockGuardrailResponse, List[str]]: + """ + Process a guardrail request and return the response. + + Returns: + Tuple of (response, list of output texts) + """ + all_text_content = [] + output_texts = [] + + # Extract all text from content items + for content_item in request.content: + if content_item.text and content_item.text.text: + all_text_content.append(content_item.text.text) + + # Combine all text for analysis + combined_text = " ".join(all_text_content) + + # Initialize response + response = BedrockGuardrailResponse() + assessment = Assessment() + has_intervention = False + + # Check for blocked words + word_policy = check_blocked_words(combined_text) + if word_policy: + assessment.wordPolicy = word_policy + has_intervention = True + + # Check for blocked topics + topic_policy = check_blocked_topics(combined_text) + if topic_policy: + assessment.topicPolicy = topic_policy + has_intervention = True + + # Check for PII + for text in all_text_content: + pii_policy, anonymized_text = check_pii(text) + if pii_policy: + assessment.sensitiveInformationPolicy = pii_policy + if GUARDRAIL_CONFIG.anonymize_pii: + # If anonymizing, we don't block, we modify the text + output_texts.append(anonymized_text) + has_intervention = True + else: + # If not anonymizing PII, we block it + output_texts.append(text) + has_intervention = True + else: + output_texts.append(text) + + # Build response + if has_intervention: + response.action = "GUARDRAIL_INTERVENED" + # Only add assessment if there were interventions + response.assessments = [assessment] + + # Add outputs (modified or original text) + response.outputs = [BedrockGuardrailOutput(text=txt) for txt in output_texts] + + return response, output_texts + + +# ============================================================================ +# API Endpoints +# ============================================================================ + + +@app.get("/") +async def root(): + """Health check endpoint""" + return { + "service": "Mock Bedrock Guardrail API", + "status": "running", + "endpoint_format": "/guardrail/{guardrailIdentifier}/version/{guardrailVersion}/apply", + } + + +@app.get("/health") +async def health(): + """Health check endpoint""" + return {"status": "healthy"} + + +@app.post( + "/guardrail/{guardrailIdentifier}/version/{guardrailVersion}/apply", + response_model=BedrockGuardrailResponse, +) +async def apply_guardrail( + guardrailIdentifier: str, + guardrailVersion: str, + request: BedrockRequest, + token: str = Depends(verify_bearer_token), +) -> BedrockGuardrailResponse: + """ + Apply guardrail to input or output content. + + This endpoint mimics the AWS Bedrock ApplyGuardrail API. + + Args: + guardrailIdentifier: The guardrail ID + guardrailVersion: The guardrail version + request: The guardrail request containing content to analyze + token: Bearer token (verified by dependency) + + Returns: + BedrockGuardrailResponse with analysis results + """ + # Process the request + response, output_texts = process_guardrail_request(request) + + # Log the request (optional, for debugging) + print(f"Guardrail applied: {guardrailIdentifier} v{guardrailVersion}") + print(f"Source: {request.source}") + print(f"Action: {response.action}") + + return response + + +""" +LiteLLM exposes a basic guardrail API with the text extracted from the request and sent to the guardrail API, as well as the received request body for any further processing. + +This works across all LiteLLM endpoints (completion, anthropic /v1/messages, responses api, image generation, embedding, etc.) + +This makes it easy to support your own guardrail API without having to make a PR to LiteLLM. + +LiteLLM supports passing any provider specific params from LiteLLM config.yaml to the guardrail API. + +Example: + +```yaml +guardrails: + - guardrail_name: "bedrock-content-guard" + litellm_params: + guardrail: generic_guardrail_api + mode: "pre_call" + api_key: os.environ/GUARDRAIL_API_KEY + api_base: os.environ/GUARDRAIL_API_BASE + additional_provider_specific_params: + api_version: os.environ/GUARDRAIL_API_VERSION # additional provider specific params +``` + +This is a beta API. Please help us improve it. +""" + + +class LitellmBasicGuardrailRequest(BaseModel): + text: str + request_body: Dict[str, Any] = Field(default_factory=dict) + additional_provider_specific_params: Dict[str, Any] = Field(default_factory=dict) + + +class LitellmBasicGuardrailResponse(BaseModel): + action: Literal[ + "BLOCKED", "NONE", "GUARDRAIL_INTERVENED" + ] # BLOCKED = litellm will raise an error, NONE = litellm will continue, GUARDRAIL_INTERVENED = litellm will continue, but the text was modified by the guardrail + blocked_reason: Optional[str] = None # only if action is BLOCKED, otherwise None + text: Optional[str] = None + + +@app.post( + "/beta/litellm_basic_guardrail_api", + response_model=LitellmBasicGuardrailResponse, +) +async def beta_litellm_basic_guardrail_api( + request: LitellmBasicGuardrailRequest, +) -> LitellmBasicGuardrailResponse: + """ + Apply guardrail to input or output content. + + This endpoint mimics the AWS Bedrock ApplyGuardrail API. + + Args: + request: The guardrail request containing content to analyze + token: Bearer token (verified by dependency) + + Returns: + LitellmBasicGuardrailResponse with analysis results + """ + print(f"request: {request}") + if "ishaan" in request.text.lower(): + return LitellmBasicGuardrailResponse( + action="BLOCKED", blocked_reason="Ishaan is not allowed" + ) + elif "pii_value" in request.text: + return LitellmBasicGuardrailResponse( + action="GUARDRAIL_INTERVENED", + text=request.text.replace("pii_value", "pii_value_redacted"), + ) + return LitellmBasicGuardrailResponse(action="NONE") + + +@app.post("/config/update") +async def update_config( + config: GuardrailConfig, token: str = Depends(verify_bearer_token) +): + """ + Update the guardrail configuration. + + This is a testing endpoint to modify the mock guardrail behavior. + + Args: + config: New guardrail configuration + token: Bearer token (verified by dependency) + + Returns: + Updated configuration + """ + global GUARDRAIL_CONFIG + GUARDRAIL_CONFIG = config + return {"status": "updated", "config": GUARDRAIL_CONFIG} + + +@app.get("/config") +async def get_config(token: str = Depends(verify_bearer_token)): + """ + Get the current guardrail configuration. + + Args: + token: Bearer token (verified by dependency) + + Returns: + Current configuration + """ + return GUARDRAIL_CONFIG + + +# ============================================================================ +# Error Handlers +# ============================================================================ + + +@app.exception_handler(HTTPException) +async def http_exception_handler(request, exc: HTTPException): + """Custom error handler for HTTP exceptions""" + return JSONResponse( + status_code=exc.status_code, + content={"error": exc.detail}, + headers=exc.headers, + ) + + +# ============================================================================ +# Main +# ============================================================================ + +if __name__ == "__main__": + import uvicorn + + # Get configuration from environment + host = os.getenv("MOCK_BEDROCK_HOST", "0.0.0.0") + port = int(os.getenv("MOCK_BEDROCK_PORT", "8080")) + bearer_token = os.getenv("MOCK_BEDROCK_TOKEN", "mock-bedrock-token-12345") + + # Update config with environment token + GUARDRAIL_CONFIG.bearer_token = bearer_token + + print("=" * 80) + print("Mock Bedrock Guardrail API Server") + print("=" * 80) + print(f"Server starting on: http://{host}:{port}") + print(f"Bearer Token: {bearer_token}") + print(f"Endpoint: POST /guardrail/{{id}}/version/{{version}}/apply") + print("=" * 80) + print("\nExample curl command:") + print( + f""" +curl -X POST "http://{host}:{port}/guardrail/test-guardrail/version/1/apply" \\ + -H "Authorization: Bearer {bearer_token}" \\ + -H "Content-Type: application/json" \\ + -d '{{ + "source": "INPUT", + "content": [ + {{ + "text": {{ + "text": "Hello, my email is test@example.com" + }} + }} + ] + }}' + """ + ) + print("=" * 80) + + uvicorn.run(app, host=host, port=port) diff --git a/docs/my-website/docs/adding_provider/generic_guardrail_api.md b/docs/my-website/docs/adding_provider/generic_guardrail_api.md new file mode 100644 index 00000000000..70b39d3c397 --- /dev/null +++ b/docs/my-website/docs/adding_provider/generic_guardrail_api.md @@ -0,0 +1,160 @@ +# [BETA] Generic Guardrail API - Integrate Without a PR + +## The Problem + +As a guardrail provider, integrating with LiteLLM traditionally requires: +- Making a PR to the LiteLLM repository +- Waiting for review and merge +- Maintaining provider-specific code in LiteLLM's codebase +- Updating the integration for changes to your API + +## The Solution + +The **Generic Guardrail API** lets you integrate with LiteLLM **instantly** by implementing a simple API endpoint. No PR required. + +### Key Benefits + +1. **No PR Needed** - Deploy and integrate immediately +2. **Universal Support** - Works across ALL LiteLLM endpoints (chat, embeddings, image generation, etc.) +3. **Simple Contract** - One endpoint, three response types +4. **Custom Parameters** - Pass provider-specific params via config +5. **Full Control** - You own and maintain your guardrail API + +## How It Works + +1. LiteLLM extracts text from any request (chat messages, embeddings, image prompts, etc.) +2. Sends extracted text + original request to your API endpoint +3. Your API responds with: `BLOCKED`, `NONE`, or `GUARDRAIL_INTERVENED` +4. LiteLLM enforces the decision + +## API Contract + +### Endpoint + +Implement `POST /beta/litellm_basic_guardrail_api` + +### Request Format + +```json +{ + "text": "extracted text from the request", + "request_body": {}, // full original request for context + "additional_provider_specific_params": { + // your custom params from config + } +} +``` + +### Response Format + +```json +{ + "action": "BLOCKED" | "NONE" | "GUARDRAIL_INTERVENED", + "blocked_reason": "why content was blocked", // required if action=BLOCKED + "text": "modified text" // required if action=GUARDRAIL_INTERVENED +} +``` + +**Actions:** +- `BLOCKED` - LiteLLM raises error and blocks request +- `NONE` - Request proceeds unchanged +- `GUARDRAIL_INTERVENED` - Request proceeds with modified text + +## LiteLLM Configuration + +Add to `config.yaml`: + +```yaml +litellm_settings: + guardrails: + - guardrail_name: "my-guardrail" + litellm_params: + guardrail: generic_guardrail_api + mode: pre_call # or post_call, during_call + api_base: https://your-guardrail-api.com + api_key: os.environ/YOUR_GUARDRAIL_API_KEY # optional + additional_provider_specific_params: + # your custom parameters + threshold: 0.8 + language: "en" +``` + +## Usage + +Users apply your guardrail by name: + +```python +response = client.chat.completions.create( + model="gpt-4", + messages=[{"role": "user", "content": "hello"}], + guardrails=["my-guardrail"] +) +``` + +Or with dynamic parameters: + +```python +response = client.chat.completions.create( + model="gpt-4", + messages=[{"role": "user", "content": "hello"}], + guardrails=[{ + "my-guardrail": { + "extra_body": { + "custom_threshold": 0.9 + } + } + }] +) +``` + +## Implementation Example + +See [mock_bedrock_guardrail_server.py](https://github.com/BerriAI/litellm/blob/main/cookbook/mock_guardrail_server/mock_bedrock_guardrail_server.py) for a complete reference implementation. + +**Minimal FastAPI example:** + +```python +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + +class GuardrailRequest(BaseModel): + text: str + request_body: dict + additional_provider_specific_params: dict + +class GuardrailResponse(BaseModel): + action: str # BLOCKED, NONE, or GUARDRAIL_INTERVENED + blocked_reason: str | None = None + text: str | None = None + +@app.post("/beta/litellm_basic_guardrail_api") +async def apply_guardrail(request: GuardrailRequest): + # Your guardrail logic here + if "badword" in request.text.lower(): + return GuardrailResponse( + action="BLOCKED", + blocked_reason="Content contains prohibited terms" + ) + + return GuardrailResponse(action="NONE") +``` + +## When to Use This + +āœ… **Use Generic Guardrail API when:** +- You want instant integration without waiting for PRs +- You maintain your own guardrail service +- You need full control over updates and features +- You want to support all LiteLLM endpoints automatically + +āŒ **Make a PR when:** +- You want deeper integration with LiteLLM internals +- Your guardrail requires complex LiteLLM-specific logic +- You want to be featured as a built-in provider + +## Questions? + +This is a **beta API**. We're actively improving it based on feedback. Open an issue or PR if you need additional capabilities. + diff --git a/docs/my-website/sidebars.js b/docs/my-website/sidebars.js index 802ffdd5bb1..2039d01186c 100644 --- a/docs/my-website/sidebars.js +++ b/docs/my-website/sidebars.js @@ -45,6 +45,7 @@ const sidebars = { type: "category", "label": "Contributing to Guardrails", items: [ + "adding_provider/generic_guardrail_api", "adding_provider/simple_guardrail_tutorial", "adding_provider/adding_guardrail_support", ] diff --git a/litellm/proxy/_new_secret_config.yaml b/litellm/proxy/_new_secret_config.yaml index 68761524794..c11848a8623 100644 --- a/litellm/proxy/_new_secret_config.yaml +++ b/litellm/proxy/_new_secret_config.yaml @@ -16,4 +16,4 @@ callback_settings: callback_type: generic_api endpoint: https://webhook.site/efc57707-9018-478c-bdf1-2ffaabb2b315 headers: - Authorization: Bearer sk-1234 \ No newline at end of file + Authorization: Bearer sk-1234 diff --git a/litellm/proxy/guardrails/guardrail_hooks/generic_guardrail_api/__init__.py b/litellm/proxy/guardrails/guardrail_hooks/generic_guardrail_api/__init__.py new file mode 100644 index 00000000000..c762f0cbfc6 --- /dev/null +++ b/litellm/proxy/guardrails/guardrail_hooks/generic_guardrail_api/__init__.py @@ -0,0 +1,37 @@ +from typing import TYPE_CHECKING + +from litellm.types.guardrails import SupportedGuardrailIntegrations + +from .generic_guardrail_api import GenericGuardrailAPI + +if TYPE_CHECKING: + from litellm.types.guardrails import Guardrail, LitellmParams + + +def initialize_guardrail(litellm_params: "LitellmParams", guardrail: "Guardrail"): + import litellm + + _generic_guardrail_api_callback = GenericGuardrailAPI( + api_base=litellm_params.api_base, + headers=getattr(litellm_params, "headers", None), + additional_provider_specific_params=getattr( + litellm_params, "additional_provider_specific_params", {} + ), + guardrail_name=guardrail.get("guardrail_name", ""), + event_hook=litellm_params.mode, + default_on=litellm_params.default_on, + ) + + litellm.logging_callback_manager.add_litellm_callback( + _generic_guardrail_api_callback + ) + return _generic_guardrail_api_callback + + +guardrail_initializer_registry = { + SupportedGuardrailIntegrations.GENERIC_GUARDRAIL_API.value: initialize_guardrail, +} + +guardrail_class_registry = { + SupportedGuardrailIntegrations.GENERIC_GUARDRAIL_API.value: GenericGuardrailAPI, +} diff --git a/litellm/proxy/guardrails/guardrail_hooks/generic_guardrail_api/example_config.yaml b/litellm/proxy/guardrails/guardrail_hooks/generic_guardrail_api/example_config.yaml new file mode 100644 index 00000000000..7ad33b24608 --- /dev/null +++ b/litellm/proxy/guardrails/guardrail_hooks/generic_guardrail_api/example_config.yaml @@ -0,0 +1,52 @@ +# Example configuration for Generic Guardrail API + +model_list: + - model_name: gpt-4 + litellm_params: + model: gpt-4 + api_key: os.environ/OPENAI_API_KEY + +litellm_settings: + guardrails: + - guardrail_name: "my-generic-guardrail" + litellm_params: + guardrail: generic_guardrail_api + mode: pre_call # Options: pre_call, post_call, during_call, [pre_call, post_call] + api_key: os.environ/GENERIC_GUARDRAIL_API_KEY # Optional if using Bearer auth + api_base: http://localhost:8080 # Required. Endpoint /beta/litellm_basic_guardrail_api is automatically appended + default_on: false # Set to true to apply to all requests by default + additional_provider_specific_params: + # Any additional parameters your guardrail API needs + api_version: "v1" + custom_param: "value" + +# Usage examples: + +# 1. Apply guardrail to a specific request: +# curl --location 'http://localhost:4000/chat/completions' \ +# --header 'Authorization: Bearer sk-1234' \ +# --header 'Content-Type: application/json' \ +# --data '{ +# "model": "gpt-4", +# "messages": [{"role": "user", "content": "Test message"}], +# "guardrails": ["my-generic-guardrail"] +# }' + +# 2. Apply guardrail with dynamic parameters: +# curl --location 'http://localhost:4000/chat/completions' \ +# --header 'Authorization: Bearer sk-1234' \ +# --header 'Content-Type: application/json' \ +# --data '{ +# "model": "gpt-4", +# "messages": [{"role": "user", "content": "Test message"}], +# "guardrails": [ +# { +# "my-generic-guardrail": { +# "extra_body": { +# "custom_threshold": 0.8 +# } +# } +# } +# ] +# }' + diff --git a/litellm/proxy/guardrails/guardrail_hooks/generic_guardrail_api/generic_guardrail_api.py b/litellm/proxy/guardrails/guardrail_hooks/generic_guardrail_api/generic_guardrail_api.py new file mode 100644 index 00000000000..e94306e172e --- /dev/null +++ b/litellm/proxy/guardrails/guardrail_hooks/generic_guardrail_api/generic_guardrail_api.py @@ -0,0 +1,235 @@ +# +-------------------------------------------------------------+ +# +# Use Generic Guardrail API for your LLM calls +# +# +-------------------------------------------------------------+ +# Thank you users! We ā¤ļø you! - Krrish & Ishaan + +import os +from typing import Any, Dict, List, Optional + +from litellm._logging import verbose_proxy_logger +from litellm.integrations.custom_guardrail import CustomGuardrail +from litellm.llms.custom_httpx.http_handler import ( + get_async_httpx_client, + httpxSpecialProvider, +) +from litellm.types.guardrails import GuardrailEventHooks + +GUARDRAIL_NAME = "generic_guardrail_api" + + +class GenericGuardrailAPIRequest: + """Request model for the Generic Guardrail API""" + + def __init__( + self, + text: str, + request_body: Dict[str, Any], + additional_provider_specific_params: Optional[Dict[str, Any]] = None, + ): + self.text = text + self.request_body = request_body + self.additional_provider_specific_params = ( + additional_provider_specific_params or {} + ) + + def to_dict(self) -> dict: + return { + "text": self.text, + "request_body": self.request_body, + "additional_provider_specific_params": self.additional_provider_specific_params, + } + + +class GenericGuardrailAPIResponse: + """Response model for the Generic Guardrail API""" + + def __init__( + self, + action: str, + blocked_reason: Optional[str] = None, + text: Optional[str] = None, + ): + self.action = action + self.blocked_reason = blocked_reason + self.text = text + + @classmethod + def from_dict(cls, data: dict) -> "GenericGuardrailAPIResponse": + return cls( + action=data.get("action", "NONE"), + blocked_reason=data.get("blocked_reason"), + text=data.get("text"), + ) + + +class GenericGuardrailAPI(CustomGuardrail): + """ + Generic Guardrail API integration for LiteLLM. + + This integration allows you to use any guardrail API that follows the + LiteLLM Basic Guardrail API spec without needing to write custom integration code. + + The API should accept a POST request with: + { + "text": str, + "request_body": dict, + "additional_provider_specific_params": dict + } + + And return: + { + "action": "BLOCKED" | "NONE" | "GUARDRAIL_INTERVENED", + "blocked_reason": str (optional, only if action is BLOCKED), + "text": str (optional, modified text if action is GUARDRAIL_INTERVENED) + } + """ + + def __init__( + self, + headers: Optional[Dict[str, Any]] = None, + api_base: Optional[str] = None, + additional_provider_specific_params: Optional[Dict[str, Any]] = None, + **kwargs, + ): + self.async_handler = get_async_httpx_client( + llm_provider=httpxSpecialProvider.GuardrailCallback + ) + self.headers = headers or {} + base_url = api_base or os.environ.get("GENERIC_GUARDRAIL_API_BASE") + + if not base_url: + raise ValueError( + "api_base is required for Generic Guardrail API. " + "Set GENERIC_GUARDRAIL_API_BASE environment variable or pass it in litellm_params" + ) + + # Append the endpoint path if not already present + if not base_url.endswith("/beta/litellm_basic_guardrail_api"): + base_url = base_url.rstrip("/") + self.api_base = f"{base_url}/beta/litellm_basic_guardrail_api" + else: + self.api_base = base_url + + self.additional_provider_specific_params = ( + additional_provider_specific_params or {} + ) + + # Set supported event hooks + if "supported_event_hooks" not in kwargs: + kwargs["supported_event_hooks"] = [ + GuardrailEventHooks.pre_call, + GuardrailEventHooks.post_call, + GuardrailEventHooks.during_call, + ] + + super().__init__(**kwargs) + + verbose_proxy_logger.debug( + "Generic Guardrail API initialized with api_base: %s", self.api_base + ) + + async def apply_guardrail( + self, + text: str, + language: Optional[str] = None, + entities: Optional[List] = None, + request_data: Optional[dict] = None, + ) -> str: + """ + Apply the Generic Guardrail API to the given text. + + This is the main method that gets called by the framework. + + Args: + text: The text to check + language: Optional language parameter (not used by Generic API) + entities: Optional entities parameter (not used by Generic API) + request_data: Optional request data dictionary for logging metadata + + Returns: + The processed text (original or modified) + + Raises: + Exception: If the guardrail blocks the request + """ + verbose_proxy_logger.debug("Generic Guardrail API: Applying guardrail to text") + + # Use provided request_data or create an empty dict + if request_data is None: + request_data = {} + + request_body = request_data.get("body") or {} + + # Merge additional provider specific params from config and dynamic params + additional_params = {**self.additional_provider_specific_params} + + # Get dynamic params from request if available + dynamic_params = self.get_guardrail_dynamic_request_body_params(request_body) + if dynamic_params: + additional_params.update(dynamic_params) + + # Create request payload + guardrail_request = GenericGuardrailAPIRequest( + text=text, + request_body=request_body, + additional_provider_specific_params=additional_params, + ) + + # Prepare headers + headers = {"Content-Type": "application/json"} + if self.headers: + headers.update(self.headers) + + verbose_proxy_logger.debug( + "Generic Guardrail API request to %s: %s", + self.api_base, + {"text_length": len(text), "has_request_body": bool(request_data)}, + ) + + try: + # Make the API request + response = await self.async_handler.post( + url=self.api_base, + json=guardrail_request.to_dict(), + headers=headers, + ) + + response.raise_for_status() + response_json = response.json() + + verbose_proxy_logger.debug( + "Generic Guardrail API response: %s", response_json + ) + + guardrail_response = GenericGuardrailAPIResponse.from_dict(response_json) + + # Handle the response + if guardrail_response.action == "BLOCKED": + # Block the request + error_message = ( + guardrail_response.blocked_reason or "Content violates policy" + ) + verbose_proxy_logger.warning( + "Generic Guardrail API blocked request: %s", error_message + ) + raise Exception(f"Content blocked by guardrail: {error_message}") + + elif guardrail_response.action == "GUARDRAIL_INTERVENED": + # Content was modified by the guardrail + if guardrail_response.text: + verbose_proxy_logger.debug("Generic Guardrail API modified text") + return guardrail_response.text + + # Action is NONE or no modifications needed + return text + + except Exception as e: + # Check if it's already an exception we raised + if "Content blocked by guardrail" in str(e): + raise + verbose_proxy_logger.error( + "Generic Guardrail API: failed to make request: %s", str(e) + ) + raise Exception(f"Generic Guardrail API failed: {str(e)}") diff --git a/litellm/types/guardrails.py b/litellm/types/guardrails.py index 24a235def59..31e301ed4de 100644 --- a/litellm/types/guardrails.py +++ b/litellm/types/guardrails.py @@ -8,6 +8,9 @@ from typing_extensions import Required, TypedDict from litellm.types.proxy.guardrails.guardrail_hooks.enkryptai import ( EnkryptAIGuardrailConfigs, ) +from litellm.types.proxy.guardrails.guardrail_hooks.generic_guardrail_api import ( + GenericGuardrailAPIOptionalParams, +) from litellm.types.proxy.guardrails.guardrail_hooks.grayswan import ( GraySwanGuardrailConfigModel, ) @@ -18,7 +21,6 @@ from litellm.types.proxy.guardrails.guardrail_hooks.tool_permission import ( ToolPermissionGuardrailConfigModel, ) - """ Pydantic object defining how to set guardrails on litellm proxy @@ -59,6 +61,7 @@ class SupportedGuardrailIntegrations(Enum): IBM_GUARDRAILS = "ibm_guardrails" LITELLM_CONTENT_FILTER = "litellm_content_filter" PROMPT_SECURITY = "prompt_security" + GENERIC_GUARDRAIL_API = "generic_guardrail_api" class Role(Enum): @@ -590,6 +593,12 @@ class BaseLitellmParams(BaseModel): # works for new and patch update guardrails description="Whether to fail the request if Model Armor encounters an error", ) + # Generic Guardrail API params + additional_provider_specific_params: Optional[Dict[str, Any]] = Field( + default=None, + description="Additional provider-specific parameters for generic guardrail APIs", + ) + model_config = ConfigDict(extra="allow", protected_namespaces=()) diff --git a/litellm/types/proxy/guardrails/guardrail_hooks/generic_guardrail_api.py b/litellm/types/proxy/guardrails/guardrail_hooks/generic_guardrail_api.py new file mode 100644 index 00000000000..a00fe76a0f0 --- /dev/null +++ b/litellm/types/proxy/guardrails/guardrail_hooks/generic_guardrail_api.py @@ -0,0 +1,29 @@ +from typing import Any, Dict, Literal, Optional + +from pydantic import BaseModel, Field + +from litellm.types.proxy.guardrails.guardrail_hooks.base import GuardrailConfigModel + + +class GenericGuardrailAPIOptionalParams(BaseModel): + """Optional parameters for the Generic Guardrail API""" + + additional_provider_specific_params: Optional[Dict[str, Any]] = Field( + default=None, + description="Additional provider-specific parameters to send with the guardrail request", + ) + + +class GenericGuardrailAPIConfigModel( + GuardrailConfigModel[GenericGuardrailAPIOptionalParams], +): + """Configuration parameters for the Generic Guardrail API guardrail""" + + optional_params: Optional[GenericGuardrailAPIOptionalParams] = Field( + default_factory=GenericGuardrailAPIOptionalParams, + description="Optional parameters for the Generic Guardrail API guardrail", + ) + + @staticmethod + def ui_friendly_name() -> str: + return "Generic Guardrail API" From 1eb06f803101d7e82761a3b3a36d6a61de22fc6f Mon Sep 17 00:00:00 2001 From: Krish Dholakia Date: Mon, 1 Dec 2025 15:40:28 -0800 Subject: [PATCH 075/370] =?UTF-8?q?Revert=20"fix:=20respect=20guardrail=20?= =?UTF-8?q?mock=5Fresponse=20during=20during=5Fcall=20to=20return=20blo?= =?UTF-8?q?=E2=80=A6"=20(#17332)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 6de610767340cadd6df1c5508325128045c8fae5. --- litellm/proxy/common_request_processing.py | 23 ++--- .../proxy/test_common_request_processing.py | 99 +------------------ 2 files changed, 11 insertions(+), 111 deletions(-) diff --git a/litellm/proxy/common_request_processing.py b/litellm/proxy/common_request_processing.py index ed4c451f8d3..d2b04410026 100644 --- a/litellm/proxy/common_request_processing.py +++ b/litellm/proxy/common_request_processing.py @@ -536,11 +536,7 @@ class ProxyBaseLLMRequestProcessing: responses = await llm_responses - # Guardrails (pre/during-call) can inject a mock response to short-circuit the LLM call. - # Prefer it when present so blocked/filtered output is returned instead of the model response. - response = self.data.get("mock_response") - if response is None: - response = responses[1] + response = responses[1] hidden_params = getattr(response, "_hidden_params", {}) or {} model_id = hidden_params.get("model_id", None) or "" @@ -808,7 +804,7 @@ class ProxyBaseLLMRequestProcessing: # This matches the original behavior before the refactor in commit 511d435f6f error_body = await e.response.aread() error_text = error_body.decode("utf-8") - + raise HTTPException( status_code=e.response.status_code, detail={"error": error_text}, @@ -1076,9 +1072,9 @@ class ProxyBaseLLMRequestProcessing: # Add cache-related fields to **params (handled by Usage.__init__) if cache_creation_input_tokens is not None: - usage_kwargs[ - "cache_creation_input_tokens" - ] = cache_creation_input_tokens + usage_kwargs["cache_creation_input_tokens"] = ( + cache_creation_input_tokens + ) if cache_read_input_tokens is not None: usage_kwargs["cache_read_input_tokens"] = cache_read_input_tokens @@ -1097,9 +1093,7 @@ class ProxyBaseLLMRequestProcessing: return obj return None - def maybe_get_model_id( - self, _logging_obj: Optional[LiteLLMLoggingObj] - ) -> Optional[str]: + def maybe_get_model_id(self, _logging_obj: Optional[LiteLLMLoggingObj]) -> Optional[str]: """ Get model_id from logging object or request metadata. @@ -1109,7 +1103,10 @@ class ProxyBaseLLMRequestProcessing: model_id = None if _logging_obj: # 1. Try getting from litellm_params (updated during call) - if hasattr(_logging_obj, "litellm_params") and _logging_obj.litellm_params: + if ( + hasattr(_logging_obj, "litellm_params") + and _logging_obj.litellm_params + ): # First check direct model_info path (set by router.py with selected deployment) model_info = _logging_obj.litellm_params.get("model_info") or {} model_id = model_info.get("id", None) diff --git a/tests/test_litellm/proxy/test_common_request_processing.py b/tests/test_litellm/proxy/test_common_request_processing.py index 8f5f182f429..4768ec42ff6 100644 --- a/tests/test_litellm/proxy/test_common_request_processing.py +++ b/tests/test_litellm/proxy/test_common_request_processing.py @@ -1,13 +1,11 @@ import copy -from types import SimpleNamespace from unittest.mock import AsyncMock, MagicMock import pytest -from fastapi import Request, Response, status +from fastapi import Request, status from fastapi.responses import StreamingResponse import litellm -import litellm.proxy.common_request_processing as common_request_processing from litellm._uuid import uuid from litellm.integrations.opentelemetry import UserAPIKeyAuth from litellm.proxy.common_request_processing import ( @@ -77,101 +75,6 @@ class TestProxyBaseLLMRequestProcessing: pytest.fail("litellm_call_id is not a valid UUID") assert data_passed["litellm_call_id"] == returned_data["litellm_call_id"] - @pytest.mark.asyncio - async def test_base_process_llm_request_prefers_guardrail_mock_response( - self, monkeypatch - ): - processing_obj = ProxyBaseLLMRequestProcessing( - data={ - "messages": [], - "metadata": {}, - "litellm_metadata": {"model_info": {"id": "fallback-model"}}, - } - ) - - guardrail_response = litellm.ModelResponse( - model="bedrock-guardrail", - hidden_params={"model_id": "guardrail-model"}, - ) - llm_response = litellm.ModelResponse( - model="real-model", - hidden_params={"model_id": "real-model"}, - ) - - async def mock_common_processing(self, *args, **kwargs): - logging_obj = SimpleNamespace(litellm_call_id="test-call-id") - self.data["litellm_call_id"] = "test-call-id" - self.data["litellm_logging_obj"] = logging_obj - return self.data, logging_obj - - monkeypatch.setattr( - ProxyBaseLLMRequestProcessing, - "common_processing_pre_call_logic", - mock_common_processing, - ) - - async def mock_route_request(*args, **kwargs): - async def _inner(): - return llm_response - - return _inner() - - monkeypatch.setattr( - common_request_processing, - "route_request", - mock_route_request, - ) - - check_response_size_is_safe_mock = AsyncMock() - monkeypatch.setattr( - common_request_processing, - "check_response_size_is_safe", - check_response_size_is_safe_mock, - ) - - async def mock_during_call_hook(*args, **kwargs): - kwargs["data"]["mock_response"] = guardrail_response - - proxy_logging_obj = MagicMock(spec=ProxyLogging) - proxy_logging_obj.during_call_hook = AsyncMock( - side_effect=mock_during_call_hook - ) - proxy_logging_obj.update_request_status = AsyncMock(return_value=None) - proxy_logging_obj.post_call_success_hook = AsyncMock( - return_value=guardrail_response - ) - - user_api_key_dict = MagicMock(spec=UserAPIKeyAuth) - user_api_key_dict.tpm_limit = None - user_api_key_dict.rpm_limit = None - user_api_key_dict.max_budget = None - user_api_key_dict.spend = 0 - user_api_key_dict.allowed_model_region = None - - fastapi_response = Response() - proxy_config = MagicMock(spec=ProxyConfig) - - result = await processing_obj.base_process_llm_request( - request=MagicMock(spec=Request), - fastapi_response=fastapi_response, - user_api_key_dict=user_api_key_dict, - route_type="acompletion", - proxy_logging_obj=proxy_logging_obj, - general_settings={}, - proxy_config=proxy_config, - select_data_generator=lambda **kwargs: None, - ) - - assert result is guardrail_response - assert ( - proxy_logging_obj.post_call_success_hook.await_args.kwargs["response"] - is guardrail_response - ) - assert ( - check_response_size_is_safe_mock.await_args.kwargs["response"] - is guardrail_response - ) - @pytest.mark.asyncio async def test_stream_timeout_header_processing(self): """ From be920d75d361519b61f43809b771bc7b107eaf85 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Tue, 2 Dec 2025 04:25:26 +0200 Subject: [PATCH 076/370] Add `claude-opus-4-5` alias (#17313) Similar to `claude-sonnet-4-5`. --- model_prices_and_context_window.json | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index af63d1e2592..6b9b8beed80 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -6717,6 +6717,33 @@ "supports_vision": true, "tool_use_system_prompt_tokens": 159 }, + "claude-opus-4-5": { + "cache_creation_input_token_cost": 6.25e-06, + "cache_creation_input_token_cost_above_1hr": 1e-05, + "cache_read_input_token_cost": 5e-07, + "input_cost_per_token": 5e-06, + "litellm_provider": "anthropic", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 2.5e-05, + "search_context_cost_per_query": { + "search_context_size_high": 0.01, + "search_context_size_low": 0.01, + "search_context_size_medium": 0.01 + }, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 159 + }, "claude-sonnet-4-20250514": { "deprecation_date": "2026-05-14", "cache_creation_input_token_cost": 3.75e-06, From 37ecb03d4f0b8bd9695126c8f0beb68ed978f7d1 Mon Sep 17 00:00:00 2001 From: Elias <55650958+eliasto@users.noreply.github.com> Date: Mon, 1 Dec 2025 21:26:39 -0500 Subject: [PATCH 077/370] Add support of audio transcription for OVHcloud (#17305) --- docs/my-website/docs/audio_transcription.md | 3 +- docs/my-website/docs/providers/ovhcloud.md | 15 ++ .../get_supported_openai_params.py | 9 + .../audio_transcription/transformation.py | 156 ++++++++++++++++++ litellm/utils.py | 6 + provider_endpoints_support.json | 2 +- ...loud_audio_transcription_transformation.py | 59 +++++++ 7 files changed, 248 insertions(+), 2 deletions(-) create mode 100644 litellm/llms/ovhcloud/audio_transcription/transformation.py create mode 100644 tests/test_litellm/llms/ovhcloud/test_ovhcloud_audio_transcription_transformation.py diff --git a/docs/my-website/docs/audio_transcription.md b/docs/my-website/docs/audio_transcription.md index fd55cc66e92..5853b5c1872 100644 --- a/docs/my-website/docs/audio_transcription.md +++ b/docs/my-website/docs/audio_transcription.md @@ -13,7 +13,7 @@ import TabItem from '@theme/TabItem'; | Fallbacks | āœ… | Works between supported models | | Loadbalancing | āœ… | Works between supported models | | Guardrails | āœ… | Applies to output transcribed text (non-streaming only) | -| Supported Providers | `openai`, `azure`, `vertex_ai`, `gemini`, `deepgram`, `groq`, `fireworks_ai` | | +| Supported Providers | `openai`, `azure`, `vertex_ai`, `gemini`, `deepgram`, `groq`, `fireworks_ai`, `ovhcloud` | | ## Quick Start @@ -126,6 +126,7 @@ transcript = client.audio.transcriptions.create( - [Fireworks AI](./providers/fireworks_ai.md#audio-transcription) - [Groq](./providers/groq.md#speech-to-text---whisper) - [Deepgram](./providers/deepgram.md) +- [OVHcloud AI Endpoints](./providers/ovhcloud.md) --- diff --git a/docs/my-website/docs/providers/ovhcloud.md b/docs/my-website/docs/providers/ovhcloud.md index 6c42208f2cc..94625b0f2ed 100644 --- a/docs/my-website/docs/providers/ovhcloud.md +++ b/docs/my-website/docs/providers/ovhcloud.md @@ -311,6 +311,21 @@ response = embedding( print(response.data) ``` +### Audio Transcription + +```python +from litellm import transcription + +audio_file = open("path/to/your/audio.wav", "rb") + +response = transcription( + model="ovhcloud/whisper-large-v3-turbo", + file=audio_file +) + +print(response.text) +``` + ## Usage with LiteLLM Proxy Server Here's how to call a OVHCloud AI Endpoints model with the LiteLLM Proxy Server diff --git a/litellm/litellm_core_utils/get_supported_openai_params.py b/litellm/litellm_core_utils/get_supported_openai_params.py index 06e650f938d..19b52d2dace 100644 --- a/litellm/litellm_core_utils/get_supported_openai_params.py +++ b/litellm/litellm_core_utils/get_supported_openai_params.py @@ -266,6 +266,15 @@ def get_supported_openai_params( # noqa: PLR0915 model=model ) ) + elif custom_llm_provider == "ovhcloud": + if request_type == "transcription": + from litellm.llms.ovhcloud.audio_transcription.transformation import ( + OVHCloudAudioTranscriptionConfig, + ) + + return OVHCloudAudioTranscriptionConfig().get_supported_openai_params( + model=model + ) elif custom_llm_provider == "elevenlabs": if request_type == "transcription": from litellm.llms.elevenlabs.audio_transcription.transformation import ( diff --git a/litellm/llms/ovhcloud/audio_transcription/transformation.py b/litellm/llms/ovhcloud/audio_transcription/transformation.py new file mode 100644 index 00000000000..7233d911b07 --- /dev/null +++ b/litellm/llms/ovhcloud/audio_transcription/transformation.py @@ -0,0 +1,156 @@ +""" +Support for OVHCloud AI Endpoints `/v1/audio/transcriptions` endpoint. + +Our unified API follows the OpenAI standard. +More information on our website: https://endpoints.ai.cloud.ovh.net +""" + +from typing import List, Optional, Union + +import httpx + +from litellm.litellm_core_utils.audio_utils.utils import process_audio_file +from litellm.llms.base_llm.audio_transcription.transformation import ( + AudioTranscriptionRequestData, + BaseAudioTranscriptionConfig, +) +from litellm.llms.base_llm.chat.transformation import BaseLLMException +from litellm.secret_managers.main import get_secret_str +from litellm.types.llms.openai import ( + AllMessageValues, + OpenAIAudioTranscriptionOptionalParams, +) +from litellm.types.utils import FileTypes, TranscriptionResponse + +from ..utils import OVHCloudException + + +class OVHCloudAudioTranscriptionConfig(BaseAudioTranscriptionConfig): + def get_supported_openai_params( + self, model: str + ) -> List[OpenAIAudioTranscriptionOptionalParams]: + # OVHCloud implements the OpenAI-compatible Whisper interface. + # We pass through the same optional params as the OpenAI Whisper API. + return ["language", "prompt", "response_format", "timestamp_granularities", "temperature"] + + def map_openai_params( + self, + non_default_params: dict, + optional_params: dict, + model: str, + drop_params: bool, + ) -> dict: + supported_params = self.get_supported_openai_params(model) + for k, v in non_default_params.items(): + if k in supported_params: + optional_params[k] = v + return optional_params + + def get_complete_url( + self, + api_base: Optional[str], + api_key: Optional[str], + model: str, + optional_params: dict, + litellm_params: dict, + stream: Optional[bool] = None, + ) -> str: + api_base = ( + "https://oai.endpoints.kepler.ai.cloud.ovh.net/v1" + if api_base is None + else api_base.rstrip("/") + ) + complete_url = f"{api_base}/audio/transcriptions" + return complete_url + + def get_error_class( + self, error_message: str, status_code: int, headers: Union[dict, httpx.Headers] + ) -> BaseLLMException: + return OVHCloudException( + message=error_message, + status_code=status_code, + headers=headers, + ) + + def validate_environment( + self, + headers: dict, + model: str, + messages: List[AllMessageValues], + optional_params: dict, + litellm_params: dict, + api_key: Optional[str] = None, + api_base: Optional[str] = None, + ) -> dict: + if api_key is None: + api_key = get_secret_str("OVHCLOUD_API_KEY") + + default_headers = { + "Authorization": f"Bearer {api_key}", + "accept": "application/json", + } + + # Caller can override / extend headers if needed + default_headers.update(headers or {}) + return default_headers + + def transform_audio_transcription_request( + self, + model: str, + audio_file: FileTypes, + optional_params: dict, + litellm_params: dict, + ) -> AudioTranscriptionRequestData: + """ + Transform the audio transcription request into OpenAI-compatible form-data. + + OVHCloud follows OpenAI's `/audio/transcriptions` format, so we: + - Build a multipart form-data body with `file`, `model`, and optional params + - Let the shared HTTP handler set the proper content-type boundary + """ + processed_audio = process_audio_file(audio_file) + + # Base form fields: model + OpenAI-compatible optional params + form_fields: dict = { + "model": model, + } + + # Include OpenAI-compatible optional params + for key in self.get_supported_openai_params(model): + value = optional_params.get(key) + if value is not None: + form_fields[key] = value + + files = { + "file": ( + processed_audio.filename, + processed_audio.file_content, + processed_audio.content_type, + ) + } + + return AudioTranscriptionRequestData(data=form_fields, files=files) + + def transform_audio_transcription_response( + self, + raw_response: httpx.Response, + ) -> TranscriptionResponse: + """ + Transform OVHCloud audio transcription response to OpenAI-compatible TranscriptionResponse. + """ + try: + response_json = raw_response.json() + except Exception: + raise OVHCloudException( + message=raw_response.text, + status_code=raw_response.status_code, + headers=raw_response.headers, + ) + + text = response_json.get("text") or response_json.get("transcript") or "" + response = TranscriptionResponse(text=text) + + response._hidden_params = response_json + return response + + diff --git a/litellm/utils.py b/litellm/utils.py index f74c3aa0693..37a71b43476 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -7384,6 +7384,12 @@ class ProviderConfigManager: ) return IBMWatsonXAudioTranscriptionConfig() + elif litellm.LlmProviders.OVHCLOUD == provider: + from litellm.llms.ovhcloud.audio_transcription.transformation import ( + OVHCloudAudioTranscriptionConfig, + ) + + return OVHCloudAudioTranscriptionConfig() return None @staticmethod diff --git a/provider_endpoints_support.json b/provider_endpoints_support.json index 5eab130cdd3..b5bde3e5ce4 100644 --- a/provider_endpoints_support.json +++ b/provider_endpoints_support.json @@ -1272,7 +1272,7 @@ "responses": true, "embeddings": false, "image_generations": false, - "audio_transcriptions": false, + "audio_transcriptions": true, "audio_speech": false, "moderations": false, "batches": false, diff --git a/tests/test_litellm/llms/ovhcloud/test_ovhcloud_audio_transcription_transformation.py b/tests/test_litellm/llms/ovhcloud/test_ovhcloud_audio_transcription_transformation.py new file mode 100644 index 00000000000..fc5e310e71b --- /dev/null +++ b/tests/test_litellm/llms/ovhcloud/test_ovhcloud_audio_transcription_transformation.py @@ -0,0 +1,59 @@ +import os +from typing import Dict + +import litellm +import pytest + +from litellm.llms.base_llm.audio_transcription.transformation import ( + BaseAudioTranscriptionConfig, +) +from litellm.utils import ProviderConfigManager +from tests.llm_translation.base_audio_transcription_unit_tests import ( + BaseLLMAudioTranscriptionTest, +) + + +@pytest.mark.skipif( + not os.getenv("OVHCLOUD_API_KEY"), + reason="OVHCLOUD_API_KEY not set, skipping OVHCloud audio transcription tests", +) +class TestOVHCloudAudioTranscription(BaseLLMAudioTranscriptionTest): + def get_base_audio_transcription_call_args(self) -> Dict: + return { + "model": "ovhcloud/whisper-large-v3-turbo", + } + + def get_custom_llm_provider(self) -> litellm.LlmProviders: + return litellm.LlmProviders.OVHCLOUD + + # Override the async base test with a sync no-op to avoid + # 'async def functions are not natively supported' failures when + # running this file in isolation without pytest-asyncio. + def test_audio_transcription_async(self): # type: ignore[override] + pytest.skip( + "Async audio transcription test for OVHCloud is skipped in this suite; " + "async test plugins (e.g. pytest-asyncio/anyio) are not configured here." + ) + + +@pytest.mark.skipif( + not os.getenv("OVHCLOUD_API_KEY"), + reason="OVHCLOUD_API_KEY not set, skipping OVHCloud audio transcription config test", +) +def test_ovhcloud_audio_transcription_config_installed(): + """ + Ensure OVHCloud audio transcription config is registered with ProviderConfigManager. + """ + model = "ovhcloud/whisper-large-v3-turbo" + provider = litellm.LlmProviders.OVHCLOUD + + config = ProviderConfigManager.get_provider_audio_transcription_config( + model=model, + provider=provider, + ) + + assert config is not None + assert isinstance(config, BaseAudioTranscriptionConfig) + + + From 860cdc81d3a540c64d17cc6112ac577f1f9dd926 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Mon, 1 Dec 2025 18:26:56 -0800 Subject: [PATCH 078/370] [Fix] Fix Watsonx Audio Transcription API (#17326) * """ add * fix transform_audio_transcription_request * fix tests * test_watsonx_transcription_request_body --- .../audio_transcription/transformation.py | 78 ++++++++++++++++--- litellm/types/llms/watsonx.py | 36 ++++++++- ...sonx_audio_transcription_transformation.py | 35 ++++++++- 3 files changed, 131 insertions(+), 18 deletions(-) diff --git a/litellm/llms/watsonx/audio_transcription/transformation.py b/litellm/llms/watsonx/audio_transcription/transformation.py index 8c8324cb72d..8fe8b4a4248 100644 --- a/litellm/llms/watsonx/audio_transcription/transformation.py +++ b/litellm/llms/watsonx/audio_transcription/transformation.py @@ -4,11 +4,17 @@ Translates from OpenAI's `/v1/audio/transcriptions` to IBM WatsonX's `/ml/v1/aud WatsonX follows the OpenAI spec for audio transcription. """ -from typing import List, Optional +from typing import Any, Dict, List, Optional import litellm +from litellm.litellm_core_utils.audio_utils.utils import process_audio_file from litellm.types.llms.openai import OpenAIAudioTranscriptionOptionalParams +from litellm.types.llms.watsonx import WatsonXAudioTranscriptionRequestBody +from litellm.types.utils import FileTypes +from ...base_llm.audio_transcription.transformation import ( + AudioTranscriptionRequestData, +) from ...openai.transcriptions.whisper_transformation import ( OpenAIWhisperAudioTranscriptionConfig, ) @@ -40,6 +46,60 @@ class IBMWatsonXAudioTranscriptionConfig( "timestamp_granularities", ] + def transform_audio_transcription_request( + self, + model: str, + audio_file: FileTypes, + optional_params: dict, + litellm_params: dict, + ) -> AudioTranscriptionRequestData: + """ + Transform the audio transcription request for WatsonX. + + WatsonX expects multipart/form-data with: + - file: the audio file + - model: the model name (without watsonx/ prefix) + - project_id: the project ID (as form field, not query param) + - other optional params + """ + # Use common utility to process the audio file + processed_audio = process_audio_file(audio_file) + + # Get API params to extract project_id + api_params = _get_api_params(params=optional_params.copy()) + + # Initialize form data with required fields + form_data: WatsonXAudioTranscriptionRequestBody = { + "model": model, + "project_id": api_params.get("project_id", ""), + } + + # Add supported OpenAI params to form data + supported_params = self.get_supported_openai_params(model) + for key, value in optional_params.items(): + if key in supported_params and value is not None: + form_data[key] = value # type: ignore + + # Set default response_format for cost calculation + if "response_format" not in form_data or ( + form_data.get("response_format") in ["text", "json"] + ): + form_data["response_format"] = "verbose_json" + + # Prepare files dict with the audio file + files = { + "file": ( + processed_audio.filename, + processed_audio.file_content, + processed_audio.content_type, + ) + } + + # Convert TypedDict to regular dict for AudioTranscriptionRequestData + form_data_dict: Dict[str, Any] = dict(form_data) + + return AudioTranscriptionRequestData(data=form_data_dict, files=files) + def get_complete_url( self, api_base: Optional[str], @@ -52,7 +112,9 @@ class IBMWatsonXAudioTranscriptionConfig( """ Construct the complete URL for WatsonX audio transcription. - URL format: {api_base}/ml/v1/audio/transcriptions?version={version}&project_id={project_id} + URL format: {api_base}/ml/v1/audio/transcriptions?version={version} + + Note: project_id is sent as form data, not as a query parameter """ # Get base URL url = self._get_base_url(api_base=api_base) @@ -61,18 +123,10 @@ class IBMWatsonXAudioTranscriptionConfig( # Add the audio transcription endpoint url = f"{url}/ml/v1/audio/transcriptions" - # Get API params for project_id - api_params = _get_api_params(params=optional_params.copy()) - - # Add version parameter - api_version = optional_params.pop( + # Add version parameter (only version in query string, not project_id) + api_version = optional_params.get( "api_version", None ) or litellm.WATSONX_DEFAULT_API_VERSION url = f"{url}?version={api_version}" - # Add project_id parameter - project_id = api_params.get("project_id") - if project_id: - url = f"{url}&project_id={project_id}" - return url diff --git a/litellm/types/llms/watsonx.py b/litellm/types/llms/watsonx.py index 4eb2f2531a0..6c42c3ecea0 100644 --- a/litellm/types/llms/watsonx.py +++ b/litellm/types/llms/watsonx.py @@ -1,9 +1,7 @@ -import json from enum import Enum -from typing import Any, List, Optional, Union +from typing import List, Optional -from pydantic import BaseModel -from typing_extensions import TypedDict +from typing_extensions import NotRequired, TypedDict class WatsonXAPIParams(TypedDict): @@ -18,6 +16,36 @@ class WatsonXCredentials(TypedDict): token: Optional[str] +class WatsonXAudioTranscriptionRequestBody(TypedDict): + """ + WatsonX Audio Transcription API request body. + + Follows multipart/form-data format for WatsonX Whisper models. + See: https://cloud.ibm.com/apidocs/watsonx-ai + """ + + model: str + """Model name (e.g., 'whisper-large-v3-turbo')""" + + project_id: str + """WatsonX project ID (required)""" + + language: NotRequired[str] + """Language code (e.g., 'en', 'es')""" + + prompt: NotRequired[str] + """Optional prompt to guide transcription""" + + response_format: NotRequired[str] + """Response format: 'json', 'text', 'srt', 'verbose_json', 'vtt'""" + + temperature: NotRequired[float] + """Sampling temperature (0-1)""" + + timestamp_granularities: NotRequired[List[str]] + """Timestamp granularities: ['word', 'segment']""" + + class WatsonXAIEndpoint(str, Enum): TEXT_GENERATION = "/ml/v1/text/generation" TEXT_GENERATION_STREAM = "/ml/v1/text/generation_stream" diff --git a/tests/test_litellm/llms/watsonx/audio_transcription/test_watsonx_audio_transcription_transformation.py b/tests/test_litellm/llms/watsonx/audio_transcription/test_watsonx_audio_transcription_transformation.py index 84a9d25d98e..1286c2d4fe6 100644 --- a/tests/test_litellm/llms/watsonx/audio_transcription/test_watsonx_audio_transcription_transformation.py +++ b/tests/test_litellm/llms/watsonx/audio_transcription/test_watsonx_audio_transcription_transformation.py @@ -4,6 +4,7 @@ Tests for IBM WatsonX Audio Transcription. Validates that litellm.transcription transforms requests correctly for WatsonX. """ +import json import os import sys from unittest.mock import AsyncMock, MagicMock, patch @@ -29,6 +30,7 @@ class TestWatsonXAudioTranscription: captured_request["url"] = str(kwargs.get("url", args[0] if args else None)) captured_request["headers"] = kwargs.get("headers", {}) captured_request["data"] = kwargs.get("data", {}) + captured_request["files"] = kwargs.get("files", {}) mock_response = MagicMock() mock_response.json.return_value = { @@ -54,16 +56,30 @@ class TestWatsonXAudioTranscription: # Validate URL contains WatsonX audio transcription endpoint assert "/ml/v1/audio/transcriptions" in captured_request["url"] assert "version=" in captured_request["url"] - assert "project_id=test-project-123" in captured_request["url"] + # project_id should NOT be in URL (it should be in form data instead) + assert "project_id=test-project-123" not in captured_request["url"] # Validate headers contain WatsonX auth assert "Authorization" in captured_request["headers"] assert "Bearer test-bearer-token" in captured_request["headers"]["Authorization"] + + # Validate project_id is in form data, not URL + assert captured_request["data"].get("project_id") == "test-project-123" + + # Validate file is in files dict + assert "file" in captured_request["files"] @pytest.mark.asyncio async def test_watsonx_transcription_request_body(self): """ Test that litellm.transcription sends correct request body for WatsonX. + + Validates that: + - Request uses multipart/form-data (data + files) + - Model name has watsonx/ prefix removed + - project_id is in form data, not URL + - Audio file is in files dict + - OpenAI params are included in form data """ captured_request = {} @@ -94,9 +110,24 @@ class TestWatsonXAudioTranscription: except Exception: pass # We just want to capture the request - # Validate request body contains expected fields + # Validate form data contains expected fields data = captured_request.get("data", {}) + + print("JSON DUMPS captured_request:") + print(json.dumps(captured_request, indent=4, default=str)) + + # Model name should NOT have watsonx/ prefix assert data.get("model") == "whisper-large-v3-turbo" + + # project_id should be in form data + assert data.get("project_id") == "test-project-123" + + # OpenAI params should be in form data assert data.get("language") == "en" assert data.get("temperature") == 0.5 assert data.get("response_format") == "verbose_json" # Default for cost calculation + + # Validate file is in files dict (multipart/form-data) + files = captured_request.get("files", {}) + assert "file" in files + assert isinstance(files["file"], tuple) # Should be (filename, content, content_type) From 1cdfb3da8fb81c293ed94a8628ce9dafbc703542 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Mon, 1 Dec 2025 19:14:12 -0800 Subject: [PATCH 079/370] [Bug Fix] - Fix `litellm_enterprise` ensure imported routes exist (#17337) * test_enterprise_routes.py * test_enterprise_routes_all_imports_exist --- .../proxy/enterprise_routes.py | 4 - .../test_litellm/enterprise/proxy/__init__.py | 0 .../proxy/test_enterprise_routes.py | 78 +++++++++++++++++++ 3 files changed, 78 insertions(+), 4 deletions(-) create mode 100644 tests/test_litellm/enterprise/proxy/__init__.py create mode 100644 tests/test_litellm/enterprise/proxy/test_enterprise_routes.py diff --git a/enterprise/litellm_enterprise/proxy/enterprise_routes.py b/enterprise/litellm_enterprise/proxy/enterprise_routes.py index f3227892bbd..e28d8b8a4c6 100644 --- a/enterprise/litellm_enterprise/proxy/enterprise_routes.py +++ b/enterprise/litellm_enterprise/proxy/enterprise_routes.py @@ -5,14 +5,10 @@ from litellm_enterprise.enterprise_callbacks.send_emails.endpoints import ( ) from .audit_logging_endpoints import router as audit_logging_router -from .guardrails.endpoints import router as guardrails_router from .management_endpoints import management_endpoints_router from .utils import _should_block_robots -from .vector_stores.endpoints import router as vector_stores_router router = APIRouter() -router.include_router(vector_stores_router) -router.include_router(guardrails_router) router.include_router(email_events_router) router.include_router(audit_logging_router) router.include_router(management_endpoints_router) diff --git a/tests/test_litellm/enterprise/proxy/__init__.py b/tests/test_litellm/enterprise/proxy/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/test_litellm/enterprise/proxy/test_enterprise_routes.py b/tests/test_litellm/enterprise/proxy/test_enterprise_routes.py new file mode 100644 index 00000000000..a9bf33a21ac --- /dev/null +++ b/tests/test_litellm/enterprise/proxy/test_enterprise_routes.py @@ -0,0 +1,78 @@ +""" +Test enterprise_routes imports work correctly + +This validates that all imports can be resolved to prevent broken imports +from breaking the enterprise proxy initialization. +""" + +import ast +import os + +import pytest + + +def test_enterprise_routes_all_imports_exist(): + """ + Validate that all relative imports in enterprise_routes.py exist in the filesystem. + + This catches any import errors from moved/deleted modules without hardcoding + specific module names. Works by checking that imported files actually exist. + """ + # Path to the enterprise_routes.py source file + enterprise_routes_path = os.path.join( + os.path.dirname(__file__), + "..", "..", "..", "..", + "enterprise", "litellm_enterprise", "proxy", "enterprise_routes.py" + ) + + enterprise_routes_path = os.path.normpath(enterprise_routes_path) + enterprise_proxy_dir = os.path.dirname(enterprise_routes_path) + + if not os.path.exists(enterprise_routes_path): + pytest.skip(f"Enterprise routes file not found at {enterprise_routes_path}") + + # Read and parse the source file + with open(enterprise_routes_path, "r") as f: + source_code = f.read() + + try: + tree = ast.parse(source_code) + except SyntaxError as e: + pytest.fail(f"Syntax error in enterprise_routes.py: {e}") + + # Check all relative imports + missing_imports = [] + + for node in ast.walk(tree): + if isinstance(node, ast.ImportFrom): + # level > 0 means it's a relative import (. or .. etc) + if node.level and node.level > 0: + module = node.module or "" + + # Convert relative import to file path + # e.g., "audit_logging_endpoints" -> "audit_logging_endpoints.py" + # e.g., "vector_stores.endpoints" -> "vector_stores/endpoints.py" + module_path = module.replace(".", os.sep) if module else "" + + # Check both .py file and package directory + file_path = os.path.join(enterprise_proxy_dir, module_path + ".py") if module_path else None + package_path = os.path.join(enterprise_proxy_dir, module_path, "__init__.py") if module_path else None + + # If module is empty (e.g., "from . import something"), skip check + if not module: + continue + + file_exists = file_path and os.path.exists(file_path) + package_exists = package_path and os.path.exists(package_path) + + if not file_exists and not package_exists: + missing_imports.append( + f"Line {node.lineno}: Cannot find '.{module}' " + f"(checked: {file_path} and {package_path})" + ) + + if missing_imports: + error_msg = "Found imports in enterprise_routes.py that don't exist:\n" + error_msg += "\n".join(missing_imports) + error_msg += "\n\nThis usually means a module was moved or deleted but the import wasn't updated." + pytest.fail(error_msg) From 70126d91302233bdb4e4b6aecf5d81b462a94527 Mon Sep 17 00:00:00 2001 From: rioiart Date: Tue, 2 Dec 2025 04:51:42 +0100 Subject: [PATCH 080/370] Fix/new org team validate against org (#17333) * fix: skip user budget/model validation for org-scoped teams When creating a team with organization_id, budget and model constraints should be validated against the organization's limits, not the user's personal limits. This allows org admins with restrictive personal budgets to create teams within their organization's more generous limits. Adds 4 unit tests to verify: - Org-scoped teams bypass user budget validation - Org-scoped teams bypass user model validation - Standalone teams still validate against user limits * fix: enforce user budget/model limits for standalone teams in update_team - Add user-level budget and model validation to update_team endpoint for standalone teams, matching the existing pattern in new_team - Org-scoped teams correctly bypass user validation and use organization limits instead - Add 5 new comprehensive tests covering standalone/org team budget/model validation * fix: Add direct TPM/RPM org limit validation and consolidate user team limit checks - Add direct TPM/RPM comparison against org limits in _check_org_team_limits() - Consolidate budget/models/TPM/RPM user validation into _check_user_team_limits() helper - Ensure user limits only apply to standalone teams (organization_id=None) - Org-scoped teams now validate TPM/RPM against org limits (not user limits) - Add 8 tests for TPM/RPM validation scenarios (org and user limits) - Reduce code duplication between new_team() and update_team() --- .../management_endpoints/team_endpoints.py | 496 +++-- .../test_team_endpoints.py | 1602 +++++++++++++++++ 2 files changed, 1901 insertions(+), 197 deletions(-) diff --git a/litellm/proxy/management_endpoints/team_endpoints.py b/litellm/proxy/management_endpoints/team_endpoints.py index 6d4faae5fd8..b697e01a6ef 100644 --- a/litellm/proxy/management_endpoints/team_endpoints.py +++ b/litellm/proxy/management_endpoints/team_endpoints.py @@ -461,11 +461,65 @@ async def _check_org_team_limits( prisma_client: PrismaClient, ) -> None: """ - Check if the organization team is allocating guaranteed throughput limits. If so, raise an error if we're overallocating. - - Only runs check if tpm_limit_type or rpm_limit_type is "guaranteed_throughput" + Check organization team limits including: + - Team budget vs organization's max_budget + - Team models vs organization's allowed models + - Guaranteed throughput limits (tpm/rpm) if applicable """ + # Validate team budget against organization's max_budget + if ( + data.max_budget is not None + and org_table.litellm_budget_table is not None + and org_table.litellm_budget_table.max_budget is not None + and data.max_budget > org_table.litellm_budget_table.max_budget + ): + raise HTTPException( + status_code=400, + detail={ + "error": f"Team max_budget ({data.max_budget}) exceeds organization's max_budget ({org_table.litellm_budget_table.max_budget}). Organization: {org_table.organization_id}" + }, + ) + + # Validate team models against organization's allowed models + if data.models is not None and len(org_table.models) > 0: + for m in data.models: + if m not in org_table.models: + raise HTTPException( + status_code=400, + detail={ + "error": f"Model '{m}' not in organization's allowed models. Organization allowed models={org_table.models}. Organization: {org_table.organization_id}" + }, + ) + + # Validate team TPM/RPM against organization's TPM/RPM limits (direct comparison) + if ( + data.tpm_limit is not None + and org_table.litellm_budget_table is not None + and org_table.litellm_budget_table.tpm_limit is not None + and data.tpm_limit > org_table.litellm_budget_table.tpm_limit + ): + raise HTTPException( + status_code=400, + detail={ + "error": f"Team tpm_limit ({data.tpm_limit}) exceeds organization's tpm_limit ({org_table.litellm_budget_table.tpm_limit}). Organization: {org_table.organization_id}" + }, + ) + + if ( + data.rpm_limit is not None + and org_table.litellm_budget_table is not None + and org_table.litellm_budget_table.rpm_limit is not None + and data.rpm_limit > org_table.litellm_budget_table.rpm_limit + ): + raise HTTPException( + status_code=400, + detail={ + "error": f"Team rpm_limit ({data.rpm_limit}) exceeds organization's rpm_limit ({org_table.litellm_budget_table.rpm_limit}). Organization: {org_table.organization_id}" + }, + ) + + # Check guaranteed throughput limits (only if applicable) rpm_limit_type = getattr(data, "rpm_limit_type", None) or ( data.metadata.get("rpm_limit_type", None) if data.metadata else None ) @@ -503,6 +557,80 @@ async def _check_org_team_limits( ) +async def _check_user_team_limits( + data: Union[NewTeamRequest, UpdateTeamRequest], + user_api_key_dict: UserAPIKeyAuth, + prisma_client: PrismaClient, + user_api_key_cache: Any, +) -> None: + """ + Check user team limits for standalone teams (not org-scoped). + + This validates: + - Team budget vs user's max_budget + - Team models vs user's allowed models + + Should only be called for standalone teams (when organization_id is None). + For org-scoped teams, use _check_org_team_limits() instead. + """ + # Validate team budget against user's max_budget + if data.max_budget is not None and user_api_key_dict.user_id is not None: + user_obj = await get_user_object( + user_id=user_api_key_dict.user_id, + prisma_client=prisma_client, + user_api_key_cache=user_api_key_cache, + user_id_upsert=False, + ) + + if ( + user_obj is not None + and user_obj.max_budget is not None + and data.max_budget > user_obj.max_budget + ): + raise HTTPException( + status_code=400, + detail={ + "error": f"max budget higher than user max. User max budget={user_obj.max_budget}. User role={user_api_key_dict.user_role}" + }, + ) + + # Validate team models against user's allowed models + if data.models is not None and len(user_api_key_dict.models) > 0: + for m in data.models: + if m not in user_api_key_dict.models: + raise HTTPException( + status_code=400, + detail={ + "error": f"Model not in allowed user models. User allowed models={user_api_key_dict.models}. User id={user_api_key_dict.user_id}" + }, + ) + + # Validate team TPM/RPM against user's TPM/RPM limits + if ( + data.tpm_limit is not None + and user_api_key_dict.tpm_limit is not None + and data.tpm_limit > user_api_key_dict.tpm_limit + ): + raise HTTPException( + status_code=400, + detail={ + "error": f"tpm limit higher than user max. User tpm limit={user_api_key_dict.tpm_limit}. User role={user_api_key_dict.user_role}" + }, + ) + + if ( + data.rpm_limit is not None + and user_api_key_dict.rpm_limit is not None + and data.rpm_limit > user_api_key_dict.rpm_limit + ): + raise HTTPException( + status_code=400, + detail={ + "error": f"rpm limit higher than user max. User rpm limit={user_api_key_dict.rpm_limit}. User role={user_api_key_dict.user_role}" + }, + ) + + #### TEAM MANAGEMENT #### @router.post( "/team/new", @@ -665,61 +793,16 @@ async def new_team( # noqa: PLR0915 user_api_key_dict.user_role is None or user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN ): # don't restrict proxy admin - if ( - data.tpm_limit is not None - and user_api_key_dict.tpm_limit is not None - and data.tpm_limit > user_api_key_dict.tpm_limit - ): - raise HTTPException( - status_code=400, - detail={ - "error": f"tpm limit higher than user max. User tpm limit={user_api_key_dict.tpm_limit}. User role={user_api_key_dict.user_role}" - }, - ) - - if ( - data.rpm_limit is not None - and user_api_key_dict.rpm_limit is not None - and data.rpm_limit > user_api_key_dict.rpm_limit - ): - raise HTTPException( - status_code=400, - detail={ - "error": f"rpm limit higher than user max. User rpm limit={user_api_key_dict.rpm_limit}. User role={user_api_key_dict.user_role}" - }, - ) - - if data.max_budget is not None and user_api_key_dict.user_id is not None: - # Fetch user object to get max_budget - user_obj = await get_user_object( - user_id=user_api_key_dict.user_id, + # Only validate user budget/models/tpm/rpm for standalone teams (not org-scoped) + # For org-scoped teams, validation is done by _check_org_team_limits() + if data.organization_id is None: + await _check_user_team_limits( + data=data, + user_api_key_dict=user_api_key_dict, prisma_client=prisma_client, user_api_key_cache=user_api_key_cache, - user_id_upsert=False, ) - if ( - user_obj is not None - and user_obj.max_budget is not None - and data.max_budget > user_obj.max_budget - ): - raise HTTPException( - status_code=400, - detail={ - "error": f"max budget higher than user max. User max budget={user_obj.max_budget}. User role={user_api_key_dict.user_role}" - }, - ) - - if data.models is not None and len(user_api_key_dict.models) > 0: - for m in data.models: - if m not in user_api_key_dict.models: - raise HTTPException( - status_code=400, - detail={ - "error": f"Model not in allowed user models. User allowed models={user_api_key_dict.models}. User id={user_api_key_dict.user_id}" - }, - ) - if user_api_key_dict.user_id is not None: creating_user_in_list = False for member in data.members_with_roles: @@ -1151,168 +1234,187 @@ async def update_team( }' ``` """ - from litellm.proxy.auth.auth_checks import _cache_team_object - from litellm.proxy.proxy_server import ( - litellm_proxy_admin_name, - llm_router, - prisma_client, - proxy_logging_obj, - user_api_key_cache, - ) - - if prisma_client is None: - raise HTTPException( - status_code=500, - detail={"error": CommonProxyErrors.db_not_connected_error.value}, + try: + from litellm.proxy.auth.auth_checks import _cache_team_object + from litellm.proxy.proxy_server import ( + litellm_proxy_admin_name, + llm_router, + prisma_client, + proxy_logging_obj, + user_api_key_cache, ) - if data.team_id is None: - raise HTTPException(status_code=400, detail={"error": "No team id passed in"}) - verbose_proxy_logger.debug("/team/update - %s", data) - - existing_team_row = await prisma_client.db.litellm_teamtable.find_unique( - where={"team_id": data.team_id} - ) - - if existing_team_row is None: - raise HTTPException( - status_code=404, - detail={"error": f"Team not found, passed team_id={data.team_id}"}, - ) - - if ( - data.organization_id is not None and len(data.organization_id) > 0 - ): # allow unsetting the organization_id - await fetch_and_validate_organization( - organization_id=data.organization_id, - existing_team_row=existing_team_row, - llm_router=llm_router, - prisma_client=prisma_client, - ) - elif data.organization_id is not None and len(data.organization_id) == 0: - # unsetting the organization_id - data.organization_id = None - - # check org team limits - if updating team that belongs to an org - org_id_to_check = ( - data.organization_id - if data.organization_id is not None - else existing_team_row.organization_id - ) - if ( - org_id_to_check is not None - and isinstance(org_id_to_check, str) - and prisma_client is not None - ): - org_table = await get_org_object( - org_id=org_id_to_check, - user_api_key_cache=user_api_key_cache, - prisma_client=prisma_client, - ) - if org_table is not None: - await _check_org_team_limits( - org_table=org_table, - data=data, - prisma_client=prisma_client, + if prisma_client is None: + raise HTTPException( + status_code=500, + detail={"error": CommonProxyErrors.db_not_connected_error.value}, ) - updated_kv = data.json(exclude_unset=True) + if data.team_id is None: + raise HTTPException(status_code=400, detail={"error": "No team id passed in"}) + verbose_proxy_logger.debug("/team/update - %s", data) - # Check budget_duration and budget_reset_at - if data.budget_duration is not None: - from litellm.proxy.common_utils.timezone_utils import get_budget_reset_time + existing_team_row = await prisma_client.db.litellm_teamtable.find_unique( + where={"team_id": data.team_id} + ) - reset_at = get_budget_reset_time(budget_duration=data.budget_duration) + if existing_team_row is None: + raise HTTPException( + status_code=404, + detail={"error": f"Team not found, passed team_id={data.team_id}"}, + ) - # set the budget_reset_at in DB - updated_kv["budget_reset_at"] = reset_at + if ( + data.organization_id is not None and len(data.organization_id) > 0 + ): # allow unsetting the organization_id + await fetch_and_validate_organization( + organization_id=data.organization_id, + existing_team_row=existing_team_row, + llm_router=llm_router, + prisma_client=prisma_client, + ) + elif data.organization_id is not None and len(data.organization_id) == 0: + # unsetting the organization_id + data.organization_id = None - if TeamMemberBudgetHandler.should_create_budget( - team_member_budget=data.team_member_budget, - team_member_rpm_limit=data.team_member_rpm_limit, - team_member_tpm_limit=data.team_member_tpm_limit, - ): - updated_kv = await TeamMemberBudgetHandler.upsert_team_member_budget_table( - team_table=existing_team_row, - user_api_key_dict=user_api_key_dict, - updated_kv=updated_kv, + # check org team limits - if updating team that belongs to an org + org_id_to_check = ( + data.organization_id + if data.organization_id is not None + else existing_team_row.organization_id + ) + if ( + org_id_to_check is not None + and isinstance(org_id_to_check, str) + and prisma_client is not None + ): + org_table = await get_org_object( + org_id=org_id_to_check, + user_api_key_cache=user_api_key_cache, + prisma_client=prisma_client, + ) + if org_table is not None: + await _check_org_team_limits( + org_table=org_table, + data=data, + prisma_client=prisma_client, + ) + + # Check user limits for standalone teams (not org-scoped) + # Skip for PROXY_ADMIN users + if ( + user_api_key_dict.user_role is None + or user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN + ): + # Only validate user budget/models for standalone teams + # For org-scoped teams, validation is done by _check_org_team_limits() above + if org_id_to_check is None: + await _check_user_team_limits( + data=data, + user_api_key_dict=user_api_key_dict, + prisma_client=prisma_client, + user_api_key_cache=user_api_key_cache, + ) + + updated_kv = data.json(exclude_unset=True) + + # Check budget_duration and budget_reset_at + if data.budget_duration is not None: + from litellm.proxy.common_utils.timezone_utils import get_budget_reset_time + + reset_at = get_budget_reset_time(budget_duration=data.budget_duration) + + # set the budget_reset_at in DB + updated_kv["budget_reset_at"] = reset_at + + if TeamMemberBudgetHandler.should_create_budget( team_member_budget=data.team_member_budget, team_member_rpm_limit=data.team_member_rpm_limit, team_member_tpm_limit=data.team_member_tpm_limit, - ) - else: - TeamMemberBudgetHandler._clean_team_member_fields(updated_kv) - - # Check object permission - if data.object_permission is not None: - updated_kv = await handle_update_object_permission( - data_json=updated_kv, - existing_team_row=existing_team_row, - ) - - # update team metadata fields - _team_metadata_fields = LiteLLM_ManagementEndpoint_MetadataFields_Premium - for field in _team_metadata_fields: - if field in updated_kv and updated_kv[field] is not None: - _update_metadata_field( + ): + updated_kv = await TeamMemberBudgetHandler.upsert_team_member_budget_table( + team_table=existing_team_row, + user_api_key_dict=user_api_key_dict, updated_kv=updated_kv, - field_name=field, + team_member_budget=data.team_member_budget, + team_member_rpm_limit=data.team_member_rpm_limit, + team_member_tpm_limit=data.team_member_tpm_limit, + ) + else: + TeamMemberBudgetHandler._clean_team_member_fields(updated_kv) + + # Check object permission + if data.object_permission is not None: + updated_kv = await handle_update_object_permission( + data_json=updated_kv, + existing_team_row=existing_team_row, ) - for field in LiteLLM_ManagementEndpoint_MetadataFields: - if field in updated_kv and updated_kv[field] is not None: - _update_metadata_field( - updated_kv=updated_kv, - field_name=field, + # update team metadata fields + _team_metadata_fields = LiteLLM_ManagementEndpoint_MetadataFields_Premium + for field in _team_metadata_fields: + if field in updated_kv and updated_kv[field] is not None: + _update_metadata_field( + updated_kv=updated_kv, + field_name=field, + ) + + for field in LiteLLM_ManagementEndpoint_MetadataFields: + if field in updated_kv and updated_kv[field] is not None: + _update_metadata_field( + updated_kv=updated_kv, + field_name=field, + ) + + if "model_aliases" in updated_kv: + updated_kv.pop("model_aliases") + _model_id = await _update_model_table( + data=data, + model_id=existing_team_row.model_id, + prisma_client=prisma_client, + user_api_key_dict=user_api_key_dict, + litellm_proxy_admin_name=litellm_proxy_admin_name, + ) + if _model_id is not None: + updated_kv["model_id"] = _model_id + + updated_kv = prisma_client.jsonify_team_object(db_data=updated_kv) + team_row: Optional[LiteLLM_TeamTable] = ( + await prisma_client.db.litellm_teamtable.update( + where={"team_id": data.team_id}, + data=updated_kv, + include={"litellm_model_table": True}, # type: ignore + ) + ) + + if team_row is None or team_row.team_id is None: + raise HTTPException( + status_code=400, + detail={"error": "Team doesn't exist. Got={}".format(team_row)}, ) - if "model_aliases" in updated_kv: - updated_kv.pop("model_aliases") - _model_id = await _update_model_table( - data=data, - model_id=existing_team_row.model_id, - prisma_client=prisma_client, - user_api_key_dict=user_api_key_dict, - litellm_proxy_admin_name=litellm_proxy_admin_name, - ) - if _model_id is not None: - updated_kv["model_id"] = _model_id - - updated_kv = prisma_client.jsonify_team_object(db_data=updated_kv) - team_row: Optional[LiteLLM_TeamTable] = ( - await prisma_client.db.litellm_teamtable.update( - where={"team_id": data.team_id}, - data=updated_kv, - include={"litellm_model_table": True}, # type: ignore - ) - ) - - if team_row is None or team_row.team_id is None: - raise HTTPException( - status_code=400, - detail={"error": "Team doesn't exist. Got={}".format(team_row)}, + verbose_proxy_logger.info("Successfully updated team - %s, info", team_row.team_id) + await _cache_team_object( + team_id=team_row.team_id, + team_table=LiteLLM_TeamTableCachedObj(**team_row.model_dump()), + user_api_key_cache=user_api_key_cache, + proxy_logging_obj=proxy_logging_obj, ) - verbose_proxy_logger.info("Successfully updated team - %s, info", team_row.team_id) - await _cache_team_object( - team_id=team_row.team_id, - team_table=LiteLLM_TeamTableCachedObj(**team_row.model_dump()), - user_api_key_cache=user_api_key_cache, - proxy_logging_obj=proxy_logging_obj, - ) + # Enterprise Feature - Audit Logging. Enable with litellm.store_audit_logs = True + if litellm.store_audit_logs is True: + await _create_team_update_audit_log( + existing_team_row=existing_team_row, + updated_kv=updated_kv, + team_id=data.team_id, + litellm_changed_by=litellm_changed_by, + user_api_key_dict=user_api_key_dict, + litellm_proxy_admin_name=litellm_proxy_admin_name, + ) - # Enterprise Feature - Audit Logging. Enable with litellm.store_audit_logs = True - if litellm.store_audit_logs is True: - await _create_team_update_audit_log( - existing_team_row=existing_team_row, - updated_kv=updated_kv, - team_id=data.team_id, - litellm_changed_by=litellm_changed_by, - user_api_key_dict=user_api_key_dict, - litellm_proxy_admin_name=litellm_proxy_admin_name, - ) - - return {"team_id": team_row.team_id, "data": team_row} + return {"team_id": team_row.team_id, "data": team_row} + except Exception as e: + raise handle_exception_on_proxy(e) async def handle_update_object_permission( diff --git a/tests/test_litellm/proxy/management_endpoints/test_team_endpoints.py b/tests/test_litellm/proxy/management_endpoints/test_team_endpoints.py index 86b23c98ba5..06ec71a84f8 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_team_endpoints.py +++ b/tests/test_litellm/proxy/management_endpoints/test_team_endpoints.py @@ -2015,3 +2015,1605 @@ async def test_new_team_max_budget_within_user_limit(): assert result is not None assert result["team_id"] == "team-within-budget-789" assert result["max_budget"] == 50.0 + + +@pytest.mark.asyncio +async def test_new_team_org_scoped_budget_bypasses_user_limit(): + """ + Test that /team/new with organization_id does NOT validate budget against user's personal max_budget. + + This is the bug fix for: When an org admin creates an org-scoped team, the team's budget should + be validated against the organization's limits, not the user's personal limits. + + Scenario: + - Organization has max_budget=$100 + - User (org admin) has personal max_budget=$3 + - Team is created with organization_id and max_budget=$50 + - Expected: Should succeed (within org's $100 limit) + - Bug behavior: Would fail with "max budget higher than user max. User max budget=3.0" + """ + from fastapi import Request + + from litellm.proxy._types import NewTeamRequest, UserAPIKeyAuth, LiteLLM_UserTable, LiteLLM_OrganizationTable + from litellm.proxy.management_endpoints.team_endpoints import new_team + + # Create non-admin user with very restrictive personal budget ($3) + org_admin_user = UserAPIKeyAuth( + user_role=LitellmUserRoles.INTERNAL_USER, + user_id="org-admin-user-123", + user_max_budget=3.0, # Restrictive personal budget + models=[], # Empty models list to bypass model validation + ) + + # Create team request with budget ($50) that's within org's limit but exceeds user's personal limit + team_request = NewTeamRequest( + team_alias="org-scoped-team", + max_budget=50.0, # Within org's $100 limit, but exceeds user's $3 limit + organization_id="test-org-123", # This makes it an org-scoped team + ) + + dummy_request = MagicMock(spec=Request) + + with patch("litellm.proxy.proxy_server.prisma_client") as mock_prisma, patch( + "litellm.proxy.proxy_server.user_api_key_cache" + ) as mock_cache, patch( + "litellm.proxy.proxy_server._license_check" + ) as mock_license, patch( + "litellm.proxy.proxy_server.litellm_proxy_admin_name", "admin" + ), patch( + "litellm.proxy.proxy_server.create_audit_log_for_update", new=AsyncMock() + ) as mock_audit, patch( + "litellm.proxy.management_endpoints.team_endpoints.get_org_object" + ) as mock_get_org: + + # Setup mocks + mock_prisma.db.litellm_teamtable.count = AsyncMock(return_value=0) + mock_license.is_team_count_over_limit.return_value = False + mock_prisma.jsonify_team_object = lambda db_data: db_data + mock_prisma.get_data = AsyncMock(return_value=None) + mock_prisma.update_data = AsyncMock() + + # Mock organization with $100 budget + mock_org = MagicMock(spec=LiteLLM_OrganizationTable) + mock_org.organization_id = "test-org-123" + mock_org.max_budget = 100.0 + mock_org.models = ["gpt-4", "gpt-3.5-turbo", "claude-3-opus"] + mock_org.litellm_budget_table = None # No budget table for this test + mock_get_org.return_value = mock_org + + # Mock user cache to return user with restrictive personal budget + mock_user_obj = LiteLLM_UserTable( + user_id="org-admin-user-123", + max_budget=3.0, # Restrictive personal budget + ) + mock_cache.async_get_cache = AsyncMock(return_value=mock_user_obj) + + # Mock team creation + mock_created_team = MagicMock() + mock_created_team.team_id = "team-org-scoped-789" + mock_created_team.team_alias = "org-scoped-team" + mock_created_team.max_budget = 50.0 + mock_created_team.organization_id = "test-org-123" + mock_created_team.members_with_roles = [] + mock_created_team.metadata = None + mock_created_team.model_dump.return_value = { + "team_id": "team-org-scoped-789", + "team_alias": "org-scoped-team", + "max_budget": 50.0, + "organization_id": "test-org-123", + "members_with_roles": [], + } + mock_prisma.db.litellm_teamtable.create = AsyncMock(return_value=mock_created_team) + mock_prisma.db.litellm_teamtable.update = AsyncMock(return_value=mock_created_team) + + # Mock model table + mock_prisma.db.litellm_modeltable = MagicMock() + mock_prisma.db.litellm_modeltable.create = AsyncMock(return_value=MagicMock(id="model123")) + + # Mock user table operations + mock_user = MagicMock() + mock_user.user_id = "org-admin-user-123" + mock_user.model_dump.return_value = {"user_id": "org-admin-user-123", "teams": ["team-org-scoped-789"]} + mock_prisma.db.litellm_usertable = MagicMock() + mock_prisma.db.litellm_usertable.upsert = AsyncMock(return_value=mock_user) + mock_prisma.db.litellm_usertable.update = AsyncMock(return_value=mock_user) + + # Mock team membership table + mock_membership = MagicMock() + mock_membership.model_dump.return_value = { + "team_id": "team-org-scoped-789", + "user_id": "org-admin-user-123", + "budget_id": None, + } + mock_prisma.db.litellm_teammembership = MagicMock() + mock_prisma.db.litellm_teammembership.create = AsyncMock(return_value=mock_membership) + + # Should NOT raise an exception - the fix should bypass user budget validation for org-scoped teams + result = await new_team( + data=team_request, + http_request=dummy_request, + user_api_key_dict=org_admin_user, + ) + + # Verify the team was created successfully with the higher budget + assert result is not None + assert result["team_id"] == "team-org-scoped-789" + assert result["max_budget"] == 50.0 + assert result["organization_id"] == "test-org-123" + + +@pytest.mark.asyncio +async def test_new_team_org_scoped_models_bypasses_user_limit(): + """ + Test that /team/new with organization_id does NOT validate models against user's personal models. + + This is the bug fix for: When an org admin creates an org-scoped team, the team's models should + be validated against the organization's models, not the user's personal models. + + Scenario: + - Organization has models=['gpt-4', 'gpt-3.5-turbo', 'claude-3-opus'] + - User (org admin) has personal models=['no-default-models'] + - Team is created with organization_id and models=['gpt-4'] + - Expected: Should succeed (within org's allowed models) + - Bug behavior: Would fail with "Model not in allowed user models" + """ + from fastapi import Request + + from litellm.proxy._types import NewTeamRequest, UserAPIKeyAuth, LiteLLM_UserTable, LiteLLM_OrganizationTable + from litellm.proxy.management_endpoints.team_endpoints import new_team + + # Create non-admin user with restrictive personal models + org_admin_user = UserAPIKeyAuth( + user_role=LitellmUserRoles.INTERNAL_USER, + user_id="org-admin-user-456", + user_max_budget=None, # No budget restriction for this test + models=["no-default-models"], # Restrictive personal models + ) + + # Create team request with models that are within org's allowed models but not user's + team_request = NewTeamRequest( + team_alias="org-scoped-models-team", + models=["gpt-4"], # Within org's allowed models, but not in user's personal models + organization_id="test-org-456", # This makes it an org-scoped team + ) + + dummy_request = MagicMock(spec=Request) + + with patch("litellm.proxy.proxy_server.prisma_client") as mock_prisma, patch( + "litellm.proxy.proxy_server.user_api_key_cache" + ) as mock_cache, patch( + "litellm.proxy.proxy_server._license_check" + ) as mock_license, patch( + "litellm.proxy.proxy_server.litellm_proxy_admin_name", "admin" + ), patch( + "litellm.proxy.proxy_server.create_audit_log_for_update", new=AsyncMock() + ) as mock_audit, patch( + "litellm.proxy.management_endpoints.team_endpoints.get_org_object" + ) as mock_get_org: + + # Setup mocks + mock_prisma.db.litellm_teamtable.count = AsyncMock(return_value=0) + mock_license.is_team_count_over_limit.return_value = False + mock_prisma.jsonify_team_object = lambda db_data: db_data + mock_prisma.get_data = AsyncMock(return_value=None) + mock_prisma.update_data = AsyncMock() + + # Mock organization with allowed models + mock_org = MagicMock(spec=LiteLLM_OrganizationTable) + mock_org.organization_id = "test-org-456" + mock_org.max_budget = 100.0 + mock_org.models = ["gpt-4", "gpt-3.5-turbo", "claude-3-opus"] + mock_org.litellm_budget_table = None + mock_get_org.return_value = mock_org + + # Mock user cache + mock_user_obj = LiteLLM_UserTable( + user_id="org-admin-user-456", + max_budget=None, + ) + mock_cache.async_get_cache = AsyncMock(return_value=mock_user_obj) + + # Mock team creation + mock_created_team = MagicMock() + mock_created_team.team_id = "team-org-scoped-models-789" + mock_created_team.team_alias = "org-scoped-models-team" + mock_created_team.max_budget = None + mock_created_team.organization_id = "test-org-456" + mock_created_team.models = ["gpt-4"] + mock_created_team.members_with_roles = [] + mock_created_team.metadata = None + mock_created_team.model_dump.return_value = { + "team_id": "team-org-scoped-models-789", + "team_alias": "org-scoped-models-team", + "max_budget": None, + "organization_id": "test-org-456", + "models": ["gpt-4"], + "members_with_roles": [], + } + mock_prisma.db.litellm_teamtable.create = AsyncMock(return_value=mock_created_team) + mock_prisma.db.litellm_teamtable.update = AsyncMock(return_value=mock_created_team) + + # Mock model table + mock_prisma.db.litellm_modeltable = MagicMock() + mock_prisma.db.litellm_modeltable.create = AsyncMock(return_value=MagicMock(id="model123")) + + # Mock user table operations + mock_user = MagicMock() + mock_user.user_id = "org-admin-user-456" + mock_user.model_dump.return_value = {"user_id": "org-admin-user-456", "teams": ["team-org-scoped-models-789"]} + mock_prisma.db.litellm_usertable = MagicMock() + mock_prisma.db.litellm_usertable.upsert = AsyncMock(return_value=mock_user) + mock_prisma.db.litellm_usertable.update = AsyncMock(return_value=mock_user) + + # Mock team membership table + mock_membership = MagicMock() + mock_membership.model_dump.return_value = { + "team_id": "team-org-scoped-models-789", + "user_id": "org-admin-user-456", + "budget_id": None, + } + mock_prisma.db.litellm_teammembership = MagicMock() + mock_prisma.db.litellm_teammembership.create = AsyncMock(return_value=mock_membership) + + # Should NOT raise an exception - the fix should bypass user model validation for org-scoped teams + result = await new_team( + data=team_request, + http_request=dummy_request, + user_api_key_dict=org_admin_user, + ) + + # Verify the team was created successfully with the org's models + assert result is not None + assert result["team_id"] == "team-org-scoped-models-789" + assert result["models"] == ["gpt-4"] + assert result["organization_id"] == "test-org-456" + + +@pytest.mark.asyncio +async def test_new_team_standalone_validates_against_user_models(): + """ + Test that /team/new WITHOUT organization_id still validates models against user's personal models. + + This ensures that standalone teams (not org-scoped) still use user-level validation. + + Scenario: + - User has personal models=['no-default-models'] + - Team is created WITHOUT organization_id and models=['gpt-4'] + - Expected: Should fail with "Model not in allowed user models" + """ + from fastapi import Request + + from litellm.proxy._types import NewTeamRequest, ProxyException, UserAPIKeyAuth + from litellm.proxy.management_endpoints.team_endpoints import new_team + + # Create non-admin user with restrictive personal models + non_admin_user = UserAPIKeyAuth( + user_role=LitellmUserRoles.INTERNAL_USER, + user_id="non-admin-user-789", + user_max_budget=None, + models=["no-default-models"], # Restrictive personal models + ) + + # Create standalone team request (no organization_id) with models not in user's list + team_request = NewTeamRequest( + team_alias="standalone-team", + models=["gpt-4"], # Not in user's allowed models + # Note: No organization_id - this is a standalone team + ) + + dummy_request = MagicMock(spec=Request) + + with patch("litellm.proxy.proxy_server.prisma_client") as mock_prisma, patch( + "litellm.proxy.proxy_server._license_check" + ) as mock_license, patch( + "litellm.proxy.proxy_server.litellm_proxy_admin_name", "admin" + ), patch( + "litellm.proxy.proxy_server.create_audit_log_for_update", new=AsyncMock() + ) as mock_audit: + # Setup basic mocks + mock_prisma.db.litellm_teamtable.count = AsyncMock(return_value=0) + mock_license.is_team_count_over_limit.return_value = False + mock_prisma.get_data = AsyncMock(return_value=None) + + # Should raise ProxyException because gpt-4 is not in user's allowed models + with pytest.raises(ProxyException) as exc_info: + await new_team( + data=team_request, + http_request=dummy_request, + user_api_key_dict=non_admin_user, + ) + + # Verify exception details + assert exc_info.value.code == '400' + assert "Model not in allowed user models" in str(exc_info.value.message) + assert "no-default-models" in str(exc_info.value.message) + + +@pytest.mark.asyncio +async def test_new_team_standalone_validates_against_user_budget(): + """ + Test that /team/new WITHOUT organization_id still validates budget against user's personal max_budget. + + This ensures that standalone teams (not org-scoped) still use user-level validation. + This is essentially the same as test_new_team_max_budget_exceeds_user_max_budget but + explicitly showing the contrast with org-scoped teams. + + Scenario: + - User has personal max_budget=$3 + - Team is created WITHOUT organization_id and max_budget=$50 + - Expected: Should fail with "max budget higher than user max" + """ + from fastapi import Request + + from litellm.proxy._types import NewTeamRequest, ProxyException, UserAPIKeyAuth, LiteLLM_UserTable + from litellm.proxy.management_endpoints.team_endpoints import new_team + + # Create non-admin user with restrictive personal budget + non_admin_user = UserAPIKeyAuth( + user_role=LitellmUserRoles.INTERNAL_USER, + user_id="non-admin-user-budget-789", + user_max_budget=100.0, # This is for key auth, actual budget is from user object + models=[], # Empty models list to bypass model validation + ) + + # Create standalone team request (no organization_id) with budget exceeding user's limit + team_request = NewTeamRequest( + team_alias="standalone-budget-team", + max_budget=50.0, # Exceeds user's personal budget + # Note: No organization_id - this is a standalone team + ) + + dummy_request = MagicMock(spec=Request) + + with patch("litellm.proxy.proxy_server.prisma_client") as mock_prisma, patch( + "litellm.proxy.proxy_server._license_check" + ) as mock_license, patch( + "litellm.proxy.proxy_server.user_api_key_cache" + ) as mock_cache, patch( + "litellm.proxy.proxy_server.litellm_proxy_admin_name", "admin" + ), patch( + "litellm.proxy.proxy_server.create_audit_log_for_update", new=AsyncMock() + ) as mock_audit: + # Setup basic mocks + mock_prisma.db.litellm_teamtable.count = AsyncMock(return_value=0) + mock_license.is_team_count_over_limit.return_value = False + mock_prisma.get_data = AsyncMock(return_value=None) + + # Mock user cache to return user with restrictive personal budget ($3) + mock_user_obj = LiteLLM_UserTable( + user_id="non-admin-user-budget-789", + max_budget=3.0, # Restrictive personal budget + ) + mock_cache.async_get_cache = AsyncMock(return_value=mock_user_obj) + + # Should raise ProxyException because budget exceeds user's max_budget + with pytest.raises(ProxyException) as exc_info: + await new_team( + data=team_request, + http_request=dummy_request, + user_api_key_dict=non_admin_user, + ) + + # Verify exception details + assert exc_info.value.code == '400' + assert "max budget higher than user max" in str(exc_info.value.message) + assert "3.0" in str(exc_info.value.message) # User's max_budget should be mentioned + + +@pytest.mark.asyncio +async def test_new_team_org_scoped_budget_exceeds_org_limit(): + """ + Test that /team/new with organization_id fails when team budget exceeds organization's max_budget. + + Scenario: + - Organization has max_budget=$100 + - Team is created with organization_id and max_budget=$150 + - Expected: Should fail with error about exceeding org budget + """ + from fastapi import Request + + from litellm.proxy._types import NewTeamRequest, ProxyException, UserAPIKeyAuth, LiteLLM_OrganizationTable, LiteLLM_BudgetTable + from litellm.proxy.management_endpoints.team_endpoints import new_team + + # Create user (org admin) + org_admin_user = UserAPIKeyAuth( + user_role=LitellmUserRoles.INTERNAL_USER, + user_id="org-admin-user-budget-test", + models=[], + ) + + # Create team request with budget ($150) that exceeds org's limit ($100) + team_request = NewTeamRequest( + team_alias="org-team-exceeds-budget", + max_budget=150.0, # Exceeds org's $100 limit + organization_id="test-org-budget-limit", + ) + + dummy_request = MagicMock(spec=Request) + + with patch("litellm.proxy.proxy_server.prisma_client") as mock_prisma, patch( + "litellm.proxy.proxy_server.user_api_key_cache" + ) as mock_cache, patch( + "litellm.proxy.proxy_server._license_check" + ) as mock_license, patch( + "litellm.proxy.proxy_server.litellm_proxy_admin_name", "admin" + ), patch( + "litellm.proxy.proxy_server.create_audit_log_for_update", new=AsyncMock() + ) as mock_audit, patch( + "litellm.proxy.management_endpoints.team_endpoints.get_org_object" + ) as mock_get_org: + + # Setup mocks + mock_prisma.db.litellm_teamtable.count = AsyncMock(return_value=0) + mock_license.is_team_count_over_limit.return_value = False + mock_prisma.get_data = AsyncMock(return_value=None) + + # Mock organization with $100 budget limit + mock_budget_table = MagicMock(spec=LiteLLM_BudgetTable) + mock_budget_table.max_budget = 100.0 + + mock_org = MagicMock(spec=LiteLLM_OrganizationTable) + mock_org.organization_id = "test-org-budget-limit" + mock_org.models = ["gpt-4", "gpt-3.5-turbo"] + mock_org.litellm_budget_table = mock_budget_table + mock_get_org.return_value = mock_org + + # Should raise ProxyException because team budget exceeds org budget + with pytest.raises(ProxyException) as exc_info: + await new_team( + data=team_request, + http_request=dummy_request, + user_api_key_dict=org_admin_user, + ) + + # Verify exception details + assert exc_info.value.code == '400' + assert "exceeds organization" in str(exc_info.value.message).lower() or "organization" in str(exc_info.value.message).lower() + + +@pytest.mark.asyncio +async def test_new_team_org_scoped_models_not_in_org_models(): + """ + Test that /team/new with organization_id fails when team models are not in organization's allowed models. + + Scenario: + - Organization has models=['gpt-4', 'gpt-3.5-turbo'] + - Team is created with organization_id and models=['claude-3-opus'] + - Expected: Should fail with error about model not in org's allowed models + """ + from fastapi import Request + + from litellm.proxy._types import NewTeamRequest, ProxyException, UserAPIKeyAuth, LiteLLM_OrganizationTable, LiteLLM_BudgetTable + from litellm.proxy.management_endpoints.team_endpoints import new_team + + # Create user (org admin) + org_admin_user = UserAPIKeyAuth( + user_role=LitellmUserRoles.INTERNAL_USER, + user_id="org-admin-user-models-test", + models=[], + ) + + # Create team request with model not in org's allowed list + team_request = NewTeamRequest( + team_alias="org-team-invalid-model", + models=["claude-3-opus"], # Not in org's allowed models + organization_id="test-org-models-limit", + ) + + dummy_request = MagicMock(spec=Request) + + with patch("litellm.proxy.proxy_server.prisma_client") as mock_prisma, patch( + "litellm.proxy.proxy_server.user_api_key_cache" + ) as mock_cache, patch( + "litellm.proxy.proxy_server._license_check" + ) as mock_license, patch( + "litellm.proxy.proxy_server.litellm_proxy_admin_name", "admin" + ), patch( + "litellm.proxy.proxy_server.create_audit_log_for_update", new=AsyncMock() + ) as mock_audit, patch( + "litellm.proxy.management_endpoints.team_endpoints.get_org_object" + ) as mock_get_org: + + # Setup mocks + mock_prisma.db.litellm_teamtable.count = AsyncMock(return_value=0) + mock_license.is_team_count_over_limit.return_value = False + mock_prisma.get_data = AsyncMock(return_value=None) + + # Mock organization with specific allowed models (not including claude-3-opus) + mock_org = MagicMock(spec=LiteLLM_OrganizationTable) + mock_org.organization_id = "test-org-models-limit" + mock_org.models = ["gpt-4", "gpt-3.5-turbo"] # claude-3-opus is NOT allowed + mock_org.litellm_budget_table = None + mock_get_org.return_value = mock_org + + # Should raise ProxyException because claude-3-opus is not in org's allowed models + with pytest.raises(ProxyException) as exc_info: + await new_team( + data=team_request, + http_request=dummy_request, + user_api_key_dict=org_admin_user, + ) + + # Verify exception details + assert exc_info.value.code == '400' + assert "claude-3-opus" in str(exc_info.value.message) or "organization" in str(exc_info.value.message).lower() + + +@pytest.mark.asyncio +async def test_update_team_standalone_budget_exceeds_user_limit(): + """ + Test that /team/update for a standalone team fails when new budget exceeds user's max_budget. + + Scenario: + - User has personal max_budget=$50 + - Standalone team exists (no organization_id) + - User tries to update team budget to $100 + - Expected: Should fail with error about exceeding user budget + """ + from fastapi import Request + + from litellm.proxy._types import UpdateTeamRequest, ProxyException, UserAPIKeyAuth, LiteLLM_UserTable + from litellm.proxy.management_endpoints.team_endpoints import update_team + + # Create non-admin user with restrictive personal budget + non_admin_user = UserAPIKeyAuth( + user_role=LitellmUserRoles.INTERNAL_USER, + user_id="non-admin-update-test", + models=[], + ) + + # Create update request with budget exceeding user's limit + update_request = UpdateTeamRequest( + team_id="standalone-team-123", + max_budget=100.0, # Exceeds user's $50 limit + ) + + dummy_request = MagicMock(spec=Request) + + with patch("litellm.proxy.proxy_server.prisma_client") as mock_prisma, patch( + "litellm.proxy.proxy_server.user_api_key_cache" + ) as mock_cache, patch( + "litellm.proxy.proxy_server.litellm_proxy_admin_name", "admin" + ), patch( + "litellm.proxy.proxy_server.create_audit_log_for_update", new=AsyncMock() + ) as mock_audit: + + # Mock existing standalone team (no organization_id) + mock_existing_team = MagicMock() + mock_existing_team.team_id = "standalone-team-123" + mock_existing_team.organization_id = None # Standalone team + mock_existing_team.max_budget = 30.0 + mock_existing_team.model_dump.return_value = { + "team_id": "standalone-team-123", + "organization_id": None, + "max_budget": 30.0, + } + mock_prisma.db.litellm_teamtable.find_unique = AsyncMock(return_value=mock_existing_team) + + # Mock user cache to return user with restrictive budget + mock_user_obj = LiteLLM_UserTable( + user_id="non-admin-update-test", + max_budget=50.0, # User's budget limit + ) + mock_cache.async_get_cache = AsyncMock(return_value=mock_user_obj) + + # Should raise ProxyException because new budget exceeds user's max_budget + with pytest.raises(ProxyException) as exc_info: + await update_team( + data=update_request, + http_request=dummy_request, + user_api_key_dict=non_admin_user, + ) + + # Verify exception details + assert exc_info.value.code == '400' + assert "budget" in str(exc_info.value.message).lower() + + +@pytest.mark.asyncio +async def test_update_team_org_scoped_budget_exceeds_org_limit(): + """ + Test that /team/update for an org-scoped team fails when new budget exceeds organization's max_budget. + + Scenario: + - Organization has max_budget=$100 + - Org-scoped team exists + - User tries to update team budget to $150 + - Expected: Should fail with error about exceeding org budget + """ + from fastapi import Request + + from litellm.proxy._types import UpdateTeamRequest, ProxyException, UserAPIKeyAuth, LiteLLM_OrganizationTable, LiteLLM_BudgetTable + from litellm.proxy.management_endpoints.team_endpoints import update_team + + # Create user (org admin) + org_admin_user = UserAPIKeyAuth( + user_role=LitellmUserRoles.INTERNAL_USER, + user_id="org-admin-update-test", + models=[], + ) + + # Create update request with budget exceeding org's limit + update_request = UpdateTeamRequest( + team_id="org-team-456", + max_budget=150.0, # Exceeds org's $100 limit + ) + + dummy_request = MagicMock(spec=Request) + + # Mock organization with $100 budget limit + mock_budget_table = MagicMock(spec=LiteLLM_BudgetTable) + mock_budget_table.max_budget = 100.0 + + mock_org = MagicMock(spec=LiteLLM_OrganizationTable) + mock_org.organization_id = "test-org-update" + mock_org.models = ["gpt-4"] + mock_org.litellm_budget_table = mock_budget_table + + with patch("litellm.proxy.proxy_server.prisma_client") as mock_prisma, patch( + "litellm.proxy.proxy_server.user_api_key_cache" + ) as mock_cache, patch( + "litellm.proxy.proxy_server.litellm_proxy_admin_name", "admin" + ), patch( + "litellm.proxy.proxy_server.create_audit_log_for_update", new=AsyncMock() + ) as mock_audit, patch( + "litellm.proxy.management_endpoints.team_endpoints.get_org_object", + new=AsyncMock(return_value=mock_org) + ) as mock_get_org: + + # Mock existing org-scoped team + mock_existing_team = MagicMock() + mock_existing_team.team_id = "org-team-456" + mock_existing_team.organization_id = "test-org-update" + mock_existing_team.max_budget = 80.0 + mock_existing_team.model_dump.return_value = { + "team_id": "org-team-456", + "organization_id": "test-org-update", + "max_budget": 80.0, + } + mock_prisma.db.litellm_teamtable.find_unique = AsyncMock(return_value=mock_existing_team) + + # Should raise ProxyException because new budget exceeds org's max_budget + with pytest.raises(ProxyException) as exc_info: + await update_team( + data=update_request, + http_request=dummy_request, + user_api_key_dict=org_admin_user, + ) + + # Verify exception details + assert exc_info.value.code == '400' + assert "organization" in str(exc_info.value.message).lower() or "budget" in str(exc_info.value.message).lower() + + +@pytest.mark.asyncio +async def test_update_team_standalone_models_exceeds_user_limit(): + """ + Test that /team/update for a standalone team fails when models are not in user's allowed models. + + Scenario: + - User has personal models=['gpt-3.5-turbo'] + - Standalone team exists (no organization_id) + - User tries to update team models to ['gpt-4'] (not in user's allowed models) + - Expected: Should fail with error about model not in user's allowed models + """ + from fastapi import Request + + from litellm.proxy._types import UpdateTeamRequest, ProxyException, UserAPIKeyAuth + from litellm.proxy.management_endpoints.team_endpoints import update_team + + # Create non-admin user with restrictive personal models + non_admin_user = UserAPIKeyAuth( + user_role=LitellmUserRoles.INTERNAL_USER, + user_id="non-admin-update-models-test", + models=["gpt-3.5-turbo"], # Restrictive model list + ) + + # Create update request with model not in user's allowed list + update_request = UpdateTeamRequest( + team_id="standalone-team-models-123", + models=["gpt-4"], # Not in user's allowed models + ) + + dummy_request = MagicMock(spec=Request) + + with patch("litellm.proxy.proxy_server.prisma_client") as mock_prisma, patch( + "litellm.proxy.proxy_server.user_api_key_cache" + ) as mock_cache, patch( + "litellm.proxy.proxy_server.litellm_proxy_admin_name", "admin" + ), patch( + "litellm.proxy.proxy_server.create_audit_log_for_update", new=AsyncMock() + ) as mock_audit: + + # Mock existing standalone team (no organization_id) + mock_existing_team = MagicMock() + mock_existing_team.team_id = "standalone-team-models-123" + mock_existing_team.organization_id = None # Standalone team + mock_existing_team.models = ["gpt-3.5-turbo"] + mock_existing_team.model_dump.return_value = { + "team_id": "standalone-team-models-123", + "organization_id": None, + "models": ["gpt-3.5-turbo"], + } + mock_prisma.db.litellm_teamtable.find_unique = AsyncMock(return_value=mock_existing_team) + + # Should raise ProxyException because model not in user's allowed models + with pytest.raises(ProxyException) as exc_info: + await update_team( + data=update_request, + http_request=dummy_request, + user_api_key_dict=non_admin_user, + ) + + # Verify exception details + assert exc_info.value.code == '400' + assert "model" in str(exc_info.value.message).lower() + + +@pytest.mark.asyncio +async def test_update_team_org_scoped_budget_bypasses_user_limit(): + """ + Test that /team/update for an org-scoped team does NOT validate budget against user's personal max_budget. + + Scenario: + - Organization has max_budget=$100 + - User (org admin) has personal max_budget=$3 + - Org-scoped team exists with current budget=$30 + - User tries to update team budget to $50 (within org limit, exceeds user limit) + - Expected: Should succeed (validated against org, not user) + """ + from fastapi import Request + + from litellm.proxy._types import UpdateTeamRequest, UserAPIKeyAuth, LiteLLM_UserTable, LiteLLM_OrganizationTable, LiteLLM_BudgetTable + from litellm.proxy.management_endpoints.team_endpoints import update_team + + # Create user with very restrictive personal budget ($3) + org_admin_user = UserAPIKeyAuth( + user_role=LitellmUserRoles.INTERNAL_USER, + user_id="org-admin-update-budget-test", + models=[], + ) + + # Create update request with budget within org limit but exceeding user limit + update_request = UpdateTeamRequest( + team_id="org-team-update-budget-123", + max_budget=50.0, # Within org's $100 limit, exceeds user's $3 limit + ) + + dummy_request = MagicMock(spec=Request) + + # Mock organization with $100 budget limit + mock_budget_table = MagicMock(spec=LiteLLM_BudgetTable) + mock_budget_table.max_budget = 100.0 + + mock_org = MagicMock(spec=LiteLLM_OrganizationTable) + mock_org.organization_id = "test-org-update-budget" + mock_org.models = ["gpt-4", "gpt-3.5-turbo"] + mock_org.litellm_budget_table = mock_budget_table + + with patch("litellm.proxy.proxy_server.prisma_client") as mock_prisma, patch( + "litellm.proxy.proxy_server.user_api_key_cache" + ) as mock_cache, patch( + "litellm.proxy.proxy_server.litellm_proxy_admin_name", "admin" + ), patch( + "litellm.proxy.proxy_server.create_audit_log_for_update", new=AsyncMock() + ) as mock_audit, patch( + "litellm.proxy.management_endpoints.team_endpoints.get_org_object", + new=AsyncMock(return_value=mock_org) + ) as mock_get_org: + + # Mock existing org-scoped team + mock_existing_team = MagicMock() + mock_existing_team.team_id = "org-team-update-budget-123" + mock_existing_team.organization_id = "test-org-update-budget" + mock_existing_team.max_budget = 30.0 + mock_existing_team.model_id = None + mock_existing_team.model_dump.return_value = { + "team_id": "org-team-update-budget-123", + "organization_id": "test-org-update-budget", + "max_budget": 30.0, + } + mock_prisma.db.litellm_teamtable.find_unique = AsyncMock(return_value=mock_existing_team) + mock_prisma.jsonify_team_object = lambda db_data: db_data + + # Mock user cache to return user with restrictive budget + mock_user_obj = LiteLLM_UserTable( + user_id="org-admin-update-budget-test", + max_budget=3.0, # Restrictive personal budget + ) + mock_cache.async_get_cache = AsyncMock(return_value=mock_user_obj) + mock_cache.async_set_cache = AsyncMock() # Mock cache set for _cache_team_object + + # Mock team update + mock_updated_team = MagicMock() + mock_updated_team.team_id = "org-team-update-budget-123" + mock_updated_team.organization_id = "test-org-update-budget" + mock_updated_team.max_budget = 50.0 + mock_updated_team.litellm_model_table = None + mock_updated_team.model_dump.return_value = { + "team_id": "org-team-update-budget-123", + "organization_id": "test-org-update-budget", + "max_budget": 50.0, + } + mock_prisma.db.litellm_teamtable.update = AsyncMock(return_value=mock_updated_team) + + # Should NOT raise an exception - bypass user budget validation for org-scoped teams + result = await update_team( + data=update_request, + http_request=dummy_request, + user_api_key_dict=org_admin_user, + ) + + # Verify the team was updated successfully with the higher budget + assert result is not None + assert result["data"].max_budget == 50.0 + + +@pytest.mark.asyncio +async def test_update_team_org_scoped_models_bypasses_user_limit(): + """ + Test that /team/update for an org-scoped team does NOT validate models against user's personal models. + + Scenario: + - Organization has models=['gpt-4', 'gpt-3.5-turbo', 'claude-3-opus'] + - User (org admin) has personal models=['no-default-models'] + - Org-scoped team exists + - User tries to update team models to ['gpt-4'] (in org's allowed, not in user's) + - Expected: Should succeed (validated against org, not user) + """ + from fastapi import Request + + from litellm.proxy._types import UpdateTeamRequest, UserAPIKeyAuth, LiteLLM_OrganizationTable + from litellm.proxy.management_endpoints.team_endpoints import update_team + + # Create user with very restrictive personal models + org_admin_user = UserAPIKeyAuth( + user_role=LitellmUserRoles.INTERNAL_USER, + user_id="org-admin-update-models-test", + models=["no-default-models"], # Restrictive model list + ) + + # Create update request with models in org's allowed but not in user's + update_request = UpdateTeamRequest( + team_id="org-team-update-models-123", + models=["gpt-4"], # In org's allowed, not in user's + ) + + dummy_request = MagicMock(spec=Request) + + # Mock organization with generous model list + mock_org = MagicMock(spec=LiteLLM_OrganizationTable) + mock_org.organization_id = "test-org-update-models" + mock_org.models = ["gpt-4", "gpt-3.5-turbo", "claude-3-opus"] + mock_org.litellm_budget_table = None + + with patch("litellm.proxy.proxy_server.prisma_client") as mock_prisma, patch( + "litellm.proxy.proxy_server.user_api_key_cache" + ) as mock_cache, patch( + "litellm.proxy.proxy_server.litellm_proxy_admin_name", "admin" + ), patch( + "litellm.proxy.proxy_server.create_audit_log_for_update", new=AsyncMock() + ) as mock_audit, patch( + "litellm.proxy.management_endpoints.team_endpoints.get_org_object", + new=AsyncMock(return_value=mock_org) + ) as mock_get_org: + + # Mock existing org-scoped team + mock_existing_team = MagicMock() + mock_existing_team.team_id = "org-team-update-models-123" + mock_existing_team.organization_id = "test-org-update-models" + mock_existing_team.models = ["gpt-3.5-turbo"] + mock_existing_team.model_id = None + mock_existing_team.model_dump.return_value = { + "team_id": "org-team-update-models-123", + "organization_id": "test-org-update-models", + "models": ["gpt-3.5-turbo"], + } + mock_prisma.db.litellm_teamtable.find_unique = AsyncMock(return_value=mock_existing_team) + mock_prisma.jsonify_team_object = lambda db_data: db_data + mock_cache.async_set_cache = AsyncMock() # Mock cache set for _cache_team_object + + # Mock team update + mock_updated_team = MagicMock() + mock_updated_team.team_id = "org-team-update-models-123" + mock_updated_team.organization_id = "test-org-update-models" + mock_updated_team.models = ["gpt-4"] + mock_updated_team.litellm_model_table = None + mock_updated_team.model_dump.return_value = { + "team_id": "org-team-update-models-123", + "organization_id": "test-org-update-models", + "models": ["gpt-4"], + } + mock_prisma.db.litellm_teamtable.update = AsyncMock(return_value=mock_updated_team) + + # Should NOT raise an exception - bypass user models validation for org-scoped teams + result = await update_team( + data=update_request, + http_request=dummy_request, + user_api_key_dict=org_admin_user, + ) + + # Verify the team was updated successfully with the new models + assert result is not None + assert result["data"].models == ["gpt-4"] + + +@pytest.mark.asyncio +async def test_update_team_org_scoped_models_not_in_org_models(): + """ + Test that /team/update for an org-scoped team fails when models are not in organization's allowed models. + + Scenario: + - Organization has models=['gpt-4', 'gpt-3.5-turbo'] + - Org-scoped team exists + - User tries to update team models to ['claude-3-opus'] (not in org's allowed models) + - Expected: Should fail with error about model not in org's allowed models + """ + from fastapi import Request + + from litellm.proxy._types import UpdateTeamRequest, ProxyException, UserAPIKeyAuth, LiteLLM_OrganizationTable + from litellm.proxy.management_endpoints.team_endpoints import update_team + + # Create user (org admin) + org_admin_user = UserAPIKeyAuth( + user_role=LitellmUserRoles.INTERNAL_USER, + user_id="org-admin-update-models-fail-test", + models=[], + ) + + # Create update request with model not in org's allowed list + update_request = UpdateTeamRequest( + team_id="org-team-update-models-fail-123", + models=["claude-3-opus"], # Not in org's allowed models + ) + + dummy_request = MagicMock(spec=Request) + + # Mock organization with restricted model list (no claude-3-opus) + mock_org = MagicMock(spec=LiteLLM_OrganizationTable) + mock_org.organization_id = "test-org-update-models-fail" + mock_org.models = ["gpt-4", "gpt-3.5-turbo"] # claude-3-opus is NOT allowed + mock_org.litellm_budget_table = None + + with patch("litellm.proxy.proxy_server.prisma_client") as mock_prisma, patch( + "litellm.proxy.proxy_server.user_api_key_cache" + ) as mock_cache, patch( + "litellm.proxy.proxy_server.litellm_proxy_admin_name", "admin" + ), patch( + "litellm.proxy.proxy_server.create_audit_log_for_update", new=AsyncMock() + ) as mock_audit, patch( + "litellm.proxy.management_endpoints.team_endpoints.get_org_object", + new=AsyncMock(return_value=mock_org) + ) as mock_get_org: + + # Mock existing org-scoped team + mock_existing_team = MagicMock() + mock_existing_team.team_id = "org-team-update-models-fail-123" + mock_existing_team.organization_id = "test-org-update-models-fail" + mock_existing_team.models = ["gpt-4"] + mock_existing_team.model_dump.return_value = { + "team_id": "org-team-update-models-fail-123", + "organization_id": "test-org-update-models-fail", + "models": ["gpt-4"], + } + mock_prisma.db.litellm_teamtable.find_unique = AsyncMock(return_value=mock_existing_team) + + # Should raise ProxyException because claude-3-opus is not in org's allowed models + with pytest.raises(ProxyException) as exc_info: + await update_team( + data=update_request, + http_request=dummy_request, + user_api_key_dict=org_admin_user, + ) + + # Verify exception details + assert exc_info.value.code == '400' + assert "claude-3-opus" in str(exc_info.value.message) or "organization" in str(exc_info.value.message).lower() + + +@pytest.mark.asyncio +async def test_update_team_tpm_limit_exceeds_user_limit(): + """ + Test that /team/update fails when TPM limit exceeds user's TPM limit. + + Scenario: + - User has tpm_limit=1000 + - User tries to update team with tpm_limit=5000 + - Expected: Should fail with error about exceeding user TPM limit + """ + from fastapi import Request + + from litellm.proxy._types import UpdateTeamRequest, ProxyException, UserAPIKeyAuth + from litellm.proxy.management_endpoints.team_endpoints import update_team + + # Create non-admin user with TPM limit + non_admin_user = UserAPIKeyAuth( + user_role=LitellmUserRoles.INTERNAL_USER, + user_id="tpm-limit-user", + models=[], + tpm_limit=1000, # User's TPM limit + ) + + # Create update request with TPM exceeding user's limit + update_request = UpdateTeamRequest( + team_id="team-tpm-test-123", + tpm_limit=5000, # Exceeds user's 1000 limit + ) + + dummy_request = MagicMock(spec=Request) + + with patch("litellm.proxy.proxy_server.prisma_client") as mock_prisma, patch( + "litellm.proxy.proxy_server.user_api_key_cache" + ) as mock_cache, patch( + "litellm.proxy.proxy_server.litellm_proxy_admin_name", "admin" + ): + + # Mock existing standalone team + mock_existing_team = MagicMock() + mock_existing_team.team_id = "team-tpm-test-123" + mock_existing_team.organization_id = None + mock_existing_team.tpm_limit = 500 + mock_existing_team.model_dump.return_value = { + "team_id": "team-tpm-test-123", + "organization_id": None, + "tpm_limit": 500, + } + mock_prisma.db.litellm_teamtable.find_unique = AsyncMock(return_value=mock_existing_team) + + # Should raise ProxyException because new TPM exceeds user's limit + with pytest.raises(ProxyException) as exc_info: + await update_team( + data=update_request, + http_request=dummy_request, + user_api_key_dict=non_admin_user, + ) + + # Verify exception details + assert exc_info.value.code == '400' + assert "tpm" in str(exc_info.value.message).lower() + + +@pytest.mark.asyncio +async def test_update_team_rpm_limit_exceeds_user_limit(): + """ + Test that /team/update fails when RPM limit exceeds user's RPM limit. + + Scenario: + - User has rpm_limit=100 + - User tries to update team with rpm_limit=500 + - Expected: Should fail with error about exceeding user RPM limit + """ + from fastapi import Request + + from litellm.proxy._types import UpdateTeamRequest, ProxyException, UserAPIKeyAuth + from litellm.proxy.management_endpoints.team_endpoints import update_team + + # Create non-admin user with RPM limit + non_admin_user = UserAPIKeyAuth( + user_role=LitellmUserRoles.INTERNAL_USER, + user_id="rpm-limit-user", + models=[], + rpm_limit=100, # User's RPM limit + ) + + # Create update request with RPM exceeding user's limit + update_request = UpdateTeamRequest( + team_id="team-rpm-test-123", + rpm_limit=500, # Exceeds user's 100 limit + ) + + dummy_request = MagicMock(spec=Request) + + with patch("litellm.proxy.proxy_server.prisma_client") as mock_prisma, patch( + "litellm.proxy.proxy_server.user_api_key_cache" + ) as mock_cache, patch( + "litellm.proxy.proxy_server.litellm_proxy_admin_name", "admin" + ): + + # Mock existing standalone team + mock_existing_team = MagicMock() + mock_existing_team.team_id = "team-rpm-test-123" + mock_existing_team.organization_id = None + mock_existing_team.rpm_limit = 50 + mock_existing_team.model_dump.return_value = { + "team_id": "team-rpm-test-123", + "organization_id": None, + "rpm_limit": 50, + } + mock_prisma.db.litellm_teamtable.find_unique = AsyncMock(return_value=mock_existing_team) + + # Should raise ProxyException because new RPM exceeds user's limit + with pytest.raises(ProxyException) as exc_info: + await update_team( + data=update_request, + http_request=dummy_request, + user_api_key_dict=non_admin_user, + ) + + # Verify exception details + assert exc_info.value.code == '400' + assert "rpm" in str(exc_info.value.message).lower() + + +@pytest.mark.asyncio +async def test_new_team_org_scoped_tpm_exceeds_org_limit(): + """ + Test that /team/new for an org-scoped team fails when TPM exceeds organization's TPM limit. + + Scenario: + - Organization has tpm_limit=10000 + - User tries to create org-scoped team with tpm_limit=20000 + - Expected: Should fail with error about exceeding org TPM limit + """ + from fastapi import Request + + from litellm.proxy._types import NewTeamRequest, ProxyException, UserAPIKeyAuth, LiteLLM_OrganizationTable, LiteLLM_BudgetTable + from litellm.proxy.management_endpoints.team_endpoints import new_team + + # Create user (with restrictive personal TPM limit that should be bypassed) + org_admin_user = UserAPIKeyAuth( + user_role=LitellmUserRoles.INTERNAL_USER, + user_id="org-admin-tpm-test", + models=[], + tpm_limit=1000, # User's personal limit (should be bypassed for org teams) + ) + + # Create team request with TPM exceeding org's limit + team_request = NewTeamRequest( + team_alias="org-tpm-test-team", + organization_id="test-org-tpm", + tpm_limit=20000, # Exceeds org's 10000 limit + ) + + dummy_request = MagicMock(spec=Request) + + # Mock organization with TPM limit + mock_budget_table = MagicMock(spec=LiteLLM_BudgetTable) + mock_budget_table.tpm_limit = 10000 # Org's TPM limit + mock_budget_table.rpm_limit = None + mock_budget_table.max_budget = None + + mock_org = MagicMock(spec=LiteLLM_OrganizationTable) + mock_org.organization_id = "test-org-tpm" + mock_org.models = ["gpt-4"] + mock_org.litellm_budget_table = mock_budget_table + + with patch("litellm.proxy.proxy_server.prisma_client") as mock_prisma, patch( + "litellm.proxy.proxy_server.user_api_key_cache" + ) as mock_cache, patch( + "litellm.proxy.proxy_server.litellm_proxy_admin_name", "admin" + ), patch( + "litellm.proxy.proxy_server._license_check" + ) as mock_license, patch( + "litellm.proxy.management_endpoints.team_endpoints.get_org_object", + new=AsyncMock(return_value=mock_org) + ): + mock_license.is_team_count_over_limit.return_value = False + mock_prisma.db.litellm_teamtable.count = AsyncMock(return_value=0) + mock_prisma.get_data = AsyncMock(return_value=None) + + # Should raise ProxyException because TPM exceeds org limit + with pytest.raises(ProxyException) as exc_info: + await new_team( + data=team_request, + http_request=dummy_request, + user_api_key_dict=org_admin_user, + ) + + # Verify exception details + assert exc_info.value.code == '400' + assert "tpm" in str(exc_info.value.message).lower() + + +@pytest.mark.asyncio +async def test_new_team_org_scoped_rpm_exceeds_org_limit(): + """ + Test that /team/new for an org-scoped team fails when RPM exceeds organization's RPM limit. + + Scenario: + - Organization has rpm_limit=1000 + - User tries to create org-scoped team with rpm_limit=2000 + - Expected: Should fail with error about exceeding org RPM limit + """ + from fastapi import Request + + from litellm.proxy._types import NewTeamRequest, ProxyException, UserAPIKeyAuth, LiteLLM_OrganizationTable, LiteLLM_BudgetTable + from litellm.proxy.management_endpoints.team_endpoints import new_team + + # Create user (with restrictive personal RPM limit that should be bypassed) + org_admin_user = UserAPIKeyAuth( + user_role=LitellmUserRoles.INTERNAL_USER, + user_id="org-admin-rpm-test", + models=[], + rpm_limit=100, # User's personal limit (should be bypassed for org teams) + ) + + # Create team request with RPM exceeding org's limit + team_request = NewTeamRequest( + team_alias="org-rpm-test-team", + organization_id="test-org-rpm", + rpm_limit=2000, # Exceeds org's 1000 limit + ) + + dummy_request = MagicMock(spec=Request) + + # Mock organization with RPM limit + mock_budget_table = MagicMock(spec=LiteLLM_BudgetTable) + mock_budget_table.tpm_limit = None + mock_budget_table.rpm_limit = 1000 # Org's RPM limit + mock_budget_table.max_budget = None + + mock_org = MagicMock(spec=LiteLLM_OrganizationTable) + mock_org.organization_id = "test-org-rpm" + mock_org.models = ["gpt-4"] + mock_org.litellm_budget_table = mock_budget_table + + with patch("litellm.proxy.proxy_server.prisma_client") as mock_prisma, patch( + "litellm.proxy.proxy_server.user_api_key_cache" + ) as mock_cache, patch( + "litellm.proxy.proxy_server.litellm_proxy_admin_name", "admin" + ), patch( + "litellm.proxy.proxy_server._license_check" + ) as mock_license, patch( + "litellm.proxy.management_endpoints.team_endpoints.get_org_object", + new=AsyncMock(return_value=mock_org) + ): + mock_license.is_team_count_over_limit.return_value = False + mock_prisma.db.litellm_teamtable.count = AsyncMock(return_value=0) + mock_prisma.get_data = AsyncMock(return_value=None) + + # Should raise ProxyException because RPM exceeds org limit + with pytest.raises(ProxyException) as exc_info: + await new_team( + data=team_request, + http_request=dummy_request, + user_api_key_dict=org_admin_user, + ) + + # Verify exception details + assert exc_info.value.code == '400' + assert "rpm" in str(exc_info.value.message).lower() + + +@pytest.mark.asyncio +async def test_new_team_org_scoped_tpm_rpm_bypasses_user_limit(): + """ + Test that /team/new for an org-scoped team bypasses user's TPM/RPM limits. + + Scenario: + - User has tpm_limit=1000, rpm_limit=100 + - Organization has tpm_limit=50000, rpm_limit=5000 + - User creates org-scoped team with tpm_limit=10000, rpm_limit=1000 + - Expected: Should succeed (bypasses user limits, within org limits) + """ + from fastapi import Request + + from litellm.proxy._types import NewTeamRequest, UserAPIKeyAuth, LiteLLM_OrganizationTable, LiteLLM_BudgetTable, LiteLLM_TeamTable + from litellm.proxy.management_endpoints.team_endpoints import new_team + + # Create user with restrictive personal limits + org_admin_user = UserAPIKeyAuth( + user_role=LitellmUserRoles.INTERNAL_USER, + user_id="org-admin-bypass-test", + models=[], + tpm_limit=1000, # Restrictive user TPM limit + rpm_limit=100, # Restrictive user RPM limit + ) + + # Create team request exceeding user limits but within org limits + team_request = NewTeamRequest( + team_alias="org-bypass-test-team", + organization_id="test-org-bypass", + tpm_limit=10000, # Exceeds user's 1000 but within org's 50000 + rpm_limit=1000, # Exceeds user's 100 but within org's 5000 + ) + + dummy_request = MagicMock(spec=Request) + + # Mock organization with generous limits + mock_budget_table = MagicMock(spec=LiteLLM_BudgetTable) + mock_budget_table.tpm_limit = 50000 # Generous org TPM limit + mock_budget_table.rpm_limit = 5000 # Generous org RPM limit + mock_budget_table.max_budget = None + + mock_org = MagicMock(spec=LiteLLM_OrganizationTable) + mock_org.organization_id = "test-org-bypass" + mock_org.models = ["gpt-4"] + mock_org.litellm_budget_table = mock_budget_table + + with patch("litellm.proxy.proxy_server.prisma_client") as mock_prisma, patch( + "litellm.proxy.proxy_server.user_api_key_cache" + ) as mock_cache, patch( + "litellm.proxy.proxy_server.litellm_proxy_admin_name", "admin" + ), patch( + "litellm.proxy.proxy_server._license_check" + ) as mock_license, patch( + "litellm.proxy.proxy_server.create_audit_log_for_update", new=AsyncMock() + ), patch( + "litellm.proxy.management_endpoints.team_endpoints.get_org_object", + new=AsyncMock(return_value=mock_org) + ), patch( + "litellm.proxy.management_endpoints.team_endpoints._add_team_members_to_team", + new=AsyncMock() + ): + mock_license.is_team_count_over_limit.return_value = False + mock_prisma.db.litellm_teamtable.count = AsyncMock(return_value=0) + mock_prisma.get_data = AsyncMock(return_value=None) + + # Mock team creation + mock_created_team = MagicMock(spec=LiteLLM_TeamTable) + mock_created_team.team_id = "new-bypass-team-id" + mock_created_team.team_alias = "org-bypass-test-team" + mock_created_team.tpm_limit = 10000 + mock_created_team.rpm_limit = 1000 + mock_created_team.metadata = None + mock_created_team.members_with_roles = [] + mock_created_team.model_dump.return_value = { + "team_id": "new-bypass-team-id", + "team_alias": "org-bypass-test-team", + "tpm_limit": 10000, + "rpm_limit": 1000, + "metadata": None, + "members_with_roles": [], + } + mock_prisma.db.litellm_teamtable.create = AsyncMock(return_value=mock_created_team) + mock_prisma.db.litellm_teamtable.update = AsyncMock(return_value=mock_created_team) + mock_prisma.jsonify_team_object = MagicMock(side_effect=lambda db_data: db_data) + + # Should succeed - bypasses user limits since org-scoped + result = await new_team( + data=team_request, + http_request=dummy_request, + user_api_key_dict=org_admin_user, + ) + + # Verify team was created + assert result["team_id"] == "new-bypass-team-id" + + +@pytest.mark.asyncio +async def test_update_team_org_scoped_tpm_exceeds_org_limit(): + """ + Test that /team/update for an org-scoped team fails when TPM exceeds organization's TPM limit. + + Scenario: + - Organization has tpm_limit=10000 + - User tries to update org-scoped team with tpm_limit=20000 + - Expected: Should fail with error about exceeding org TPM limit + """ + from fastapi import Request + + from litellm.proxy._types import UpdateTeamRequest, ProxyException, UserAPIKeyAuth, LiteLLM_OrganizationTable, LiteLLM_BudgetTable + from litellm.proxy.management_endpoints.team_endpoints import update_team + + # Create user (with restrictive personal TPM limit that should be bypassed) + org_admin_user = UserAPIKeyAuth( + user_role=LitellmUserRoles.INTERNAL_USER, + user_id="org-admin-update-tpm-test", + models=[], + tpm_limit=1000, # User's personal limit (should be bypassed for org teams) + ) + + # Create update request with TPM exceeding org's limit + update_request = UpdateTeamRequest( + team_id="org-team-update-tpm-123", + tpm_limit=20000, # Exceeds org's 10000 limit + ) + + dummy_request = MagicMock(spec=Request) + + # Mock organization with TPM limit + mock_budget_table = MagicMock(spec=LiteLLM_BudgetTable) + mock_budget_table.tpm_limit = 10000 # Org's TPM limit + mock_budget_table.rpm_limit = None + mock_budget_table.max_budget = None + + mock_org = MagicMock(spec=LiteLLM_OrganizationTable) + mock_org.organization_id = "test-org-update-tpm" + mock_org.models = ["gpt-4"] + mock_org.litellm_budget_table = mock_budget_table + + with patch("litellm.proxy.proxy_server.prisma_client") as mock_prisma, patch( + "litellm.proxy.proxy_server.user_api_key_cache" + ) as mock_cache, patch( + "litellm.proxy.proxy_server.litellm_proxy_admin_name", "admin" + ), patch( + "litellm.proxy.management_endpoints.team_endpoints.get_org_object", + new=AsyncMock(return_value=mock_org) + ): + + # Mock existing org-scoped team + mock_existing_team = MagicMock() + mock_existing_team.team_id = "org-team-update-tpm-123" + mock_existing_team.organization_id = "test-org-update-tpm" + mock_existing_team.tpm_limit = 5000 + mock_existing_team.model_dump.return_value = { + "team_id": "org-team-update-tpm-123", + "organization_id": "test-org-update-tpm", + "tpm_limit": 5000, + } + mock_prisma.db.litellm_teamtable.find_unique = AsyncMock(return_value=mock_existing_team) + + # Should raise ProxyException because TPM exceeds org limit + with pytest.raises(ProxyException) as exc_info: + await update_team( + data=update_request, + http_request=dummy_request, + user_api_key_dict=org_admin_user, + ) + + # Verify exception details + assert exc_info.value.code == '400' + assert "tpm" in str(exc_info.value.message).lower() + + +@pytest.mark.asyncio +async def test_update_team_org_scoped_rpm_exceeds_org_limit(): + """ + Test that /team/update for an org-scoped team fails when RPM exceeds organization's RPM limit. + + Scenario: + - Organization has rpm_limit=1000 + - User tries to update org-scoped team with rpm_limit=2000 + - Expected: Should fail with error about exceeding org RPM limit + """ + from fastapi import Request + + from litellm.proxy._types import UpdateTeamRequest, ProxyException, UserAPIKeyAuth, LiteLLM_OrganizationTable, LiteLLM_BudgetTable + from litellm.proxy.management_endpoints.team_endpoints import update_team + + # Create user (with restrictive personal RPM limit that should be bypassed) + org_admin_user = UserAPIKeyAuth( + user_role=LitellmUserRoles.INTERNAL_USER, + user_id="org-admin-update-rpm-test", + models=[], + rpm_limit=100, # User's personal limit (should be bypassed for org teams) + ) + + # Create update request with RPM exceeding org's limit + update_request = UpdateTeamRequest( + team_id="org-team-update-rpm-123", + rpm_limit=2000, # Exceeds org's 1000 limit + ) + + dummy_request = MagicMock(spec=Request) + + # Mock organization with RPM limit + mock_budget_table = MagicMock(spec=LiteLLM_BudgetTable) + mock_budget_table.tpm_limit = None + mock_budget_table.rpm_limit = 1000 # Org's RPM limit + mock_budget_table.max_budget = None + + mock_org = MagicMock(spec=LiteLLM_OrganizationTable) + mock_org.organization_id = "test-org-update-rpm" + mock_org.models = ["gpt-4"] + mock_org.litellm_budget_table = mock_budget_table + + with patch("litellm.proxy.proxy_server.prisma_client") as mock_prisma, patch( + "litellm.proxy.proxy_server.user_api_key_cache" + ) as mock_cache, patch( + "litellm.proxy.proxy_server.litellm_proxy_admin_name", "admin" + ), patch( + "litellm.proxy.management_endpoints.team_endpoints.get_org_object", + new=AsyncMock(return_value=mock_org) + ): + + # Mock existing org-scoped team + mock_existing_team = MagicMock() + mock_existing_team.team_id = "org-team-update-rpm-123" + mock_existing_team.organization_id = "test-org-update-rpm" + mock_existing_team.rpm_limit = 500 + mock_existing_team.model_dump.return_value = { + "team_id": "org-team-update-rpm-123", + "organization_id": "test-org-update-rpm", + "rpm_limit": 500, + } + mock_prisma.db.litellm_teamtable.find_unique = AsyncMock(return_value=mock_existing_team) + + # Should raise ProxyException because RPM exceeds org limit + with pytest.raises(ProxyException) as exc_info: + await update_team( + data=update_request, + http_request=dummy_request, + user_api_key_dict=org_admin_user, + ) + + # Verify exception details + assert exc_info.value.code == '400' + assert "rpm" in str(exc_info.value.message).lower() + + +@pytest.mark.asyncio +async def test_update_team_org_scoped_tpm_rpm_bypasses_user_limit(): + """ + Test that /team/update for an org-scoped team bypasses user's TPM/RPM limits. + + Scenario: + - User has tpm_limit=1000, rpm_limit=100 + - Organization has tpm_limit=50000, rpm_limit=5000 + - User updates org-scoped team with tpm_limit=10000, rpm_limit=1000 + - Expected: Should succeed (bypasses user limits, within org limits) + """ + from fastapi import Request + + from litellm.proxy._types import UpdateTeamRequest, UserAPIKeyAuth, LiteLLM_OrganizationTable, LiteLLM_BudgetTable, LiteLLM_TeamTable + from litellm.proxy.management_endpoints.team_endpoints import update_team + + # Create user with restrictive personal limits + org_admin_user = UserAPIKeyAuth( + user_role=LitellmUserRoles.INTERNAL_USER, + user_id="org-admin-update-bypass-test", + models=[], + tpm_limit=1000, # Restrictive user TPM limit + rpm_limit=100, # Restrictive user RPM limit + ) + + # Create update request exceeding user limits but within org limits + update_request = UpdateTeamRequest( + team_id="org-team-update-bypass-123", + tpm_limit=10000, # Exceeds user's 1000 but within org's 50000 + rpm_limit=1000, # Exceeds user's 100 but within org's 5000 + ) + + dummy_request = MagicMock(spec=Request) + + # Mock organization with generous limits + mock_budget_table = MagicMock(spec=LiteLLM_BudgetTable) + mock_budget_table.tpm_limit = 50000 # Generous org TPM limit + mock_budget_table.rpm_limit = 5000 # Generous org RPM limit + mock_budget_table.max_budget = None + + mock_org = MagicMock(spec=LiteLLM_OrganizationTable) + mock_org.organization_id = "test-org-update-bypass" + mock_org.models = ["gpt-4"] + mock_org.litellm_budget_table = mock_budget_table + + with patch("litellm.proxy.proxy_server.prisma_client") as mock_prisma, patch( + "litellm.proxy.proxy_server.user_api_key_cache" + ) as mock_cache, patch( + "litellm.proxy.proxy_server.litellm_proxy_admin_name", "admin" + ), patch( + "litellm.proxy.proxy_server.proxy_logging_obj" + ) as mock_logging, patch( + "litellm.proxy.management_endpoints.team_endpoints.get_org_object", + new=AsyncMock(return_value=mock_org) + ): + + # Mock existing org-scoped team + mock_existing_team = MagicMock() + mock_existing_team.team_id = "org-team-update-bypass-123" + mock_existing_team.organization_id = "test-org-update-bypass" + mock_existing_team.tpm_limit = 5000 + mock_existing_team.rpm_limit = 500 + mock_existing_team.model_id = None + mock_existing_team.model_dump.return_value = { + "team_id": "org-team-update-bypass-123", + "organization_id": "test-org-update-bypass", + "tpm_limit": 5000, + "rpm_limit": 500, + } + mock_prisma.db.litellm_teamtable.find_unique = AsyncMock(return_value=mock_existing_team) + mock_cache.async_set_cache = AsyncMock() + + # Mock team update + mock_updated_team = MagicMock(spec=LiteLLM_TeamTable) + mock_updated_team.team_id = "org-team-update-bypass-123" + mock_updated_team.tpm_limit = 10000 + mock_updated_team.rpm_limit = 1000 + mock_updated_team.model_dump.return_value = { + "team_id": "org-team-update-bypass-123", + "tpm_limit": 10000, + "rpm_limit": 1000, + } + mock_prisma.db.litellm_teamtable.update = AsyncMock(return_value=mock_updated_team) + mock_prisma.jsonify_team_object = MagicMock(side_effect=lambda db_data: db_data) + + # Should succeed - bypasses user limits since org-scoped + result = await update_team( + data=update_request, + http_request=dummy_request, + user_api_key_dict=org_admin_user, + ) + + # Verify team was updated + assert result["team_id"] == "org-team-update-bypass-123" \ No newline at end of file From 98a244450e1d14649f9edbd43c4ace5962d605c7 Mon Sep 17 00:00:00 2001 From: rioiart Date: Tue, 2 Dec 2025 04:53:30 +0100 Subject: [PATCH 081/370] Fix sso users not added to entra synced team (#17331) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test: add failing tests for SSO user not added to Entra-synced teams bug Adds tests reproducing the bug where new SSO users with teams=None (from NewUserResponse) are not added to Entra ID synced teams because add_missing_team_member() returns early when teams is None. Tests demonstrate: - NewUserResponse with teams=None fails to add user to teams (bug) - LiteLLM_UserTable with teams=[] correctly adds user to teams (control) šŸ¤– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * fix: treat None as empty list in add_missing_team_member for new SSO users Fixed bug where new SSO users logging in via Microsoft SSO were not added to their Entra-synced teams. The issue was an early return when user_info.teams is None (default for NewUserResponse). Now treats None as an empty list so new users are properly added to all their SSO teams. Location: litellm/proxy/management_endpoints/ui_sso.py:438-440 šŸ¤– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Claude --- litellm/proxy/management_endpoints/ui_sso.py | 6 +- .../proxy/management_endpoints/test_ui_sso.py | 209 +++++++++++++++++- 2 files changed, 211 insertions(+), 4 deletions(-) diff --git a/litellm/proxy/management_endpoints/ui_sso.py b/litellm/proxy/management_endpoints/ui_sso.py index a033e2cf5f4..59a93f3c486 100644 --- a/litellm/proxy/management_endpoints/ui_sso.py +++ b/litellm/proxy/management_endpoints/ui_sso.py @@ -435,9 +435,9 @@ async def add_missing_team_member( - Get missing teams (diff b/w user_info.team_ids and sso_teams) - Add missing user to missing teams """ - if user_info.teams is None: - return - missing_teams = set(sso_teams) - set(user_info.teams) + # Handle None as empty list for new users + user_teams = user_info.teams if user_info.teams is not None else [] + missing_teams = set(sso_teams) - set(user_teams) missing_teams_list = list(missing_teams) tasks = [] tasks = [ diff --git a/tests/test_litellm/proxy/management_endpoints/test_ui_sso.py b/tests/test_litellm/proxy/management_endpoints/test_ui_sso.py index f01813fa587..8d7aa51fa0f 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_ui_sso.py +++ b/tests/test_litellm/proxy/management_endpoints/test_ui_sso.py @@ -16,7 +16,7 @@ sys.path.insert( ) # Adds the parent directory to the system path import litellm -from litellm.proxy._types import NewTeamRequest +from litellm.proxy._types import LiteLLM_UserTable, NewTeamRequest, NewUserResponse from litellm.proxy.auth.handle_jwt import JWTHandler from litellm.proxy.management_endpoints.types import CustomOpenID from litellm.proxy.management_endpoints.ui_sso import ( @@ -2573,3 +2573,210 @@ class TestPKCEFunctionality: assert "code_challenge=" in updated_location assert "code_challenge_method=S256" in updated_location assert f"state={test_state}" in updated_location + + +# Tests for SSO user team assignment bug (Issue: SSO Users Not Added to Entra-Synced Teams on First Login) +class TestAddMissingTeamMember: + """Tests for the add_missing_team_member function""" + + @pytest.mark.asyncio + async def test_add_missing_team_member_with_new_user_response_teams_none(self): + """ + Bug reproduction: When a NewUserResponse has teams=None (new SSO user), + add_missing_team_member() should still add the user to the SSO teams. + + Currently FAILS: The function returns early when teams is None. + """ + from litellm.proxy._types import NewUserResponse + from litellm.proxy.management_endpoints.ui_sso import add_missing_team_member + + # Simulate a new SSO user - NewUserResponse has teams=None by default + new_user = NewUserResponse( + user_id="new-sso-user-123", + key="sk-xxxxx", + teams=None, # This is the default for NewUserResponse + ) + + sso_teams = ["team-from-entra-1", "team-from-entra-2"] + + with patch( + "litellm.proxy.management_endpoints.ui_sso.create_team_member_add_task" + ) as mock_add_task: + mock_add_task.return_value = AsyncMock() + + await add_missing_team_member(user_info=new_user, sso_teams=sso_teams) + + # Bug: This assertion currently FAILS - no teams are added + # because function returns early when teams is None + assert ( + mock_add_task.call_count == 2 + ), f"Expected 2 calls to add user to teams, but got {mock_add_task.call_count}" + called_team_ids = [call.args[0] for call in mock_add_task.call_args_list] + assert set(called_team_ids) == { + "team-from-entra-1", + "team-from-entra-2", + } + + @pytest.mark.asyncio + async def test_add_missing_team_member_with_litellm_user_table_empty_teams(self): + """ + Control test: When a LiteLLM_UserTable has teams=[] (existing user, no teams), + add_missing_team_member() should add the user to SSO teams. + + This test PASSES because LiteLLM_UserTable defaults teams to [] not None. + """ + from litellm.proxy._types import LiteLLM_UserTable + from litellm.proxy.management_endpoints.ui_sso import add_missing_team_member + + # Existing user has teams=[] by default (not None) + existing_user = LiteLLM_UserTable( + user_id="existing-user-456", + teams=[], # Empty list, not None + ) + + sso_teams = ["team-from-entra-1", "team-from-entra-2"] + + with patch( + "litellm.proxy.management_endpoints.ui_sso.create_team_member_add_task" + ) as mock_add_task: + mock_add_task.return_value = AsyncMock() + + await add_missing_team_member(user_info=existing_user, sso_teams=sso_teams) + + # This PASSES - teams are added because teams=[] not None + assert mock_add_task.call_count == 2 + + @pytest.mark.asyncio + async def test_add_user_to_teams_from_sso_response_new_user(self): + """ + Integration test: Simulates the SSO response handler with a new user + that has teams=None from NewUserResponse. + """ + from litellm.proxy._types import NewUserResponse + from litellm.proxy.management_endpoints.types import CustomOpenID + from litellm.proxy.management_endpoints.ui_sso import ( + SSOAuthenticationHandler, + ) + + # SSO response with team_ids from Entra ID + sso_result = CustomOpenID( + id="new-sso-user-id", + email="newuser@example.com", + team_ids=["entra-group-1", "entra-group-2"], + ) + + # New user response (simulates what new_user() returns) + new_user_info = NewUserResponse( + user_id="new-sso-user-id", + key="sk-xxxxx", + teams=None, # Bug: NewUserResponse defaults to None + ) + + with patch( + "litellm.proxy.management_endpoints.ui_sso.add_missing_team_member" + ) as mock_add_member: + await SSOAuthenticationHandler.add_user_to_teams_from_sso_response( + result=sso_result, + user_info=new_user_info, + ) + + # Verify add_missing_team_member was called with correct args + mock_add_member.assert_called_once_with( + user_info=new_user_info, sso_teams=["entra-group-1", "entra-group-2"] + ) + + @pytest.mark.asyncio + async def test_sso_first_login_full_flow_adds_user_to_teams(self): + """ + End-to-end test: Simulates complete first-time SSO login with Entra groups. + Verifies teams are created AND user is added as a member. + """ + from litellm.proxy._types import NewUserResponse + from litellm.proxy.management_endpoints.ui_sso import add_missing_team_member + + team_member_calls = [] + + async def track_team_member_add(team_id, user_info): + team_member_calls.append( + {"team_id": team_id, "user_id": user_info.user_id} + ) + + # New SSO user with Entra groups + new_user = NewUserResponse( + user_id="first-time-sso-user", + key="sk-xxxxx", + teams=None, # The problematic default + ) + + sso_teams = ["entra-team-alpha", "entra-team-beta"] + + with patch( + "litellm.proxy.management_endpoints.ui_sso.create_team_member_add_task", + side_effect=track_team_member_add, + ): + await add_missing_team_member(user_info=new_user, sso_teams=sso_teams) + + # Bug: With current code, team_member_calls will be empty + # After fix: Should have 2 entries + assert ( + len(team_member_calls) == 2 + ), f"Expected 2 teams to be added, but got {len(team_member_calls)}" + assert {c["team_id"] for c in team_member_calls} == { + "entra-team-alpha", + "entra-team-beta", + } + assert all(c["user_id"] == "first-time-sso-user" for c in team_member_calls) + + @pytest.mark.asyncio + @pytest.mark.parametrize( + "user_info_factory,teams_value,expected_teams_added", + [ + # Bug case: NewUserResponse with teams=None + pytest.param( + lambda uid: NewUserResponse(user_id=uid, key="sk-xxx", teams=None), + None, + ["team-1", "team-2"], # Should still add teams + id="new_user_teams_none", + ), + # Working case: LiteLLM_UserTable with teams=[] + pytest.param( + lambda uid: LiteLLM_UserTable(user_id=uid, teams=[]), + [], + ["team-1", "team-2"], + id="existing_user_empty_teams", + ), + # Existing user with some teams already + pytest.param( + lambda uid: LiteLLM_UserTable(user_id=uid, teams=["team-1"]), + ["team-1"], + ["team-2"], # Only missing team should be added + id="existing_user_partial_teams", + ), + ], + ) + async def test_add_missing_team_member_handles_all_user_types( + self, user_info_factory, teams_value, expected_teams_added + ): + """ + Parametrized test ensuring add_missing_team_member works for all user types. + """ + from litellm.proxy._types import LiteLLM_UserTable + from litellm.proxy.management_endpoints.ui_sso import add_missing_team_member + + user_info = user_info_factory("test-user-id") + sso_teams = ["team-1", "team-2"] + + added_teams = [] + + async def mock_create_task(team_id, user): + added_teams.append(team_id) + + with patch( + "litellm.proxy.management_endpoints.ui_sso.create_team_member_add_task", + side_effect=mock_create_task, + ): + await add_missing_team_member(user_info=user_info, sso_teams=sso_teams) + + assert set(added_teams) == set( + expected_teams_added + ), f"Expected teams {expected_teams_added}, but got {added_teams}" From 71efcb71151aedb216e411815ce871376605da55 Mon Sep 17 00:00:00 2001 From: idola9 Date: Tue, 2 Dec 2025 05:56:14 +0200 Subject: [PATCH 082/370] Refactor Noma guardrail to use shared Responses transformation and include system instructions (#17315) * Support system prompts in noma guardrails * Use litellm util to covert chat completions to responses api --- .../transformation.py | 6 +- .../guardrails/guardrail_hooks/noma/noma.py | 78 ++-- .../guardrails/guardrail_hooks/test_noma.py | 335 ++++++++++++------ 3 files changed, 263 insertions(+), 156 deletions(-) diff --git a/litellm/completion_extras/litellm_responses_transformation/transformation.py b/litellm/completion_extras/litellm_responses_transformation/transformation.py index 07d9de5a016..2045836387f 100644 --- a/litellm/completion_extras/litellm_responses_transformation/transformation.py +++ b/litellm/completion_extras/litellm_responses_transformation/transformation.py @@ -148,7 +148,11 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge): if role == "system": # Extract system message as instructions if isinstance(content, str): - instructions = content + if instructions: + # Concatenate multiple system prompts with a space + instructions = f"{instructions} {content}" + else: + instructions = content else: input_items.append( { diff --git a/litellm/proxy/guardrails/guardrail_hooks/noma/noma.py b/litellm/proxy/guardrails/guardrail_hooks/noma/noma.py index 3ae2d519c45..a0ea90ccf21 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/noma/noma.py +++ b/litellm/proxy/guardrails/guardrail_hooks/noma/noma.py @@ -28,6 +28,9 @@ from fastapi import HTTPException import litellm from litellm import DualCache, ModelResponse from litellm._logging import verbose_proxy_logger +from litellm.completion_extras.litellm_responses_transformation.transformation import ( + LiteLLMResponsesTransformationHandler, +) from litellm.integrations.custom_guardrail import CustomGuardrail from litellm.llms.base_llm.base_model_iterator import MockResponseIterator from litellm.llms.custom_httpx.http_handler import ( @@ -111,6 +114,7 @@ class NomaGuardrail(CustomGuardrail): self.async_handler = get_async_httpx_client( llm_provider=httpxSpecialProvider.GuardrailCallback ) + self._responses_transform_handler = LiteLLMResponsesTransformationHandler() self.api_key = api_key or os.environ.get("NOMA_API_KEY") self.api_base = api_base or os.environ.get( "NOMA_API_BASE", NomaGuardrail._DEFAULT_API_BASE @@ -164,13 +168,28 @@ class NomaGuardrail(CustomGuardrail): start_time = datetime.now() extra_data = self.get_guardrail_dynamic_request_body_params(request_data) - user_message = await self._extract_user_message(request_data) - if not user_message: + messages = request_data.get("messages") or [] + if not messages: return None - payload = { - "input": [{"type": "message", "role": "user", "content": user_message}] - } + input_items, instructions = self._responses_transform_handler.convert_chat_completion_messages_to_responses_api( # type: ignore[arg-type] + messages + ) + + if instructions: + system_message = { + "type": "message", + "role": "system", + "content": [ + {"type": "input_text", "text": instructions}, + ], + } + input_items.insert(0, system_message) + + if not input_items: + return None + + payload = {"input": input_items} response_json = await self._call_noma_api( payload=payload, llm_request_id=None, @@ -198,9 +217,9 @@ class NomaGuardrail(CustomGuardrail): if self.monitor_mode: await self._handle_verdict_background( - USER_ROLE, json.dumps(user_message), response_json + USER_ROLE, json.dumps(input_items), response_json ) - return json.dumps(user_message) + return json.dumps(input_items) # Check if we should anonymize content if self._should_anonymize(response_json, USER_ROLE): @@ -215,8 +234,8 @@ class NomaGuardrail(CustomGuardrail): ) return anonymized_content - await self._check_verdict(USER_ROLE, json.dumps(user_message), response_json) - return json.dumps(user_message) + await self._check_verdict(USER_ROLE, json.dumps(input_items), response_json) + return json.dumps(input_items) async def _process_llm_response_check( self, @@ -732,47 +751,6 @@ class NomaGuardrail(CustomGuardrail): return response - async def _extract_user_message(self, data: dict) -> Optional[List[dict]]: - """Extract the last user message from request data""" - messages = data.get("messages", []) - if not messages: - return None - - # Get the last user message - user_messages = [msg for msg in messages if msg.get("role") == USER_ROLE] - if not user_messages: - return None - - last_user_message = user_messages[-1].get("content", "") - if isinstance(last_user_message, str): - return [{"type": "input_text", "text": last_user_message}] - elif isinstance(last_user_message, list): - converted_messages = [] - for message in last_user_message: - converted_message = self._convert_single_user_message_to_payload( - message - ) - if converted_message is not None: - converted_messages.append(converted_message) - return converted_messages - else: - return None - - def _convert_single_user_message_to_payload( - self, user_message: Any - ) -> Optional[dict]: - if isinstance(user_message, str): - return {"type": "input_text", "text": user_message} - elif user_message.get("type", "") == "image_url": - return { - "type": "input_image", - "image_url": user_message.get("image_url", {}).get("url", ""), - } - elif user_message.get("type", "") == "text": - return {"type": "input_text", "text": user_message.get("text", "")} - else: - return None - async def _call_noma_api( self, payload: dict, diff --git a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_noma.py b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_noma.py index 94cb831a30c..f1ac6ef14b1 100644 --- a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_noma.py +++ b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_noma.py @@ -1,5 +1,6 @@ import copy import os +from typing import cast from unittest.mock import AsyncMock, MagicMock, patch import httpx @@ -14,6 +15,7 @@ from litellm.proxy.guardrails.guardrail_hooks.noma import ( ) from litellm.proxy.guardrails.guardrail_hooks.noma.noma import NomaBlockedMessage from litellm.proxy.guardrails.init_guardrails import init_guardrails_v2 +from litellm.types.llms.openai import AllMessageValues from litellm.types.utils import Choices, Message @@ -413,7 +415,7 @@ class TestNomaGuardrailHooks: # Verify API call details call_args = mock_post.call_args - # Verify the URL endpoint + # Verify the URL endpoint assert call_args.args[0].endswith("/ai-dr/v2/prompt/scan") # Verify headers and JSON payload if "headers" in call_args.kwargs: @@ -426,6 +428,130 @@ class TestNomaGuardrailHooks: assert "x-noma-context" in json_payload assert json_payload["x-noma-context"]["applicationId"] == "test-app" + @pytest.mark.asyncio + async def test_pre_call_hook_with_system_prompt( + self, noma_guardrail, mock_user_api_key_dict + ): + """Test pre-call hook includes system prompt in Noma API request""" + request_data = { + "messages": [ + {"role": "system", "content": "You are a helpful assistant"}, + {"role": "user", "content": "Hello, how are you?"}, + ], + "litellm_call_id": "test-call-id", + "metadata": {"requester_ip_address": "192.168.1.1"}, + } + + mock_response = MagicMock() + mock_response.json.return_value = { + "aggregatedScanResult": False, # False means safe + "scanResult": [ + { + "role": "system", + "type": "message", + "results": {} + }, + { + "role": "user", + "type": "message", + "results": {} + } + ] + } + mock_response.raise_for_status = MagicMock() + + with patch.object( + noma_guardrail.async_handler, "post", return_value=mock_response + ) as mock_post: + result = await noma_guardrail.async_pre_call_hook( + user_api_key_dict=mock_user_api_key_dict, + cache=MagicMock(), + data=request_data, + call_type="completion", + ) + + assert result == request_data + mock_post.assert_called_once() + + # Verify the payload includes both system and user messages + call_args = mock_post.call_args + json_payload = call_args.kwargs["json"] + assert "input" in json_payload + messages = json_payload["input"] + + # Should have 2 messages: system and user + assert len(messages) == 2 + + # First message should be system + assert messages[0]["type"] == "message" + assert messages[0]["role"] == "system" + assert messages[0]["content"][0]["type"] == "input_text" + assert messages[0]["content"][0]["text"] == "You are a helpful assistant" + + # Second message should be user + assert messages[1]["type"] == "message" + assert messages[1]["role"] == "user" + assert messages[1]["content"][0]["type"] == "input_text" + assert messages[1]["content"][0]["text"] == "Hello, how are you?" + + @pytest.mark.asyncio + async def test_pre_call_hook_with_multiple_system_prompts( + self, noma_guardrail, mock_user_api_key_dict + ): + """Test pre-call hook combines multiple system prompts into single message""" + request_data = { + "messages": [ + {"role": "system", "content": "You are a helpful assistant"}, + {"role": "system", "content": "You should be polite and respectful"}, + {"role": "user", "content": "Hello, how are you?"}, + ], + "litellm_call_id": "test-call-id", + } + + mock_response = MagicMock() + mock_response.json.return_value = { + "aggregatedScanResult": False, + "scanResult": [ + {"role": "system", "type": "message", "results": {}}, + {"role": "user", "type": "message", "results": {}} + ] + } + mock_response.raise_for_status = MagicMock() + + with patch.object( + noma_guardrail.async_handler, "post", return_value=mock_response + ) as mock_post: + result = await noma_guardrail.async_pre_call_hook( + user_api_key_dict=mock_user_api_key_dict, + cache=MagicMock(), + data=request_data, + call_type="completion", + ) + + assert result == request_data + mock_post.assert_called_once() + + # Verify the payload combines system prompts into single message + call_args = mock_post.call_args + json_payload = call_args.kwargs["json"] + messages = json_payload["input"] + + # Should have 2 messages: 1 combined system and 1 user + assert len(messages) == 2 + + # First message should be system with combined content + assert messages[0]["role"] == "system" + assert messages[0]["content"][0]["type"] == "input_text" + assert ( + messages[0]["content"][0]["text"] + == "You are a helpful assistant You should be polite and respectful" + ) + + # Second message should be user + assert messages[1]["role"] == "user" + assert messages[1]["content"][0]["type"] == "input_text" + assert messages[1]["content"][0]["text"] == "Hello, how are you?" + @pytest.mark.asyncio async def test_pre_call_hook_blocked( self, noma_guardrail, mock_user_api_key_dict, mock_request_data @@ -644,34 +770,6 @@ class TestNomaGuardrailHooks: assert result == mock_request_data - def test_extract_user_message(self, noma_guardrail): - data = { - "messages": [ - {"role": "system", "content": "System prompt"}, - {"role": "user", "content": "First user message"}, - {"role": "assistant", "content": "Assistant response"}, - {"role": "user", "content": "Second user message"}, - ] - } - - import asyncio - - message = asyncio.run(noma_guardrail._extract_user_message(data)) - assert message == [{"type": "input_text", "text": "Second user message"}] - - data = {"messages": [{"role": "system", "content": "System prompt"}]} - message = asyncio.run(noma_guardrail._extract_user_message(data)) - assert message is None - - data = {"messages": []} - message = asyncio.run(noma_guardrail._extract_user_message(data)) - assert message is None - - data = {} - message = asyncio.run(noma_guardrail._extract_user_message(data)) - assert message is None - - class TestBackgroundProcessing: """Test the new background processing functionality""" @@ -1025,57 +1123,66 @@ class TestNomaImageProcessing: metadata={}, ) - def test_extract_user_message_with_image_url(self, noma_guardrail): - """Test extracting user message with image_url content""" - import asyncio - - data = { - "messages": [ - { - "role": "user", - "content": [ - { - "type": "image_url", - "image_url": { - "url": "https://example.com/image.jpg" - } - } - ] - } - ] - } + def test_extract_user_message_with_image_url(self): + """User message with only image_url becomes a single input_image content item.""" + from litellm.completion_extras.litellm_responses_transformation.transformation import ( + LiteLLMResponsesTransformationHandler, + ) - message = asyncio.run(noma_guardrail._extract_user_message(data)) + handler = LiteLLMResponsesTransformationHandler() + messages: list[AllMessageValues] = [ + { + "role": "user", + "content": [ + { + "type": "image_url", + "image_url": { + "url": "https://example.com/image.jpg" + } + } + ] + } + ] + + input_items, _ = handler.convert_chat_completion_messages_to_responses_api(messages) + + assert len(input_items) == 1 + message = input_items[0]["content"] assert message is not None assert len(message) == 1 assert message[0]["type"] == "input_image" assert message[0]["image_url"] == "https://example.com/image.jpg" - def test_extract_user_message_with_mixed_content(self, noma_guardrail): - """Test extracting user message with mixed text and image content""" - import asyncio - - data = { - "messages": [ - { - "role": "user", - "content": [ - { - "type": "text", - "text": "What's in this image?" - }, - { - "type": "image_url", - "image_url": { - "url": "https://example.com/image.jpg" - } - } - ] - } - ] - } + def test_extract_user_message_with_mixed_content(self): + """User message with text + image becomes input_text then input_image in content list.""" + from litellm.completion_extras.litellm_responses_transformation.transformation import ( + LiteLLMResponsesTransformationHandler, + ) - message = asyncio.run(noma_guardrail._extract_user_message(data)) + handler = LiteLLMResponsesTransformationHandler() + messages: list[AllMessageValues] = [ + { + "role": "user", + "content": [ + { + "type": "text", + "text": "What's in this image?", + }, + { + "type": "image_url", + "image_url": { + "url": "https://example.com/image.jpg" + } + } + ] + } + ] + + input_items, _ = handler.convert_chat_completion_messages_to_responses_api(messages) + + # Match the original assertions: `message` is the content list + assert len(input_items) == 1 + message = input_items[0]["content"] assert message is not None assert len(message) == 2 # First item should be text @@ -1085,37 +1192,43 @@ class TestNomaImageProcessing: assert message[1]["type"] == "input_image" assert message[1]["image_url"] == "https://example.com/image.jpg" - def test_extract_user_message_with_multiple_images(self, noma_guardrail): - """Test extracting user message with multiple images""" - import asyncio - - data = { - "messages": [ - { - "role": "user", - "content": [ - { - "type": "text", - "text": "Compare these images" - }, - { - "type": "image_url", - "image_url": { - "url": "https://example.com/image1.jpg" - } - }, - { - "type": "image_url", - "image_url": { - "url": "https://example.com/image2.jpg" - } - } - ] - } - ] - } + def test_extract_user_message_with_multiple_images(self): + """User message with multiple images becomes multiple input_image items.""" + from litellm.completion_extras.litellm_responses_transformation.transformation import ( + LiteLLMResponsesTransformationHandler, + ) - message = asyncio.run(noma_guardrail._extract_user_message(data)) + handler = LiteLLMResponsesTransformationHandler() + + messages: list[AllMessageValues] = [ + { + "role": "user", + "content": [ + { + "type": "text", + "text": "Compare these images", + }, + { + "type": "image_url", + "image_url": { + "url": "https://example.com/image1.jpg" + } + }, + { + "type": "image_url", + "image_url": { + "url": "https://example.com/image2.jpg" + } + } + ] + } + ] + + input_items, _ = handler.convert_chat_completion_messages_to_responses_api(messages) + + # Match the original assertions + assert len(input_items) == 1 + message = input_items[0]["content"] assert message is not None assert len(message) == 3 assert message[0]["type"] == "input_text" @@ -1301,8 +1414,14 @@ class TestNomaImageProcessing: assert exc_info.value.status_code == 400 @pytest.mark.asyncio - async def test_image_with_base64_data(self, noma_guardrail): + async def test_image_with_base64_data( + self, noma_guardrail, mock_user_api_key_dict + ): """Test extracting image with base64 data URL""" + from litellm.completion_extras.litellm_responses_transformation.transformation import ( + LiteLLMResponsesTransformationHandler, + ) + data = { "messages": [ { @@ -1319,7 +1438,13 @@ class TestNomaImageProcessing: ] } - message = await noma_guardrail._extract_user_message(data) + handler = LiteLLMResponsesTransformationHandler() + messages = cast(list[AllMessageValues], data["messages"]) + + input_items, _ = handler.convert_chat_completion_messages_to_responses_api(messages) + + assert len(input_items) == 1 + message = input_items[0]["content"] assert message is not None assert len(message) == 1 assert message[0]["type"] == "input_image" From 965406c643077dc5375a2c7bcd28c801caf807a6 Mon Sep 17 00:00:00 2001 From: Cesar Garcia <128240629+Chesars@users.noreply.github.com> Date: Tue, 2 Dec 2025 00:56:47 -0300 Subject: [PATCH 083/370] feat(provider): add Z.AI (Zhipu AI) as built-in provider (#17307) * feat(provider): add Z.AI (Zhipu AI) as built-in provider Add support for Z.AI GLM models as a native OpenAI-compatible provider. - Add "zai" to openai_compatible_providers list - Add ZAI enum to LlmProviders - Add provider URL resolution for https://api.z.ai/api/paas/v4 - Add 8 GLM models with pricing to model cost maps: - glm-4.6 (200K context, $0.6/$2.2 per 1M tokens) - glm-4.5, glm-4.5v, glm-4.5-x, glm-4.5-air, glm-4.5-airx - glm-4-32b-0414-128k - glm-4.5-flash (free tier) - Add unit tests for provider integration Closes #17289 * docs: add Z.AI provider documentation - Add zai.md with usage examples, model list, and pricing - Add to sidebars.js navigation --- docs/my-website/docs/providers/zai.md | 135 ++++++++++++++++ docs/my-website/sidebars.js | 1 + litellm/constants.py | 1 + .../get_llm_provider_logic.py | 7 + ...odel_prices_and_context_window_backup.json | 89 +++++++++++ litellm/types/utils.py | 1 + model_prices_and_context_window.json | 89 +++++++++++ .../llms/zai/test_zai_provider.py | 144 ++++++++++++++++++ 8 files changed, 467 insertions(+) create mode 100644 docs/my-website/docs/providers/zai.md create mode 100644 tests/test_litellm/llms/zai/test_zai_provider.py diff --git a/docs/my-website/docs/providers/zai.md b/docs/my-website/docs/providers/zai.md new file mode 100644 index 00000000000..5055d0c1cdd --- /dev/null +++ b/docs/my-website/docs/providers/zai.md @@ -0,0 +1,135 @@ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Z.AI (Zhipu AI) +https://z.ai/ + +**We support Z.AI GLM text/chat models, just set `zai/` as a prefix when sending completion requests** + +## API Key +```python +# env variable +os.environ['ZAI_API_KEY'] +``` + +## Sample Usage +```python +from litellm import completion +import os + +os.environ['ZAI_API_KEY'] = "" +response = completion( + model="zai/glm-4.6", + messages=[ + {"role": "user", "content": "hello from litellm"} + ], +) +print(response) +``` + +## Sample Usage - Streaming +```python +from litellm import completion +import os + +os.environ['ZAI_API_KEY'] = "" +response = completion( + model="zai/glm-4.6", + messages=[ + {"role": "user", "content": "hello from litellm"} + ], + stream=True +) + +for chunk in response: + print(chunk) +``` + +## Supported Models + +We support ALL Z.AI GLM models, just set `zai/` as a prefix when sending completion requests. + +| Model Name | Function Call | Notes | +|------------|---------------|-------| +| glm-4.6 | `completion(model="zai/glm-4.6", messages)` | Latest flagship model, 200K context | +| glm-4.5 | `completion(model="zai/glm-4.5", messages)` | 128K context | +| glm-4.5v | `completion(model="zai/glm-4.5v", messages)` | Vision model | +| glm-4.5-x | `completion(model="zai/glm-4.5-x", messages)` | Premium tier | +| glm-4.5-air | `completion(model="zai/glm-4.5-air", messages)` | Lightweight | +| glm-4.5-airx | `completion(model="zai/glm-4.5-airx", messages)` | Fast lightweight | +| glm-4-32b-0414-128k | `completion(model="zai/glm-4-32b-0414-128k", messages)` | 32B parameter model | +| glm-4.5-flash | `completion(model="zai/glm-4.5-flash", messages)` | **FREE tier** | + +## Model Pricing + +| Model | Input ($/1M tokens) | Output ($/1M tokens) | Context Window | +|-------|---------------------|----------------------|----------------| +| glm-4.6 | $0.60 | $2.20 | 200K | +| glm-4.5 | $0.60 | $2.20 | 128K | +| glm-4.5v | $0.60 | $1.80 | 128K | +| glm-4.5-x | $2.20 | $8.90 | 128K | +| glm-4.5-air | $0.20 | $1.10 | 128K | +| glm-4.5-airx | $1.10 | $4.50 | 128K | +| glm-4-32b-0414-128k | $0.10 | $0.10 | 128K | +| glm-4.5-flash | **FREE** | **FREE** | 128K | + +## Using with LiteLLM Proxy + + + + +```python +from litellm import completion +import os + +os.environ['ZAI_API_KEY'] = "" +response = completion( + model="zai/glm-4.6", + messages=[{"role": "user", "content": "Hello, how are you?"}], +) + +print(response.choices[0].message.content) +``` + + + + +1. Setup config.yaml + +```yaml +model_list: + - model_name: glm-4.6 + litellm_params: + model: zai/glm-4.6 + api_key: os.environ/ZAI_API_KEY + - model_name: glm-4.5-flash # Free tier + litellm_params: + model: zai/glm-4.5-flash + api_key: os.environ/ZAI_API_KEY +``` + +2. Run proxy + +```bash +litellm --config config.yaml +``` + +3. Test it! + +```bash +curl -L -X POST 'http://0.0.0.0:4000/v1/chat/completions' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: Bearer sk-1234' \ +-d '{ + "model": "glm-4.6", + "messages": [ + { + "role": "user", + "content": "Hello, how are you?" + } + ] +}' +``` + + + diff --git a/docs/my-website/sidebars.js b/docs/my-website/sidebars.js index 2039d01186c..e467711b59d 100644 --- a/docs/my-website/sidebars.js +++ b/docs/my-website/sidebars.js @@ -656,6 +656,7 @@ const sidebars = { }, "providers/xai", "providers/xinference", + "providers/zai", ], }, { diff --git a/litellm/constants.py b/litellm/constants.py index 65de5d7b555..e3de7368c8a 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -555,6 +555,7 @@ openai_compatible_providers: List = [ "perplexity", "xinference", "xai", + "zai", "together_ai", "fireworks_ai", "empower", diff --git a/litellm/litellm_core_utils/get_llm_provider_logic.py b/litellm/litellm_core_utils/get_llm_provider_logic.py index 4d29a74ddbc..b10011befcd 100644 --- a/litellm/litellm_core_utils/get_llm_provider_logic.py +++ b/litellm/litellm_core_utils/get_llm_provider_logic.py @@ -662,6 +662,13 @@ def _get_openai_compatible_provider_info( # noqa: PLR0915 ) = litellm.XAIChatConfig()._get_openai_compatible_provider_info( api_base, api_key ) + elif custom_llm_provider == "zai": + api_base = ( + api_base + or get_secret_str("ZAI_API_BASE") + or "https://api.z.ai/api/paas/v4" + ) + dynamic_api_key = api_key or get_secret_str("ZAI_API_KEY") elif custom_llm_provider == "together_ai": api_base = ( api_base diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index af63d1e2592..9fdc1704f41 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -26855,6 +26855,95 @@ "supports_vision": true, "supports_web_search": true }, + "zai/glm-4.6": { + "input_cost_per_token": 6e-07, + "output_cost_per_token": 2.2e-06, + "litellm_provider": "zai", + "max_input_tokens": 200000, + "max_output_tokens": 128000, + "mode": "chat", + "supports_function_calling": true, + "supports_tool_choice": true, + "source": "https://docs.z.ai/guides/overview/pricing" + }, + "zai/glm-4.5": { + "input_cost_per_token": 6e-07, + "output_cost_per_token": 2.2e-06, + "litellm_provider": "zai", + "max_input_tokens": 128000, + "max_output_tokens": 32000, + "mode": "chat", + "supports_function_calling": true, + "supports_tool_choice": true, + "source": "https://docs.z.ai/guides/overview/pricing" + }, + "zai/glm-4.5v": { + "input_cost_per_token": 6e-07, + "output_cost_per_token": 1.8e-06, + "litellm_provider": "zai", + "max_input_tokens": 128000, + "max_output_tokens": 32000, + "mode": "chat", + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_vision": true, + "source": "https://docs.z.ai/guides/overview/pricing" + }, + "zai/glm-4.5-x": { + "input_cost_per_token": 2.2e-06, + "output_cost_per_token": 8.9e-06, + "litellm_provider": "zai", + "max_input_tokens": 128000, + "max_output_tokens": 32000, + "mode": "chat", + "supports_function_calling": true, + "supports_tool_choice": true, + "source": "https://docs.z.ai/guides/overview/pricing" + }, + "zai/glm-4.5-air": { + "input_cost_per_token": 2e-07, + "output_cost_per_token": 1.1e-06, + "litellm_provider": "zai", + "max_input_tokens": 128000, + "max_output_tokens": 32000, + "mode": "chat", + "supports_function_calling": true, + "supports_tool_choice": true, + "source": "https://docs.z.ai/guides/overview/pricing" + }, + "zai/glm-4.5-airx": { + "input_cost_per_token": 1.1e-06, + "output_cost_per_token": 4.5e-06, + "litellm_provider": "zai", + "max_input_tokens": 128000, + "max_output_tokens": 32000, + "mode": "chat", + "supports_function_calling": true, + "supports_tool_choice": true, + "source": "https://docs.z.ai/guides/overview/pricing" + }, + "zai/glm-4-32b-0414-128k": { + "input_cost_per_token": 1e-07, + "output_cost_per_token": 1e-07, + "litellm_provider": "zai", + "max_input_tokens": 128000, + "max_output_tokens": 32000, + "mode": "chat", + "supports_function_calling": true, + "supports_tool_choice": true, + "source": "https://docs.z.ai/guides/overview/pricing" + }, + "zai/glm-4.5-flash": { + "input_cost_per_token": 0, + "output_cost_per_token": 0, + "litellm_provider": "zai", + "max_input_tokens": 128000, + "max_output_tokens": 32000, + "mode": "chat", + "supports_function_calling": true, + "supports_tool_choice": true, + "source": "https://docs.z.ai/guides/overview/pricing" + }, "vertex_ai/search_api": { "input_cost_per_query": 1.5e-03, "litellm_provider": "vertex_ai", diff --git a/litellm/types/utils.py b/litellm/types/utils.py index 2456c87044c..58267fdfea9 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -2553,6 +2553,7 @@ class LlmProviders(str, Enum): OPENAI_LIKE = "openai_like" # embedding only JINA_AI = "jina_ai" XAI = "xai" + ZAI = "zai" CUSTOM_OPENAI = "custom_openai" TEXT_COMPLETION_OPENAI = "text-completion-openai" COHERE = "cohere" diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 6b9b8beed80..1f8f1c7511e 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -26882,6 +26882,95 @@ "supports_vision": true, "supports_web_search": true }, + "zai/glm-4.6": { + "input_cost_per_token": 6e-07, + "output_cost_per_token": 2.2e-06, + "litellm_provider": "zai", + "max_input_tokens": 200000, + "max_output_tokens": 128000, + "mode": "chat", + "supports_function_calling": true, + "supports_tool_choice": true, + "source": "https://docs.z.ai/guides/overview/pricing" + }, + "zai/glm-4.5": { + "input_cost_per_token": 6e-07, + "output_cost_per_token": 2.2e-06, + "litellm_provider": "zai", + "max_input_tokens": 128000, + "max_output_tokens": 32000, + "mode": "chat", + "supports_function_calling": true, + "supports_tool_choice": true, + "source": "https://docs.z.ai/guides/overview/pricing" + }, + "zai/glm-4.5v": { + "input_cost_per_token": 6e-07, + "output_cost_per_token": 1.8e-06, + "litellm_provider": "zai", + "max_input_tokens": 128000, + "max_output_tokens": 32000, + "mode": "chat", + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_vision": true, + "source": "https://docs.z.ai/guides/overview/pricing" + }, + "zai/glm-4.5-x": { + "input_cost_per_token": 2.2e-06, + "output_cost_per_token": 8.9e-06, + "litellm_provider": "zai", + "max_input_tokens": 128000, + "max_output_tokens": 32000, + "mode": "chat", + "supports_function_calling": true, + "supports_tool_choice": true, + "source": "https://docs.z.ai/guides/overview/pricing" + }, + "zai/glm-4.5-air": { + "input_cost_per_token": 2e-07, + "output_cost_per_token": 1.1e-06, + "litellm_provider": "zai", + "max_input_tokens": 128000, + "max_output_tokens": 32000, + "mode": "chat", + "supports_function_calling": true, + "supports_tool_choice": true, + "source": "https://docs.z.ai/guides/overview/pricing" + }, + "zai/glm-4.5-airx": { + "input_cost_per_token": 1.1e-06, + "output_cost_per_token": 4.5e-06, + "litellm_provider": "zai", + "max_input_tokens": 128000, + "max_output_tokens": 32000, + "mode": "chat", + "supports_function_calling": true, + "supports_tool_choice": true, + "source": "https://docs.z.ai/guides/overview/pricing" + }, + "zai/glm-4-32b-0414-128k": { + "input_cost_per_token": 1e-07, + "output_cost_per_token": 1e-07, + "litellm_provider": "zai", + "max_input_tokens": 128000, + "max_output_tokens": 32000, + "mode": "chat", + "supports_function_calling": true, + "supports_tool_choice": true, + "source": "https://docs.z.ai/guides/overview/pricing" + }, + "zai/glm-4.5-flash": { + "input_cost_per_token": 0, + "output_cost_per_token": 0, + "litellm_provider": "zai", + "max_input_tokens": 128000, + "max_output_tokens": 32000, + "mode": "chat", + "supports_function_calling": true, + "supports_tool_choice": true, + "source": "https://docs.z.ai/guides/overview/pricing" + }, "vertex_ai/search_api": { "input_cost_per_query": 1.5e-03, "litellm_provider": "vertex_ai", diff --git a/tests/test_litellm/llms/zai/test_zai_provider.py b/tests/test_litellm/llms/zai/test_zai_provider.py new file mode 100644 index 00000000000..a3d47d666bc --- /dev/null +++ b/tests/test_litellm/llms/zai/test_zai_provider.py @@ -0,0 +1,144 @@ +""" +Tests for Z.AI (Zhipu AI) provider - GLM models +""" +import json +import math + +import pytest +import respx + +import litellm +from litellm import completion +from litellm.cost_calculator import cost_per_token + + +@pytest.fixture +def zai_response(): + """Mock response from Z.AI API""" + return { + "id": "chatcmpl-zai-123", + "object": "chat.completion", + "created": 1677652288, + "model": "glm-4.6", + "choices": [ + { + "index": 0, + "message": {"role": "assistant", "content": "Hello! How can I help you today?"}, + "finish_reason": "stop", + } + ], + "usage": {"prompt_tokens": 10, "completion_tokens": 15, "total_tokens": 25}, + } + + +def test_get_llm_provider_zai(): + """Test that get_llm_provider correctly identifies zai provider""" + from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider + + model, provider, api_key, api_base = get_llm_provider("zai/glm-4.6") + assert model == "glm-4.6" + assert provider == "zai" + assert api_base == "https://api.z.ai/api/paas/v4" + + +def test_zai_in_provider_lists(): + """Test that zai is registered in all necessary provider lists""" + assert "zai" in litellm.openai_compatible_providers + assert "zai" in litellm.provider_list + + +def test_zai_models_in_model_cost(): + """Test that ZAI models are in the model cost map""" + import os + os.environ["LITELLM_LOCAL_MODEL_COST_MAP"] = "True" + litellm.model_cost = litellm.get_model_cost_map(url="") + + zai_models = [ + "zai/glm-4.6", + "zai/glm-4.5", + "zai/glm-4.5v", + "zai/glm-4.5-x", + "zai/glm-4.5-air", + "zai/glm-4.5-airx", + "zai/glm-4-32b-0414-128k", + "zai/glm-4.5-flash", + ] + + for model in zai_models: + assert model in litellm.model_cost, f"Model {model} not found in model_cost" + assert litellm.model_cost[model]["litellm_provider"] == "zai" + + +def test_zai_glm46_cost_calculation(): + """Test the cost calculation for glm-4.6""" + import os + os.environ["LITELLM_LOCAL_MODEL_COST_MAP"] = "True" + litellm.model_cost = litellm.get_model_cost_map(url="") + + key = "zai/glm-4.6" + info = litellm.model_cost[key] + + prompt_cost, completion_cost = cost_per_token( + model="zai/glm-4.6", + prompt_tokens=1000000, # 1M tokens + completion_tokens=1000000, + ) + + # GLM-4.6: $0.6/M input, $2.2/M output + assert math.isclose(prompt_cost, 0.6, rel_tol=1e-6) + assert math.isclose(completion_cost, 2.2, rel_tol=1e-6) + + +def test_zai_flash_model_is_free(): + """Test that glm-4.5-flash has zero cost""" + import os + os.environ["LITELLM_LOCAL_MODEL_COST_MAP"] = "True" + litellm.model_cost = litellm.get_model_cost_map(url="") + + key = "zai/glm-4.5-flash" + info = litellm.model_cost[key] + + assert info["input_cost_per_token"] == 0 + assert info["output_cost_per_token"] == 0 + + +@pytest.mark.asyncio +async def test_zai_completion_call(respx_mock, zai_response, monkeypatch): + """Test completion call with zai provider using mocked response""" + monkeypatch.setenv("ZAI_API_KEY", "test-api-key") + litellm.disable_aiohttp_transport = True + + respx_mock.post("https://api.z.ai/api/paas/v4/chat/completions").respond(json=zai_response) + + response = await litellm.acompletion( + model="zai/glm-4.6", + messages=[{"role": "user", "content": "Hello"}], + max_tokens=20, + ) + + assert response.choices[0].message.content == "Hello! How can I help you today?" + assert response.usage.total_tokens == 25 + + assert len(respx_mock.calls) == 1 + request = respx_mock.calls[0].request + assert request.method == "POST" + assert "api.z.ai" in str(request.url) + assert "Authorization" in request.headers + assert request.headers["Authorization"] == "Bearer test-api-key" + + +def test_zai_sync_completion(respx_mock, zai_response, monkeypatch): + """Test synchronous completion call""" + monkeypatch.setenv("ZAI_API_KEY", "test-api-key") + litellm.disable_aiohttp_transport = True + + respx_mock.post("https://api.z.ai/api/paas/v4/chat/completions").respond(json=zai_response) + + response = completion( + model="zai/glm-4.6", + messages=[{"role": "user", "content": "Hello"}], + max_tokens=20, + ) + + assert response.choices[0].message.content == "Hello! How can I help you today?" + assert response.usage.total_tokens == 25 From 01dfc3561acb1baf60209786fc24e19d77384b08 Mon Sep 17 00:00:00 2001 From: Cesar Garcia <128240629+Chesars@users.noreply.github.com> Date: Tue, 2 Dec 2025 00:58:27 -0300 Subject: [PATCH 084/370] Fix AttributeError when metadata is null in request body (#17263) (#17306) Handle the case where metadata is explicitly set to null/None in the request body. This was causing a 401 error with "'NoneType' object has no attribute 'get'" when calling /v1/batches with metadata: null. The fix uses `or {}` instead of a default dict value since the key exists but has a None value. --- .../proxy/common_utils/http_parsing_utils.py | 2 +- .../common_utils/test_http_parsing_utils.py | 23 +++++++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/litellm/proxy/common_utils/http_parsing_utils.py b/litellm/proxy/common_utils/http_parsing_utils.py index 59b3ec20b4a..259755f5ef9 100644 --- a/litellm/proxy/common_utils/http_parsing_utils.py +++ b/litellm/proxy/common_utils/http_parsing_utils.py @@ -309,7 +309,7 @@ def get_tags_from_request_body(request_body: dict) -> List[str]: List of tag names (strings), empty list if no valid tags found """ metadata_variable_name = get_metadata_variable_name_from_kwargs(request_body) - metadata = request_body.get(metadata_variable_name, {}) + metadata = request_body.get(metadata_variable_name) or {} tags_in_metadata: Any = metadata.get("tags", []) tags_in_request_body: Any = request_body.get("tags", []) combined_tags: List[str] = [] diff --git a/tests/test_litellm/proxy/common_utils/test_http_parsing_utils.py b/tests/test_litellm/proxy/common_utils/test_http_parsing_utils.py index 85858866dda..2361decc5af 100644 --- a/tests/test_litellm/proxy/common_utils/test_http_parsing_utils.py +++ b/tests/test_litellm/proxy/common_utils/test_http_parsing_utils.py @@ -606,8 +606,27 @@ def test_get_tags_from_request_body_with_dict_tags(): } } } - + result = get_tags_from_request_body(request_body=request_body) - + + assert result == [] + assert isinstance(result, list) + + +def test_get_tags_from_request_body_with_null_metadata(): + """ + Test that function handles null metadata gracefully without crashing. + + This is a regression test for https://github.com/BerriAI/litellm/issues/17263 + When metadata is explicitly set to null/None, the function should return + an empty list instead of raising AttributeError. + """ + request_body = { + "model": "gpt-4", + "metadata": None # OpenAI API accepts metadata: null + } + + result = get_tags_from_request_body(request_body=request_body) + assert result == [] assert isinstance(result, list) From 860270a7927b0d13319bead2fdff9a4c00e3d00f Mon Sep 17 00:00:00 2001 From: Saar wintrov Date: Tue, 2 Dec 2025 06:01:36 +0200 Subject: [PATCH 085/370] SSO: Clear sso integration for all users (#17287) --- .../_buildManifest.js | 0 .../_ssgManifest.js | 0 .../static/chunks/1518-21c80a799b5c426e.js | 1 - .../static/chunks/1518-4475f8385da5ac78.js | 1 + ...6050ee3518d4.js => 1529-59ce29afdf8ccc9b.js} | 2 +- ...971a192714f2.js => 1674-de8248fbd0c554ba.js} | 2 +- .../static/chunks/1973-26a414084f96c69b.js | 1 + .../static/chunks/1994-6637a121c9ee1602.js | 1 - .../static/chunks/1994-a4d0b99849c16b62.js | 1 + .../static/chunks/2004-294ce010a90069b4.js | 1 + .../static/chunks/2004-8b1ad3d8c195646a.js | 1 - .../static/chunks/2012-9200c205d5b0405a.js | 1 - .../static/chunks/2012-c09fa25a9cbf6028.js | 1 + .../static/chunks/2019-15183fcc4c29249f.js | 1 - .../static/chunks/2249-01a36f26b1cecba3.js | 1 + .../static/chunks/2249-3e3c0a9e241e35dc.js | 1 - ...0eb77e9f4fa7.js => 3250-6c57da6c11f342fa.js} | 2 +- .../static/chunks/3325-4a3c766c7d12465e.js | 1 - .../static/chunks/3341-852c4599adcc0f2b.js | 1 + ...9f5df18d8716.js => 3705-124a560b74decaa8.js} | 2 +- .../static/chunks/3801-9878b21c4f9ae250.js | 1 - .../static/chunks/3801-ff2404f6d0c38247.js | 1 + .../static/chunks/4182-1ec11708566c0483.js | 1 - .../static/chunks/4267-eb59bdfbffb79a80.js | 1 - .../static/chunks/4292-28669d6dfecbbf62.js | 1 + .../static/chunks/4292-913ecd28879b76a8.js | 1 - .../static/chunks/4612-06e9d10957e990c0.js | 1 + .../_next/static/chunks/475-3985fee235e827f8.js | 1 + .../static/chunks/4865-c1c0885a93c327fa.js | 1 - .../static/chunks/5074-51f1824c21869900.js | 1 - .../static/chunks/5096-d9222b69b30b3d56.js | 1 + ...9ffa75db75f8.js => 5170-eddf033da66a3d25.js} | 2 +- .../_next/static/chunks/544-3d98fdc8d64554e8.js | 1 - .../static/chunks/5572-9290ae3dc2551207.js | 1 - .../static/chunks/5572-d4f8dc9b2bf09618.js | 1 + .../static/chunks/5830-30dbbe6913297258.js | 1 + ...68ba6ad0ce0c.js => 5869-99bf8c2997f4811f.js} | 2 +- .../static/chunks/5945-8b3b7713d7f416a2.js | 1 + .../_next/static/chunks/605-102c0e6d8bb7517c.js | 1 + .../static/chunks/6062-89f63f71675c6a08.js | 1 - .../static/chunks/6264-a48a17494c2e1d26.js | 1 - .../_next/static/chunks/630-1e0342aa26bb0fe8.js | 1 - .../_next/static/chunks/630-f305780b75c36612.js | 1 + ...e6266dea9539.js => 6600-1c55511ad9da9e4d.js} | 2 +- .../static/chunks/6609-3e081758ffbe3786.js | 1 - .../static/chunks/6609-d93906f43161f066.js | 1 + .../_next/static/chunks/667-213a9fbd82e0ada7.js | 1 + .../static/chunks/6843-98abf1271c25c6e0.js | 1 + .../static/chunks/6843-b8ebdf2bb4fe5c67.js | 1 - .../static/chunks/7155-1a3e4c5a6aefae2b.js | 1 - .../static/chunks/7155-459bc53437553b96.js | 1 + .../static/chunks/7164-8de9ea967cd5d031.js | 1 + .../static/chunks/7164-b089dfb991cc1d8c.js | 1 - .../static/chunks/7187-d4c57193fb558148.js | 1 + ...1f347ea9707a.js => 7526-9d5ec51e0920ffc6.js} | 2 +- .../static/chunks/7641-f70830b7a61a3f9c.js | 1 - .../static/chunks/7641-fa9cc1f68c670e1c.js | 1 + ...579c5c97ecaba.js => 773-b02e89f4d1193982.js} | 2 +- ...16ddcb35e063.js => 7975-d5ed9d0e73f8f3a9.js} | 2 +- .../static/chunks/8008-851877152eb2be38.js | 1 - .../static/chunks/8541-04c822145b2301f8.js | 1 + .../static/chunks/8661-1cf4178f6bffc981.js | 1 - .../static/chunks/9028-2bfc9f09930a0d61.js | 1 - .../static/chunks/9111-3cb8240098962e8a.js | 1 - .../static/chunks/9111-9b9192c9fb4809ff.js | 1 + ...dfb8fa3d2ed7.js => 9611-8bd2ffcee22edc34.js} | 2 +- .../static/chunks/9798-a47f1a4423863a8a.js | 1 + .../static/chunks/9877-f58702e3cb433729.js | 1 - .../static/chunks/9877-ff2a01b39a318119.js | 1 + .../api-reference/page-6ead8448e1510439.js | 1 - .../api-reference/page-efca3b67652c1db6.js | 1 + .../api-playground/page-8047d2cef33b9999.js | 1 - .../api-playground/page-e66957ea53741305.js | 1 + ...cab0cb418464.js => page-349dab403faa8586.js} | 2 +- ...6b7e7489b565.js => page-29593a3a38ff72cd.js} | 2 +- ...6697f4d6f550.js => page-392368af0265ebf3.js} | 2 +- .../prompts/page-843a18f5283af912.js | 1 - .../prompts/page-a188489df21ffc96.js | 1 + ...5395c754c862.js => page-04b44e5847f0e275.js} | 2 +- ...350eb16ca3ba.js => page-df254f7363ecac47.js} | 2 +- .../app/(dashboard)/layout-a0258e2243643336.js | 1 + .../app/(dashboard)/layout-a928c135835301f0.js | 1 - .../(dashboard)/logs/page-24f7ccafa5658895.js | 1 + .../(dashboard)/logs/page-974be1d69803befc.js | 1 - ...b2d51a5f5567.js => page-cb5b5c184df1920f.js} | 2 +- .../page-7526ca663daec9bf.js | 1 + .../page-a11b969ee66b82c0.js | 1 - ...977ecb7e4aea.js => page-c5c54ec599dda90a.js} | 2 +- ...e021acdc06f3.js => page-f66c8c75efc80fa3.js} | 2 +- .../admin-settings/page-41bcefda7b19fcbe.js | 1 - .../admin-settings/page-b14017f2434341b6.js | 1 + ...c91c28de94ff.js => page-73e2aa132fcafea5.js} | 2 +- .../router-settings/page-7e77ec8e3ff58278.js | 1 - .../router-settings/page-ce416427bf19a1dc.js | 1 + ...0a8b43105b4e.js => page-e723d4c81fc7d9a9.js} | 2 +- ...d0b0d541c84f.js => page-464d4ef166df7211.js} | 2 +- ...2c9c481d0375.js => page-4dd219948b528c92.js} | 2 +- ...df83dfce71fa.js => page-4a1119ecd30d2b39.js} | 2 +- ...498724555fa1.js => page-c4aed80b18ca0651.js} | 2 +- ...feee0752e151.js => page-2098a2b6e214223c.js} | 2 +- .../(dashboard)/users/page-607b92cfac56e9f9.js | 1 - .../(dashboard)/users/page-80eaf816a6ca5c75.js | 1 + .../virtual-keys/page-52c22b525906afcf.js | 1 + .../virtual-keys/page-681e2e7643e3068c.js | 1 - .../chunks/app/layout-4e0c2c971ccc1e6d.js | 1 - .../chunks/app/layout-5681449b28aa197a.js | 1 + ...c0d632ab220d.js => page-e50863ece139886b.js} | 2 +- ...17915c7f9cff.js => page-ca976de28014d49a.js} | 2 +- ...536062f9ecd9.js => page-623abbf7f2315887.js} | 2 +- .../app/onboarding/page-6f2572027a406495.js | 1 + .../app/onboarding/page-7cc24917468a90ab.js | 1 - .../static/chunks/app/page-28eb040917ca1710.js | 1 + .../static/chunks/app/page-dda848d817541095.js | 1 - ...04ee9adf.js => main-app-ce1f29ef0860719b.js} | 2 +- ...ed3b4a921.js => webpack-134f5d194761e240.js} | 2 +- .../out/_next/static/css/0fc668a8750043fe.css | 1 + .../proxy/_experimental/out/api-reference.html | 1 - .../proxy/_experimental/out/api-reference.txt | 17 ++++++++--------- .../_experimental/out/api-reference/index.html | 1 + .../out/experimental/api-playground.html | 2 +- .../out/experimental/api-playground.txt | 17 ++++++++--------- .../_experimental/out/experimental/budgets.html | 2 +- .../_experimental/out/experimental/budgets.txt | 17 ++++++++--------- .../_experimental/out/experimental/caching.html | 2 +- .../_experimental/out/experimental/caching.txt | 17 ++++++++--------- .../out/experimental/old-usage.html | 2 +- .../out/experimental/old-usage.txt | 17 ++++++++--------- .../_experimental/out/experimental/prompts.html | 2 +- .../_experimental/out/experimental/prompts.txt | 17 ++++++++--------- .../out/experimental/tag-management.html | 2 +- .../out/experimental/tag-management.txt | 17 ++++++++--------- litellm/proxy/_experimental/out/guardrails.html | 1 - litellm/proxy/_experimental/out/guardrails.txt | 17 ++++++++--------- litellm/proxy/_experimental/out/index.html | 2 +- litellm/proxy/_experimental/out/index.txt | 11 +++++------ litellm/proxy/_experimental/out/logs.html | 1 - litellm/proxy/_experimental/out/logs.txt | 17 ++++++++--------- litellm/proxy/_experimental/out/logs/index.html | 1 + .../_experimental/out/mcp/oauth/callback.html | 2 +- .../_experimental/out/mcp/oauth/callback.txt | 7 +++---- litellm/proxy/_experimental/out/model-hub.html | 1 - litellm/proxy/_experimental/out/model-hub.txt | 17 ++++++++--------- .../_experimental/out/model-hub/index.html | 1 + litellm/proxy/_experimental/out/model_hub.txt | 7 +++---- .../_experimental/out/model_hub_table.html | 1 - .../proxy/_experimental/out/model_hub_table.txt | 7 +++---- .../out/model_hub_table/index.html | 1 + .../_experimental/out/models-and-endpoints.html | 1 - .../_experimental/out/models-and-endpoints.txt | 17 ++++++++--------- .../out/models-and-endpoints/index.html | 1 + litellm/proxy/_experimental/out/onboarding.html | 1 - litellm/proxy/_experimental/out/onboarding.txt | 7 +++---- .../proxy/_experimental/out/organizations.html | 1 - .../proxy/_experimental/out/organizations.txt | 17 ++++++++--------- .../_experimental/out/organizations/index.html | 1 + litellm/proxy/_experimental/out/playground.html | 1 - litellm/proxy/_experimental/out/playground.txt | 17 ++++++++--------- .../_experimental/out/playground/index.html | 1 + .../out/settings/admin-settings.html | 2 +- .../out/settings/admin-settings.txt | 17 ++++++++--------- .../out/settings/logging-and-alerts.html | 2 +- .../out/settings/logging-and-alerts.txt | 17 ++++++++--------- .../out/settings/router-settings.html | 2 +- .../out/settings/router-settings.txt | 17 ++++++++--------- .../_experimental/out/settings/ui-theme.html | 2 +- .../_experimental/out/settings/ui-theme.txt | 17 ++++++++--------- litellm/proxy/_experimental/out/teams.html | 1 - litellm/proxy/_experimental/out/teams.txt | 17 ++++++++--------- .../proxy/_experimental/out/teams/index.html | 1 + litellm/proxy/_experimental/out/test-key.html | 1 - litellm/proxy/_experimental/out/test-key.txt | 17 ++++++++--------- .../proxy/_experimental/out/test-key/index.html | 1 + .../_experimental/out/tools/mcp-servers.html | 2 +- .../_experimental/out/tools/mcp-servers.txt | 17 ++++++++--------- .../_experimental/out/tools/vector-stores.html | 2 +- .../_experimental/out/tools/vector-stores.txt | 17 ++++++++--------- litellm/proxy/_experimental/out/usage.html | 1 - litellm/proxy/_experimental/out/usage.txt | 17 ++++++++--------- .../proxy/_experimental/out/usage/index.html | 1 + litellm/proxy/_experimental/out/users.html | 1 - litellm/proxy/_experimental/out/users.txt | 17 ++++++++--------- .../proxy/_experimental/out/users/index.html | 1 + .../proxy/_experimental/out/virtual-keys.html | 1 - .../proxy/_experimental/out/virtual-keys.txt | 17 ++++++++--------- .../_experimental/out/virtual-keys/index.html | 1 + ui/litellm-dashboard/src/components/admins.tsx | 2 +- 186 files changed, 309 insertions(+), 339 deletions(-) rename litellm/proxy/_experimental/out/_next/static/{V73dwfVXi9kkAaHXHHR5u => 6DVsIIQxhiSKdAYpN-pIf}/_buildManifest.js (100%) rename litellm/proxy/_experimental/out/_next/static/{V73dwfVXi9kkAaHXHHR5u => 6DVsIIQxhiSKdAYpN-pIf}/_ssgManifest.js (100%) delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/1518-21c80a799b5c426e.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/1518-4475f8385da5ac78.js rename litellm/proxy/_experimental/out/_next/static/chunks/{1529-aa686050ee3518d4.js => 1529-59ce29afdf8ccc9b.js} (99%) rename litellm/proxy/_experimental/out/_next/static/chunks/{1674-475a971a192714f2.js => 1674-de8248fbd0c554ba.js} (99%) create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/1973-26a414084f96c69b.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/1994-6637a121c9ee1602.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/1994-a4d0b99849c16b62.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/2004-294ce010a90069b4.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/2004-8b1ad3d8c195646a.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/2012-9200c205d5b0405a.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/2012-c09fa25a9cbf6028.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/2019-15183fcc4c29249f.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/2249-01a36f26b1cecba3.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/2249-3e3c0a9e241e35dc.js rename litellm/proxy/_experimental/out/_next/static/chunks/{3250-d3d70eb77e9f4fa7.js => 3250-6c57da6c11f342fa.js} (99%) delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/3325-4a3c766c7d12465e.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/3341-852c4599adcc0f2b.js rename litellm/proxy/_experimental/out/_next/static/chunks/{3705-05649f5df18d8716.js => 3705-124a560b74decaa8.js} (99%) delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/3801-9878b21c4f9ae250.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/3801-ff2404f6d0c38247.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/4182-1ec11708566c0483.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/4267-eb59bdfbffb79a80.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/4292-28669d6dfecbbf62.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/4292-913ecd28879b76a8.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/4612-06e9d10957e990c0.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/475-3985fee235e827f8.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/4865-c1c0885a93c327fa.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/5074-51f1824c21869900.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/5096-d9222b69b30b3d56.js rename litellm/proxy/_experimental/out/_next/static/chunks/{5170-56859ffa75db75f8.js => 5170-eddf033da66a3d25.js} (99%) delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/544-3d98fdc8d64554e8.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/5572-9290ae3dc2551207.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/5572-d4f8dc9b2bf09618.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/5830-30dbbe6913297258.js rename litellm/proxy/_experimental/out/_next/static/chunks/{5869-426268ba6ad0ce0c.js => 5869-99bf8c2997f4811f.js} (99%) create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/5945-8b3b7713d7f416a2.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/605-102c0e6d8bb7517c.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/6062-89f63f71675c6a08.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/6264-a48a17494c2e1d26.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/630-1e0342aa26bb0fe8.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/630-f305780b75c36612.js rename litellm/proxy/_experimental/out/_next/static/chunks/{6600-3c16e6266dea9539.js => 6600-1c55511ad9da9e4d.js} (99%) delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/6609-3e081758ffbe3786.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/6609-d93906f43161f066.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/667-213a9fbd82e0ada7.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/6843-98abf1271c25c6e0.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/6843-b8ebdf2bb4fe5c67.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/7155-1a3e4c5a6aefae2b.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/7155-459bc53437553b96.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/7164-8de9ea967cd5d031.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/7164-b089dfb991cc1d8c.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/7187-d4c57193fb558148.js rename litellm/proxy/_experimental/out/_next/static/chunks/{7526-e29a1f347ea9707a.js => 7526-9d5ec51e0920ffc6.js} (51%) delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/7641-f70830b7a61a3f9c.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/7641-fa9cc1f68c670e1c.js rename litellm/proxy/_experimental/out/_next/static/chunks/{773-870579c5c97ecaba.js => 773-b02e89f4d1193982.js} (74%) rename litellm/proxy/_experimental/out/_next/static/chunks/{7975-afe816ddcb35e063.js => 7975-d5ed9d0e73f8f3a9.js} (65%) delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/8008-851877152eb2be38.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/8541-04c822145b2301f8.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/8661-1cf4178f6bffc981.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/9028-2bfc9f09930a0d61.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/9111-3cb8240098962e8a.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/9111-9b9192c9fb4809ff.js rename litellm/proxy/_experimental/out/_next/static/chunks/{9611-e0c4dfb8fa3d2ed7.js => 9611-8bd2ffcee22edc34.js} (99%) create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/9798-a47f1a4423863a8a.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/9877-f58702e3cb433729.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/9877-ff2a01b39a318119.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/app/(dashboard)/api-reference/page-6ead8448e1510439.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/app/(dashboard)/api-reference/page-efca3b67652c1db6.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/app/(dashboard)/experimental/api-playground/page-8047d2cef33b9999.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/app/(dashboard)/experimental/api-playground/page-e66957ea53741305.js rename litellm/proxy/_experimental/out/_next/static/chunks/app/(dashboard)/experimental/budgets/{page-3234cab0cb418464.js => page-349dab403faa8586.js} (96%) rename litellm/proxy/_experimental/out/_next/static/chunks/app/(dashboard)/experimental/caching/{page-0a286b7e7489b565.js => page-29593a3a38ff72cd.js} (92%) rename litellm/proxy/_experimental/out/_next/static/chunks/app/(dashboard)/experimental/old-usage/{page-bdfb6697f4d6f550.js => page-392368af0265ebf3.js} (98%) delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/app/(dashboard)/experimental/prompts/page-843a18f5283af912.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/app/(dashboard)/experimental/prompts/page-a188489df21ffc96.js rename litellm/proxy/_experimental/out/_next/static/chunks/app/(dashboard)/experimental/tag-management/{page-e5395395c754c862.js => page-04b44e5847f0e275.js} (96%) rename litellm/proxy/_experimental/out/_next/static/chunks/app/(dashboard)/guardrails/{page-fbd5350eb16ca3ba.js => page-df254f7363ecac47.js} (93%) create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/app/(dashboard)/layout-a0258e2243643336.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/app/(dashboard)/layout-a928c135835301f0.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/app/(dashboard)/logs/page-24f7ccafa5658895.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/app/(dashboard)/logs/page-974be1d69803befc.js rename litellm/proxy/_experimental/out/_next/static/chunks/app/(dashboard)/model-hub/{page-a4e1b2d51a5f5567.js => page-cb5b5c184df1920f.js} (95%) create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/app/(dashboard)/models-and-endpoints/page-7526ca663daec9bf.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/app/(dashboard)/models-and-endpoints/page-a11b969ee66b82c0.js rename litellm/proxy/_experimental/out/_next/static/chunks/app/(dashboard)/organizations/{page-c9a9977ecb7e4aea.js => page-c5c54ec599dda90a.js} (97%) rename litellm/proxy/_experimental/out/_next/static/chunks/app/(dashboard)/playground/{page-6729e021acdc06f3.js => page-f66c8c75efc80fa3.js} (97%) delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/app/(dashboard)/settings/admin-settings/page-41bcefda7b19fcbe.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/app/(dashboard)/settings/admin-settings/page-b14017f2434341b6.js rename litellm/proxy/_experimental/out/_next/static/chunks/app/(dashboard)/settings/logging-and-alerts/{page-2470c91c28de94ff.js => page-73e2aa132fcafea5.js} (97%) delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/app/(dashboard)/settings/router-settings/page-7e77ec8e3ff58278.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/app/(dashboard)/settings/router-settings/page-ce416427bf19a1dc.js rename litellm/proxy/_experimental/out/_next/static/chunks/app/(dashboard)/settings/ui-theme/{page-ee5d0a8b43105b4e.js => page-e723d4c81fc7d9a9.js} (98%) rename litellm/proxy/_experimental/out/_next/static/chunks/app/(dashboard)/teams/{page-866cd0b0d541c84f.js => page-464d4ef166df7211.js} (99%) rename litellm/proxy/_experimental/out/_next/static/chunks/app/(dashboard)/test-key/{page-69022c9c481d0375.js => page-4dd219948b528c92.js} (98%) rename litellm/proxy/_experimental/out/_next/static/chunks/app/(dashboard)/tools/mcp-servers/{page-9474df83dfce71fa.js => page-4a1119ecd30d2b39.js} (93%) rename litellm/proxy/_experimental/out/_next/static/chunks/app/(dashboard)/tools/vector-stores/{page-6ddf498724555fa1.js => page-c4aed80b18ca0651.js} (96%) rename litellm/proxy/_experimental/out/_next/static/chunks/app/(dashboard)/usage/{page-6882feee0752e151.js => page-2098a2b6e214223c.js} (95%) delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/app/(dashboard)/users/page-607b92cfac56e9f9.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/app/(dashboard)/users/page-80eaf816a6ca5c75.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/app/(dashboard)/virtual-keys/page-52c22b525906afcf.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/app/(dashboard)/virtual-keys/page-681e2e7643e3068c.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/app/layout-4e0c2c971ccc1e6d.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/app/layout-5681449b28aa197a.js rename litellm/proxy/_experimental/out/_next/static/chunks/app/mcp/oauth/callback/{page-4cdcc0d632ab220d.js => page-e50863ece139886b.js} (89%) rename litellm/proxy/_experimental/out/_next/static/chunks/app/model_hub/{page-16d517915c7f9cff.js => page-ca976de28014d49a.js} (84%) rename litellm/proxy/_experimental/out/_next/static/chunks/app/model_hub_table/{page-e60a536062f9ecd9.js => page-623abbf7f2315887.js} (95%) create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/app/onboarding/page-6f2572027a406495.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/app/onboarding/page-7cc24917468a90ab.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/app/page-28eb040917ca1710.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/app/page-dda848d817541095.js rename litellm/proxy/_experimental/out/_next/static/chunks/{main-app-77a6ca3c04ee9adf.js => main-app-ce1f29ef0860719b.js} (81%) rename litellm/proxy/_experimental/out/_next/static/chunks/{webpack-db32e14ed3b4a921.js => webpack-134f5d194761e240.js} (77%) create mode 100644 litellm/proxy/_experimental/out/_next/static/css/0fc668a8750043fe.css delete mode 100644 litellm/proxy/_experimental/out/api-reference.html create mode 100644 litellm/proxy/_experimental/out/api-reference/index.html delete mode 100644 litellm/proxy/_experimental/out/guardrails.html delete mode 100644 litellm/proxy/_experimental/out/logs.html create mode 100644 litellm/proxy/_experimental/out/logs/index.html delete mode 100644 litellm/proxy/_experimental/out/model-hub.html create mode 100644 litellm/proxy/_experimental/out/model-hub/index.html delete mode 100644 litellm/proxy/_experimental/out/model_hub_table.html create mode 100644 litellm/proxy/_experimental/out/model_hub_table/index.html delete mode 100644 litellm/proxy/_experimental/out/models-and-endpoints.html create mode 100644 litellm/proxy/_experimental/out/models-and-endpoints/index.html delete mode 100644 litellm/proxy/_experimental/out/onboarding.html delete mode 100644 litellm/proxy/_experimental/out/organizations.html create mode 100644 litellm/proxy/_experimental/out/organizations/index.html delete mode 100644 litellm/proxy/_experimental/out/playground.html create mode 100644 litellm/proxy/_experimental/out/playground/index.html delete mode 100644 litellm/proxy/_experimental/out/teams.html create mode 100644 litellm/proxy/_experimental/out/teams/index.html delete mode 100644 litellm/proxy/_experimental/out/test-key.html create mode 100644 litellm/proxy/_experimental/out/test-key/index.html delete mode 100644 litellm/proxy/_experimental/out/usage.html create mode 100644 litellm/proxy/_experimental/out/usage/index.html delete mode 100644 litellm/proxy/_experimental/out/users.html create mode 100644 litellm/proxy/_experimental/out/users/index.html delete mode 100644 litellm/proxy/_experimental/out/virtual-keys.html create mode 100644 litellm/proxy/_experimental/out/virtual-keys/index.html diff --git a/litellm/proxy/_experimental/out/_next/static/V73dwfVXi9kkAaHXHHR5u/_buildManifest.js b/litellm/proxy/_experimental/out/_next/static/6DVsIIQxhiSKdAYpN-pIf/_buildManifest.js similarity index 100% rename from litellm/proxy/_experimental/out/_next/static/V73dwfVXi9kkAaHXHHR5u/_buildManifest.js rename to litellm/proxy/_experimental/out/_next/static/6DVsIIQxhiSKdAYpN-pIf/_buildManifest.js diff --git a/litellm/proxy/_experimental/out/_next/static/V73dwfVXi9kkAaHXHHR5u/_ssgManifest.js b/litellm/proxy/_experimental/out/_next/static/6DVsIIQxhiSKdAYpN-pIf/_ssgManifest.js similarity index 100% rename from litellm/proxy/_experimental/out/_next/static/V73dwfVXi9kkAaHXHHR5u/_ssgManifest.js rename to litellm/proxy/_experimental/out/_next/static/6DVsIIQxhiSKdAYpN-pIf/_ssgManifest.js diff --git a/litellm/proxy/_experimental/out/_next/static/chunks/1518-21c80a799b5c426e.js b/litellm/proxy/_experimental/out/_next/static/chunks/1518-21c80a799b5c426e.js deleted file mode 100644 index 52544b97059..00000000000 --- a/litellm/proxy/_experimental/out/_next/static/chunks/1518-21c80a799b5c426e.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[1518],{81518:function(e,s,a){a.r(s),a.d(s,{default:function(){return Y}});var t=a(57437),r=a(2265),n=a(85572),l=a(93837),i=a(37592),o=a(4260),d=a(99981),c=a(5545),m=a(26430),u=a(96473),x=a(9114),h=a(10703),p=a(95459),g=a(32489),f=a(98728),v=a(62831),j=a(17906),y=a(94263),b=a(79862),N=a(82222),k=a(51817),w=a(94331),A=a(38398),S=a(33152);function C(e){let{messages:s,isLoading:a}=e;if(0===s.length)return(0,t.jsx)("div",{className:"h-full"});let r=[],n=0;for(;n(0,t.jsx)("div",{className:"whitespace-pre-wrap break-words",style:{wordWrap:"break-word",overflowWrap:"break-word",wordBreak:"break-word",hyphens:"auto"},children:(0,t.jsx)(v.UG,{components:{code(e){let{node:s,inline:a,className:r,children:n,...l}=e,i=/language-(\w+)/.exec(r||"");return!a&&i?(0,t.jsx)(j.Z,{style:y.Z,language:i[1],PreTag:"div",className:"rounded-md my-2",wrapLines:!0,wrapLongLines:!0,...l,children:String(n).replace(/\n$/,"")}):(0,t.jsx)("code",{className:"".concat(r," px-1.5 py-0.5 rounded bg-gray-100 text-sm font-mono"),...l,children:n})},pre:e=>{let{node:s,...a}=e;return(0,t.jsx)("pre",{style:{overflowX:"auto",maxWidth:"100%"},...a})}},children:"string"==typeof e.content?e.content:""})});return(0,t.jsxs)("div",{className:"flex flex-col gap-6 min-w-0 w-full p-4",children:[r.map((e,s)=>{let n=e.assistant,i=(null==n?void 0:n.model)||"Assistant";return(0,t.jsxs)("div",{className:"space-y-4",children:[e.user&&(0,t.jsxs)("div",{className:"space-y-2 min-w-0",children:[(0,t.jsxs)("div",{className:"flex items-center gap-3",children:[(0,t.jsx)("div",{className:"flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-blue-100 text-blue-600",children:(0,t.jsx)(b.Z,{size:16})}),(0,t.jsx)("div",{className:"text-sm font-semibold text-gray-700",children:"You"})]}),l(e.user)]}),(0,t.jsx)("div",{className:"border-t border-gray-200"}),n?(0,t.jsxs)("div",{className:"space-y-3 min-w-0",children:[(0,t.jsxs)("div",{className:"flex items-center gap-3",children:[(0,t.jsx)("div",{className:"flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-gray-100 text-gray-600",children:(0,t.jsx)(N.Z,{size:16})}),(0,t.jsxs)("div",{className:"flex items-center gap-2",children:[(0,t.jsx)("span",{className:"text-sm font-semibold text-gray-700",children:i}),n.toolName&&(0,t.jsx)("span",{className:"rounded bg-gray-100 px-2 py-0.5 text-xs text-gray-600",children:n.toolName})]})]}),n.reasoningContent&&(0,t.jsx)(w.Z,{reasoningContent:n.reasoningContent}),n.searchResults&&(0,t.jsx)(S.J,{searchResults:n.searchResults}),l(n),(n.timeToFirstToken||n.totalLatency||n.usage)&&(0,t.jsx)(A.Z,{timeToFirstToken:n.timeToFirstToken,totalLatency:n.totalLatency,usage:n.usage,toolName:n.toolName})]}):a&&s===r.length-1?(0,t.jsxs)("div",{className:"flex items-center gap-2 text-sm text-gray-500",children:[(0,t.jsx)(k.Z,{size:18,className:"animate-spin"}),(0,t.jsx)("span",{children:"Generating response..."})]}):(0,t.jsx)("div",{className:"text-sm text-gray-500",children:"Waiting for a response..."})]},s)}),a&&0===r.length&&(0,t.jsxs)("div",{className:"flex items-center gap-2 text-gray-500",children:[(0,t.jsx)(k.Z,{size:18,className:"animate-spin"}),(0,t.jsx)("span",{children:"Generating response..."})]})]})}var T=a(31283);function P(e){let{value:s,onChange:a,models:n,loading:l,disabled:o}=e,[d,c]=(0,r.useState)(!1),[m,u]=(0,r.useState)(""),x=(0,r.useMemo)(()=>Array.from(new Set(n)).sort(),[n]),h=(0,r.useMemo)(()=>s&&!x.includes(s)?[s,...x]:x,[x,s]),p=d?"__custom__":s||void 0,g=()=>{let e=m.trim();if(!e){c(!1),u("");return}a(e),c(!1),u("")};return(0,t.jsxs)("div",{className:"flex-1 min-w-0",children:[(0,t.jsxs)(i.default,{value:p,onChange:e=>{if("__custom__"===e){c(!0),s&&!x.includes(s)?u(s):u("");return}c(!1),u(""),a(e)},disabled:o,loading:l,placeholder:l?"Loading models...":"Select a model",className:"w-full rounded-md",showSearch:!0,optionFilterProp:"children",children:[h.map(e=>(0,t.jsx)(i.default.Option,{value:e,children:e},e)),(0,t.jsx)(i.default.Option,{value:"__custom__",children:"+ Add custom model"})]}),d&&(0,t.jsx)(T.o,{className:"mt-2",placeholder:"Custom Model Name (Enter to add)",value:m,onValueChange:u,onKeyDown:e=>{"Enter"===e.key&&(e.preventDefault(),g())},onBlur:g,autoFocus:!0})]})}var Z=a(99020),_=a(97415),L=a(67479),E=a(4156),M=a(23496),I=a(85847),O=a(79326);function R(e){let{comparison:s,onUpdate:a,onRemove:n,canRemove:l,modelOptions:i,isLoadingModels:o,apiKey:d}=e,[c,m]=(0,r.useState)(!1),u=e=>{e?a({applyAcrossModels:!0,temperature:s.temperature,maxTokens:s.maxTokens,tags:[...s.tags],vectorStores:[...s.vectorStores],guardrails:[...s.guardrails],useAdvancedParams:s.useAdvancedParams},{applyToAll:!0,keysToApply:["temperature","maxTokens","tags","vectorStores","guardrails","useAdvancedParams"]}):a({applyAcrossModels:!1})},x=e=>{a({useAdvancedParams:e},s.applyAcrossModels?{applyToAll:!0,keysToApply:["useAdvancedParams"]}:void 0)},h=(e,t)=>{a({[e]:t},s.applyAcrossModels?{applyToAll:!0,keysToApply:[e]}:void 0)},p=s.useAdvancedParams?1:.4,v=s.useAdvancedParams?"text-gray-700":"text-gray-400",j=()=>{m(e=>!e)},y=(0,t.jsxs)("div",{className:"w-[300px] max-h-[65vh] overflow-y-auto relative",children:[(0,t.jsx)("button",{onClick:()=>{m(!1)},className:"absolute top-0 right-0 p-1 hover:bg-gray-100 rounded transition-colors text-gray-500 hover:text-gray-700 z-10",children:(0,t.jsx)(g.Z,{size:14})}),(0,t.jsxs)("div",{className:"space-y-2",children:[(0,t.jsx)("div",{className:"flex items-center gap-2",children:(0,t.jsx)(E.Z,{checked:s.applyAcrossModels,onChange:e=>u(e.target.checked),children:(0,t.jsx)("span",{className:"text-xs font-medium",children:"Sync Settings Across Models"})})}),(0,t.jsx)(M.Z,{className:"border-gray-200"}),(0,t.jsxs)("div",{children:[(0,t.jsx)("h4",{className:"text-xs font-semibold text-gray-700 mb-1.5 uppercase tracking-wide",children:"General Settings"}),(0,t.jsxs)("div",{className:"space-y-2",children:[(0,t.jsxs)("div",{children:[(0,t.jsx)("label",{className:"text-xs font-medium text-gray-600 block mb-0.5",children:"Tags"}),(0,t.jsx)(Z.Z,{value:s.tags,onChange:e=>h("tags",e),accessToken:d})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)("label",{className:"text-xs font-medium text-gray-600 block mb-0.5",children:"Vector Stores"}),(0,t.jsx)(_.Z,{value:s.vectorStores,onChange:e=>h("vectorStores",e),accessToken:d})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)("label",{className:"text-xs font-medium text-gray-600 block mb-0.5",children:"Guardrails"}),(0,t.jsx)(L.Z,{value:s.guardrails,onChange:e=>h("guardrails",e),accessToken:d})]})]})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)("h4",{className:"text-xs font-semibold text-gray-700 mb-1.5 uppercase tracking-wide",children:"Advanced Settings"}),(0,t.jsxs)("div",{className:"space-y-2",children:[(0,t.jsx)("div",{className:"flex items-center gap-2 pb-1",children:(0,t.jsx)(E.Z,{checked:s.useAdvancedParams,onChange:e=>x(e.target.checked),children:(0,t.jsx)("span",{className:"text-sm font-medium",children:"Use Advanced Parameters"})})}),(0,t.jsxs)("div",{className:"space-y-2 transition-opacity duration-200",style:{opacity:p},children:[(0,t.jsxs)("div",{children:[(0,t.jsxs)("div",{className:"flex items-center justify-between mb-1",children:[(0,t.jsx)("label",{className:"text-xs font-medium ".concat(v),children:"Temperature"}),(0,t.jsx)("span",{className:"text-xs ".concat(v),children:s.temperature.toFixed(2)})]}),(0,t.jsx)(I.Z,{min:0,max:2,step:.01,value:s.temperature,onChange:e=>{h("temperature",Math.min(2,Math.max(0,Number((Array.isArray(e)?e[0]:e).toFixed(2)))))},disabled:!s.useAdvancedParams})]}),(0,t.jsxs)("div",{children:[(0,t.jsxs)("div",{className:"flex items-center justify-between mb-1",children:[(0,t.jsx)("label",{className:"text-xs font-medium ".concat(v),children:"Max Tokens"}),(0,t.jsx)("span",{className:"text-xs ".concat(v),children:s.maxTokens})]}),(0,t.jsx)(I.Z,{min:1,max:32768,step:1,value:s.maxTokens,onChange:e=>{h("maxTokens",Math.min(32768,Math.max(1,Math.round(Array.isArray(e)?e[0]:e))))},disabled:!s.useAdvancedParams})]})]})]})]})]})]});return(0,t.jsxs)("div",{className:"bg-white first:border-l-0 border-l border-gray-200 flex flex-col min-h-0",children:[(0,t.jsxs)("div",{className:"border-b flex items-center justify-between gap-3 px-4 py-3",children:[(0,t.jsxs)("div",{className:"flex items-center gap-3 flex-1",children:[(0,t.jsx)(P,{value:s.model,models:i,loading:o,onChange:e=>a({model:e})}),(0,t.jsx)("div",{className:"flex items-center gap-2",children:(0,t.jsx)(O.Z,{content:y,trigger:[],open:c,onOpenChange:()=>{},placement:"bottomRight",destroyTooltipOnHide:!1,children:(0,t.jsx)("button",{onClick:e=>{e.stopPropagation(),j()},className:"p-2 rounded-lg transition-colors ".concat(c?"bg-gray-200 text-gray-700":"hover:bg-gray-100 text-gray-600"),children:(0,t.jsx)(f.Z,{size:18})})})})]}),l&&(0,t.jsx)("button",{onClick:e=>{e.stopPropagation(),n()},className:"p-2 hover:bg-red-50 text-red-600 rounded-lg transition-colors",children:(0,t.jsx)(g.Z,{size:18})})]}),(0,t.jsx)("div",{className:"relative flex-1 flex flex-col min-h-0",children:(0,t.jsx)("div",{className:"flex-1 max-h-[calc(100vh-385px)] overflow-auto rounded-b-2xl",children:(0,t.jsx)(C,{messages:s.messages,isLoading:s.isLoading})})})]})}var z=a(79276);let{TextArea:U}=o.default;function K(e){let{value:s,onChange:a,onSend:r,disabled:n}=e;return(0,t.jsx)("div",{className:"flex items-center gap-2",children:(0,t.jsxs)("div",{className:"flex items-center flex-1 bg-white border border-gray-300 rounded-xl px-3 py-1 min-h-[44px]",children:[(0,t.jsx)(U,{value:s,onChange:e=>a(e.target.value),onKeyDown:e=>{"Enter"===e.key&&!e.shiftKey&&(e.preventDefault(),!n&&s.trim()&&r())},placeholder:"Type your message... (Shift+Enter for new line)",disabled:n,className:"flex-1",autoSize:{minRows:1,maxRows:4},style:{resize:"none",border:"none",boxShadow:"none",background:"transparent",padding:"4px 0",fontSize:"14px",lineHeight:"20px"}}),(0,t.jsx)(c.ZP,{onClick:r,disabled:n||!s.trim(),icon:(0,t.jsx)(z.Z,{}),shape:"circle"})]})})}let B=["Can you summarize the key points?","What assumptions did you make?","What are the next steps?"],D=["Write me a poem","Explain quantum computing","Draft a polite email requesting a meeting"],F="/v1/chat/completions";function W(e){let{accessToken:s,disabledPersonalKeyCreation:a}=e,[n,g]=(0,r.useState)([{id:"1",model:"",messages:[],isLoading:!1,tags:[],mcpTools:[],vectorStores:[],guardrails:[],temperature:1,maxTokens:2048,applyAcrossModels:!1,useAdvancedParams:!1},{id:"2",model:"",messages:[],isLoading:!1,tags:[],mcpTools:[],vectorStores:[],guardrails:[],temperature:1,maxTokens:2048,applyAcrossModels:!1,useAdvancedParams:!1}]),[f,v]=(0,r.useState)([]),[j,y]=(0,r.useState)(!1),[b,N]=(0,r.useState)(""),[k,w]=(0,r.useState)(a?"custom":"session"),[A,S]=(0,r.useState)(""),[C,T]=(0,r.useState)("");(0,r.useEffect)(()=>{let e=setTimeout(()=>{T(A)},300);return()=>clearTimeout(e)},[A]);let P=(0,r.useMemo)(()=>"session"===k?s||"":C.trim(),[k,s,C]),Z=(0,r.useMemo)(()=>n.length>0&&n.every(e=>!e.isLoading&&e.messages.some(e=>"assistant"===e.role)),[n]);(0,r.useEffect)(()=>{let e=!0;return(async()=>{if(!P){v([]);return}y(!0);try{let s=await (0,h.p)(P);if(!e)return;let a=Array.from(new Set(s.map(e=>e.model_group)));v(a)}catch(s){console.error("CompareUI: failed to fetch models",s),e&&v([])}finally{e&&y(!1)}})(),()=>{e=!1}},[P]),(0,r.useEffect)(()=>{0!==f.length&&g(e=>e.map((e,s)=>{var a,t,r,n,l;return{...e,temperature:null!==(a=e.temperature)&&void 0!==a?a:1,maxTokens:null!==(t=e.maxTokens)&&void 0!==t?t:2048,applyAcrossModels:null!==(r=e.applyAcrossModels)&&void 0!==r&&r,useAdvancedParams:null!==(n=e.useAdvancedParams)&&void 0!==n&&n,...e.model?{}:{model:null!==(l=f[s%f.length])&&void 0!==l?l:""}}}))},[f]);let _=e=>{n.length>1&&g(s=>s.filter(s=>s.id!==e))},L=(e,s,a)=>{g(t=>{var r;if((null==a?void 0:a.applyToAll)&&(null===(r=a.keysToApply)||void 0===r?void 0:r.length)){let r={};a.keysToApply.forEach(e=>{let a=s[e];void 0!==a&&(r[e]=Array.isArray(a)?[...a]:a)});let n=Object.keys(r).length>0;return t.map(a=>a.id===e?{...a,...s}:n?{...a,...r}:a)}return t.map(a=>a.id===e?{...a,...s}:a)})},E=(e,s,a)=>{s&&g(t=>t.map(t=>{if(t.id!==e)return t;let r=[...t.messages],n=r[r.length-1];if(n&&"assistant"===n.role){var l;let e="string"==typeof n.content?n.content:"";r[r.length-1]={...n,content:e+s,model:null!==(l=n.model)&&void 0!==l?l:a}}else r.push({role:"assistant",content:s,model:a});return{...t,messages:r}}))},M=(e,s)=>{s&&g(a=>a.map(a=>{if(a.id!==e)return a;let t=[...a.messages],r=t[t.length-1];return r&&"assistant"===r.role?t[t.length-1]={...r,reasoningContent:(r.reasoningContent||"")+s}:r&&"user"===r.role&&t.push({role:"assistant",content:"",reasoningContent:s}),{...a,messages:t}}))},I=(e,s)=>{g(a=>a.map(a=>{if(a.id!==e)return a;let t=[...a.messages],r=t[t.length-1];return r&&"assistant"===r.role?t[t.length-1]={...r,timeToFirstToken:s}:r&&"user"===r.role&&t.push({role:"assistant",content:"",timeToFirstToken:s}),{...a,messages:t}}))},O=(e,s)=>{g(a=>a.map(a=>{if(a.id!==e)return a;let t=[...a.messages],r=t[t.length-1];return r&&"assistant"===r.role?t[t.length-1]={...r,totalLatency:s}:r&&"user"===r.role&&t.push({role:"assistant",content:"",totalLatency:s}),{...a,messages:t}}))},z=(e,s,a)=>{g(t=>t.map(t=>{if(t.id!==e)return t;let r=[...t.messages],n=r[r.length-1];return n&&"assistant"===n.role&&(r[r.length-1]={...n,usage:s,toolName:a}),{...t,messages:r}}))},U=(e,s)=>{s&&g(a=>a.map(a=>{if(a.id!==e)return a;let t=[...a.messages],r=t[t.length-1];return r&&"assistant"===r.role&&(t[t.length-1]={...r,searchResults:s}),{...a,messages:t}}))},W=!!s,G=e=>{let s=e.trim();if(!s)return;if(!P){x.Z.fromBackend("Please provide an API key or select Current UI Session");return}if(0===n.length)return;if(n.some(e=>!e.model)){x.Z.fromBackend("Select a model before sending a message.");return}let a=new Map;n.forEach(e=>{var t;let r=null!==(t=e.traceId)&&void 0!==t?t:(0,l.Z)();a.set(e.id,{id:e.id,model:e.model,traceId:r,tags:e.tags,vectorStores:e.vectorStores,guardrails:e.guardrails,temperature:e.temperature,maxTokens:e.maxTokens,messages:[...e.messages,{role:"user",content:s}]})}),0!==a.size&&(g(e=>e.map(e=>{let s=a.get(e.id);return s?{...e,traceId:s.traceId,messages:s.messages,isLoading:!0}:e})),a.forEach(e=>{var s;let a=e.messages.map(e=>{let{role:s,content:a}=e;return{role:s,content:"string"==typeof a?a:""}}),t=e.tags.length>0?e.tags:void 0,r=e.vectorStores.length>0?e.vectorStores:void 0,l=e.guardrails.length>0?e.guardrails:void 0,i=n.find(s=>s.id===e.id),o=null!==(s=null==i?void 0:i.useAdvancedParams)&&void 0!==s&&s;(0,p.n)(a,(s,a)=>E(e.id,s,a),e.model,P,t,void 0,s=>M(e.id,s),s=>I(e.id,s),s=>z(e.id,s),e.traceId,r,l,void 0,void 0,s=>U(e.id,s),o?e.temperature:void 0,o?e.maxTokens:void 0,s=>O(e.id,s)).catch(s=>{let a=s instanceof Error?s.message:String(s);console.error("CompareUI: failed to fetch response",s),x.Z.fromBackend(a),g(s=>s.map(s=>{if(s.id!==e.id)return s;let t=[...s.messages],r=t[t.length-1],n=r&&"assistant"===r.role&&"string"==typeof r.content?r.content:"";return r&&"assistant"===r.role?t[t.length-1]={...r,content:n?"".concat(n,"\nError fetching response: ").concat(a):"Error fetching response: ".concat(a)}:t.push({role:"assistant",content:"Error fetching response: ".concat(a)}),{...s,messages:t}}))}).finally(()=>{g(s=>s.map(s=>s.id===e.id?{...s,isLoading:!1}:s))})}))},V=e=>{N(e)},X=n.some(e=>e.messages.length>0),Y=n.some(e=>e.isLoading);return(0,t.jsx)("div",{className:"w-full h-full p-4 bg-white",children:(0,t.jsxs)("div",{className:"rounded-2xl border border-gray-200 bg-white shadow-sm min-h-[calc(100vh-140px)] flex flex-col",children:[(0,t.jsx)("div",{className:"border-b px-4 py-2",children:(0,t.jsxs)("div",{className:"flex flex-wrap items-center justify-between gap-3",children:[(0,t.jsxs)("div",{className:"flex items-center gap-2",children:[(0,t.jsx)("span",{className:"text-sm font-medium text-gray-600",children:"API Key Source"}),(0,t.jsxs)(i.default,{value:k,onChange:e=>w(e),disabled:a,className:"w-48",children:[(0,t.jsx)(i.default.Option,{value:"session",disabled:!W,children:"Current UI Session"}),(0,t.jsx)(i.default.Option,{value:"custom",children:"Virtual Key"})]}),"custom"===k&&(0,t.jsx)(o.default.Password,{value:A,onChange:e=>S(e.target.value),placeholder:"Enter API key",className:"w-56"})]}),(0,t.jsxs)("div",{className:"flex items-center gap-2",children:[(0,t.jsx)("span",{className:"text-sm font-medium text-gray-600",children:"Endpoint"}),(0,t.jsx)(d.Z,{title:"Other endpoints will be available soon",children:(0,t.jsx)(i.default,{value:F,disabled:!0,className:"w-56",children:(0,t.jsx)(i.default.Option,{value:F,children:F})})})]}),(0,t.jsxs)("div",{className:"flex items-center gap-3",children:[(0,t.jsx)(c.ZP,{onClick:()=>{g(e=>e.map(e=>({...e,messages:[],traceId:void 0,isLoading:!1}))),N("")},disabled:!X,icon:(0,t.jsx)(m.Z,{}),children:"Clear All Chats"}),(0,t.jsx)(d.Z,{title:n.length>=3?"Compare up to 3 models at a time":"Add another comparison",children:(0,t.jsx)(c.ZP,{onClick:()=>{var e;if(n.length>=3)return;let s=null!==(e=f[n.length%(f.length||1)])&&void 0!==e?e:"",a={id:Date.now().toString(),model:s,messages:[],isLoading:!1,tags:[],mcpTools:[],vectorStores:[],guardrails:[],temperature:1,maxTokens:2048,applyAcrossModels:!1,useAdvancedParams:!1};g(e=>[...e,a])},disabled:n.length>=3,icon:(0,t.jsx)(u.Z,{}),children:"Add Comparison"})})]})]})}),(0,t.jsx)("div",{className:"grid flex-1 min-h-0 auto-rows-[minmax(0,1fr)]",style:{gridTemplateColumns:"repeat(".concat(n.length,", minmax(0, 1fr))")},children:n.map(e=>(0,t.jsx)(R,{comparison:e,onUpdate:(s,a)=>L(e.id,s,a),onRemove:()=>_(e.id),canRemove:n.length>1,modelOptions:f,isLoadingModels:j,apiKey:P},e.id))}),(0,t.jsx)("div",{className:"flex justify-center pb-4",children:(0,t.jsx)("div",{className:"w-full max-w-3xl px-4",children:(0,t.jsxs)("div",{className:"border border-gray-200 shadow-lg rounded-xl bg-white p-4",children:[(0,t.jsx)("div",{className:"flex items-center justify-between gap-4 mb-3 min-h-8",children:X||Y?Z?(0,t.jsx)("div",{className:"flex items-center gap-2 overflow-x-auto",children:B.map(e=>(0,t.jsx)("button",{type:"button",onClick:()=>V(e),className:"shrink-0 rounded-full border border-gray-200 px-3 py-1 text-xs font-medium text-gray-600 transition-colors hover:bg-gray-100 cursor-pointer",children:e},e))}):Y?(0,t.jsxs)("span",{className:"flex items-center gap-2 text-sm text-gray-500",children:[(0,t.jsx)("span",{className:"h-2 w-2 rounded-full bg-blue-500 animate-pulse","aria-hidden":!0}),"Gathering responses from all models..."]}):(0,t.jsx)("span",{className:"text-sm text-gray-500",children:"Send a prompt to compare models"}):(0,t.jsx)("div",{className:"flex items-center gap-2 overflow-x-auto",children:D.map(e=>(0,t.jsx)("button",{type:"button",onClick:()=>V(e),className:"shrink-0 rounded-full border border-gray-200 px-3 py-1 text-xs font-medium text-gray-600 transition-colors hover:bg-gray-100 cursor-pointer",children:e},e))})}),(0,t.jsx)(K,{value:b,onChange:e=>{N(e)},onSend:()=>{G(b),N("")},disabled:0===n.length||n.every(e=>e.isLoading)})]})})})]})})}var G=a(58643),V=a(80443),X=a(91624);function Y(){let{accessToken:e,userRole:s,userId:a,disabledPersonalKeyCreation:l,token:i}=(0,V.Z)(),[o,d]=(0,r.useState)(void 0);return(0,r.useEffect)(()=>{(async()=>{if(e){let s=await (0,X.C)(e);s&&d({PROXY_BASE_URL:s.PROXY_BASE_URL,LITELLM_UI_API_DOC_BASE_URL:s.LITELLM_UI_API_DOC_BASE_URL})}})()},[e]),(0,t.jsxs)(G.v0,{className:"h-full w-full",children:[(0,t.jsxs)(G.td,{className:"mb-0",children:[(0,t.jsx)(G.OK,{children:"Chat"}),(0,t.jsx)(G.OK,{children:"Compare"})]}),(0,t.jsxs)(G.nP,{className:"h-full",children:[(0,t.jsx)(G.x4,{className:"h-full",children:(0,t.jsx)(n.Z,{accessToken:e,token:i,userRole:s,userID:a,disabledPersonalKeyCreation:l,proxySettings:o})}),(0,t.jsx)(G.x4,{className:"h-full",children:(0,t.jsx)(W,{accessToken:e,disabledPersonalKeyCreation:l})})]})]})}}}]); \ No newline at end of file diff --git a/litellm/proxy/_experimental/out/_next/static/chunks/1518-4475f8385da5ac78.js b/litellm/proxy/_experimental/out/_next/static/chunks/1518-4475f8385da5ac78.js new file mode 100644 index 00000000000..476fabcb02f --- /dev/null +++ b/litellm/proxy/_experimental/out/_next/static/chunks/1518-4475f8385da5ac78.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[1518],{81518:function(e,s,a){a.r(s),a.d(s,{default:function(){return Y}});var t=a(57437),r=a(2265),n=a(85572),l=a(93837),i=a(37592),o=a(4260),d=a(99981),c=a(5545),m=a(26430),u=a(96473),x=a(9114),h=a(10703),p=a(95459),g=a(32489),f=a(98728),v=a(62831),j=a(17906),y=a(94263),b=a(79862),N=a(82222),k=a(51817),w=a(94331),A=a(38398),S=a(33152);function C(e){let{messages:s,isLoading:a}=e;if(0===s.length)return(0,t.jsx)("div",{className:"h-full"});let r=[],n=0;for(;n(0,t.jsx)("div",{className:"whitespace-pre-wrap break-words",style:{wordWrap:"break-word",overflowWrap:"break-word",wordBreak:"break-word",hyphens:"auto"},children:(0,t.jsx)(v.UG,{components:{code(e){let{node:s,inline:a,className:r,children:n,...l}=e,i=/language-(\w+)/.exec(r||"");return!a&&i?(0,t.jsx)(j.Z,{style:y.Z,language:i[1],PreTag:"div",className:"rounded-md my-2",wrapLines:!0,wrapLongLines:!0,...l,children:String(n).replace(/\n$/,"")}):(0,t.jsx)("code",{className:"".concat(r," px-1.5 py-0.5 rounded bg-gray-100 text-sm font-mono"),...l,children:n})},pre:e=>{let{node:s,...a}=e;return(0,t.jsx)("pre",{style:{overflowX:"auto",maxWidth:"100%"},...a})}},children:"string"==typeof e.content?e.content:""})});return(0,t.jsxs)("div",{className:"flex flex-col gap-6 min-w-0 w-full p-4",children:[r.map((e,s)=>{let n=e.assistant,i=(null==n?void 0:n.model)||"Assistant";return(0,t.jsxs)("div",{className:"space-y-4",children:[e.user&&(0,t.jsxs)("div",{className:"space-y-2 min-w-0",children:[(0,t.jsxs)("div",{className:"flex items-center gap-3",children:[(0,t.jsx)("div",{className:"flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-blue-100 text-blue-600",children:(0,t.jsx)(b.Z,{size:16})}),(0,t.jsx)("div",{className:"text-sm font-semibold text-gray-700",children:"You"})]}),l(e.user)]}),(0,t.jsx)("div",{className:"border-t border-gray-200"}),n?(0,t.jsxs)("div",{className:"space-y-3 min-w-0",children:[(0,t.jsxs)("div",{className:"flex items-center gap-3",children:[(0,t.jsx)("div",{className:"flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-gray-100 text-gray-600",children:(0,t.jsx)(N.Z,{size:16})}),(0,t.jsxs)("div",{className:"flex items-center gap-2",children:[(0,t.jsx)("span",{className:"text-sm font-semibold text-gray-700",children:i}),n.toolName&&(0,t.jsx)("span",{className:"rounded bg-gray-100 px-2 py-0.5 text-xs text-gray-600",children:n.toolName})]})]}),n.reasoningContent&&(0,t.jsx)(w.Z,{reasoningContent:n.reasoningContent}),n.searchResults&&(0,t.jsx)(S.J,{searchResults:n.searchResults}),l(n),(n.timeToFirstToken||n.totalLatency||n.usage)&&(0,t.jsx)(A.Z,{timeToFirstToken:n.timeToFirstToken,totalLatency:n.totalLatency,usage:n.usage,toolName:n.toolName})]}):a&&s===r.length-1?(0,t.jsxs)("div",{className:"flex items-center gap-2 text-sm text-gray-500",children:[(0,t.jsx)(k.Z,{size:18,className:"animate-spin"}),(0,t.jsx)("span",{children:"Generating response..."})]}):(0,t.jsx)("div",{className:"text-sm text-gray-500",children:"Waiting for a response..."})]},s)}),a&&0===r.length&&(0,t.jsxs)("div",{className:"flex items-center gap-2 text-gray-500",children:[(0,t.jsx)(k.Z,{size:18,className:"animate-spin"}),(0,t.jsx)("span",{children:"Generating response..."})]})]})}var T=a(31283);function P(e){let{value:s,onChange:a,models:n,loading:l,disabled:o}=e,[d,c]=(0,r.useState)(!1),[m,u]=(0,r.useState)(""),x=(0,r.useMemo)(()=>Array.from(new Set(n)).sort(),[n]),h=(0,r.useMemo)(()=>s&&!x.includes(s)?[s,...x]:x,[x,s]),p=d?"__custom__":s||void 0,g=()=>{let e=m.trim();if(!e){c(!1),u("");return}a(e),c(!1),u("")};return(0,t.jsxs)("div",{className:"flex-1 min-w-0",children:[(0,t.jsxs)(i.default,{value:p,onChange:e=>{if("__custom__"===e){c(!0),s&&!x.includes(s)?u(s):u("");return}c(!1),u(""),a(e)},disabled:o,loading:l,placeholder:l?"Loading models...":"Select a model",className:"w-full rounded-md",showSearch:!0,optionFilterProp:"children",children:[h.map(e=>(0,t.jsx)(i.default.Option,{value:e,children:e},e)),(0,t.jsx)(i.default.Option,{value:"__custom__",children:"+ Add custom model"})]}),d&&(0,t.jsx)(T.o,{className:"mt-2",placeholder:"Custom Model Name (Enter to add)",value:m,onValueChange:u,onKeyDown:e=>{"Enter"===e.key&&(e.preventDefault(),g())},onBlur:g,autoFocus:!0})]})}var Z=a(99020),_=a(97415),L=a(67479),E=a(61994),M=a(23496),I=a(85847),O=a(79326);function R(e){let{comparison:s,onUpdate:a,onRemove:n,canRemove:l,modelOptions:i,isLoadingModels:o,apiKey:d}=e,[c,m]=(0,r.useState)(!1),u=e=>{e?a({applyAcrossModels:!0,temperature:s.temperature,maxTokens:s.maxTokens,tags:[...s.tags],vectorStores:[...s.vectorStores],guardrails:[...s.guardrails],useAdvancedParams:s.useAdvancedParams},{applyToAll:!0,keysToApply:["temperature","maxTokens","tags","vectorStores","guardrails","useAdvancedParams"]}):a({applyAcrossModels:!1})},x=e=>{a({useAdvancedParams:e},s.applyAcrossModels?{applyToAll:!0,keysToApply:["useAdvancedParams"]}:void 0)},h=(e,t)=>{a({[e]:t},s.applyAcrossModels?{applyToAll:!0,keysToApply:[e]}:void 0)},p=s.useAdvancedParams?1:.4,v=s.useAdvancedParams?"text-gray-700":"text-gray-400",j=()=>{m(e=>!e)},y=(0,t.jsxs)("div",{className:"w-[300px] max-h-[65vh] overflow-y-auto relative",children:[(0,t.jsx)("button",{onClick:()=>{m(!1)},className:"absolute top-0 right-0 p-1 hover:bg-gray-100 rounded transition-colors text-gray-500 hover:text-gray-700 z-10",children:(0,t.jsx)(g.Z,{size:14})}),(0,t.jsxs)("div",{className:"space-y-2",children:[(0,t.jsx)("div",{className:"flex items-center gap-2",children:(0,t.jsx)(E.Z,{checked:s.applyAcrossModels,onChange:e=>u(e.target.checked),children:(0,t.jsx)("span",{className:"text-xs font-medium",children:"Sync Settings Across Models"})})}),(0,t.jsx)(M.Z,{className:"border-gray-200"}),(0,t.jsxs)("div",{children:[(0,t.jsx)("h4",{className:"text-xs font-semibold text-gray-700 mb-1.5 uppercase tracking-wide",children:"General Settings"}),(0,t.jsxs)("div",{className:"space-y-2",children:[(0,t.jsxs)("div",{children:[(0,t.jsx)("label",{className:"text-xs font-medium text-gray-600 block mb-0.5",children:"Tags"}),(0,t.jsx)(Z.Z,{value:s.tags,onChange:e=>h("tags",e),accessToken:d})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)("label",{className:"text-xs font-medium text-gray-600 block mb-0.5",children:"Vector Stores"}),(0,t.jsx)(_.Z,{value:s.vectorStores,onChange:e=>h("vectorStores",e),accessToken:d})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)("label",{className:"text-xs font-medium text-gray-600 block mb-0.5",children:"Guardrails"}),(0,t.jsx)(L.Z,{value:s.guardrails,onChange:e=>h("guardrails",e),accessToken:d})]})]})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)("h4",{className:"text-xs font-semibold text-gray-700 mb-1.5 uppercase tracking-wide",children:"Advanced Settings"}),(0,t.jsxs)("div",{className:"space-y-2",children:[(0,t.jsx)("div",{className:"flex items-center gap-2 pb-1",children:(0,t.jsx)(E.Z,{checked:s.useAdvancedParams,onChange:e=>x(e.target.checked),children:(0,t.jsx)("span",{className:"text-sm font-medium",children:"Use Advanced Parameters"})})}),(0,t.jsxs)("div",{className:"space-y-2 transition-opacity duration-200",style:{opacity:p},children:[(0,t.jsxs)("div",{children:[(0,t.jsxs)("div",{className:"flex items-center justify-between mb-1",children:[(0,t.jsx)("label",{className:"text-xs font-medium ".concat(v),children:"Temperature"}),(0,t.jsx)("span",{className:"text-xs ".concat(v),children:s.temperature.toFixed(2)})]}),(0,t.jsx)(I.Z,{min:0,max:2,step:.01,value:s.temperature,onChange:e=>{h("temperature",Math.min(2,Math.max(0,Number((Array.isArray(e)?e[0]:e).toFixed(2)))))},disabled:!s.useAdvancedParams})]}),(0,t.jsxs)("div",{children:[(0,t.jsxs)("div",{className:"flex items-center justify-between mb-1",children:[(0,t.jsx)("label",{className:"text-xs font-medium ".concat(v),children:"Max Tokens"}),(0,t.jsx)("span",{className:"text-xs ".concat(v),children:s.maxTokens})]}),(0,t.jsx)(I.Z,{min:1,max:32768,step:1,value:s.maxTokens,onChange:e=>{h("maxTokens",Math.min(32768,Math.max(1,Math.round(Array.isArray(e)?e[0]:e))))},disabled:!s.useAdvancedParams})]})]})]})]})]})]});return(0,t.jsxs)("div",{className:"bg-white first:border-l-0 border-l border-gray-200 flex flex-col min-h-0",children:[(0,t.jsxs)("div",{className:"border-b flex items-center justify-between gap-3 px-4 py-3",children:[(0,t.jsxs)("div",{className:"flex items-center gap-3 flex-1",children:[(0,t.jsx)(P,{value:s.model,models:i,loading:o,onChange:e=>a({model:e})}),(0,t.jsx)("div",{className:"flex items-center gap-2",children:(0,t.jsx)(O.Z,{content:y,trigger:[],open:c,onOpenChange:()=>{},placement:"bottomRight",destroyTooltipOnHide:!1,children:(0,t.jsx)("button",{onClick:e=>{e.stopPropagation(),j()},className:"p-2 rounded-lg transition-colors ".concat(c?"bg-gray-200 text-gray-700":"hover:bg-gray-100 text-gray-600"),children:(0,t.jsx)(f.Z,{size:18})})})})]}),l&&(0,t.jsx)("button",{onClick:e=>{e.stopPropagation(),n()},className:"p-2 hover:bg-red-50 text-red-600 rounded-lg transition-colors",children:(0,t.jsx)(g.Z,{size:18})})]}),(0,t.jsx)("div",{className:"relative flex-1 flex flex-col min-h-0",children:(0,t.jsx)("div",{className:"flex-1 max-h-[calc(100vh-385px)] overflow-auto rounded-b-2xl",children:(0,t.jsx)(C,{messages:s.messages,isLoading:s.isLoading})})})]})}var z=a(79276);let{TextArea:U}=o.default;function K(e){let{value:s,onChange:a,onSend:r,disabled:n}=e;return(0,t.jsx)("div",{className:"flex items-center gap-2",children:(0,t.jsxs)("div",{className:"flex items-center flex-1 bg-white border border-gray-300 rounded-xl px-3 py-1 min-h-[44px]",children:[(0,t.jsx)(U,{value:s,onChange:e=>a(e.target.value),onKeyDown:e=>{"Enter"===e.key&&!e.shiftKey&&(e.preventDefault(),!n&&s.trim()&&r())},placeholder:"Type your message... (Shift+Enter for new line)",disabled:n,className:"flex-1",autoSize:{minRows:1,maxRows:4},style:{resize:"none",border:"none",boxShadow:"none",background:"transparent",padding:"4px 0",fontSize:"14px",lineHeight:"20px"}}),(0,t.jsx)(c.ZP,{onClick:r,disabled:n||!s.trim(),icon:(0,t.jsx)(z.Z,{}),shape:"circle"})]})})}let B=["Can you summarize the key points?","What assumptions did you make?","What are the next steps?"],D=["Write me a poem","Explain quantum computing","Draft a polite email requesting a meeting"],F="/v1/chat/completions";function W(e){let{accessToken:s,disabledPersonalKeyCreation:a}=e,[n,g]=(0,r.useState)([{id:"1",model:"",messages:[],isLoading:!1,tags:[],mcpTools:[],vectorStores:[],guardrails:[],temperature:1,maxTokens:2048,applyAcrossModels:!1,useAdvancedParams:!1},{id:"2",model:"",messages:[],isLoading:!1,tags:[],mcpTools:[],vectorStores:[],guardrails:[],temperature:1,maxTokens:2048,applyAcrossModels:!1,useAdvancedParams:!1}]),[f,v]=(0,r.useState)([]),[j,y]=(0,r.useState)(!1),[b,N]=(0,r.useState)(""),[k,w]=(0,r.useState)(a?"custom":"session"),[A,S]=(0,r.useState)(""),[C,T]=(0,r.useState)("");(0,r.useEffect)(()=>{let e=setTimeout(()=>{T(A)},300);return()=>clearTimeout(e)},[A]);let P=(0,r.useMemo)(()=>"session"===k?s||"":C.trim(),[k,s,C]),Z=(0,r.useMemo)(()=>n.length>0&&n.every(e=>!e.isLoading&&e.messages.some(e=>"assistant"===e.role)),[n]);(0,r.useEffect)(()=>{let e=!0;return(async()=>{if(!P){v([]);return}y(!0);try{let s=await (0,h.p)(P);if(!e)return;let a=Array.from(new Set(s.map(e=>e.model_group)));v(a)}catch(s){console.error("CompareUI: failed to fetch models",s),e&&v([])}finally{e&&y(!1)}})(),()=>{e=!1}},[P]),(0,r.useEffect)(()=>{0!==f.length&&g(e=>e.map((e,s)=>{var a,t,r,n,l;return{...e,temperature:null!==(a=e.temperature)&&void 0!==a?a:1,maxTokens:null!==(t=e.maxTokens)&&void 0!==t?t:2048,applyAcrossModels:null!==(r=e.applyAcrossModels)&&void 0!==r&&r,useAdvancedParams:null!==(n=e.useAdvancedParams)&&void 0!==n&&n,...e.model?{}:{model:null!==(l=f[s%f.length])&&void 0!==l?l:""}}}))},[f]);let _=e=>{n.length>1&&g(s=>s.filter(s=>s.id!==e))},L=(e,s,a)=>{g(t=>{var r;if((null==a?void 0:a.applyToAll)&&(null===(r=a.keysToApply)||void 0===r?void 0:r.length)){let r={};a.keysToApply.forEach(e=>{let a=s[e];void 0!==a&&(r[e]=Array.isArray(a)?[...a]:a)});let n=Object.keys(r).length>0;return t.map(a=>a.id===e?{...a,...s}:n?{...a,...r}:a)}return t.map(a=>a.id===e?{...a,...s}:a)})},E=(e,s,a)=>{s&&g(t=>t.map(t=>{if(t.id!==e)return t;let r=[...t.messages],n=r[r.length-1];if(n&&"assistant"===n.role){var l;let e="string"==typeof n.content?n.content:"";r[r.length-1]={...n,content:e+s,model:null!==(l=n.model)&&void 0!==l?l:a}}else r.push({role:"assistant",content:s,model:a});return{...t,messages:r}}))},M=(e,s)=>{s&&g(a=>a.map(a=>{if(a.id!==e)return a;let t=[...a.messages],r=t[t.length-1];return r&&"assistant"===r.role?t[t.length-1]={...r,reasoningContent:(r.reasoningContent||"")+s}:r&&"user"===r.role&&t.push({role:"assistant",content:"",reasoningContent:s}),{...a,messages:t}}))},I=(e,s)=>{g(a=>a.map(a=>{if(a.id!==e)return a;let t=[...a.messages],r=t[t.length-1];return r&&"assistant"===r.role?t[t.length-1]={...r,timeToFirstToken:s}:r&&"user"===r.role&&t.push({role:"assistant",content:"",timeToFirstToken:s}),{...a,messages:t}}))},O=(e,s)=>{g(a=>a.map(a=>{if(a.id!==e)return a;let t=[...a.messages],r=t[t.length-1];return r&&"assistant"===r.role?t[t.length-1]={...r,totalLatency:s}:r&&"user"===r.role&&t.push({role:"assistant",content:"",totalLatency:s}),{...a,messages:t}}))},z=(e,s,a)=>{g(t=>t.map(t=>{if(t.id!==e)return t;let r=[...t.messages],n=r[r.length-1];return n&&"assistant"===n.role&&(r[r.length-1]={...n,usage:s,toolName:a}),{...t,messages:r}}))},U=(e,s)=>{s&&g(a=>a.map(a=>{if(a.id!==e)return a;let t=[...a.messages],r=t[t.length-1];return r&&"assistant"===r.role&&(t[t.length-1]={...r,searchResults:s}),{...a,messages:t}}))},W=!!s,G=e=>{let s=e.trim();if(!s)return;if(!P){x.Z.fromBackend("Please provide an API key or select Current UI Session");return}if(0===n.length)return;if(n.some(e=>!e.model)){x.Z.fromBackend("Select a model before sending a message.");return}let a=new Map;n.forEach(e=>{var t;let r=null!==(t=e.traceId)&&void 0!==t?t:(0,l.Z)();a.set(e.id,{id:e.id,model:e.model,traceId:r,tags:e.tags,vectorStores:e.vectorStores,guardrails:e.guardrails,temperature:e.temperature,maxTokens:e.maxTokens,messages:[...e.messages,{role:"user",content:s}]})}),0!==a.size&&(g(e=>e.map(e=>{let s=a.get(e.id);return s?{...e,traceId:s.traceId,messages:s.messages,isLoading:!0}:e})),a.forEach(e=>{var s;let a=e.messages.map(e=>{let{role:s,content:a}=e;return{role:s,content:"string"==typeof a?a:""}}),t=e.tags.length>0?e.tags:void 0,r=e.vectorStores.length>0?e.vectorStores:void 0,l=e.guardrails.length>0?e.guardrails:void 0,i=n.find(s=>s.id===e.id),o=null!==(s=null==i?void 0:i.useAdvancedParams)&&void 0!==s&&s;(0,p.n)(a,(s,a)=>E(e.id,s,a),e.model,P,t,void 0,s=>M(e.id,s),s=>I(e.id,s),s=>z(e.id,s),e.traceId,r,l,void 0,void 0,s=>U(e.id,s),o?e.temperature:void 0,o?e.maxTokens:void 0,s=>O(e.id,s)).catch(s=>{let a=s instanceof Error?s.message:String(s);console.error("CompareUI: failed to fetch response",s),x.Z.fromBackend(a),g(s=>s.map(s=>{if(s.id!==e.id)return s;let t=[...s.messages],r=t[t.length-1],n=r&&"assistant"===r.role&&"string"==typeof r.content?r.content:"";return r&&"assistant"===r.role?t[t.length-1]={...r,content:n?"".concat(n,"\nError fetching response: ").concat(a):"Error fetching response: ".concat(a)}:t.push({role:"assistant",content:"Error fetching response: ".concat(a)}),{...s,messages:t}}))}).finally(()=>{g(s=>s.map(s=>s.id===e.id?{...s,isLoading:!1}:s))})}))},V=e=>{N(e)},X=n.some(e=>e.messages.length>0),Y=n.some(e=>e.isLoading);return(0,t.jsx)("div",{className:"w-full h-full p-4 bg-white",children:(0,t.jsxs)("div",{className:"rounded-2xl border border-gray-200 bg-white shadow-sm min-h-[calc(100vh-140px)] flex flex-col",children:[(0,t.jsx)("div",{className:"border-b px-4 py-2",children:(0,t.jsxs)("div",{className:"flex flex-wrap items-center justify-between gap-3",children:[(0,t.jsxs)("div",{className:"flex items-center gap-2",children:[(0,t.jsx)("span",{className:"text-sm font-medium text-gray-600",children:"API Key Source"}),(0,t.jsxs)(i.default,{value:k,onChange:e=>w(e),disabled:a,className:"w-48",children:[(0,t.jsx)(i.default.Option,{value:"session",disabled:!W,children:"Current UI Session"}),(0,t.jsx)(i.default.Option,{value:"custom",children:"Virtual Key"})]}),"custom"===k&&(0,t.jsx)(o.default.Password,{value:A,onChange:e=>S(e.target.value),placeholder:"Enter API key",className:"w-56"})]}),(0,t.jsxs)("div",{className:"flex items-center gap-2",children:[(0,t.jsx)("span",{className:"text-sm font-medium text-gray-600",children:"Endpoint"}),(0,t.jsx)(d.Z,{title:"Other endpoints will be available soon",children:(0,t.jsx)(i.default,{value:F,disabled:!0,className:"w-56",children:(0,t.jsx)(i.default.Option,{value:F,children:F})})})]}),(0,t.jsxs)("div",{className:"flex items-center gap-3",children:[(0,t.jsx)(c.ZP,{onClick:()=>{g(e=>e.map(e=>({...e,messages:[],traceId:void 0,isLoading:!1}))),N("")},disabled:!X,icon:(0,t.jsx)(m.Z,{}),children:"Clear All Chats"}),(0,t.jsx)(d.Z,{title:n.length>=3?"Compare up to 3 models at a time":"Add another comparison",children:(0,t.jsx)(c.ZP,{onClick:()=>{var e;if(n.length>=3)return;let s=null!==(e=f[n.length%(f.length||1)])&&void 0!==e?e:"",a={id:Date.now().toString(),model:s,messages:[],isLoading:!1,tags:[],mcpTools:[],vectorStores:[],guardrails:[],temperature:1,maxTokens:2048,applyAcrossModels:!1,useAdvancedParams:!1};g(e=>[...e,a])},disabled:n.length>=3,icon:(0,t.jsx)(u.Z,{}),children:"Add Comparison"})})]})]})}),(0,t.jsx)("div",{className:"grid flex-1 min-h-0 auto-rows-[minmax(0,1fr)]",style:{gridTemplateColumns:"repeat(".concat(n.length,", minmax(0, 1fr))")},children:n.map(e=>(0,t.jsx)(R,{comparison:e,onUpdate:(s,a)=>L(e.id,s,a),onRemove:()=>_(e.id),canRemove:n.length>1,modelOptions:f,isLoadingModels:j,apiKey:P},e.id))}),(0,t.jsx)("div",{className:"flex justify-center pb-4",children:(0,t.jsx)("div",{className:"w-full max-w-3xl px-4",children:(0,t.jsxs)("div",{className:"border border-gray-200 shadow-lg rounded-xl bg-white p-4",children:[(0,t.jsx)("div",{className:"flex items-center justify-between gap-4 mb-3 min-h-8",children:X||Y?Z?(0,t.jsx)("div",{className:"flex items-center gap-2 overflow-x-auto",children:B.map(e=>(0,t.jsx)("button",{type:"button",onClick:()=>V(e),className:"shrink-0 rounded-full border border-gray-200 px-3 py-1 text-xs font-medium text-gray-600 transition-colors hover:bg-gray-100 cursor-pointer",children:e},e))}):Y?(0,t.jsxs)("span",{className:"flex items-center gap-2 text-sm text-gray-500",children:[(0,t.jsx)("span",{className:"h-2 w-2 rounded-full bg-blue-500 animate-pulse","aria-hidden":!0}),"Gathering responses from all models..."]}):(0,t.jsx)("span",{className:"text-sm text-gray-500",children:"Send a prompt to compare models"}):(0,t.jsx)("div",{className:"flex items-center gap-2 overflow-x-auto",children:D.map(e=>(0,t.jsx)("button",{type:"button",onClick:()=>V(e),className:"shrink-0 rounded-full border border-gray-200 px-3 py-1 text-xs font-medium text-gray-600 transition-colors hover:bg-gray-100 cursor-pointer",children:e},e))})}),(0,t.jsx)(K,{value:b,onChange:e=>{N(e)},onSend:()=>{G(b),N("")},disabled:0===n.length||n.every(e=>e.isLoading)})]})})})]})})}var G=a(58643),V=a(39760),X=a(91624);function Y(){let{accessToken:e,userRole:s,userId:a,disabledPersonalKeyCreation:l,token:i}=(0,V.Z)(),[o,d]=(0,r.useState)(void 0);return(0,r.useEffect)(()=>{(async()=>{if(e){let s=await (0,X.C)(e);s&&d({PROXY_BASE_URL:s.PROXY_BASE_URL,LITELLM_UI_API_DOC_BASE_URL:s.LITELLM_UI_API_DOC_BASE_URL})}})()},[e]),(0,t.jsxs)(G.v0,{className:"h-full w-full",children:[(0,t.jsxs)(G.td,{className:"mb-0",children:[(0,t.jsx)(G.OK,{children:"Chat"}),(0,t.jsx)(G.OK,{children:"Compare"})]}),(0,t.jsxs)(G.nP,{className:"h-full",children:[(0,t.jsx)(G.x4,{className:"h-full",children:(0,t.jsx)(n.Z,{accessToken:e,token:i,userRole:s,userID:a,disabledPersonalKeyCreation:l,proxySettings:o})}),(0,t.jsx)(G.x4,{className:"h-full",children:(0,t.jsx)(W,{accessToken:e,disabledPersonalKeyCreation:l})})]})]})}}}]); \ No newline at end of file diff --git a/litellm/proxy/_experimental/out/_next/static/chunks/1529-aa686050ee3518d4.js b/litellm/proxy/_experimental/out/_next/static/chunks/1529-59ce29afdf8ccc9b.js similarity index 99% rename from litellm/proxy/_experimental/out/_next/static/chunks/1529-aa686050ee3518d4.js rename to litellm/proxy/_experimental/out/_next/static/chunks/1529-59ce29afdf8ccc9b.js index e0d9b0cf48c..c98ed86dee5 100644 --- a/litellm/proxy/_experimental/out/_next/static/chunks/1529-aa686050ee3518d4.js +++ b/litellm/proxy/_experimental/out/_next/static/chunks/1529-59ce29afdf8ccc9b.js @@ -1 +1 @@ -"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[1529],{39760:function(e,n,t){t.d(n,{Z:function(){return a}});var r=t(1119),o=t(2265),i={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M176 511a56 56 0 10112 0 56 56 0 10-112 0zm280 0a56 56 0 10112 0 56 56 0 10-112 0zm280 0a56 56 0 10112 0 56 56 0 10-112 0z"}}]},name:"ellipsis",theme:"outlined"},l=t(55015),a=o.forwardRef(function(e,n){return o.createElement(l.Z,(0,r.Z)({},e,{ref:n,icon:i}))})},77565:function(e,n,t){t.d(n,{Z:function(){return a}});var r=t(1119),o=t(2265),i={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M765.7 486.8L314.9 134.7A7.97 7.97 0 00302 141v77.3c0 4.9 2.3 9.6 6.1 12.6l360 281.1-360 281.1c-3.9 3-6.1 7.7-6.1 12.6V883c0 6.7 7.7 10.4 12.9 6.3l450.8-352.1a31.96 31.96 0 000-50.4z"}}]},name:"right",theme:"outlined"},l=t(55015),a=o.forwardRef(function(e,n){return o.createElement(l.Z,(0,r.Z)({},e,{ref:n,icon:i}))})},71030:function(e,n,t){t.d(n,{Z:function(){return C}});var r=t(1119),o=t(11993),i=t(26365),l=t(6989),a=t(97821),u=t(36760),c=t.n(u),s=t(28791),f=t(2265),d=t(95814),p=t(53346),v=d.Z.ESC,m=d.Z.TAB,b=(0,f.forwardRef)(function(e,n){var t=e.overlay,r=e.arrow,o=e.prefixCls,i=(0,f.useMemo)(function(){return"function"==typeof t?t():t},[t]),l=(0,s.sQ)(n,(0,s.C4)(i));return f.createElement(f.Fragment,null,r&&f.createElement("div",{className:"".concat(o,"-arrow")}),f.cloneElement(i,{ref:(0,s.Yr)(i)?l:void 0}))}),y={adjustX:1,adjustY:1},h=[0,0],g={topLeft:{points:["bl","tl"],overflow:y,offset:[0,-4],targetOffset:h},top:{points:["bc","tc"],overflow:y,offset:[0,-4],targetOffset:h},topRight:{points:["br","tr"],overflow:y,offset:[0,-4],targetOffset:h},bottomLeft:{points:["tl","bl"],overflow:y,offset:[0,4],targetOffset:h},bottom:{points:["tc","bc"],overflow:y,offset:[0,4],targetOffset:h},bottomRight:{points:["tr","br"],overflow:y,offset:[0,4],targetOffset:h}},Z=["arrow","prefixCls","transitionName","animation","align","placement","placements","getPopupContainer","showAction","hideAction","overlayClassName","overlayStyle","visible","trigger","autoFocus","overlay","children","onVisibleChange"],C=f.forwardRef(function(e,n){var t,u,d,y,h,C,E,w,k,M,R,x,N,P,S=e.arrow,I=void 0!==S&&S,K=e.prefixCls,A=void 0===K?"rc-dropdown":K,O=e.transitionName,T=e.animation,L=e.align,D=e.placement,_=e.placements,V=e.getPopupContainer,z=e.showAction,F=e.hideAction,j=e.overlayClassName,B=e.overlayStyle,W=e.visible,H=e.trigger,Y=void 0===H?["hover"]:H,q=e.autoFocus,X=e.overlay,G=e.children,Q=e.onVisibleChange,U=(0,l.Z)(e,Z),J=f.useState(),$=(0,i.Z)(J,2),ee=$[0],en=$[1],et="visible"in e?W:ee,er=f.useRef(null),eo=f.useRef(null),ei=f.useRef(null);f.useImperativeHandle(n,function(){return er.current});var el=function(e){en(e),null==Q||Q(e)};u=(t={visible:et,triggerRef:ei,onVisibleChange:el,autoFocus:q,overlayRef:eo}).visible,d=t.triggerRef,y=t.onVisibleChange,h=t.autoFocus,C=t.overlayRef,E=f.useRef(!1),w=function(){if(u){var e,n;null===(e=d.current)||void 0===e||null===(n=e.focus)||void 0===n||n.call(e),null==y||y(!1)}},k=function(){var e;return null!==(e=C.current)&&void 0!==e&&!!e.focus&&(C.current.focus(),E.current=!0,!0)},M=function(e){switch(e.keyCode){case v:w();break;case m:var n=!1;E.current||(n=k()),n?e.preventDefault():w()}},f.useEffect(function(){return u?(window.addEventListener("keydown",M),h&&(0,p.Z)(k,3),function(){window.removeEventListener("keydown",M),E.current=!1}):function(){E.current=!1}},[u]);var ea=function(){return f.createElement(b,{ref:eo,overlay:X,prefixCls:A,arrow:I})},eu=f.cloneElement(G,{className:c()(null===(P=G.props)||void 0===P?void 0:P.className,et&&(void 0!==(R=e.openClassName)?R:"".concat(A,"-open"))),ref:(0,s.Yr)(G)?(0,s.sQ)(ei,(0,s.C4)(G)):void 0}),ec=F;return ec||-1===Y.indexOf("contextMenu")||(ec=["click"]),f.createElement(a.Z,(0,r.Z)({builtinPlacements:void 0===_?g:_},U,{prefixCls:A,ref:er,popupClassName:c()(j,(0,o.Z)({},"".concat(A,"-show-arrow"),I)),popupStyle:B,action:Y,showAction:z,hideAction:ec,popupPlacement:void 0===D?"bottomLeft":D,popupAlign:L,popupTransitionName:O,popupAnimation:T,popupVisible:et,stretch:(x=e.minOverlayWidthMatchTrigger,N=e.alignPoint,"minOverlayWidthMatchTrigger"in e?x:!N)?"minWidth":"",popup:"function"==typeof X?ea:ea(),onPopupVisibleChange:el,onPopupClick:function(n){var t=e.onOverlayClick;en(!1),t&&t(n)},getPopupContainer:V}),eu)})},33082:function(e,n,t){t.d(n,{iz:function(){return eA},ck:function(){return ev},BW:function(){return eL},sN:function(){return ev},Wd:function(){return eI},ZP:function(){return ej},Xl:function(){return x}});var r=t(1119),o=t(11993),i=t(31686),l=t(83145),a=t(26365),u=t(6989),c=t(36760),s=t.n(c),f=t(1699),d=t(50506),p=t(16671),v=t(32559),m=t(2265),b=t(54887),y=m.createContext(null);function h(e,n){return void 0===e?null:"".concat(e,"-").concat(n)}function g(e){return h(m.useContext(y),e)}var Z=t(6397),C=["children","locked"],E=m.createContext(null);function w(e){var n=e.children,t=e.locked,r=(0,u.Z)(e,C),o=m.useContext(E),l=(0,Z.Z)(function(){var e;return e=(0,i.Z)({},o),Object.keys(r).forEach(function(n){var t=r[n];void 0!==t&&(e[n]=t)}),e},[o,r],function(e,n){return!t&&(e[0]!==n[0]||!(0,p.Z)(e[1],n[1],!0))});return m.createElement(E.Provider,{value:l},n)}var k=m.createContext(null);function M(){return m.useContext(k)}var R=m.createContext([]);function x(e){var n=m.useContext(R);return m.useMemo(function(){return void 0!==e?[].concat((0,l.Z)(n),[e]):n},[n,e])}var N=m.createContext(null),P=m.createContext({}),S=t(2857);function I(e){var n=arguments.length>1&&void 0!==arguments[1]&&arguments[1];if((0,S.Z)(e)){var t=e.nodeName.toLowerCase(),r=["input","select","textarea","button"].includes(t)||e.isContentEditable||"a"===t&&!!e.getAttribute("href"),o=e.getAttribute("tabindex"),i=Number(o),l=null;return o&&!Number.isNaN(i)?l=i:r&&null===l&&(l=0),r&&e.disabled&&(l=null),null!==l&&(l>=0||n&&l<0)}return!1}var K=t(95814),A=t(53346),O=K.Z.LEFT,T=K.Z.RIGHT,L=K.Z.UP,D=K.Z.DOWN,_=K.Z.ENTER,V=K.Z.ESC,z=K.Z.HOME,F=K.Z.END,j=[L,D,O,T];function B(e,n){return(function(e){var n=arguments.length>1&&void 0!==arguments[1]&&arguments[1],t=(0,l.Z)(e.querySelectorAll("*")).filter(function(e){return I(e,n)});return I(e,n)&&t.unshift(e),t})(e,!0).filter(function(e){return n.has(e)})}function W(e,n,t){var r=arguments.length>3&&void 0!==arguments[3]?arguments[3]:1;if(!e)return null;var o=B(e,n),i=o.length,l=o.findIndex(function(e){return t===e});return r<0?-1===l?l=i-1:l-=1:r>0&&(l+=1),o[l=(l+i)%i]}var H=function(e,n){var t=new Set,r=new Map,o=new Map;return e.forEach(function(e){var i=document.querySelector("[data-menu-id='".concat(h(n,e),"']"));i&&(t.add(i),o.set(i,e),r.set(e,i))}),{elements:t,key2element:r,element2key:o}},Y="__RC_UTIL_PATH_SPLIT__",q=function(e){return e.join(Y)},X="rc-menu-more";function G(e){var n=m.useRef(e);n.current=e;var t=m.useCallback(function(){for(var e,t=arguments.length,r=Array(t),o=0;o1&&(k.motionAppear=!1);var M=k.onVisibleChanged;return(k.onVisibleChanged=function(e){return b.current||e||Z(!0),null==M?void 0:M(e)},g)?null:m.createElement(w,{mode:u,locked:!b.current},m.createElement(eR.ZP,(0,r.Z)({visible:C},k,{forceRender:f,removeOnLeave:!1,leavedClassName:"".concat(s,"-hidden")}),function(e){var t=e.className,r=e.style;return m.createElement(eb,{id:n,className:t,style:r},l)}))}var eN=["style","className","title","eventKey","warnKey","disabled","internalPopupClose","children","itemIcon","expandIcon","popupClassName","popupOffset","popupStyle","onClick","onMouseEnter","onMouseLeave","onTitleClick","onTitleMouseEnter","onTitleMouseLeave"],eP=["active"],eS=m.forwardRef(function(e,n){var t=e.style,l=e.className,c=e.title,d=e.eventKey,p=(e.warnKey,e.disabled),v=e.internalPopupClose,b=e.children,y=e.itemIcon,h=e.expandIcon,Z=e.popupClassName,C=e.popupOffset,k=e.popupStyle,M=e.onClick,R=e.onMouseEnter,S=e.onMouseLeave,I=e.onTitleClick,K=e.onTitleMouseEnter,A=e.onTitleMouseLeave,O=(0,u.Z)(e,eN),T=g(d),L=m.useContext(E),D=L.prefixCls,_=L.mode,V=L.openKeys,z=L.disabled,F=L.overflowDisabled,j=L.activeKey,B=L.selectedKeys,W=L.itemIcon,H=L.expandIcon,Y=L.onItemClick,q=L.onOpenChange,X=L.onActive,Q=m.useContext(P)._internalRenderSubMenuItem,U=m.useContext(N).isSubPathKey,J=x(),$="".concat(D,"-submenu"),ee=z||p,en=m.useRef(),et=m.useRef(),er=null!=h?h:H,ea=V.includes(d),ec=!F&&ea,es=U(B,d),ef=eo(d,ee,K,A),ed=ef.active,ep=(0,u.Z)(ef,eP),ev=m.useState(!1),em=(0,a.Z)(ev,2),ey=em[0],eh=em[1],eg=function(e){ee||eh(e)},eZ=m.useMemo(function(){return ed||"inline"!==_&&(ey||U([j],d))},[_,ed,j,ey,d,U]),eC=ei(J.length),eE=G(function(e){null==M||M(eu(e)),Y(e)}),ew=T&&"".concat(T,"-popup"),ek=m.useMemo(function(){return m.createElement(el,{icon:"horizontal"!==_?er:void 0,props:(0,i.Z)((0,i.Z)({},e),{},{isOpen:ec,isSubMenu:!0})},m.createElement("i",{className:"".concat($,"-arrow")}))},[_,er,e,ec,$]),eR=m.createElement("div",(0,r.Z)({role:"menuitem",style:eC,className:"".concat($,"-title"),tabIndex:ee?null:-1,ref:en,title:"string"==typeof c?c:null,"data-menu-id":F&&T?null:T,"aria-expanded":ec,"aria-haspopup":!0,"aria-controls":ew,"aria-disabled":ee,onClick:function(e){ee||(null==I||I({key:d,domEvent:e}),"inline"===_&&q(d,!ea))},onFocus:function(){X(d)}},ep),c,ek),eS=m.useRef(_);if("inline"!==_&&J.length>1?eS.current="vertical":eS.current=_,!F){var eI=eS.current;eR=m.createElement(eM,{mode:eI,prefixCls:$,visible:!v&&ec&&"inline"!==_,popupClassName:Z,popupOffset:C,popupStyle:k,popup:m.createElement(w,{mode:"horizontal"===eI?"vertical":eI},m.createElement(eb,{id:ew,ref:et},b)),disabled:ee,onVisibleChange:function(e){"inline"!==_&&q(d,e)}},eR)}var eK=m.createElement(f.Z.Item,(0,r.Z)({ref:n,role:"none"},O,{component:"li",style:t,className:s()($,"".concat($,"-").concat(_),l,(0,o.Z)((0,o.Z)((0,o.Z)((0,o.Z)({},"".concat($,"-open"),ec),"".concat($,"-active"),eZ),"".concat($,"-selected"),es),"".concat($,"-disabled"),ee)),onMouseEnter:function(e){eg(!0),null==R||R({key:d,domEvent:e})},onMouseLeave:function(e){eg(!1),null==S||S({key:d,domEvent:e})}}),eR,!F&&m.createElement(ex,{id:ew,open:ec,keyPath:J},b));return Q&&(eK=Q(eK,e,{selected:es,active:eZ,open:ec,disabled:ee})),m.createElement(w,{onItemClick:eE,mode:"horizontal"===_?"vertical":_,itemIcon:null!=y?y:W,expandIcon:er},eK)}),eI=m.forwardRef(function(e,n){var t,o=e.eventKey,i=e.children,l=x(o),a=eh(i,l),u=M();return m.useEffect(function(){if(u)return u.registerPath(o,l),function(){u.unregisterPath(o,l)}},[l]),t=u?a:m.createElement(eS,(0,r.Z)({ref:n},e),a),m.createElement(R.Provider,{value:l},t)}),eK=t(41154);function eA(e){var n=e.className,t=e.style,r=m.useContext(E).prefixCls;return M()?null:m.createElement("li",{role:"separator",className:s()("".concat(r,"-item-divider"),n),style:t})}var eO=["className","title","eventKey","children"],eT=m.forwardRef(function(e,n){var t=e.className,o=e.title,i=(e.eventKey,e.children),l=(0,u.Z)(e,eO),a=m.useContext(E).prefixCls,c="".concat(a,"-item-group");return m.createElement("li",(0,r.Z)({ref:n,role:"presentation"},l,{onClick:function(e){return e.stopPropagation()},className:s()(c,t)}),m.createElement("div",{role:"presentation",className:"".concat(c,"-title"),title:"string"==typeof o?o:void 0},o),m.createElement("ul",{role:"group",className:"".concat(c,"-list")},i))}),eL=m.forwardRef(function(e,n){var t=e.eventKey,o=eh(e.children,x(t));return M()?o:m.createElement(eT,(0,r.Z)({ref:n},(0,et.Z)(e,["warnKey"])),o)}),eD=["label","children","key","type","extra"];function e_(e,n,t,o,l){var a=e,c=(0,i.Z)({divider:eA,item:ev,group:eL,submenu:eI},o);return n&&(a=function e(n,t,o){var i=t.item,l=t.group,a=t.submenu,c=t.divider;return(n||[]).map(function(n,s){if(n&&"object"===(0,eK.Z)(n)){var f=n.label,d=n.children,p=n.key,v=n.type,b=n.extra,y=(0,u.Z)(n,eD),h=null!=p?p:"tmp-".concat(s);return d||"group"===v?"group"===v?m.createElement(l,(0,r.Z)({key:h},y,{title:f}),e(d,t,o)):m.createElement(a,(0,r.Z)({key:h},y,{title:f}),e(d,t,o)):"divider"===v?m.createElement(c,(0,r.Z)({key:h},y)):m.createElement(i,(0,r.Z)({key:h},y,{extra:b}),f,(!!b||0===b)&&m.createElement("span",{className:"".concat(o,"-item-extra")},b))}return null}).filter(function(e){return e})}(n,c,l)),eh(a,t)}var eV=["prefixCls","rootClassName","style","className","tabIndex","items","children","direction","id","mode","inlineCollapsed","disabled","disabledOverflow","subMenuOpenDelay","subMenuCloseDelay","forceSubMenuRender","defaultOpenKeys","openKeys","activeKey","defaultActiveFirst","selectable","multiple","defaultSelectedKeys","selectedKeys","onSelect","onDeselect","inlineIndent","motion","defaultMotions","triggerSubMenuAction","builtinPlacements","itemIcon","expandIcon","overflowedIndicator","overflowedIndicatorPopupClassName","getPopupContainer","onClick","onOpenChange","onKeyDown","openAnimation","openTransitionName","_internalRenderMenuItem","_internalRenderSubMenuItem","_internalComponents"],ez=[],eF=m.forwardRef(function(e,n){var t,c,v,h,g,Z,C,E,M,R,x,S,I,K,J,$,ee,en,et,er,eo,ei,el,ea,ec,es,ef=e.prefixCls,ed=void 0===ef?"rc-menu":ef,ep=e.rootClassName,em=e.style,eb=e.className,ey=e.tabIndex,eh=e.items,eg=e.children,eZ=e.direction,eC=e.id,eE=e.mode,ew=void 0===eE?"vertical":eE,ek=e.inlineCollapsed,eM=e.disabled,eR=e.disabledOverflow,ex=e.subMenuOpenDelay,eN=e.subMenuCloseDelay,eP=e.forceSubMenuRender,eS=e.defaultOpenKeys,eK=e.openKeys,eA=e.activeKey,eO=e.defaultActiveFirst,eT=e.selectable,eL=void 0===eT||eT,eD=e.multiple,eF=void 0!==eD&&eD,ej=e.defaultSelectedKeys,eB=e.selectedKeys,eW=e.onSelect,eH=e.onDeselect,eY=e.inlineIndent,eq=e.motion,eX=e.defaultMotions,eG=e.triggerSubMenuAction,eQ=e.builtinPlacements,eU=e.itemIcon,eJ=e.expandIcon,e$=e.overflowedIndicator,e0=void 0===e$?"...":e$,e1=e.overflowedIndicatorPopupClassName,e6=e.getPopupContainer,e2=e.onClick,e5=e.onOpenChange,e9=e.onKeyDown,e3=(e.openAnimation,e.openTransitionName,e._internalRenderMenuItem),e4=e._internalRenderSubMenuItem,e7=e._internalComponents,e8=(0,u.Z)(e,eV),ne=m.useMemo(function(){return[e_(eg,eh,ez,e7,ed),e_(eg,eh,ez,{},ed)]},[eg,eh,e7]),nn=(0,a.Z)(ne,2),nt=nn[0],nr=nn[1],no=m.useState(!1),ni=(0,a.Z)(no,2),nl=ni[0],na=ni[1],nu=m.useRef(),nc=(t=(0,d.Z)(eC,{value:eC}),v=(c=(0,a.Z)(t,2))[0],h=c[1],m.useEffect(function(){U+=1;var e="".concat(Q,"-").concat(U);h("rc-menu-uuid-".concat(e))},[]),v),ns="rtl"===eZ,nf=(0,d.Z)(eS,{value:eK,postState:function(e){return e||ez}}),nd=(0,a.Z)(nf,2),np=nd[0],nv=nd[1],nm=function(e){var n=arguments.length>1&&void 0!==arguments[1]&&arguments[1];function t(){nv(e),null==e5||e5(e)}n?(0,b.flushSync)(t):t()},nb=m.useState(np),ny=(0,a.Z)(nb,2),nh=ny[0],ng=ny[1],nZ=m.useRef(!1),nC=m.useMemo(function(){return("inline"===ew||"vertical"===ew)&&ek?["vertical",ek]:[ew,!1]},[ew,ek]),nE=(0,a.Z)(nC,2),nw=nE[0],nk=nE[1],nM="inline"===nw,nR=m.useState(nw),nx=(0,a.Z)(nR,2),nN=nx[0],nP=nx[1],nS=m.useState(nk),nI=(0,a.Z)(nS,2),nK=nI[0],nA=nI[1];m.useEffect(function(){nP(nw),nA(nk),nZ.current&&(nM?nv(nh):nm(ez))},[nw,nk]);var nO=m.useState(0),nT=(0,a.Z)(nO,2),nL=nT[0],nD=nT[1],n_=nL>=nt.length-1||"horizontal"!==nN||eR;m.useEffect(function(){nM&&ng(np)},[np]),m.useEffect(function(){return nZ.current=!0,function(){nZ.current=!1}},[]);var nV=(g=m.useState({}),Z=(0,a.Z)(g,2)[1],C=(0,m.useRef)(new Map),E=(0,m.useRef)(new Map),M=m.useState([]),x=(R=(0,a.Z)(M,2))[0],S=R[1],I=(0,m.useRef)(0),K=(0,m.useRef)(!1),J=function(){K.current||Z({})},$=(0,m.useCallback)(function(e,n){var t,r=q(n);E.current.set(r,e),C.current.set(e,r),I.current+=1;var o=I.current;t=function(){o===I.current&&J()},Promise.resolve().then(t)},[]),ee=(0,m.useCallback)(function(e,n){var t=q(n);E.current.delete(t),C.current.delete(e)},[]),en=(0,m.useCallback)(function(e){S(e)},[]),et=(0,m.useCallback)(function(e,n){var t=(C.current.get(e)||"").split(Y);return n&&x.includes(t[0])&&t.unshift(X),t},[x]),er=(0,m.useCallback)(function(e,n){return e.filter(function(e){return void 0!==e}).some(function(e){return et(e,!0).includes(n)})},[et]),eo=(0,m.useCallback)(function(e){var n="".concat(C.current.get(e)).concat(Y),t=new Set;return(0,l.Z)(E.current.keys()).forEach(function(e){e.startsWith(n)&&t.add(E.current.get(e))}),t},[]),m.useEffect(function(){return function(){K.current=!0}},[]),{registerPath:$,unregisterPath:ee,refreshOverflowKeys:en,isSubPathKey:er,getKeyPath:et,getKeys:function(){var e=(0,l.Z)(C.current.keys());return x.length&&e.push(X),e},getSubPathKeys:eo}),nz=nV.registerPath,nF=nV.unregisterPath,nj=nV.refreshOverflowKeys,nB=nV.isSubPathKey,nW=nV.getKeyPath,nH=nV.getKeys,nY=nV.getSubPathKeys,nq=m.useMemo(function(){return{registerPath:nz,unregisterPath:nF}},[nz,nF]),nX=m.useMemo(function(){return{isSubPathKey:nB}},[nB]);m.useEffect(function(){nj(n_?ez:nt.slice(nL+1).map(function(e){return e.key}))},[nL,n_]);var nG=(0,d.Z)(eA||eO&&(null===(es=nt[0])||void 0===es?void 0:es.key),{value:eA}),nQ=(0,a.Z)(nG,2),nU=nQ[0],nJ=nQ[1],n$=G(function(e){nJ(e)}),n0=G(function(){nJ(void 0)});(0,m.useImperativeHandle)(n,function(){return{list:nu.current,focus:function(e){var n,t,r=H(nH(),nc),o=r.elements,i=r.key2element,l=r.element2key,a=B(nu.current,o),u=null!=nU?nU:a[0]?l.get(a[0]):null===(n=nt.find(function(e){return!e.props.disabled}))||void 0===n?void 0:n.key,c=i.get(u);u&&c&&(null==c||null===(t=c.focus)||void 0===t||t.call(c,e))}}});var n1=(0,d.Z)(ej||[],{value:eB,postState:function(e){return Array.isArray(e)?e:null==e?ez:[e]}}),n6=(0,a.Z)(n1,2),n2=n6[0],n5=n6[1],n9=function(e){if(eL){var n,t=e.key,r=n2.includes(t);n5(n=eF?r?n2.filter(function(e){return e!==t}):[].concat((0,l.Z)(n2),[t]):[t]);var o=(0,i.Z)((0,i.Z)({},e),{},{selectedKeys:n});r?null==eH||eH(o):null==eW||eW(o)}!eF&&np.length&&"inline"!==nN&&nm(ez)},n3=G(function(e){null==e2||e2(eu(e)),n9(e)}),n4=G(function(e,n){var t=np.filter(function(n){return n!==e});if(n)t.push(e);else if("inline"!==nN){var r=nY(e);t=t.filter(function(e){return!r.has(e)})}(0,p.Z)(np,t,!0)||nm(t,!0)}),n7=(ei=function(e,n){var t=null!=n?n:!np.includes(e);n4(e,t)},el=m.useRef(),(ea=m.useRef()).current=nU,ec=function(){A.Z.cancel(el.current)},m.useEffect(function(){return function(){ec()}},[]),function(e){var n=e.which;if([].concat(j,[_,V,z,F]).includes(n)){var t=nH(),r=H(t,nc),i=r,l=i.elements,a=i.key2element,u=i.element2key,c=function(e,n){for(var t=e||document.activeElement;t;){if(n.has(t))return t;t=t.parentElement}return null}(a.get(nU),l),s=u.get(c),f=function(e,n,t,r){var i,l="prev",a="next",u="children",c="parent";if("inline"===e&&r===_)return{inlineTrigger:!0};var s=(0,o.Z)((0,o.Z)({},L,l),D,a),f=(0,o.Z)((0,o.Z)((0,o.Z)((0,o.Z)({},O,t?a:l),T,t?l:a),D,u),_,u),d=(0,o.Z)((0,o.Z)((0,o.Z)((0,o.Z)((0,o.Z)((0,o.Z)({},L,l),D,a),_,u),V,c),O,t?u:c),T,t?c:u);switch(null===(i=({inline:s,horizontal:f,vertical:d,inlineSub:s,horizontalSub:d,verticalSub:d})["".concat(e).concat(n?"":"Sub")])||void 0===i?void 0:i[r]){case l:return{offset:-1,sibling:!0};case a:return{offset:1,sibling:!0};case c:return{offset:-1,sibling:!1};case u:return{offset:1,sibling:!1};default:return null}}(nN,1===nW(s,!0).length,ns,n);if(!f&&n!==z&&n!==F)return;(j.includes(n)||[z,F].includes(n))&&e.preventDefault();var d=function(e){if(e){var n=e,t=e.querySelector("a");null!=t&&t.getAttribute("href")&&(n=t);var r=u.get(e);nJ(r),ec(),el.current=(0,A.Z)(function(){ea.current===r&&n.focus()})}};if([z,F].includes(n)||f.sibling||!c){var p,v=B(p=c&&"inline"!==nN?function(e){for(var n=e;n;){if(n.getAttribute("data-menu-list"))return n;n=n.parentElement}return null}(c):nu.current,l);d(n===z?v[0]:n===F?v[v.length-1]:W(p,l,c,f.offset))}else if(f.inlineTrigger)ei(s);else if(f.offset>0)ei(s,!0),ec(),el.current=(0,A.Z)(function(){r=H(t,nc);var e=c.getAttribute("aria-controls");d(W(document.getElementById(e),r.elements))},5);else if(f.offset<0){var m=nW(s,!0),b=m[m.length-2],y=a.get(b);ei(b,!1),d(y)}}null==e9||e9(e)});m.useEffect(function(){na(!0)},[]);var n8=m.useMemo(function(){return{_internalRenderMenuItem:e3,_internalRenderSubMenuItem:e4}},[e3,e4]),te="horizontal"!==nN||eR?nt:nt.map(function(e,n){return m.createElement(w,{key:e.key,overflowDisabled:n>nL},e)}),tn=m.createElement(f.Z,(0,r.Z)({id:eC,ref:nu,prefixCls:"".concat(ed,"-overflow"),component:"ul",itemComponent:ev,className:s()(ed,"".concat(ed,"-root"),"".concat(ed,"-").concat(nN),eb,(0,o.Z)((0,o.Z)({},"".concat(ed,"-inline-collapsed"),nK),"".concat(ed,"-rtl"),ns),ep),dir:eZ,style:em,role:"menu",tabIndex:void 0===ey?0:ey,data:te,renderRawItem:function(e){return e},renderRawRest:function(e){var n=e.length,t=n?nt.slice(-n):null;return m.createElement(eI,{eventKey:X,title:e0,disabled:n_,internalPopupClose:0===n,popupClassName:e1},t)},maxCount:"horizontal"!==nN||eR?f.Z.INVALIDATE:f.Z.RESPONSIVE,ssr:"full","data-menu-list":!0,onVisibleChange:function(e){nD(e)},onKeyDown:n7},e8));return m.createElement(P.Provider,{value:n8},m.createElement(y.Provider,{value:nc},m.createElement(w,{prefixCls:ed,rootClassName:ep,mode:nN,openKeys:np,rtl:ns,disabled:eM,motion:nl?eq:null,defaultMotions:nl?eX:null,activeKey:nU,onActive:n$,onInactive:n0,selectedKeys:n2,inlineIndent:void 0===eY?24:eY,subMenuOpenDelay:void 0===ex?.1:ex,subMenuCloseDelay:void 0===eN?.1:eN,forceSubMenuRender:eP,builtinPlacements:eQ,triggerSubMenuAction:void 0===eG?"hover":eG,getPopupContainer:e6,itemIcon:eU,expandIcon:eJ,onItemClick:n3,onOpenChange:n4},m.createElement(N.Provider,{value:nX},tn),m.createElement("div",{style:{display:"none"},"aria-hidden":!0},m.createElement(k.Provider,{value:nq},nr)))))});eF.Item=ev,eF.SubMenu=eI,eF.ItemGroup=eL,eF.Divider=eA;var ej=eF}}]); \ No newline at end of file +"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[1529],{60440:function(e,n,t){t.d(n,{Z:function(){return a}});var r=t(1119),o=t(2265),i={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M176 511a56 56 0 10112 0 56 56 0 10-112 0zm280 0a56 56 0 10112 0 56 56 0 10-112 0zm280 0a56 56 0 10112 0 56 56 0 10-112 0z"}}]},name:"ellipsis",theme:"outlined"},l=t(55015),a=o.forwardRef(function(e,n){return o.createElement(l.Z,(0,r.Z)({},e,{ref:n,icon:i}))})},77565:function(e,n,t){t.d(n,{Z:function(){return a}});var r=t(1119),o=t(2265),i={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M765.7 486.8L314.9 134.7A7.97 7.97 0 00302 141v77.3c0 4.9 2.3 9.6 6.1 12.6l360 281.1-360 281.1c-3.9 3-6.1 7.7-6.1 12.6V883c0 6.7 7.7 10.4 12.9 6.3l450.8-352.1a31.96 31.96 0 000-50.4z"}}]},name:"right",theme:"outlined"},l=t(55015),a=o.forwardRef(function(e,n){return o.createElement(l.Z,(0,r.Z)({},e,{ref:n,icon:i}))})},71030:function(e,n,t){t.d(n,{Z:function(){return C}});var r=t(1119),o=t(11993),i=t(26365),l=t(6989),a=t(97821),u=t(36760),c=t.n(u),s=t(28791),f=t(2265),d=t(95814),p=t(53346),v=d.Z.ESC,m=d.Z.TAB,b=(0,f.forwardRef)(function(e,n){var t=e.overlay,r=e.arrow,o=e.prefixCls,i=(0,f.useMemo)(function(){return"function"==typeof t?t():t},[t]),l=(0,s.sQ)(n,(0,s.C4)(i));return f.createElement(f.Fragment,null,r&&f.createElement("div",{className:"".concat(o,"-arrow")}),f.cloneElement(i,{ref:(0,s.Yr)(i)?l:void 0}))}),y={adjustX:1,adjustY:1},h=[0,0],g={topLeft:{points:["bl","tl"],overflow:y,offset:[0,-4],targetOffset:h},top:{points:["bc","tc"],overflow:y,offset:[0,-4],targetOffset:h},topRight:{points:["br","tr"],overflow:y,offset:[0,-4],targetOffset:h},bottomLeft:{points:["tl","bl"],overflow:y,offset:[0,4],targetOffset:h},bottom:{points:["tc","bc"],overflow:y,offset:[0,4],targetOffset:h},bottomRight:{points:["tr","br"],overflow:y,offset:[0,4],targetOffset:h}},Z=["arrow","prefixCls","transitionName","animation","align","placement","placements","getPopupContainer","showAction","hideAction","overlayClassName","overlayStyle","visible","trigger","autoFocus","overlay","children","onVisibleChange"],C=f.forwardRef(function(e,n){var t,u,d,y,h,C,E,w,k,M,R,x,N,P,S=e.arrow,I=void 0!==S&&S,K=e.prefixCls,A=void 0===K?"rc-dropdown":K,O=e.transitionName,T=e.animation,L=e.align,D=e.placement,_=e.placements,V=e.getPopupContainer,z=e.showAction,F=e.hideAction,j=e.overlayClassName,B=e.overlayStyle,W=e.visible,H=e.trigger,Y=void 0===H?["hover"]:H,q=e.autoFocus,X=e.overlay,G=e.children,Q=e.onVisibleChange,U=(0,l.Z)(e,Z),J=f.useState(),$=(0,i.Z)(J,2),ee=$[0],en=$[1],et="visible"in e?W:ee,er=f.useRef(null),eo=f.useRef(null),ei=f.useRef(null);f.useImperativeHandle(n,function(){return er.current});var el=function(e){en(e),null==Q||Q(e)};u=(t={visible:et,triggerRef:ei,onVisibleChange:el,autoFocus:q,overlayRef:eo}).visible,d=t.triggerRef,y=t.onVisibleChange,h=t.autoFocus,C=t.overlayRef,E=f.useRef(!1),w=function(){if(u){var e,n;null===(e=d.current)||void 0===e||null===(n=e.focus)||void 0===n||n.call(e),null==y||y(!1)}},k=function(){var e;return null!==(e=C.current)&&void 0!==e&&!!e.focus&&(C.current.focus(),E.current=!0,!0)},M=function(e){switch(e.keyCode){case v:w();break;case m:var n=!1;E.current||(n=k()),n?e.preventDefault():w()}},f.useEffect(function(){return u?(window.addEventListener("keydown",M),h&&(0,p.Z)(k,3),function(){window.removeEventListener("keydown",M),E.current=!1}):function(){E.current=!1}},[u]);var ea=function(){return f.createElement(b,{ref:eo,overlay:X,prefixCls:A,arrow:I})},eu=f.cloneElement(G,{className:c()(null===(P=G.props)||void 0===P?void 0:P.className,et&&(void 0!==(R=e.openClassName)?R:"".concat(A,"-open"))),ref:(0,s.Yr)(G)?(0,s.sQ)(ei,(0,s.C4)(G)):void 0}),ec=F;return ec||-1===Y.indexOf("contextMenu")||(ec=["click"]),f.createElement(a.Z,(0,r.Z)({builtinPlacements:void 0===_?g:_},U,{prefixCls:A,ref:er,popupClassName:c()(j,(0,o.Z)({},"".concat(A,"-show-arrow"),I)),popupStyle:B,action:Y,showAction:z,hideAction:ec,popupPlacement:void 0===D?"bottomLeft":D,popupAlign:L,popupTransitionName:O,popupAnimation:T,popupVisible:et,stretch:(x=e.minOverlayWidthMatchTrigger,N=e.alignPoint,"minOverlayWidthMatchTrigger"in e?x:!N)?"minWidth":"",popup:"function"==typeof X?ea:ea(),onPopupVisibleChange:el,onPopupClick:function(n){var t=e.onOverlayClick;en(!1),t&&t(n)},getPopupContainer:V}),eu)})},33082:function(e,n,t){t.d(n,{iz:function(){return eA},ck:function(){return ev},BW:function(){return eL},sN:function(){return ev},Wd:function(){return eI},ZP:function(){return ej},Xl:function(){return x}});var r=t(1119),o=t(11993),i=t(31686),l=t(83145),a=t(26365),u=t(6989),c=t(36760),s=t.n(c),f=t(1699),d=t(50506),p=t(16671),v=t(32559),m=t(2265),b=t(54887),y=m.createContext(null);function h(e,n){return void 0===e?null:"".concat(e,"-").concat(n)}function g(e){return h(m.useContext(y),e)}var Z=t(6397),C=["children","locked"],E=m.createContext(null);function w(e){var n=e.children,t=e.locked,r=(0,u.Z)(e,C),o=m.useContext(E),l=(0,Z.Z)(function(){var e;return e=(0,i.Z)({},o),Object.keys(r).forEach(function(n){var t=r[n];void 0!==t&&(e[n]=t)}),e},[o,r],function(e,n){return!t&&(e[0]!==n[0]||!(0,p.Z)(e[1],n[1],!0))});return m.createElement(E.Provider,{value:l},n)}var k=m.createContext(null);function M(){return m.useContext(k)}var R=m.createContext([]);function x(e){var n=m.useContext(R);return m.useMemo(function(){return void 0!==e?[].concat((0,l.Z)(n),[e]):n},[n,e])}var N=m.createContext(null),P=m.createContext({}),S=t(2857);function I(e){var n=arguments.length>1&&void 0!==arguments[1]&&arguments[1];if((0,S.Z)(e)){var t=e.nodeName.toLowerCase(),r=["input","select","textarea","button"].includes(t)||e.isContentEditable||"a"===t&&!!e.getAttribute("href"),o=e.getAttribute("tabindex"),i=Number(o),l=null;return o&&!Number.isNaN(i)?l=i:r&&null===l&&(l=0),r&&e.disabled&&(l=null),null!==l&&(l>=0||n&&l<0)}return!1}var K=t(95814),A=t(53346),O=K.Z.LEFT,T=K.Z.RIGHT,L=K.Z.UP,D=K.Z.DOWN,_=K.Z.ENTER,V=K.Z.ESC,z=K.Z.HOME,F=K.Z.END,j=[L,D,O,T];function B(e,n){return(function(e){var n=arguments.length>1&&void 0!==arguments[1]&&arguments[1],t=(0,l.Z)(e.querySelectorAll("*")).filter(function(e){return I(e,n)});return I(e,n)&&t.unshift(e),t})(e,!0).filter(function(e){return n.has(e)})}function W(e,n,t){var r=arguments.length>3&&void 0!==arguments[3]?arguments[3]:1;if(!e)return null;var o=B(e,n),i=o.length,l=o.findIndex(function(e){return t===e});return r<0?-1===l?l=i-1:l-=1:r>0&&(l+=1),o[l=(l+i)%i]}var H=function(e,n){var t=new Set,r=new Map,o=new Map;return e.forEach(function(e){var i=document.querySelector("[data-menu-id='".concat(h(n,e),"']"));i&&(t.add(i),o.set(i,e),r.set(e,i))}),{elements:t,key2element:r,element2key:o}},Y="__RC_UTIL_PATH_SPLIT__",q=function(e){return e.join(Y)},X="rc-menu-more";function G(e){var n=m.useRef(e);n.current=e;var t=m.useCallback(function(){for(var e,t=arguments.length,r=Array(t),o=0;o1&&(k.motionAppear=!1);var M=k.onVisibleChanged;return(k.onVisibleChanged=function(e){return b.current||e||Z(!0),null==M?void 0:M(e)},g)?null:m.createElement(w,{mode:u,locked:!b.current},m.createElement(eR.ZP,(0,r.Z)({visible:C},k,{forceRender:f,removeOnLeave:!1,leavedClassName:"".concat(s,"-hidden")}),function(e){var t=e.className,r=e.style;return m.createElement(eb,{id:n,className:t,style:r},l)}))}var eN=["style","className","title","eventKey","warnKey","disabled","internalPopupClose","children","itemIcon","expandIcon","popupClassName","popupOffset","popupStyle","onClick","onMouseEnter","onMouseLeave","onTitleClick","onTitleMouseEnter","onTitleMouseLeave"],eP=["active"],eS=m.forwardRef(function(e,n){var t=e.style,l=e.className,c=e.title,d=e.eventKey,p=(e.warnKey,e.disabled),v=e.internalPopupClose,b=e.children,y=e.itemIcon,h=e.expandIcon,Z=e.popupClassName,C=e.popupOffset,k=e.popupStyle,M=e.onClick,R=e.onMouseEnter,S=e.onMouseLeave,I=e.onTitleClick,K=e.onTitleMouseEnter,A=e.onTitleMouseLeave,O=(0,u.Z)(e,eN),T=g(d),L=m.useContext(E),D=L.prefixCls,_=L.mode,V=L.openKeys,z=L.disabled,F=L.overflowDisabled,j=L.activeKey,B=L.selectedKeys,W=L.itemIcon,H=L.expandIcon,Y=L.onItemClick,q=L.onOpenChange,X=L.onActive,Q=m.useContext(P)._internalRenderSubMenuItem,U=m.useContext(N).isSubPathKey,J=x(),$="".concat(D,"-submenu"),ee=z||p,en=m.useRef(),et=m.useRef(),er=null!=h?h:H,ea=V.includes(d),ec=!F&&ea,es=U(B,d),ef=eo(d,ee,K,A),ed=ef.active,ep=(0,u.Z)(ef,eP),ev=m.useState(!1),em=(0,a.Z)(ev,2),ey=em[0],eh=em[1],eg=function(e){ee||eh(e)},eZ=m.useMemo(function(){return ed||"inline"!==_&&(ey||U([j],d))},[_,ed,j,ey,d,U]),eC=ei(J.length),eE=G(function(e){null==M||M(eu(e)),Y(e)}),ew=T&&"".concat(T,"-popup"),ek=m.useMemo(function(){return m.createElement(el,{icon:"horizontal"!==_?er:void 0,props:(0,i.Z)((0,i.Z)({},e),{},{isOpen:ec,isSubMenu:!0})},m.createElement("i",{className:"".concat($,"-arrow")}))},[_,er,e,ec,$]),eR=m.createElement("div",(0,r.Z)({role:"menuitem",style:eC,className:"".concat($,"-title"),tabIndex:ee?null:-1,ref:en,title:"string"==typeof c?c:null,"data-menu-id":F&&T?null:T,"aria-expanded":ec,"aria-haspopup":!0,"aria-controls":ew,"aria-disabled":ee,onClick:function(e){ee||(null==I||I({key:d,domEvent:e}),"inline"===_&&q(d,!ea))},onFocus:function(){X(d)}},ep),c,ek),eS=m.useRef(_);if("inline"!==_&&J.length>1?eS.current="vertical":eS.current=_,!F){var eI=eS.current;eR=m.createElement(eM,{mode:eI,prefixCls:$,visible:!v&&ec&&"inline"!==_,popupClassName:Z,popupOffset:C,popupStyle:k,popup:m.createElement(w,{mode:"horizontal"===eI?"vertical":eI},m.createElement(eb,{id:ew,ref:et},b)),disabled:ee,onVisibleChange:function(e){"inline"!==_&&q(d,e)}},eR)}var eK=m.createElement(f.Z.Item,(0,r.Z)({ref:n,role:"none"},O,{component:"li",style:t,className:s()($,"".concat($,"-").concat(_),l,(0,o.Z)((0,o.Z)((0,o.Z)((0,o.Z)({},"".concat($,"-open"),ec),"".concat($,"-active"),eZ),"".concat($,"-selected"),es),"".concat($,"-disabled"),ee)),onMouseEnter:function(e){eg(!0),null==R||R({key:d,domEvent:e})},onMouseLeave:function(e){eg(!1),null==S||S({key:d,domEvent:e})}}),eR,!F&&m.createElement(ex,{id:ew,open:ec,keyPath:J},b));return Q&&(eK=Q(eK,e,{selected:es,active:eZ,open:ec,disabled:ee})),m.createElement(w,{onItemClick:eE,mode:"horizontal"===_?"vertical":_,itemIcon:null!=y?y:W,expandIcon:er},eK)}),eI=m.forwardRef(function(e,n){var t,o=e.eventKey,i=e.children,l=x(o),a=eh(i,l),u=M();return m.useEffect(function(){if(u)return u.registerPath(o,l),function(){u.unregisterPath(o,l)}},[l]),t=u?a:m.createElement(eS,(0,r.Z)({ref:n},e),a),m.createElement(R.Provider,{value:l},t)}),eK=t(41154);function eA(e){var n=e.className,t=e.style,r=m.useContext(E).prefixCls;return M()?null:m.createElement("li",{role:"separator",className:s()("".concat(r,"-item-divider"),n),style:t})}var eO=["className","title","eventKey","children"],eT=m.forwardRef(function(e,n){var t=e.className,o=e.title,i=(e.eventKey,e.children),l=(0,u.Z)(e,eO),a=m.useContext(E).prefixCls,c="".concat(a,"-item-group");return m.createElement("li",(0,r.Z)({ref:n,role:"presentation"},l,{onClick:function(e){return e.stopPropagation()},className:s()(c,t)}),m.createElement("div",{role:"presentation",className:"".concat(c,"-title"),title:"string"==typeof o?o:void 0},o),m.createElement("ul",{role:"group",className:"".concat(c,"-list")},i))}),eL=m.forwardRef(function(e,n){var t=e.eventKey,o=eh(e.children,x(t));return M()?o:m.createElement(eT,(0,r.Z)({ref:n},(0,et.Z)(e,["warnKey"])),o)}),eD=["label","children","key","type","extra"];function e_(e,n,t,o,l){var a=e,c=(0,i.Z)({divider:eA,item:ev,group:eL,submenu:eI},o);return n&&(a=function e(n,t,o){var i=t.item,l=t.group,a=t.submenu,c=t.divider;return(n||[]).map(function(n,s){if(n&&"object"===(0,eK.Z)(n)){var f=n.label,d=n.children,p=n.key,v=n.type,b=n.extra,y=(0,u.Z)(n,eD),h=null!=p?p:"tmp-".concat(s);return d||"group"===v?"group"===v?m.createElement(l,(0,r.Z)({key:h},y,{title:f}),e(d,t,o)):m.createElement(a,(0,r.Z)({key:h},y,{title:f}),e(d,t,o)):"divider"===v?m.createElement(c,(0,r.Z)({key:h},y)):m.createElement(i,(0,r.Z)({key:h},y,{extra:b}),f,(!!b||0===b)&&m.createElement("span",{className:"".concat(o,"-item-extra")},b))}return null}).filter(function(e){return e})}(n,c,l)),eh(a,t)}var eV=["prefixCls","rootClassName","style","className","tabIndex","items","children","direction","id","mode","inlineCollapsed","disabled","disabledOverflow","subMenuOpenDelay","subMenuCloseDelay","forceSubMenuRender","defaultOpenKeys","openKeys","activeKey","defaultActiveFirst","selectable","multiple","defaultSelectedKeys","selectedKeys","onSelect","onDeselect","inlineIndent","motion","defaultMotions","triggerSubMenuAction","builtinPlacements","itemIcon","expandIcon","overflowedIndicator","overflowedIndicatorPopupClassName","getPopupContainer","onClick","onOpenChange","onKeyDown","openAnimation","openTransitionName","_internalRenderMenuItem","_internalRenderSubMenuItem","_internalComponents"],ez=[],eF=m.forwardRef(function(e,n){var t,c,v,h,g,Z,C,E,M,R,x,S,I,K,J,$,ee,en,et,er,eo,ei,el,ea,ec,es,ef=e.prefixCls,ed=void 0===ef?"rc-menu":ef,ep=e.rootClassName,em=e.style,eb=e.className,ey=e.tabIndex,eh=e.items,eg=e.children,eZ=e.direction,eC=e.id,eE=e.mode,ew=void 0===eE?"vertical":eE,ek=e.inlineCollapsed,eM=e.disabled,eR=e.disabledOverflow,ex=e.subMenuOpenDelay,eN=e.subMenuCloseDelay,eP=e.forceSubMenuRender,eS=e.defaultOpenKeys,eK=e.openKeys,eA=e.activeKey,eO=e.defaultActiveFirst,eT=e.selectable,eL=void 0===eT||eT,eD=e.multiple,eF=void 0!==eD&&eD,ej=e.defaultSelectedKeys,eB=e.selectedKeys,eW=e.onSelect,eH=e.onDeselect,eY=e.inlineIndent,eq=e.motion,eX=e.defaultMotions,eG=e.triggerSubMenuAction,eQ=e.builtinPlacements,eU=e.itemIcon,eJ=e.expandIcon,e$=e.overflowedIndicator,e0=void 0===e$?"...":e$,e1=e.overflowedIndicatorPopupClassName,e6=e.getPopupContainer,e2=e.onClick,e5=e.onOpenChange,e9=e.onKeyDown,e3=(e.openAnimation,e.openTransitionName,e._internalRenderMenuItem),e4=e._internalRenderSubMenuItem,e7=e._internalComponents,e8=(0,u.Z)(e,eV),ne=m.useMemo(function(){return[e_(eg,eh,ez,e7,ed),e_(eg,eh,ez,{},ed)]},[eg,eh,e7]),nn=(0,a.Z)(ne,2),nt=nn[0],nr=nn[1],no=m.useState(!1),ni=(0,a.Z)(no,2),nl=ni[0],na=ni[1],nu=m.useRef(),nc=(t=(0,d.Z)(eC,{value:eC}),v=(c=(0,a.Z)(t,2))[0],h=c[1],m.useEffect(function(){U+=1;var e="".concat(Q,"-").concat(U);h("rc-menu-uuid-".concat(e))},[]),v),ns="rtl"===eZ,nf=(0,d.Z)(eS,{value:eK,postState:function(e){return e||ez}}),nd=(0,a.Z)(nf,2),np=nd[0],nv=nd[1],nm=function(e){var n=arguments.length>1&&void 0!==arguments[1]&&arguments[1];function t(){nv(e),null==e5||e5(e)}n?(0,b.flushSync)(t):t()},nb=m.useState(np),ny=(0,a.Z)(nb,2),nh=ny[0],ng=ny[1],nZ=m.useRef(!1),nC=m.useMemo(function(){return("inline"===ew||"vertical"===ew)&&ek?["vertical",ek]:[ew,!1]},[ew,ek]),nE=(0,a.Z)(nC,2),nw=nE[0],nk=nE[1],nM="inline"===nw,nR=m.useState(nw),nx=(0,a.Z)(nR,2),nN=nx[0],nP=nx[1],nS=m.useState(nk),nI=(0,a.Z)(nS,2),nK=nI[0],nA=nI[1];m.useEffect(function(){nP(nw),nA(nk),nZ.current&&(nM?nv(nh):nm(ez))},[nw,nk]);var nO=m.useState(0),nT=(0,a.Z)(nO,2),nL=nT[0],nD=nT[1],n_=nL>=nt.length-1||"horizontal"!==nN||eR;m.useEffect(function(){nM&&ng(np)},[np]),m.useEffect(function(){return nZ.current=!0,function(){nZ.current=!1}},[]);var nV=(g=m.useState({}),Z=(0,a.Z)(g,2)[1],C=(0,m.useRef)(new Map),E=(0,m.useRef)(new Map),M=m.useState([]),x=(R=(0,a.Z)(M,2))[0],S=R[1],I=(0,m.useRef)(0),K=(0,m.useRef)(!1),J=function(){K.current||Z({})},$=(0,m.useCallback)(function(e,n){var t,r=q(n);E.current.set(r,e),C.current.set(e,r),I.current+=1;var o=I.current;t=function(){o===I.current&&J()},Promise.resolve().then(t)},[]),ee=(0,m.useCallback)(function(e,n){var t=q(n);E.current.delete(t),C.current.delete(e)},[]),en=(0,m.useCallback)(function(e){S(e)},[]),et=(0,m.useCallback)(function(e,n){var t=(C.current.get(e)||"").split(Y);return n&&x.includes(t[0])&&t.unshift(X),t},[x]),er=(0,m.useCallback)(function(e,n){return e.filter(function(e){return void 0!==e}).some(function(e){return et(e,!0).includes(n)})},[et]),eo=(0,m.useCallback)(function(e){var n="".concat(C.current.get(e)).concat(Y),t=new Set;return(0,l.Z)(E.current.keys()).forEach(function(e){e.startsWith(n)&&t.add(E.current.get(e))}),t},[]),m.useEffect(function(){return function(){K.current=!0}},[]),{registerPath:$,unregisterPath:ee,refreshOverflowKeys:en,isSubPathKey:er,getKeyPath:et,getKeys:function(){var e=(0,l.Z)(C.current.keys());return x.length&&e.push(X),e},getSubPathKeys:eo}),nz=nV.registerPath,nF=nV.unregisterPath,nj=nV.refreshOverflowKeys,nB=nV.isSubPathKey,nW=nV.getKeyPath,nH=nV.getKeys,nY=nV.getSubPathKeys,nq=m.useMemo(function(){return{registerPath:nz,unregisterPath:nF}},[nz,nF]),nX=m.useMemo(function(){return{isSubPathKey:nB}},[nB]);m.useEffect(function(){nj(n_?ez:nt.slice(nL+1).map(function(e){return e.key}))},[nL,n_]);var nG=(0,d.Z)(eA||eO&&(null===(es=nt[0])||void 0===es?void 0:es.key),{value:eA}),nQ=(0,a.Z)(nG,2),nU=nQ[0],nJ=nQ[1],n$=G(function(e){nJ(e)}),n0=G(function(){nJ(void 0)});(0,m.useImperativeHandle)(n,function(){return{list:nu.current,focus:function(e){var n,t,r=H(nH(),nc),o=r.elements,i=r.key2element,l=r.element2key,a=B(nu.current,o),u=null!=nU?nU:a[0]?l.get(a[0]):null===(n=nt.find(function(e){return!e.props.disabled}))||void 0===n?void 0:n.key,c=i.get(u);u&&c&&(null==c||null===(t=c.focus)||void 0===t||t.call(c,e))}}});var n1=(0,d.Z)(ej||[],{value:eB,postState:function(e){return Array.isArray(e)?e:null==e?ez:[e]}}),n6=(0,a.Z)(n1,2),n2=n6[0],n5=n6[1],n9=function(e){if(eL){var n,t=e.key,r=n2.includes(t);n5(n=eF?r?n2.filter(function(e){return e!==t}):[].concat((0,l.Z)(n2),[t]):[t]);var o=(0,i.Z)((0,i.Z)({},e),{},{selectedKeys:n});r?null==eH||eH(o):null==eW||eW(o)}!eF&&np.length&&"inline"!==nN&&nm(ez)},n3=G(function(e){null==e2||e2(eu(e)),n9(e)}),n4=G(function(e,n){var t=np.filter(function(n){return n!==e});if(n)t.push(e);else if("inline"!==nN){var r=nY(e);t=t.filter(function(e){return!r.has(e)})}(0,p.Z)(np,t,!0)||nm(t,!0)}),n7=(ei=function(e,n){var t=null!=n?n:!np.includes(e);n4(e,t)},el=m.useRef(),(ea=m.useRef()).current=nU,ec=function(){A.Z.cancel(el.current)},m.useEffect(function(){return function(){ec()}},[]),function(e){var n=e.which;if([].concat(j,[_,V,z,F]).includes(n)){var t=nH(),r=H(t,nc),i=r,l=i.elements,a=i.key2element,u=i.element2key,c=function(e,n){for(var t=e||document.activeElement;t;){if(n.has(t))return t;t=t.parentElement}return null}(a.get(nU),l),s=u.get(c),f=function(e,n,t,r){var i,l="prev",a="next",u="children",c="parent";if("inline"===e&&r===_)return{inlineTrigger:!0};var s=(0,o.Z)((0,o.Z)({},L,l),D,a),f=(0,o.Z)((0,o.Z)((0,o.Z)((0,o.Z)({},O,t?a:l),T,t?l:a),D,u),_,u),d=(0,o.Z)((0,o.Z)((0,o.Z)((0,o.Z)((0,o.Z)((0,o.Z)({},L,l),D,a),_,u),V,c),O,t?u:c),T,t?c:u);switch(null===(i=({inline:s,horizontal:f,vertical:d,inlineSub:s,horizontalSub:d,verticalSub:d})["".concat(e).concat(n?"":"Sub")])||void 0===i?void 0:i[r]){case l:return{offset:-1,sibling:!0};case a:return{offset:1,sibling:!0};case c:return{offset:-1,sibling:!1};case u:return{offset:1,sibling:!1};default:return null}}(nN,1===nW(s,!0).length,ns,n);if(!f&&n!==z&&n!==F)return;(j.includes(n)||[z,F].includes(n))&&e.preventDefault();var d=function(e){if(e){var n=e,t=e.querySelector("a");null!=t&&t.getAttribute("href")&&(n=t);var r=u.get(e);nJ(r),ec(),el.current=(0,A.Z)(function(){ea.current===r&&n.focus()})}};if([z,F].includes(n)||f.sibling||!c){var p,v=B(p=c&&"inline"!==nN?function(e){for(var n=e;n;){if(n.getAttribute("data-menu-list"))return n;n=n.parentElement}return null}(c):nu.current,l);d(n===z?v[0]:n===F?v[v.length-1]:W(p,l,c,f.offset))}else if(f.inlineTrigger)ei(s);else if(f.offset>0)ei(s,!0),ec(),el.current=(0,A.Z)(function(){r=H(t,nc);var e=c.getAttribute("aria-controls");d(W(document.getElementById(e),r.elements))},5);else if(f.offset<0){var m=nW(s,!0),b=m[m.length-2],y=a.get(b);ei(b,!1),d(y)}}null==e9||e9(e)});m.useEffect(function(){na(!0)},[]);var n8=m.useMemo(function(){return{_internalRenderMenuItem:e3,_internalRenderSubMenuItem:e4}},[e3,e4]),te="horizontal"!==nN||eR?nt:nt.map(function(e,n){return m.createElement(w,{key:e.key,overflowDisabled:n>nL},e)}),tn=m.createElement(f.Z,(0,r.Z)({id:eC,ref:nu,prefixCls:"".concat(ed,"-overflow"),component:"ul",itemComponent:ev,className:s()(ed,"".concat(ed,"-root"),"".concat(ed,"-").concat(nN),eb,(0,o.Z)((0,o.Z)({},"".concat(ed,"-inline-collapsed"),nK),"".concat(ed,"-rtl"),ns),ep),dir:eZ,style:em,role:"menu",tabIndex:void 0===ey?0:ey,data:te,renderRawItem:function(e){return e},renderRawRest:function(e){var n=e.length,t=n?nt.slice(-n):null;return m.createElement(eI,{eventKey:X,title:e0,disabled:n_,internalPopupClose:0===n,popupClassName:e1},t)},maxCount:"horizontal"!==nN||eR?f.Z.INVALIDATE:f.Z.RESPONSIVE,ssr:"full","data-menu-list":!0,onVisibleChange:function(e){nD(e)},onKeyDown:n7},e8));return m.createElement(P.Provider,{value:n8},m.createElement(y.Provider,{value:nc},m.createElement(w,{prefixCls:ed,rootClassName:ep,mode:nN,openKeys:np,rtl:ns,disabled:eM,motion:nl?eq:null,defaultMotions:nl?eX:null,activeKey:nU,onActive:n$,onInactive:n0,selectedKeys:n2,inlineIndent:void 0===eY?24:eY,subMenuOpenDelay:void 0===ex?.1:ex,subMenuCloseDelay:void 0===eN?.1:eN,forceSubMenuRender:eP,builtinPlacements:eQ,triggerSubMenuAction:void 0===eG?"hover":eG,getPopupContainer:e6,itemIcon:eU,expandIcon:eJ,onItemClick:n3,onOpenChange:n4},m.createElement(N.Provider,{value:nX},tn),m.createElement("div",{style:{display:"none"},"aria-hidden":!0},m.createElement(k.Provider,{value:nq},nr)))))});eF.Item=ev,eF.SubMenu=eI,eF.ItemGroup=eL,eF.Divider=eA;var ej=eF}}]); \ No newline at end of file diff --git a/litellm/proxy/_experimental/out/_next/static/chunks/1674-475a971a192714f2.js b/litellm/proxy/_experimental/out/_next/static/chunks/1674-de8248fbd0c554ba.js similarity index 99% rename from litellm/proxy/_experimental/out/_next/static/chunks/1674-475a971a192714f2.js rename to litellm/proxy/_experimental/out/_next/static/chunks/1674-de8248fbd0c554ba.js index cd70c451170..e8fa6e80e42 100644 --- a/litellm/proxy/_experimental/out/_next/static/chunks/1674-475a971a192714f2.js +++ b/litellm/proxy/_experimental/out/_next/static/chunks/1674-de8248fbd0c554ba.js @@ -1 +1 @@ -(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[1674],{14042:function(e,t,n){"use strict";n.d(t,{Z:function(){return eM}});var r=n(5853),o=n(7084),i=n(26898),a=n(13241),c=n(1153),l=n(2265),s=n(60474),u=n(47625),p=n(93765),f=n(86757),d=n.n(f),y=n(61994),m=n(9841),v=n(81889),h=n(82944),b=["points","className","baseLinePoints","connectNulls"];function g(){return(g=Object.assign?Object.assign.bind():function(e){for(var t=1;te.length)&&(t=e.length);for(var n=0,r=Array(t);n0&&void 0!==arguments[0]?arguments[0]:[],t=[[]];return e.forEach(function(e){k(e)?t[t.length-1].push(e):t[t.length-1].length>0&&t.push([])}),k(e[0])&&t[t.length-1].push(e[0]),t[t.length-1].length<=0&&(t=t.slice(0,-1)),t},j=function(e,t){var n=x(e);t&&(n=[n.reduce(function(e,t){return[].concat(A(e),A(t))},[])]);var r=n.map(function(e){return e.reduce(function(e,t,n){return"".concat(e).concat(0===n?"M":"L").concat(t.x,",").concat(t.y)},"")}).join("");return 1===n.length?"".concat(r,"Z"):r},w=function(e,t,n){var r=j(e,n);return"".concat("Z"===r.slice(-1)?r.slice(0,-1):r,"L").concat(j(t.reverse(),n).slice(1))},P=function(e){var t=e.points,n=e.className,r=e.baseLinePoints,o=e.connectNulls,i=function(e,t){if(null==e)return{};var n,r,o=function(e,t){if(null==e)return{};var n={};for(var r in e)if(Object.prototype.hasOwnProperty.call(e,r)){if(t.indexOf(r)>=0)continue;n[r]=e[r]}return n}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(r=0;r=0)&&Object.prototype.propertyIsEnumerable.call(e,n)&&(o[n]=e[n])}return o}(e,b);if(!t||!t.length)return null;var a=(0,y.Z)("recharts-polygon",n);if(r&&r.length){var c=i.stroke&&"none"!==i.stroke,s=w(t,r,o);return l.createElement("g",{className:a},l.createElement("path",g({},(0,h.L6)(i,!0),{fill:"Z"===s.slice(-1)?i.fill:"none",stroke:"none",d:s})),c?l.createElement("path",g({},(0,h.L6)(i,!0),{fill:"none",d:j(t,o)})):null,c?l.createElement("path",g({},(0,h.L6)(i,!0),{fill:"none",d:j(r,o)})):null)}var u=j(t,o);return l.createElement("path",g({},(0,h.L6)(i,!0),{fill:"Z"===u.slice(-1)?i.fill:"none",className:a,d:u}))},E=n(58811),S=n(41637),T=n(39206);function L(e){return(L="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function R(){return(R=Object.assign?Object.assign.bind():function(e){for(var t=1;t1e-5?"outer"===t?"start":"end":n<-.00001?"outer"===t?"end":"start":"middle"}},{key:"renderAxisLine",value:function(){var e=this.props,t=e.cx,n=e.cy,r=e.radius,o=e.axisLine,i=e.axisLineType,a=I(I({},(0,h.L6)(this.props,!1)),{},{fill:"none"},(0,h.L6)(o,!1));if("circle"===i)return l.createElement(v.o,R({className:"recharts-polar-angle-axis-line"},a,{cx:t,cy:n,r:r}));var c=this.props.ticks.map(function(e){return(0,T.op)(t,n,r,e.coordinate)});return l.createElement(P,R({className:"recharts-polar-angle-axis-line"},a,{points:c}))}},{key:"renderTicks",value:function(){var e=this,t=this.props,n=t.ticks,o=t.tick,i=t.tickLine,a=t.tickFormatter,c=t.stroke,s=(0,h.L6)(this.props,!1),u=(0,h.L6)(o,!1),p=I(I({},s),{},{fill:"none"},(0,h.L6)(i,!1)),f=n.map(function(t,n){var f=e.getTickLineCoord(t),d=I(I(I({textAnchor:e.getTickTextAnchor(t)},s),{},{stroke:"none",fill:c},u),{},{index:n,payload:t,x:f.x2,y:f.y2});return l.createElement(m.m,R({className:(0,y.Z)("recharts-polar-angle-axis-tick",(0,T.$S)(o)),key:"tick-".concat(t.coordinate)},(0,S.bw)(e.props,t,n)),i&&l.createElement("line",R({className:"recharts-polar-angle-axis-tick-line"},p,f)),o&&r.renderTickItem(o,d,a?a(t.value,n):t.value))});return l.createElement(m.m,{className:"recharts-polar-angle-axis-ticks"},f)}},{key:"render",value:function(){var e=this.props,t=e.ticks,n=e.radius,r=e.axisLine;return!(n<=0)&&t&&t.length?l.createElement(m.m,{className:(0,y.Z)("recharts-polar-angle-axis",this.props.className)},r&&this.renderAxisLine(),this.renderTicks()):null}}],n=[{key:"renderTickItem",value:function(e,t,n){return l.isValidElement(e)?l.cloneElement(e,t):d()(e)?e(t):l.createElement(E.x,R({},t,{className:"recharts-polar-angle-axis-tick-value"}),n)}}],t&&C(r.prototype,t),n&&C(r,n),Object.defineProperty(r,"prototype",{writable:!1}),r}(l.PureComponent);Z(M,"displayName","PolarAngleAxis"),Z(M,"axisType","angleAxis"),Z(M,"defaultProps",{type:"category",angleAxisId:0,scale:"auto",cx:0,cy:0,orientation:"outer",axisLine:!0,tickLine:!0,tickSize:8,tick:!0,hide:!1,allowDuplicatedCategory:!0});var V=n(35802),$=n.n(V),q=n(37891),z=n.n(q),G=n(26680),W=["cx","cy","angle","ticks","axisLine"],Y=["ticks","tick","angle","tickFormatter","stroke"];function H(e){return(H="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function U(){return(U=Object.assign?Object.assign.bind():function(e){for(var t=1;t=0)continue;n[r]=e[r]}return n}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(r=0;r=0)&&Object.prototype.propertyIsEnumerable.call(e,n)&&(o[n]=e[n])}return o}function ee(e,t){for(var n=0;n0?es()(e,"paddingAngle",0):0;if(n){var c=(0,eb.k4)(n.endAngle-n.startAngle,e.endAngle-e.startAngle),l=ew(ew({},e),{},{startAngle:i+a,endAngle:i+c(r)+a});o.push(l),i=l.endAngle}else{var s=e.endAngle,p=e.startAngle,f=(0,eb.k4)(0,s-p)(r),d=ew(ew({},e),{},{startAngle:i+a,endAngle:i+f+a});o.push(d),i=d.endAngle}}),l.createElement(m.m,null,e.renderSectorsStatically(o))})}},{key:"attachKeyboardHandlers",value:function(e){var t=this;e.onkeydown=function(e){if(!e.altKey)switch(e.key){case"ArrowLeft":var n=++t.state.sectorToFocus%t.sectorRefs.length;t.sectorRefs[n].focus(),t.setState({sectorToFocus:n});break;case"ArrowRight":var r=--t.state.sectorToFocus<0?t.sectorRefs.length-1:t.state.sectorToFocus%t.sectorRefs.length;t.sectorRefs[r].focus(),t.setState({sectorToFocus:r});break;case"Escape":t.sectorRefs[t.state.sectorToFocus].blur(),t.setState({sectorToFocus:0})}}}},{key:"renderSectors",value:function(){var e=this.props,t=e.sectors,n=e.isAnimationActive,r=this.state.prevSectors;return n&&t&&t.length&&(!r||!ep()(r,t))?this.renderSectorsWithAnimation():this.renderSectorsStatically(t)}},{key:"componentDidMount",value:function(){this.pieRef&&this.attachKeyboardHandlers(this.pieRef)}},{key:"render",value:function(){var e=this,t=this.props,n=t.hide,r=t.sectors,o=t.className,i=t.label,a=t.cx,c=t.cy,s=t.innerRadius,u=t.outerRadius,p=t.isAnimationActive,f=this.state.isAnimationFinished;if(n||!r||!r.length||!(0,eb.hj)(a)||!(0,eb.hj)(c)||!(0,eb.hj)(s)||!(0,eb.hj)(u))return null;var d=(0,y.Z)("recharts-pie",o);return l.createElement(m.m,{tabIndex:this.props.rootTabIndex,className:d,ref:function(t){e.pieRef=t}},this.renderSectors(),i&&this.renderLabels(r),G._.renderCallByParent(this.props,null,!1),(!p||f)&&em.e.renderCallByParent(this.props,r,!1))}}],n=[{key:"getDerivedStateFromProps",value:function(e,t){return t.prevIsAnimationActive!==e.isAnimationActive?{prevIsAnimationActive:e.isAnimationActive,prevAnimationId:e.animationId,curSectors:e.sectors,prevSectors:[],isAnimationFinished:!0}:e.isAnimationActive&&e.animationId!==t.prevAnimationId?{prevAnimationId:e.animationId,curSectors:e.sectors,prevSectors:t.curSectors,isAnimationFinished:!0}:e.sectors!==t.curSectors?{curSectors:e.sectors,isAnimationFinished:!0}:null}},{key:"getTextAnchor",value:function(e,t){return e>t?"start":e=360?A:A-1)*u,k=a.reduce(function(e,t){var n=(0,eg.F$)(t,g,0);return e+((0,eb.hj)(n)?n:0)},0);return k>0&&(t=a.map(function(e,t){var r,o=(0,eg.F$)(e,g,0),i=(0,eg.F$)(e,f,t),a=((0,eb.hj)(o)?o:0)/k,s=(r=t?n.endAngle+(0,eb.uY)(h)*u*(0!==o?1:0):l)+(0,eb.uY)(h)*((0!==o?m:0)+a*O),p=(r+s)/2,d=(v.innerRadius+v.outerRadius)/2,b=[{name:i,value:o,payload:e,dataKey:g,type:y}],A=(0,T.op)(v.cx,v.cy,d,p);return n=ew(ew(ew({percent:a,cornerRadius:c,name:i,tooltipPayload:b,midAngle:p,middleRadius:d,tooltipPosition:A},e),v),{},{value:(0,eg.F$)(e,g),startAngle:r,endAngle:s,payload:e,paddingAngle:(0,eb.uY)(h)*u})})),ew(ew({},v),{},{sectors:t,data:a})});var eI=(0,p.z)({chartName:"PieChart",GraphicalChild:eN,validateTooltipEventTypes:["item"],defaultTooltipEventType:"item",legendContent:"children",axisComponents:[{axisType:"angleAxis",AxisComp:M},{axisType:"radiusAxis",AxisComp:ea}],formatAxisMap:T.t9,defaultProps:{layout:"centric",startAngle:0,endAngle:360,cx:"50%",cy:"50%",innerRadius:0,outerRadius:"80%"}}),eC=n(8147),eD=n(92666),eF=n(98593);let e_=e=>{let{active:t,payload:n,valueFormatter:r}=e;if(t&&(null==n?void 0:n[0])){let e=null==n?void 0:n[0];return l.createElement(eF.$B,null,l.createElement("div",{className:(0,a.q)("px-4 py-2")},l.createElement(eF.zX,{value:r(e.value),name:e.name,color:e.payload.color})))}return null},eZ=(e,t)=>e.map((e,n)=>{let r=ne||t((0,c.vP)(n.map(e=>e[r]))),eK=e=>{let{cx:t,cy:n,innerRadius:r,outerRadius:o,startAngle:i,endAngle:a,className:c}=e;return l.createElement("g",null,l.createElement(s.L,{cx:t,cy:n,innerRadius:r,outerRadius:o,startAngle:i,endAngle:a,className:c,fill:"",opacity:.3,style:{outline:"none"}}))},eM=l.forwardRef((e,t)=>{let{data:n=[],category:s="value",index:p="name",colors:f=i.s,variant:d="donut",valueFormatter:y=c.Cj,label:m,showLabel:v=!0,animationDuration:h=900,showAnimation:b=!1,showTooltip:g=!0,noDataText:A,onValueChange:O,customTooltip:k,className:x}=e,j=(0,r._T)(e,["data","category","index","colors","variant","valueFormatter","label","showLabel","animationDuration","showAnimation","showTooltip","noDataText","onValueChange","customTooltip","className"]),w="donut"==d,P=eB(m,y,n,s),[E,S]=l.useState(void 0),T=!!O;return(0,l.useEffect)(()=>{let e=document.querySelectorAll(".recharts-pie-sector");e&&e.forEach(e=>{e.setAttribute("style","outline: none")})},[E]),l.createElement("div",Object.assign({ref:t,className:(0,a.q)("w-full h-40",x)},j),l.createElement(u.h,{className:"h-full w-full"},(null==n?void 0:n.length)?l.createElement(eI,{onClick:T&&E?()=>{S(void 0),null==O||O(null)}:void 0,margin:{top:0,left:0,right:0,bottom:0}},v&&w?l.createElement("text",{className:(0,a.q)("fill-tremor-content-emphasis","dark:fill-dark-tremor-content-emphasis"),x:"50%",y:"50%",textAnchor:"middle",dominantBaseline:"middle"},P):null,l.createElement(eN,{className:(0,a.q)("stroke-tremor-background dark:stroke-dark-tremor-background",O?"cursor-pointer":"cursor-default"),data:eZ(n,f),cx:"50%",cy:"50%",startAngle:90,endAngle:-270,innerRadius:w?"75%":"0%",outerRadius:"100%",stroke:"",strokeLinejoin:"round",dataKey:s,nameKey:p,isAnimationActive:b,animationDuration:h,onClick:function(e,t,n){n.stopPropagation(),T&&(E===t?(S(void 0),null==O||O(null)):(S(t),null==O||O(Object.assign({eventType:"slice"},e.payload.payload))))},activeIndex:E,inactiveShape:eK,style:{outline:"none"}}),l.createElement(eC.u,{wrapperStyle:{outline:"none"},isAnimationActive:!1,content:g?e=>{var t;let{active:n,payload:r}=e;return k?l.createElement(k,{payload:null==r?void 0:r.map(e=>{var t,n,i;return Object.assign(Object.assign({},e),{color:null!==(i=null===(n=null===(t=null==r?void 0:r[0])||void 0===t?void 0:t.payload)||void 0===n?void 0:n.color)&&void 0!==i?i:o.fr.Gray})}),active:n,label:null===(t=null==r?void 0:r[0])||void 0===t?void 0:t.name}):l.createElement(e_,{active:n,payload:r,valueFormatter:y})}:l.createElement(l.Fragment,null)})):l.createElement(eD.Z,{noDataText:A})))});eM.displayName="DonutChart"},35802:function(e,t,n){var r=n(67646),o=n(58905),i=n(88157);e.exports=function(e,t){return e&&e.length?r(e,i(t,2),o):void 0}},37891:function(e,t,n){var r=n(67646),o=n(88157),i=n(20121);e.exports=function(e,t){return e&&e.length?r(e,o(t,2),i):void 0}},44633:function(e,t,n){"use strict";var r=n(2265);let o=r.forwardRef(function(e,t){return r.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:2,stroke:"currentColor","aria-hidden":"true",ref:t},e),r.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M5 15l7-7 7 7"}))});t.Z=o},58710:function(e,t,n){"use strict";var r=n(2265);let o=r.forwardRef(function(e,t){return r.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:2,stroke:"currentColor","aria-hidden":"true",ref:t},e),r.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"}))});t.Z=o},92668:function(e,t,n){"use strict";n.d(t,{I:function(){return c}});var r=n(59121),o=n(31091),i=n(63497),a=n(99649);function c(e,t){let{years:n=0,months:c=0,weeks:l=0,days:s=0,hours:u=0,minutes:p=0,seconds:f=0}=t,d=(0,a.Q)(e),y=c||n?(0,o.z)(d,c+12*n):d,m=s||l?(0,r.E)(y,s+7*l):y;return(0,i.L)(e,m.getTime()+1e3*(f+60*(p+60*u)))}}}]); \ No newline at end of file +(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[1674],{14042:function(e,t,n){"use strict";n.d(t,{Z:function(){return eM}});var r=n(5853),o=n(7084),i=n(26898),a=n(13241),c=n(1153),l=n(2265),s=n(60474),u=n(47625),p=n(93765),f=n(86757),d=n.n(f),y=n(87602),m=n(9841),v=n(81889),h=n(82944),b=["points","className","baseLinePoints","connectNulls"];function g(){return(g=Object.assign?Object.assign.bind():function(e){for(var t=1;te.length)&&(t=e.length);for(var n=0,r=Array(t);n0&&void 0!==arguments[0]?arguments[0]:[],t=[[]];return e.forEach(function(e){k(e)?t[t.length-1].push(e):t[t.length-1].length>0&&t.push([])}),k(e[0])&&t[t.length-1].push(e[0]),t[t.length-1].length<=0&&(t=t.slice(0,-1)),t},j=function(e,t){var n=x(e);t&&(n=[n.reduce(function(e,t){return[].concat(A(e),A(t))},[])]);var r=n.map(function(e){return e.reduce(function(e,t,n){return"".concat(e).concat(0===n?"M":"L").concat(t.x,",").concat(t.y)},"")}).join("");return 1===n.length?"".concat(r,"Z"):r},w=function(e,t,n){var r=j(e,n);return"".concat("Z"===r.slice(-1)?r.slice(0,-1):r,"L").concat(j(t.reverse(),n).slice(1))},P=function(e){var t=e.points,n=e.className,r=e.baseLinePoints,o=e.connectNulls,i=function(e,t){if(null==e)return{};var n,r,o=function(e,t){if(null==e)return{};var n={};for(var r in e)if(Object.prototype.hasOwnProperty.call(e,r)){if(t.indexOf(r)>=0)continue;n[r]=e[r]}return n}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(r=0;r=0)&&Object.prototype.propertyIsEnumerable.call(e,n)&&(o[n]=e[n])}return o}(e,b);if(!t||!t.length)return null;var a=(0,y.Z)("recharts-polygon",n);if(r&&r.length){var c=i.stroke&&"none"!==i.stroke,s=w(t,r,o);return l.createElement("g",{className:a},l.createElement("path",g({},(0,h.L6)(i,!0),{fill:"Z"===s.slice(-1)?i.fill:"none",stroke:"none",d:s})),c?l.createElement("path",g({},(0,h.L6)(i,!0),{fill:"none",d:j(t,o)})):null,c?l.createElement("path",g({},(0,h.L6)(i,!0),{fill:"none",d:j(r,o)})):null)}var u=j(t,o);return l.createElement("path",g({},(0,h.L6)(i,!0),{fill:"Z"===u.slice(-1)?i.fill:"none",className:a,d:u}))},E=n(58811),S=n(41637),T=n(39206);function L(e){return(L="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function R(){return(R=Object.assign?Object.assign.bind():function(e){for(var t=1;t1e-5?"outer"===t?"start":"end":n<-.00001?"outer"===t?"end":"start":"middle"}},{key:"renderAxisLine",value:function(){var e=this.props,t=e.cx,n=e.cy,r=e.radius,o=e.axisLine,i=e.axisLineType,a=I(I({},(0,h.L6)(this.props,!1)),{},{fill:"none"},(0,h.L6)(o,!1));if("circle"===i)return l.createElement(v.o,R({className:"recharts-polar-angle-axis-line"},a,{cx:t,cy:n,r:r}));var c=this.props.ticks.map(function(e){return(0,T.op)(t,n,r,e.coordinate)});return l.createElement(P,R({className:"recharts-polar-angle-axis-line"},a,{points:c}))}},{key:"renderTicks",value:function(){var e=this,t=this.props,n=t.ticks,o=t.tick,i=t.tickLine,a=t.tickFormatter,c=t.stroke,s=(0,h.L6)(this.props,!1),u=(0,h.L6)(o,!1),p=I(I({},s),{},{fill:"none"},(0,h.L6)(i,!1)),f=n.map(function(t,n){var f=e.getTickLineCoord(t),d=I(I(I({textAnchor:e.getTickTextAnchor(t)},s),{},{stroke:"none",fill:c},u),{},{index:n,payload:t,x:f.x2,y:f.y2});return l.createElement(m.m,R({className:(0,y.Z)("recharts-polar-angle-axis-tick",(0,T.$S)(o)),key:"tick-".concat(t.coordinate)},(0,S.bw)(e.props,t,n)),i&&l.createElement("line",R({className:"recharts-polar-angle-axis-tick-line"},p,f)),o&&r.renderTickItem(o,d,a?a(t.value,n):t.value))});return l.createElement(m.m,{className:"recharts-polar-angle-axis-ticks"},f)}},{key:"render",value:function(){var e=this.props,t=e.ticks,n=e.radius,r=e.axisLine;return!(n<=0)&&t&&t.length?l.createElement(m.m,{className:(0,y.Z)("recharts-polar-angle-axis",this.props.className)},r&&this.renderAxisLine(),this.renderTicks()):null}}],n=[{key:"renderTickItem",value:function(e,t,n){return l.isValidElement(e)?l.cloneElement(e,t):d()(e)?e(t):l.createElement(E.x,R({},t,{className:"recharts-polar-angle-axis-tick-value"}),n)}}],t&&C(r.prototype,t),n&&C(r,n),Object.defineProperty(r,"prototype",{writable:!1}),r}(l.PureComponent);Z(M,"displayName","PolarAngleAxis"),Z(M,"axisType","angleAxis"),Z(M,"defaultProps",{type:"category",angleAxisId:0,scale:"auto",cx:0,cy:0,orientation:"outer",axisLine:!0,tickLine:!0,tickSize:8,tick:!0,hide:!1,allowDuplicatedCategory:!0});var V=n(35802),$=n.n(V),q=n(37891),z=n.n(q),G=n(26680),W=["cx","cy","angle","ticks","axisLine"],Y=["ticks","tick","angle","tickFormatter","stroke"];function H(e){return(H="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function U(){return(U=Object.assign?Object.assign.bind():function(e){for(var t=1;t=0)continue;n[r]=e[r]}return n}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(r=0;r=0)&&Object.prototype.propertyIsEnumerable.call(e,n)&&(o[n]=e[n])}return o}function ee(e,t){for(var n=0;n0?es()(e,"paddingAngle",0):0;if(n){var c=(0,eb.k4)(n.endAngle-n.startAngle,e.endAngle-e.startAngle),l=ew(ew({},e),{},{startAngle:i+a,endAngle:i+c(r)+a});o.push(l),i=l.endAngle}else{var s=e.endAngle,p=e.startAngle,f=(0,eb.k4)(0,s-p)(r),d=ew(ew({},e),{},{startAngle:i+a,endAngle:i+f+a});o.push(d),i=d.endAngle}}),l.createElement(m.m,null,e.renderSectorsStatically(o))})}},{key:"attachKeyboardHandlers",value:function(e){var t=this;e.onkeydown=function(e){if(!e.altKey)switch(e.key){case"ArrowLeft":var n=++t.state.sectorToFocus%t.sectorRefs.length;t.sectorRefs[n].focus(),t.setState({sectorToFocus:n});break;case"ArrowRight":var r=--t.state.sectorToFocus<0?t.sectorRefs.length-1:t.state.sectorToFocus%t.sectorRefs.length;t.sectorRefs[r].focus(),t.setState({sectorToFocus:r});break;case"Escape":t.sectorRefs[t.state.sectorToFocus].blur(),t.setState({sectorToFocus:0})}}}},{key:"renderSectors",value:function(){var e=this.props,t=e.sectors,n=e.isAnimationActive,r=this.state.prevSectors;return n&&t&&t.length&&(!r||!ep()(r,t))?this.renderSectorsWithAnimation():this.renderSectorsStatically(t)}},{key:"componentDidMount",value:function(){this.pieRef&&this.attachKeyboardHandlers(this.pieRef)}},{key:"render",value:function(){var e=this,t=this.props,n=t.hide,r=t.sectors,o=t.className,i=t.label,a=t.cx,c=t.cy,s=t.innerRadius,u=t.outerRadius,p=t.isAnimationActive,f=this.state.isAnimationFinished;if(n||!r||!r.length||!(0,eb.hj)(a)||!(0,eb.hj)(c)||!(0,eb.hj)(s)||!(0,eb.hj)(u))return null;var d=(0,y.Z)("recharts-pie",o);return l.createElement(m.m,{tabIndex:this.props.rootTabIndex,className:d,ref:function(t){e.pieRef=t}},this.renderSectors(),i&&this.renderLabels(r),G._.renderCallByParent(this.props,null,!1),(!p||f)&&em.e.renderCallByParent(this.props,r,!1))}}],n=[{key:"getDerivedStateFromProps",value:function(e,t){return t.prevIsAnimationActive!==e.isAnimationActive?{prevIsAnimationActive:e.isAnimationActive,prevAnimationId:e.animationId,curSectors:e.sectors,prevSectors:[],isAnimationFinished:!0}:e.isAnimationActive&&e.animationId!==t.prevAnimationId?{prevAnimationId:e.animationId,curSectors:e.sectors,prevSectors:t.curSectors,isAnimationFinished:!0}:e.sectors!==t.curSectors?{curSectors:e.sectors,isAnimationFinished:!0}:null}},{key:"getTextAnchor",value:function(e,t){return e>t?"start":e=360?A:A-1)*u,k=a.reduce(function(e,t){var n=(0,eg.F$)(t,g,0);return e+((0,eb.hj)(n)?n:0)},0);return k>0&&(t=a.map(function(e,t){var r,o=(0,eg.F$)(e,g,0),i=(0,eg.F$)(e,f,t),a=((0,eb.hj)(o)?o:0)/k,s=(r=t?n.endAngle+(0,eb.uY)(h)*u*(0!==o?1:0):l)+(0,eb.uY)(h)*((0!==o?m:0)+a*O),p=(r+s)/2,d=(v.innerRadius+v.outerRadius)/2,b=[{name:i,value:o,payload:e,dataKey:g,type:y}],A=(0,T.op)(v.cx,v.cy,d,p);return n=ew(ew(ew({percent:a,cornerRadius:c,name:i,tooltipPayload:b,midAngle:p,middleRadius:d,tooltipPosition:A},e),v),{},{value:(0,eg.F$)(e,g),startAngle:r,endAngle:s,payload:e,paddingAngle:(0,eb.uY)(h)*u})})),ew(ew({},v),{},{sectors:t,data:a})});var eI=(0,p.z)({chartName:"PieChart",GraphicalChild:eN,validateTooltipEventTypes:["item"],defaultTooltipEventType:"item",legendContent:"children",axisComponents:[{axisType:"angleAxis",AxisComp:M},{axisType:"radiusAxis",AxisComp:ea}],formatAxisMap:T.t9,defaultProps:{layout:"centric",startAngle:0,endAngle:360,cx:"50%",cy:"50%",innerRadius:0,outerRadius:"80%"}}),eC=n(8147),eD=n(92666),eF=n(98593);let e_=e=>{let{active:t,payload:n,valueFormatter:r}=e;if(t&&(null==n?void 0:n[0])){let e=null==n?void 0:n[0];return l.createElement(eF.$B,null,l.createElement("div",{className:(0,a.q)("px-4 py-2")},l.createElement(eF.zX,{value:r(e.value),name:e.name,color:e.payload.color})))}return null},eZ=(e,t)=>e.map((e,n)=>{let r=ne||t((0,c.vP)(n.map(e=>e[r]))),eK=e=>{let{cx:t,cy:n,innerRadius:r,outerRadius:o,startAngle:i,endAngle:a,className:c}=e;return l.createElement("g",null,l.createElement(s.L,{cx:t,cy:n,innerRadius:r,outerRadius:o,startAngle:i,endAngle:a,className:c,fill:"",opacity:.3,style:{outline:"none"}}))},eM=l.forwardRef((e,t)=>{let{data:n=[],category:s="value",index:p="name",colors:f=i.s,variant:d="donut",valueFormatter:y=c.Cj,label:m,showLabel:v=!0,animationDuration:h=900,showAnimation:b=!1,showTooltip:g=!0,noDataText:A,onValueChange:O,customTooltip:k,className:x}=e,j=(0,r._T)(e,["data","category","index","colors","variant","valueFormatter","label","showLabel","animationDuration","showAnimation","showTooltip","noDataText","onValueChange","customTooltip","className"]),w="donut"==d,P=eB(m,y,n,s),[E,S]=l.useState(void 0),T=!!O;return(0,l.useEffect)(()=>{let e=document.querySelectorAll(".recharts-pie-sector");e&&e.forEach(e=>{e.setAttribute("style","outline: none")})},[E]),l.createElement("div",Object.assign({ref:t,className:(0,a.q)("w-full h-40",x)},j),l.createElement(u.h,{className:"h-full w-full"},(null==n?void 0:n.length)?l.createElement(eI,{onClick:T&&E?()=>{S(void 0),null==O||O(null)}:void 0,margin:{top:0,left:0,right:0,bottom:0}},v&&w?l.createElement("text",{className:(0,a.q)("fill-tremor-content-emphasis","dark:fill-dark-tremor-content-emphasis"),x:"50%",y:"50%",textAnchor:"middle",dominantBaseline:"middle"},P):null,l.createElement(eN,{className:(0,a.q)("stroke-tremor-background dark:stroke-dark-tremor-background",O?"cursor-pointer":"cursor-default"),data:eZ(n,f),cx:"50%",cy:"50%",startAngle:90,endAngle:-270,innerRadius:w?"75%":"0%",outerRadius:"100%",stroke:"",strokeLinejoin:"round",dataKey:s,nameKey:p,isAnimationActive:b,animationDuration:h,onClick:function(e,t,n){n.stopPropagation(),T&&(E===t?(S(void 0),null==O||O(null)):(S(t),null==O||O(Object.assign({eventType:"slice"},e.payload.payload))))},activeIndex:E,inactiveShape:eK,style:{outline:"none"}}),l.createElement(eC.u,{wrapperStyle:{outline:"none"},isAnimationActive:!1,content:g?e=>{var t;let{active:n,payload:r}=e;return k?l.createElement(k,{payload:null==r?void 0:r.map(e=>{var t,n,i;return Object.assign(Object.assign({},e),{color:null!==(i=null===(n=null===(t=null==r?void 0:r[0])||void 0===t?void 0:t.payload)||void 0===n?void 0:n.color)&&void 0!==i?i:o.fr.Gray})}),active:n,label:null===(t=null==r?void 0:r[0])||void 0===t?void 0:t.name}):l.createElement(e_,{active:n,payload:r,valueFormatter:y})}:l.createElement(l.Fragment,null)})):l.createElement(eD.Z,{noDataText:A})))});eM.displayName="DonutChart"},35802:function(e,t,n){var r=n(67646),o=n(58905),i=n(88157);e.exports=function(e,t){return e&&e.length?r(e,i(t,2),o):void 0}},37891:function(e,t,n){var r=n(67646),o=n(88157),i=n(20121);e.exports=function(e,t){return e&&e.length?r(e,o(t,2),i):void 0}},44633:function(e,t,n){"use strict";var r=n(2265);let o=r.forwardRef(function(e,t){return r.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:2,stroke:"currentColor","aria-hidden":"true",ref:t},e),r.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M5 15l7-7 7 7"}))});t.Z=o},58710:function(e,t,n){"use strict";var r=n(2265);let o=r.forwardRef(function(e,t){return r.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:2,stroke:"currentColor","aria-hidden":"true",ref:t},e),r.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"}))});t.Z=o},92668:function(e,t,n){"use strict";n.d(t,{I:function(){return c}});var r=n(59121),o=n(31091),i=n(63497),a=n(99649);function c(e,t){let{years:n=0,months:c=0,weeks:l=0,days:s=0,hours:u=0,minutes:p=0,seconds:f=0}=t,d=(0,a.Q)(e),y=c||n?(0,o.z)(d,c+12*n):d,m=s||l?(0,r.E)(y,s+7*l):y;return(0,i.L)(e,m.getTime()+1e3*(f+60*(p+60*u)))}}}]); \ No newline at end of file diff --git a/litellm/proxy/_experimental/out/_next/static/chunks/1973-26a414084f96c69b.js b/litellm/proxy/_experimental/out/_next/static/chunks/1973-26a414084f96c69b.js new file mode 100644 index 00000000000..8ba0b21bc05 --- /dev/null +++ b/litellm/proxy/_experimental/out/_next/static/chunks/1973-26a414084f96c69b.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[1973],{83669:function(e,t,n){n.d(t,{Z:function(){return l}});var a=n(1119),c=n(2265),o={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M699 353h-46.9c-10.2 0-19.9 4.9-25.9 13.3L469 584.3l-71.2-98.8c-6-8.3-15.6-13.3-25.9-13.3H325c-6.5 0-10.3 7.4-6.5 12.7l124.6 172.8a31.8 31.8 0 0051.7 0l210.6-292c3.9-5.3.1-12.7-6.4-12.7z"}},{tag:"path",attrs:{d:"M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z"}}]},name:"check-circle",theme:"outlined"},r=n(55015),l=c.forwardRef(function(e,t){return c.createElement(r.Z,(0,a.Z)({},e,{ref:t,icon:o}))})},5540:function(e,t,n){n.d(t,{Z:function(){return l}});var a=n(1119),c=n(2265),o={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z"}},{tag:"path",attrs:{d:"M686.7 638.6L544.1 535.5V288c0-4.4-3.6-8-8-8H488c-4.4 0-8 3.6-8 8v275.4c0 2.6 1.2 5 3.3 6.5l165.4 120.6c3.6 2.6 8.6 1.8 11.2-1.7l28.6-39c2.6-3.7 1.8-8.7-1.8-11.2z"}}]},name:"clock-circle",theme:"outlined"},r=n(55015),l=c.forwardRef(function(e,t){return c.createElement(r.Z,(0,a.Z)({},e,{ref:t,icon:o}))})},41169:function(e,t,n){n.d(t,{Z:function(){return l}});var a=n(1119),c=n(2265),o={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M512 472a40 40 0 1080 0 40 40 0 10-80 0zm367 352.9L696.3 352V178H768v-68H256v68h71.7v174L145 824.9c-2.8 7.4-4.3 15.2-4.3 23.1 0 35.3 28.7 64 64 64h614.6c7.9 0 15.7-1.5 23.1-4.3 33-12.7 49.4-49.8 36.6-82.8zM395.7 364.7V180h232.6v184.7L719.2 600c-20.7-5.3-42.1-8-63.9-8-61.2 0-119.2 21.5-165.3 60a188.78 188.78 0 01-121.3 43.9c-32.7 0-64.1-8.3-91.8-23.7l118.8-307.5zM210.5 844l41.7-107.8c35.7 18.1 75.4 27.8 116.6 27.8 61.2 0 119.2-21.5 165.3-60 33.9-28.2 76.3-43.9 121.3-43.9 35 0 68.4 9.5 97.6 27.1L813.5 844h-603z"}}]},name:"experiment",theme:"outlined"},r=n(55015),l=c.forwardRef(function(e,t){return c.createElement(r.Z,(0,a.Z)({},e,{ref:t,icon:o}))})},10798:function(e,t,n){n.d(t,{Z:function(){return l}});var a=n(1119),c=n(2265),o={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M880.1 154H143.9c-24.5 0-39.8 26.7-27.5 48L349 597.4V838c0 17.7 14.2 32 31.8 32h262.4c17.6 0 31.8-14.3 31.8-32V597.4L907.7 202c12.2-21.3-3.1-48-27.6-48zM603.4 798H420.6V642h182.9v156zm9.6-236.6l-9.5 16.6h-183l-9.5-16.6L212.7 226h598.6L613 561.4z"}}]},name:"filter",theme:"outlined"},r=n(55015),l=c.forwardRef(function(e,t){return c.createElement(r.Z,(0,a.Z)({},e,{ref:t,icon:o}))})},8881:function(e,t,n){n.d(t,{Z:function(){return l}});var a=n(1119),c=n(2265),o={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372 0-89 31.3-170.8 83.5-234.8l523.3 523.3C682.8 852.7 601 884 512 884zm288.5-137.2L277.2 223.5C341.2 171.3 423 140 512 140c205.4 0 372 166.6 372 372 0 89-31.3 170.8-83.5 234.8z"}}]},name:"stop",theme:"outlined"},r=n(55015),l=c.forwardRef(function(e,t){return c.createElement(r.Z,(0,a.Z)({},e,{ref:t,icon:o}))})},3632:function(e,t,n){n.d(t,{Z:function(){return l}});var a=n(1119),c=n(2265),o={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M400 317.7h73.9V656c0 4.4 3.6 8 8 8h60c4.4 0 8-3.6 8-8V317.7H624c6.7 0 10.4-7.7 6.3-12.9L518.3 163a8 8 0 00-12.6 0l-112 141.7c-4.1 5.3-.4 13 6.3 13zM878 626h-60c-4.4 0-8 3.6-8 8v154H214V634c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v198c0 17.7 14.3 32 32 32h684c17.7 0 32-14.3 32-32V634c0-4.4-3.6-8-8-8z"}}]},name:"upload",theme:"outlined"},r=n(55015),l=c.forwardRef(function(e,t){return c.createElement(r.Z,(0,a.Z)({},e,{ref:t,icon:o}))})},30150:function(e,t,n){n.d(t,{Z:function(){return u}});var a=n(5853),c=n(2265);let o=e=>{var t=(0,a._T)(e,[]);return c.createElement("svg",Object.assign({},t,{xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor",strokeWidth:"2.5"}),c.createElement("path",{d:"M12 4v16m8-8H4"}))},r=e=>{var t=(0,a._T)(e,[]);return c.createElement("svg",Object.assign({},t,{xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor",strokeWidth:"2.5"}),c.createElement("path",{d:"M20 12H4"}))};var l=n(13241),i=n(1153),s=n(69262);let d="flex mx-auto text-tremor-content-subtle dark:text-dark-tremor-content-subtle",m="cursor-pointer hover:text-tremor-content dark:hover:text-dark-tremor-content",u=c.forwardRef((e,t)=>{let{onSubmit:n,enableStepper:u=!0,disabled:g,onValueChange:p,onChange:f}=e,h=(0,a._T)(e,["onSubmit","enableStepper","disabled","onValueChange","onChange"]),v=(0,c.useRef)(null),[b,x]=c.useState(!1),y=c.useCallback(()=>{x(!0)},[]),k=c.useCallback(()=>{x(!1)},[]),[S,w]=c.useState(!1),E=c.useCallback(()=>{w(!0)},[]),C=c.useCallback(()=>{w(!1)},[]);return c.createElement(s.Z,Object.assign({type:"number",ref:(0,i.lq)([v,t]),disabled:g,makeInputClassName:(0,i.fn)("NumberInput"),onKeyDown:e=>{var t;if("Enter"===e.key&&!e.ctrlKey&&!e.altKey&&!e.shiftKey){let e=null===(t=v.current)||void 0===t?void 0:t.value;null==n||n(parseFloat(null!=e?e:""))}"ArrowDown"===e.key&&y(),"ArrowUp"===e.key&&E()},onKeyUp:e=>{"ArrowDown"===e.key&&k(),"ArrowUp"===e.key&&C()},onChange:e=>{g||(null==p||p(parseFloat(e.target.value)),null==f||f(e))},stepper:u?c.createElement("div",{className:(0,l.q)("flex justify-center align-middle")},c.createElement("div",{tabIndex:-1,onClick:e=>e.preventDefault(),onMouseDown:e=>e.preventDefault(),onTouchStart:e=>{e.cancelable&&e.preventDefault()},onMouseUp:()=>{var e,t;g||(null===(e=v.current)||void 0===e||e.stepDown(),null===(t=v.current)||void 0===t||t.dispatchEvent(new Event("input",{bubbles:!0})))},className:(0,l.q)(!g&&m,d,"group py-[10px] px-2.5 border-l border-tremor-border dark:border-dark-tremor-border")},c.createElement(r,{"data-testid":"step-down",className:(b?"scale-95":"")+" h-4 w-4 duration-75 transition group-active:scale-95"})),c.createElement("div",{tabIndex:-1,onClick:e=>e.preventDefault(),onMouseDown:e=>e.preventDefault(),onTouchStart:e=>{e.cancelable&&e.preventDefault()},onMouseUp:()=>{var e,t;g||(null===(e=v.current)||void 0===e||e.stepUp(),null===(t=v.current)||void 0===t||t.dispatchEvent(new Event("input",{bubbles:!0})))},className:(0,l.q)(!g&&m,d,"group py-[10px] px-2.5 border-l border-tremor-border dark:border-dark-tremor-border")},c.createElement(o,{"data-testid":"step-up",className:(S?"scale-95":"")+" h-4 w-4 duration-75 transition group-active:scale-95"}))):null},h))});u.displayName="NumberInput"},16853:function(e,t,n){n.d(t,{Z:function(){return d}});var a=n(5853),c=n(96398),o=n(44140),r=n(2265),l=n(13241),i=n(1153);let s=(0,i.fn)("Textarea"),d=r.forwardRef((e,t)=>{let{value:n,defaultValue:d="",placeholder:m="Type...",error:u=!1,errorMessage:g,disabled:p=!1,className:f,onChange:h,onValueChange:v,autoHeight:b=!1}=e,x=(0,a._T)(e,["value","defaultValue","placeholder","error","errorMessage","disabled","className","onChange","onValueChange","autoHeight"]),[y,k]=(0,o.Z)(d,n),S=(0,r.useRef)(null),w=(0,c.Uh)(y);return(0,r.useEffect)(()=>{let e=S.current;if(b&&e){e.style.height="60px";let t=e.scrollHeight;e.style.height=t+"px"}},[b,S,y]),r.createElement(r.Fragment,null,r.createElement("textarea",Object.assign({ref:(0,i.lq)([S,t]),value:y,placeholder:m,disabled:p,className:(0,l.q)(s("Textarea"),"w-full flex items-center outline-none rounded-tremor-default px-3 py-2 text-tremor-default focus:ring-2 transition duration-100 border","shadow-tremor-input focus:border-tremor-brand-subtle focus:ring-tremor-brand-muted","dark:shadow-dark-tremor-input focus:dark:border-dark-tremor-brand-subtle focus:dark:ring-dark-tremor-brand-muted",(0,c.um)(w,p,u),p?"placeholder:text-tremor-content-subtle dark:placeholder:text-dark-tremor-content-subtle":"placeholder:text-tremor-content dark:placeholder:text-dark-tremor-content",f),"data-testid":"text-area",onChange:e=>{null==h||h(e),k(e.target.value),null==v||v(e.target.value)}},x)),u&&g?r.createElement("p",{className:(0,l.q)(s("errorMessage"),"text-sm text-red-500 mt-1")},g):null)});d.displayName="Textarea"},67101:function(e,t,n){n.d(t,{Z:function(){return d}});var a=n(5853),c=n(13241),o=n(1153),r=n(2265),l=n(9496);let i=(0,o.fn)("Grid"),s=(e,t)=>e&&Object.keys(t).includes(String(e))?t[e]:"",d=r.forwardRef((e,t)=>{let{numItems:n=1,numItemsSm:o,numItemsMd:d,numItemsLg:m,children:u,className:g}=e,p=(0,a._T)(e,["numItems","numItemsSm","numItemsMd","numItemsLg","children","className"]),f=s(n,l._m),h=s(o,l.LH),v=s(d,l.l5),b=s(m,l.N4),x=(0,c.q)(f,h,v,b);return r.createElement("div",Object.assign({ref:t,className:(0,c.q)(i("root"),"grid",x,g)},p),u)});d.displayName="Grid"},9496:function(e,t,n){n.d(t,{LH:function(){return c},N4:function(){return r},PT:function(){return l},SP:function(){return i},VS:function(){return s},_m:function(){return a},_w:function(){return d},l5:function(){return o}});let a={0:"grid-cols-none",1:"grid-cols-1",2:"grid-cols-2",3:"grid-cols-3",4:"grid-cols-4",5:"grid-cols-5",6:"grid-cols-6",7:"grid-cols-7",8:"grid-cols-8",9:"grid-cols-9",10:"grid-cols-10",11:"grid-cols-11",12:"grid-cols-12"},c={0:"sm:grid-cols-none",1:"sm:grid-cols-1",2:"sm:grid-cols-2",3:"sm:grid-cols-3",4:"sm:grid-cols-4",5:"sm:grid-cols-5",6:"sm:grid-cols-6",7:"sm:grid-cols-7",8:"sm:grid-cols-8",9:"sm:grid-cols-9",10:"sm:grid-cols-10",11:"sm:grid-cols-11",12:"sm:grid-cols-12"},o={0:"md:grid-cols-none",1:"md:grid-cols-1",2:"md:grid-cols-2",3:"md:grid-cols-3",4:"md:grid-cols-4",5:"md:grid-cols-5",6:"md:grid-cols-6",7:"md:grid-cols-7",8:"md:grid-cols-8",9:"md:grid-cols-9",10:"md:grid-cols-10",11:"md:grid-cols-11",12:"md:grid-cols-12"},r={0:"lg:grid-cols-none",1:"lg:grid-cols-1",2:"lg:grid-cols-2",3:"lg:grid-cols-3",4:"lg:grid-cols-4",5:"lg:grid-cols-5",6:"lg:grid-cols-6",7:"lg:grid-cols-7",8:"lg:grid-cols-8",9:"lg:grid-cols-9",10:"lg:grid-cols-10",11:"lg:grid-cols-11",12:"lg:grid-cols-12"},l={1:"col-span-1",2:"col-span-2",3:"col-span-3",4:"col-span-4",5:"col-span-5",6:"col-span-6",7:"col-span-7",8:"col-span-8",9:"col-span-9",10:"col-span-10",11:"col-span-11",12:"col-span-12",13:"col-span-13"},i={1:"sm:col-span-1",2:"sm:col-span-2",3:"sm:col-span-3",4:"sm:col-span-4",5:"sm:col-span-5",6:"sm:col-span-6",7:"sm:col-span-7",8:"sm:col-span-8",9:"sm:col-span-9",10:"sm:col-span-10",11:"sm:col-span-11",12:"sm:col-span-12",13:"sm:col-span-13"},s={1:"md:col-span-1",2:"md:col-span-2",3:"md:col-span-3",4:"md:col-span-4",5:"md:col-span-5",6:"md:col-span-6",7:"md:col-span-7",8:"md:col-span-8",9:"md:col-span-9",10:"md:col-span-10",11:"md:col-span-11",12:"md:col-span-12",13:"md:col-span-13"},d={1:"lg:col-span-1",2:"lg:col-span-2",3:"lg:col-span-3",4:"lg:col-span-4",5:"lg:col-span-5",6:"lg:col-span-6",7:"lg:col-span-7",8:"lg:col-span-8",9:"lg:col-span-9",10:"lg:col-span-10",11:"lg:col-span-11",12:"lg:col-span-12",13:"lg:col-span-13"}},44140:function(e,t,n){n.d(t,{Z:function(){return c}});var a=n(2265);let c=(e,t)=>{let n=void 0!==t,[c,o]=(0,a.useState)(e);return[n?t:c,e=>{n||o(e)}]}},35631:function(e,t,n){n.d(t,{Z:function(){return I}});var a=n(83145),c=n(2265),o=n(36760),r=n.n(o),l=n(53253),i=n(6543),s=n(71744),d=n(91086),m=n(33759),u=n(77774),g=n(28617),p=n(40049),f=n(10353);let h=c.createContext({});h.Consumer;var v=n(19722),b=n(54998),x=function(e,t){var n={};for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&0>t.indexOf(a)&&(n[a]=e[a]);if(null!=e&&"function"==typeof Object.getOwnPropertySymbols)for(var c=0,a=Object.getOwnPropertySymbols(e);ct.indexOf(a[c])&&Object.prototype.propertyIsEnumerable.call(e,a[c])&&(n[a[c]]=e[a[c]]);return n};let y=c.forwardRef((e,t)=>{let n;let{prefixCls:a,children:o,actions:l,extra:i,styles:d,className:m,classNames:u,colStyle:g}=e,p=x(e,["prefixCls","children","actions","extra","styles","className","classNames","colStyle"]),{grid:f,itemLayout:y}=(0,c.useContext)(h),{getPrefixCls:k,list:S}=(0,c.useContext)(s.E_),w=e=>{var t,n;return r()(null===(n=null===(t=null==S?void 0:S.item)||void 0===t?void 0:t.classNames)||void 0===n?void 0:n[e],null==u?void 0:u[e])},E=e=>{var t,n;return Object.assign(Object.assign({},null===(n=null===(t=null==S?void 0:S.item)||void 0===t?void 0:t.styles)||void 0===n?void 0:n[e]),null==d?void 0:d[e])},C=k("list",a),N=l&&l.length>0&&c.createElement("ul",{className:r()("".concat(C,"-item-action"),w("actions")),key:"actions",style:E("actions")},l.map((e,t)=>c.createElement("li",{key:"".concat(C,"-item-action-").concat(t)},e,t!==l.length-1&&c.createElement("em",{className:"".concat(C,"-item-action-split")})))),z=c.createElement(f?"div":"li",Object.assign({},p,f?{}:{ref:t},{className:r()("".concat(C,"-item"),{["".concat(C,"-item-no-flex")]:!("vertical"===y?!!i:(n=!1,c.Children.forEach(o,e=>{"string"==typeof e&&(n=!0)}),!(n&&c.Children.count(o)>1)))},m)}),"vertical"===y&&i?[c.createElement("div",{className:"".concat(C,"-item-main"),key:"content"},o,N),c.createElement("div",{className:r()("".concat(C,"-item-extra"),w("extra")),key:"extra",style:E("extra")},i)]:[o,N,(0,v.Tm)(i,{key:"extra"})]);return f?c.createElement(b.Z,{ref:t,flex:1,style:g},z):z});y.Meta=e=>{var{prefixCls:t,className:n,avatar:a,title:o,description:l}=e,i=x(e,["prefixCls","className","avatar","title","description"]);let{getPrefixCls:d}=(0,c.useContext)(s.E_),m=d("list",t),u=r()("".concat(m,"-item-meta"),n),g=c.createElement("div",{className:"".concat(m,"-item-meta-content")},o&&c.createElement("h4",{className:"".concat(m,"-item-meta-title")},o),l&&c.createElement("div",{className:"".concat(m,"-item-meta-description")},l));return c.createElement("div",Object.assign({},i,{className:u}),a&&c.createElement("div",{className:"".concat(m,"-item-meta-avatar")},a),(o||l)&&g)};var k=n(93463),S=n(12918),w=n(99320),E=n(71140);let C=e=>{let{listBorderedCls:t,componentCls:n,paddingLG:a,margin:c,itemPaddingSM:o,itemPaddingLG:r,marginLG:l,borderRadiusLG:i}=e,s=(0,k.bf)(e.calc(i).sub(e.lineWidth).equal());return{[t]:{border:"".concat((0,k.bf)(e.lineWidth)," ").concat(e.lineType," ").concat(e.colorBorder),borderRadius:i,["".concat(n,"-header")]:{borderRadius:"".concat(s," ").concat(s," 0 0")},["".concat(n,"-footer")]:{borderRadius:"0 0 ".concat(s," ").concat(s)},["".concat(n,"-header,").concat(n,"-footer,").concat(n,"-item")]:{paddingInline:a},["".concat(n,"-pagination")]:{margin:"".concat((0,k.bf)(c)," ").concat((0,k.bf)(l))}},["".concat(t).concat(n,"-sm")]:{["".concat(n,"-item,").concat(n,"-header,").concat(n,"-footer")]:{padding:o}},["".concat(t).concat(n,"-lg")]:{["".concat(n,"-item,").concat(n,"-header,").concat(n,"-footer")]:{padding:r}}}},N=e=>{let{componentCls:t,screenSM:n,screenMD:a,marginLG:c,marginSM:o,margin:r}=e;return{["@media screen and (max-width:".concat(a,"px)")]:{[t]:{["".concat(t,"-item")]:{["".concat(t,"-item-action")]:{marginInlineStart:c}}},["".concat(t,"-vertical")]:{["".concat(t,"-item")]:{["".concat(t,"-item-extra")]:{marginInlineStart:c}}}},["@media screen and (max-width: ".concat(n,"px)")]:{[t]:{["".concat(t,"-item")]:{flexWrap:"wrap",["".concat(t,"-action")]:{marginInlineStart:o}}},["".concat(t,"-vertical")]:{["".concat(t,"-item")]:{flexWrap:"wrap-reverse",["".concat(t,"-item-main")]:{minWidth:e.contentWidth},["".concat(t,"-item-extra")]:{margin:"auto auto ".concat((0,k.bf)(r))}}}}}},z=e=>{let{componentCls:t,antCls:n,controlHeight:a,minHeight:c,paddingSM:o,marginLG:r,padding:l,itemPadding:i,colorPrimary:s,itemPaddingSM:d,itemPaddingLG:m,paddingXS:u,margin:g,colorText:p,colorTextDescription:f,motionDurationSlow:h,lineWidth:v,headerBg:b,footerBg:x,emptyTextPadding:y,metaMarginBottom:w,avatarMarginRight:E,titleMarginBottom:C,descriptionFontSize:N}=e;return{[t]:Object.assign(Object.assign({},(0,S.Wf)(e)),{position:"relative","--rc-virtual-list-scrollbar-bg":e.colorSplit,"*":{outline:"none"},["".concat(t,"-header")]:{background:b},["".concat(t,"-footer")]:{background:x},["".concat(t,"-header, ").concat(t,"-footer")]:{paddingBlock:o},["".concat(t,"-pagination")]:{marginBlockStart:r,["".concat(n,"-pagination-options")]:{textAlign:"start"}},["".concat(t,"-spin")]:{minHeight:c,textAlign:"center"},["".concat(t,"-items")]:{margin:0,padding:0,listStyle:"none"},["".concat(t,"-item")]:{display:"flex",alignItems:"center",justifyContent:"space-between",padding:i,color:p,["".concat(t,"-item-meta")]:{display:"flex",flex:1,alignItems:"flex-start",maxWidth:"100%",["".concat(t,"-item-meta-avatar")]:{marginInlineEnd:E},["".concat(t,"-item-meta-content")]:{flex:"1 0",width:0,color:p},["".concat(t,"-item-meta-title")]:{margin:"0 0 ".concat((0,k.bf)(e.marginXXS)," 0"),color:p,fontSize:e.fontSize,lineHeight:e.lineHeight,"> a":{color:p,transition:"all ".concat(h),"&:hover":{color:s}}},["".concat(t,"-item-meta-description")]:{color:f,fontSize:N,lineHeight:e.lineHeight}},["".concat(t,"-item-action")]:{flex:"0 0 auto",marginInlineStart:e.marginXXL,padding:0,fontSize:0,listStyle:"none","& > li":{position:"relative",display:"inline-block",padding:"0 ".concat((0,k.bf)(u)),color:f,fontSize:e.fontSize,lineHeight:e.lineHeight,textAlign:"center","&:first-child":{paddingInlineStart:0}},["".concat(t,"-item-action-split")]:{position:"absolute",insetBlockStart:"50%",insetInlineEnd:0,width:v,height:e.calc(e.fontHeight).sub(e.calc(e.marginXXS).mul(2)).equal(),transform:"translateY(-50%)",backgroundColor:e.colorSplit}}},["".concat(t,"-empty")]:{padding:"".concat((0,k.bf)(l)," 0"),color:f,fontSize:e.fontSizeSM,textAlign:"center"},["".concat(t,"-empty-text")]:{padding:y,color:e.colorTextDisabled,fontSize:e.fontSize,textAlign:"center"},["".concat(t,"-item-no-flex")]:{display:"block"}}),["".concat(t,"-grid ").concat(n,"-col > ").concat(t,"-item")]:{display:"block",maxWidth:"100%",marginBlockEnd:g,paddingBlock:0,borderBlockEnd:"none"},["".concat(t,"-vertical ").concat(t,"-item")]:{alignItems:"initial",["".concat(t,"-item-main")]:{display:"block",flex:1},["".concat(t,"-item-extra")]:{marginInlineStart:r},["".concat(t,"-item-meta")]:{marginBlockEnd:w,["".concat(t,"-item-meta-title")]:{marginBlockStart:0,marginBlockEnd:C,color:p,fontSize:e.fontSizeLG,lineHeight:e.lineHeightLG}},["".concat(t,"-item-action")]:{marginBlockStart:l,marginInlineStart:"auto","> li":{padding:"0 ".concat((0,k.bf)(l)),"&:first-child":{paddingInlineStart:0}}}},["".concat(t,"-split ").concat(t,"-item")]:{borderBlockEnd:"".concat((0,k.bf)(e.lineWidth)," ").concat(e.lineType," ").concat(e.colorSplit),"&:last-child":{borderBlockEnd:"none"}},["".concat(t,"-split ").concat(t,"-header")]:{borderBlockEnd:"".concat((0,k.bf)(e.lineWidth)," ").concat(e.lineType," ").concat(e.colorSplit)},["".concat(t,"-split").concat(t,"-empty ").concat(t,"-footer")]:{borderTop:"".concat((0,k.bf)(e.lineWidth)," ").concat(e.lineType," ").concat(e.colorSplit)},["".concat(t,"-loading ").concat(t,"-spin-nested-loading")]:{minHeight:a},["".concat(t,"-split").concat(t,"-something-after-last-item ").concat(n,"-spin-container > ").concat(t,"-items > ").concat(t,"-item:last-child")]:{borderBlockEnd:"".concat((0,k.bf)(e.lineWidth)," ").concat(e.lineType," ").concat(e.colorSplit)},["".concat(t,"-lg ").concat(t,"-item")]:{padding:m},["".concat(t,"-sm ").concat(t,"-item")]:{padding:d},["".concat(t,":not(").concat(t,"-vertical)")]:{["".concat(t,"-item-no-flex")]:{["".concat(t,"-item-action")]:{float:"right"}}}}};var M=(0,w.I$)("List",e=>{let t=(0,E.IX)(e,{listBorderedCls:"".concat(e.componentCls,"-bordered"),minHeight:e.controlHeightLG});return[z(t),C(t),N(t)]},e=>({contentWidth:220,itemPadding:"".concat((0,k.bf)(e.paddingContentVertical)," 0"),itemPaddingSM:"".concat((0,k.bf)(e.paddingContentVerticalSM)," ").concat((0,k.bf)(e.paddingContentHorizontal)),itemPaddingLG:"".concat((0,k.bf)(e.paddingContentVerticalLG)," ").concat((0,k.bf)(e.paddingContentHorizontalLG)),headerBg:"transparent",footerBg:"transparent",emptyTextPadding:e.padding,metaMarginBottom:e.padding,avatarMarginRight:e.padding,titleMarginBottom:e.paddingSM,descriptionFontSize:e.fontSize})),O=function(e,t){var n={};for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&0>t.indexOf(a)&&(n[a]=e[a]);if(null!=e&&"function"==typeof Object.getOwnPropertySymbols)for(var c=0,a=Object.getOwnPropertySymbols(e);ct.indexOf(a[c])&&Object.prototype.propertyIsEnumerable.call(e,a[c])&&(n[a[c]]=e[a[c]]);return n};let Z=c.forwardRef(function(e,t){let{pagination:n=!1,prefixCls:o,bordered:v=!1,split:b=!0,className:x,rootClassName:y,style:k,children:S,itemLayout:w,loadMore:E,grid:C,dataSource:N=[],size:z,header:Z,footer:I,loading:j=!1,rowKey:H,renderItem:B,locale:L}=e,T=O(e,["pagination","prefixCls","bordered","split","className","rootClassName","style","children","itemLayout","loadMore","grid","dataSource","size","header","footer","loading","rowKey","renderItem","locale"]),V=n&&"object"==typeof n?n:{},[R,W]=c.useState(V.defaultCurrent||1),[_,P]=c.useState(V.defaultPageSize||10),{getPrefixCls:D,direction:q,className:A,style:G}=(0,s.dj)("list"),{renderEmpty:U}=c.useContext(s.E_),X=e=>(t,a)=>{var c;W(t),P(a),n&&(null===(c=null==n?void 0:n[e])||void 0===c||c.call(n,t,a))},K=X("onChange"),F=X("onShowSizeChange"),J=!!(E||n||I),Y=D("list",o),[$,Q,ee]=M(Y),et=j;"boolean"==typeof et&&(et={spinning:et});let en=!!(null==et?void 0:et.spinning),ea=(0,m.Z)(z),ec="";switch(ea){case"large":ec="lg";break;case"small":ec="sm"}let eo=r()(Y,{["".concat(Y,"-vertical")]:"vertical"===w,["".concat(Y,"-").concat(ec)]:ec,["".concat(Y,"-split")]:b,["".concat(Y,"-bordered")]:v,["".concat(Y,"-loading")]:en,["".concat(Y,"-grid")]:!!C,["".concat(Y,"-something-after-last-item")]:J,["".concat(Y,"-rtl")]:"rtl"===q},A,x,y,Q,ee),er=(0,l.Z)({current:1,total:0,position:"bottom"},{total:N.length,current:R,pageSize:_},n||{}),el=Math.ceil(er.total/er.pageSize);er.current=Math.min(er.current,el);let ei=n&&c.createElement("div",{className:r()("".concat(Y,"-pagination"))},c.createElement(p.Z,Object.assign({align:"end"},er,{onChange:K,onShowSizeChange:F}))),es=(0,a.Z)(N);n&&N.length>(er.current-1)*er.pageSize&&(es=(0,a.Z)(N).splice((er.current-1)*er.pageSize,er.pageSize));let ed=Object.keys(C||{}).some(e=>["xs","sm","md","lg","xl","xxl"].includes(e)),em=(0,g.Z)(ed),eu=c.useMemo(()=>{for(let e=0;e{if(!C)return;let e=eu&&C[eu]?C[eu]:C.column;if(e)return{width:"".concat(100/e,"%"),maxWidth:"".concat(100/e,"%")}},[JSON.stringify(C),eu]),ep=en&&c.createElement("div",{style:{minHeight:53}});if(es.length>0){let e=es.map((e,t)=>{let n;return B?((n="function"==typeof H?H(e):H?e[H]:e.key)||(n="list-item-".concat(t)),c.createElement(c.Fragment,{key:n},B(e,t))):null});ep=C?c.createElement(u.Z,{gutter:C.gutter},c.Children.map(e,e=>c.createElement("div",{key:null==e?void 0:e.key,style:eg},e))):c.createElement("ul",{className:"".concat(Y,"-items")},e)}else S||en||(ep=c.createElement("div",{className:"".concat(Y,"-empty-text")},(null==L?void 0:L.emptyText)||(null==U?void 0:U("List"))||c.createElement(d.Z,{componentName:"List"})));let ef=er.position,eh=c.useMemo(()=>({grid:C,itemLayout:w}),[JSON.stringify(C),w]);return $(c.createElement(h.Provider,{value:eh},c.createElement("div",Object.assign({ref:t,style:Object.assign(Object.assign({},G),k),className:eo},T),("top"===ef||"both"===ef)&&ei,Z&&c.createElement("div",{className:"".concat(Y,"-header")},Z),c.createElement(f.Z,Object.assign({},et),ep,S),I&&c.createElement("div",{className:"".concat(Y,"-footer")},I),E||("bottom"===ef||"both"===ef)&&ei)))});Z.Item=y;var I=Z},30401:function(e,t,n){n.d(t,{Z:function(){return a}});let a=(0,n(79205).Z)("check",[["path",{d:"M20 6 9 17l-5-5",key:"1gmf2c"}]])},10900:function(e,t,n){var a=n(2265);let c=a.forwardRef(function(e,t){return a.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:2,stroke:"currentColor","aria-hidden":"true",ref:t},e),a.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M10 19l-7-7m0 0l7-7m-7 7h18"}))});t.Z=c}}]); \ No newline at end of file diff --git a/litellm/proxy/_experimental/out/_next/static/chunks/1994-6637a121c9ee1602.js b/litellm/proxy/_experimental/out/_next/static/chunks/1994-6637a121c9ee1602.js deleted file mode 100644 index 90f29480d63..00000000000 --- a/litellm/proxy/_experimental/out/_next/static/chunks/1994-6637a121c9ee1602.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[1994],{4156:function(e,n,t){t.d(n,{Z:function(){return O}});var o=t(2265),a=t(36760),r=t.n(a),c=t(20873),l=t(28791),i=t(6694),s=t(34709),u=t(71744),d=t(86586),b=t(64024),p=t(39109);let f=o.createContext(null);var v=t(23159),m=t(66531),h=function(e,n){var t={};for(var o in e)Object.prototype.hasOwnProperty.call(e,o)&&0>n.indexOf(o)&&(t[o]=e[o]);if(null!=e&&"function"==typeof Object.getOwnPropertySymbols)for(var a=0,o=Object.getOwnPropertySymbols(e);an.indexOf(o[a])&&Object.prototype.propertyIsEnumerable.call(e,o[a])&&(t[o[a]]=e[o[a]]);return t};let g=o.forwardRef((e,n)=>{var t;let{prefixCls:a,className:g,rootClassName:C,children:y,indeterminate:k=!1,style:x,onMouseEnter:O,onMouseLeave:E,skipGroup:S=!1,disabled:w}=e,Z=h(e,["prefixCls","className","rootClassName","children","indeterminate","style","onMouseEnter","onMouseLeave","skipGroup","disabled"]),{getPrefixCls:P,direction:N,checkbox:j}=o.useContext(u.E_),I=o.useContext(f),{isFormItemInput:R}=o.useContext(p.aM),z=o.useContext(d.Z),B=null!==(t=(null==I?void 0:I.disabled)||w)&&void 0!==t?t:z,D=o.useRef(Z.value),M=o.useRef(null),_=(0,l.sQ)(n,M);o.useEffect(()=>{null==I||I.registerValue(Z.value)},[]),o.useEffect(()=>{if(!S)return Z.value!==D.current&&(null==I||I.cancelValue(D.current),null==I||I.registerValue(Z.value),D.current=Z.value),()=>null==I?void 0:I.cancelValue(Z.value)},[Z.value]),o.useEffect(()=>{var e;(null===(e=M.current)||void 0===e?void 0:e.input)&&(M.current.input.indeterminate=k)},[k]);let W=P("checkbox",a),q=(0,b.Z)(W),[H,T,G]=(0,v.ZP)(W,q),V=Object.assign({},Z);I&&!S&&(V.onChange=function(){for(var e=arguments.length,n=Array(e),t=0;tn.indexOf(o)&&(t[o]=e[o]);if(null!=e&&"function"==typeof Object.getOwnPropertySymbols)for(var a=0,o=Object.getOwnPropertySymbols(e);an.indexOf(o[a])&&Object.prototype.propertyIsEnumerable.call(e,o[a])&&(t[o[a]]=e[o[a]]);return t};let x=o.forwardRef((e,n)=>{let{defaultValue:t,children:a,options:c=[],prefixCls:l,className:i,rootClassName:s,style:d,onChange:p}=e,m=k(e,["defaultValue","children","options","prefixCls","className","rootClassName","style","onChange"]),{getPrefixCls:h,direction:x}=o.useContext(u.E_),[O,E]=o.useState(m.value||t||[]),[S,w]=o.useState([]);o.useEffect(()=>{"value"in m&&E(m.value||[])},[m.value]);let Z=o.useMemo(()=>c.map(e=>"string"==typeof e||"number"==typeof e?{label:e,value:e}:e),[c]),P=e=>{w(n=>n.filter(n=>n!==e))},N=e=>{w(n=>[].concat((0,C.Z)(n),[e]))},j=e=>{let n=O.indexOf(e.value),t=(0,C.Z)(O);-1===n?t.push(e.value):t.splice(n,1),"value"in m||E(t),null==p||p(t.filter(e=>S.includes(e)).sort((e,n)=>Z.findIndex(n=>n.value===e)-Z.findIndex(e=>e.value===n)))},I=h("checkbox",l),R="".concat(I,"-group"),z=(0,b.Z)(I),[B,D,M]=(0,v.ZP)(I,z),_=(0,y.Z)(m,["value","disabled"]),W=c.length?Z.map(e=>o.createElement(g,{prefixCls:I,key:e.value.toString(),disabled:"disabled"in e?e.disabled:m.disabled,value:e.value,checked:O.includes(e.value),onChange:e.onChange,className:r()("".concat(R,"-item"),e.className),style:e.style,title:e.title,id:e.id,required:e.required},e.label)):a,q=o.useMemo(()=>({toggleOption:j,value:O,disabled:m.disabled,name:m.name,registerValue:N,cancelValue:P}),[j,O,m.disabled,m.name,N,P]),H=r()(R,{["".concat(R,"-rtl")]:"rtl"===x},i,s,M,z,D);return B(o.createElement("div",Object.assign({className:H,style:d},_,{ref:n}),o.createElement(f.Provider,{value:q},W)))});g.Group=x,g.__ANT_CHECKBOX=!0;var O=g},23159:function(e,n,t){t.d(n,{C2:function(){return i}});var o=t(93463),a=t(12918),r=t(71140),c=t(99320);let l=e=>{let{checkboxCls:n}=e,t="".concat(n,"-wrapper");return[{["".concat(n,"-group")]:Object.assign(Object.assign({},(0,a.Wf)(e)),{display:"inline-flex",flexWrap:"wrap",columnGap:e.marginXS,["> ".concat(e.antCls,"-row")]:{flex:1}}),[t]:Object.assign(Object.assign({},(0,a.Wf)(e)),{display:"inline-flex",alignItems:"baseline",cursor:"pointer","&:after":{display:"inline-block",width:0,overflow:"hidden",content:"'\\a0'"},["& + ".concat(t)]:{marginInlineStart:0},["&".concat(t,"-in-form-item")]:{'input[type="checkbox"]':{width:14,height:14}}}),[n]:Object.assign(Object.assign({},(0,a.Wf)(e)),{position:"relative",whiteSpace:"nowrap",lineHeight:1,cursor:"pointer",borderRadius:e.borderRadiusSM,alignSelf:"center",["".concat(n,"-input")]:{position:"absolute",inset:0,zIndex:1,cursor:"pointer",opacity:0,margin:0,["&:focus-visible + ".concat(n,"-inner")]:(0,a.oN)(e)},["".concat(n,"-inner")]:{boxSizing:"border-box",display:"block",width:e.checkboxSize,height:e.checkboxSize,direction:"ltr",backgroundColor:e.colorBgContainer,border:"".concat((0,o.bf)(e.lineWidth)," ").concat(e.lineType," ").concat(e.colorBorder),borderRadius:e.borderRadiusSM,borderCollapse:"separate",transition:"all ".concat(e.motionDurationSlow),"&:after":{boxSizing:"border-box",position:"absolute",top:"50%",insetInlineStart:"25%",display:"table",width:e.calc(e.checkboxSize).div(14).mul(5).equal(),height:e.calc(e.checkboxSize).div(14).mul(8).equal(),border:"".concat((0,o.bf)(e.lineWidthBold)," solid ").concat(e.colorWhite),borderTop:0,borderInlineStart:0,transform:"rotate(45deg) scale(0) translate(-50%,-50%)",opacity:0,content:'""',transition:"all ".concat(e.motionDurationFast," ").concat(e.motionEaseInBack,", opacity ").concat(e.motionDurationFast)}},"& + span":{paddingInlineStart:e.paddingXS,paddingInlineEnd:e.paddingXS}})},{["\n ".concat(t,":not(").concat(t,"-disabled),\n ").concat(n,":not(").concat(n,"-disabled)\n ")]:{["&:hover ".concat(n,"-inner")]:{borderColor:e.colorPrimary}},["".concat(t,":not(").concat(t,"-disabled)")]:{["&:hover ".concat(n,"-checked:not(").concat(n,"-disabled) ").concat(n,"-inner")]:{backgroundColor:e.colorPrimaryHover,borderColor:"transparent"},["&:hover ".concat(n,"-checked:not(").concat(n,"-disabled):after")]:{borderColor:e.colorPrimaryHover}}},{["".concat(n,"-checked")]:{["".concat(n,"-inner")]:{backgroundColor:e.colorPrimary,borderColor:e.colorPrimary,"&:after":{opacity:1,transform:"rotate(45deg) scale(1) translate(-50%,-50%)",transition:"all ".concat(e.motionDurationMid," ").concat(e.motionEaseOutBack," ").concat(e.motionDurationFast)}}},["\n ".concat(t,"-checked:not(").concat(t,"-disabled),\n ").concat(n,"-checked:not(").concat(n,"-disabled)\n ")]:{["&:hover ".concat(n,"-inner")]:{backgroundColor:e.colorPrimaryHover,borderColor:"transparent"}}},{[n]:{"&-indeterminate":{"&":{["".concat(n,"-inner")]:{backgroundColor:"".concat(e.colorBgContainer),borderColor:"".concat(e.colorBorder),"&:after":{top:"50%",insetInlineStart:"50%",width:e.calc(e.fontSizeLG).div(2).equal(),height:e.calc(e.fontSizeLG).div(2).equal(),backgroundColor:e.colorPrimary,border:0,transform:"translate(-50%, -50%) scale(1)",opacity:1,content:'""'}},["&:hover ".concat(n,"-inner")]:{backgroundColor:"".concat(e.colorBgContainer),borderColor:"".concat(e.colorPrimary)}}}}},{["".concat(t,"-disabled")]:{cursor:"not-allowed"},["".concat(n,"-disabled")]:{["&, ".concat(n,"-input")]:{cursor:"not-allowed",pointerEvents:"none"},["".concat(n,"-inner")]:{background:e.colorBgContainerDisabled,borderColor:e.colorBorder,"&:after":{borderColor:e.colorTextDisabled}},"&:after":{display:"none"},"& + span":{color:e.colorTextDisabled},["&".concat(n,"-indeterminate ").concat(n,"-inner::after")]:{background:e.colorTextDisabled}}}]};function i(e,n){return l((0,r.IX)(n,{checkboxCls:".".concat(e),checkboxSize:n.controlInteractiveSize}))}n.ZP=(0,c.I$)("Checkbox",(e,n)=>{let{prefixCls:t}=n;return[i(t,e)]})},66531:function(e,n,t){t.d(n,{Z:function(){return r}});var o=t(2265),a=t(53346);function r(e){let n=o.useRef(null),t=()=>{a.Z.cancel(n.current),n.current=null};return[()=>{t(),n.current=(0,a.Z)(()=>{n.current=null})},o=>{n.current&&(o.stopPropagation(),t()),null==e||e(o)}]}},20873:function(e,n,t){var o=t(1119),a=t(31686),r=t(11993),c=t(26365),l=t(6989),i=t(36760),s=t.n(i),u=t(50506),d=t(2265),b=["prefixCls","className","style","checked","disabled","defaultChecked","type","title","onChange"],p=(0,d.forwardRef)(function(e,n){var t=e.prefixCls,i=void 0===t?"rc-checkbox":t,p=e.className,f=e.style,v=e.checked,m=e.disabled,h=e.defaultChecked,g=e.type,C=void 0===g?"checkbox":g,y=e.title,k=e.onChange,x=(0,l.Z)(e,b),O=(0,d.useRef)(null),E=(0,d.useRef)(null),S=(0,u.Z)(void 0!==h&&h,{value:v}),w=(0,c.Z)(S,2),Z=w[0],P=w[1];(0,d.useImperativeHandle)(n,function(){return{focus:function(e){var n;null===(n=O.current)||void 0===n||n.focus(e)},blur:function(){var e;null===(e=O.current)||void 0===e||e.blur()},input:O.current,nativeElement:E.current}});var N=s()(i,p,(0,r.Z)((0,r.Z)({},"".concat(i,"-checked"),Z),"".concat(i,"-disabled"),m));return d.createElement("span",{className:N,title:y,style:f,ref:E},d.createElement("input",(0,o.Z)({},x,{className:"".concat(i,"-input"),ref:O,onChange:function(n){m||("checked"in e||P(n.target.checked),null==k||k({target:(0,a.Z)((0,a.Z)({},e),{},{type:C,checked:n.target.checked}),stopPropagation:function(){n.stopPropagation()},preventDefault:function(){n.preventDefault()},nativeEvent:n.nativeEvent}))},disabled:m,checked:!!Z,type:C})),d.createElement("span",{className:"".concat(i,"-inner")}))});n.Z=p}}]); \ No newline at end of file diff --git a/litellm/proxy/_experimental/out/_next/static/chunks/1994-a4d0b99849c16b62.js b/litellm/proxy/_experimental/out/_next/static/chunks/1994-a4d0b99849c16b62.js new file mode 100644 index 00000000000..3211472683d --- /dev/null +++ b/litellm/proxy/_experimental/out/_next/static/chunks/1994-a4d0b99849c16b62.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[1994],{61994:function(e,n,t){t.d(n,{Z:function(){return O}});var o=t(2265),a=t(36760),r=t.n(a),c=t(20873),l=t(28791),i=t(6694),s=t(34709),u=t(71744),d=t(86586),b=t(64024),p=t(39109);let f=o.createContext(null);var v=t(23159),m=t(66531),h=function(e,n){var t={};for(var o in e)Object.prototype.hasOwnProperty.call(e,o)&&0>n.indexOf(o)&&(t[o]=e[o]);if(null!=e&&"function"==typeof Object.getOwnPropertySymbols)for(var a=0,o=Object.getOwnPropertySymbols(e);an.indexOf(o[a])&&Object.prototype.propertyIsEnumerable.call(e,o[a])&&(t[o[a]]=e[o[a]]);return t};let g=o.forwardRef((e,n)=>{var t;let{prefixCls:a,className:g,rootClassName:C,children:y,indeterminate:k=!1,style:x,onMouseEnter:O,onMouseLeave:E,skipGroup:S=!1,disabled:w}=e,Z=h(e,["prefixCls","className","rootClassName","children","indeterminate","style","onMouseEnter","onMouseLeave","skipGroup","disabled"]),{getPrefixCls:P,direction:N,checkbox:j}=o.useContext(u.E_),I=o.useContext(f),{isFormItemInput:R}=o.useContext(p.aM),z=o.useContext(d.Z),B=null!==(t=(null==I?void 0:I.disabled)||w)&&void 0!==t?t:z,D=o.useRef(Z.value),M=o.useRef(null),_=(0,l.sQ)(n,M);o.useEffect(()=>{null==I||I.registerValue(Z.value)},[]),o.useEffect(()=>{if(!S)return Z.value!==D.current&&(null==I||I.cancelValue(D.current),null==I||I.registerValue(Z.value),D.current=Z.value),()=>null==I?void 0:I.cancelValue(Z.value)},[Z.value]),o.useEffect(()=>{var e;(null===(e=M.current)||void 0===e?void 0:e.input)&&(M.current.input.indeterminate=k)},[k]);let W=P("checkbox",a),q=(0,b.Z)(W),[H,T,G]=(0,v.ZP)(W,q),V=Object.assign({},Z);I&&!S&&(V.onChange=function(){for(var e=arguments.length,n=Array(e),t=0;tn.indexOf(o)&&(t[o]=e[o]);if(null!=e&&"function"==typeof Object.getOwnPropertySymbols)for(var a=0,o=Object.getOwnPropertySymbols(e);an.indexOf(o[a])&&Object.prototype.propertyIsEnumerable.call(e,o[a])&&(t[o[a]]=e[o[a]]);return t};let x=o.forwardRef((e,n)=>{let{defaultValue:t,children:a,options:c=[],prefixCls:l,className:i,rootClassName:s,style:d,onChange:p}=e,m=k(e,["defaultValue","children","options","prefixCls","className","rootClassName","style","onChange"]),{getPrefixCls:h,direction:x}=o.useContext(u.E_),[O,E]=o.useState(m.value||t||[]),[S,w]=o.useState([]);o.useEffect(()=>{"value"in m&&E(m.value||[])},[m.value]);let Z=o.useMemo(()=>c.map(e=>"string"==typeof e||"number"==typeof e?{label:e,value:e}:e),[c]),P=e=>{w(n=>n.filter(n=>n!==e))},N=e=>{w(n=>[].concat((0,C.Z)(n),[e]))},j=e=>{let n=O.indexOf(e.value),t=(0,C.Z)(O);-1===n?t.push(e.value):t.splice(n,1),"value"in m||E(t),null==p||p(t.filter(e=>S.includes(e)).sort((e,n)=>Z.findIndex(n=>n.value===e)-Z.findIndex(e=>e.value===n)))},I=h("checkbox",l),R="".concat(I,"-group"),z=(0,b.Z)(I),[B,D,M]=(0,v.ZP)(I,z),_=(0,y.Z)(m,["value","disabled"]),W=c.length?Z.map(e=>o.createElement(g,{prefixCls:I,key:e.value.toString(),disabled:"disabled"in e?e.disabled:m.disabled,value:e.value,checked:O.includes(e.value),onChange:e.onChange,className:r()("".concat(R,"-item"),e.className),style:e.style,title:e.title,id:e.id,required:e.required},e.label)):a,q=o.useMemo(()=>({toggleOption:j,value:O,disabled:m.disabled,name:m.name,registerValue:N,cancelValue:P}),[j,O,m.disabled,m.name,N,P]),H=r()(R,{["".concat(R,"-rtl")]:"rtl"===x},i,s,M,z,D);return B(o.createElement("div",Object.assign({className:H,style:d},_,{ref:n}),o.createElement(f.Provider,{value:q},W)))});g.Group=x,g.__ANT_CHECKBOX=!0;var O=g},23159:function(e,n,t){t.d(n,{C2:function(){return i}});var o=t(93463),a=t(12918),r=t(71140),c=t(99320);let l=e=>{let{checkboxCls:n}=e,t="".concat(n,"-wrapper");return[{["".concat(n,"-group")]:Object.assign(Object.assign({},(0,a.Wf)(e)),{display:"inline-flex",flexWrap:"wrap",columnGap:e.marginXS,["> ".concat(e.antCls,"-row")]:{flex:1}}),[t]:Object.assign(Object.assign({},(0,a.Wf)(e)),{display:"inline-flex",alignItems:"baseline",cursor:"pointer","&:after":{display:"inline-block",width:0,overflow:"hidden",content:"'\\a0'"},["& + ".concat(t)]:{marginInlineStart:0},["&".concat(t,"-in-form-item")]:{'input[type="checkbox"]':{width:14,height:14}}}),[n]:Object.assign(Object.assign({},(0,a.Wf)(e)),{position:"relative",whiteSpace:"nowrap",lineHeight:1,cursor:"pointer",borderRadius:e.borderRadiusSM,alignSelf:"center",["".concat(n,"-input")]:{position:"absolute",inset:0,zIndex:1,cursor:"pointer",opacity:0,margin:0,["&:focus-visible + ".concat(n,"-inner")]:(0,a.oN)(e)},["".concat(n,"-inner")]:{boxSizing:"border-box",display:"block",width:e.checkboxSize,height:e.checkboxSize,direction:"ltr",backgroundColor:e.colorBgContainer,border:"".concat((0,o.bf)(e.lineWidth)," ").concat(e.lineType," ").concat(e.colorBorder),borderRadius:e.borderRadiusSM,borderCollapse:"separate",transition:"all ".concat(e.motionDurationSlow),"&:after":{boxSizing:"border-box",position:"absolute",top:"50%",insetInlineStart:"25%",display:"table",width:e.calc(e.checkboxSize).div(14).mul(5).equal(),height:e.calc(e.checkboxSize).div(14).mul(8).equal(),border:"".concat((0,o.bf)(e.lineWidthBold)," solid ").concat(e.colorWhite),borderTop:0,borderInlineStart:0,transform:"rotate(45deg) scale(0) translate(-50%,-50%)",opacity:0,content:'""',transition:"all ".concat(e.motionDurationFast," ").concat(e.motionEaseInBack,", opacity ").concat(e.motionDurationFast)}},"& + span":{paddingInlineStart:e.paddingXS,paddingInlineEnd:e.paddingXS}})},{["\n ".concat(t,":not(").concat(t,"-disabled),\n ").concat(n,":not(").concat(n,"-disabled)\n ")]:{["&:hover ".concat(n,"-inner")]:{borderColor:e.colorPrimary}},["".concat(t,":not(").concat(t,"-disabled)")]:{["&:hover ".concat(n,"-checked:not(").concat(n,"-disabled) ").concat(n,"-inner")]:{backgroundColor:e.colorPrimaryHover,borderColor:"transparent"},["&:hover ".concat(n,"-checked:not(").concat(n,"-disabled):after")]:{borderColor:e.colorPrimaryHover}}},{["".concat(n,"-checked")]:{["".concat(n,"-inner")]:{backgroundColor:e.colorPrimary,borderColor:e.colorPrimary,"&:after":{opacity:1,transform:"rotate(45deg) scale(1) translate(-50%,-50%)",transition:"all ".concat(e.motionDurationMid," ").concat(e.motionEaseOutBack," ").concat(e.motionDurationFast)}}},["\n ".concat(t,"-checked:not(").concat(t,"-disabled),\n ").concat(n,"-checked:not(").concat(n,"-disabled)\n ")]:{["&:hover ".concat(n,"-inner")]:{backgroundColor:e.colorPrimaryHover,borderColor:"transparent"}}},{[n]:{"&-indeterminate":{"&":{["".concat(n,"-inner")]:{backgroundColor:"".concat(e.colorBgContainer),borderColor:"".concat(e.colorBorder),"&:after":{top:"50%",insetInlineStart:"50%",width:e.calc(e.fontSizeLG).div(2).equal(),height:e.calc(e.fontSizeLG).div(2).equal(),backgroundColor:e.colorPrimary,border:0,transform:"translate(-50%, -50%) scale(1)",opacity:1,content:'""'}},["&:hover ".concat(n,"-inner")]:{backgroundColor:"".concat(e.colorBgContainer),borderColor:"".concat(e.colorPrimary)}}}}},{["".concat(t,"-disabled")]:{cursor:"not-allowed"},["".concat(n,"-disabled")]:{["&, ".concat(n,"-input")]:{cursor:"not-allowed",pointerEvents:"none"},["".concat(n,"-inner")]:{background:e.colorBgContainerDisabled,borderColor:e.colorBorder,"&:after":{borderColor:e.colorTextDisabled}},"&:after":{display:"none"},"& + span":{color:e.colorTextDisabled},["&".concat(n,"-indeterminate ").concat(n,"-inner::after")]:{background:e.colorTextDisabled}}}]};function i(e,n){return l((0,r.IX)(n,{checkboxCls:".".concat(e),checkboxSize:n.controlInteractiveSize}))}n.ZP=(0,c.I$)("Checkbox",(e,n)=>{let{prefixCls:t}=n;return[i(t,e)]})},66531:function(e,n,t){t.d(n,{Z:function(){return r}});var o=t(2265),a=t(53346);function r(e){let n=o.useRef(null),t=()=>{a.Z.cancel(n.current),n.current=null};return[()=>{t(),n.current=(0,a.Z)(()=>{n.current=null})},o=>{n.current&&(o.stopPropagation(),t()),null==e||e(o)}]}},20873:function(e,n,t){var o=t(1119),a=t(31686),r=t(11993),c=t(26365),l=t(6989),i=t(36760),s=t.n(i),u=t(50506),d=t(2265),b=["prefixCls","className","style","checked","disabled","defaultChecked","type","title","onChange"],p=(0,d.forwardRef)(function(e,n){var t=e.prefixCls,i=void 0===t?"rc-checkbox":t,p=e.className,f=e.style,v=e.checked,m=e.disabled,h=e.defaultChecked,g=e.type,C=void 0===g?"checkbox":g,y=e.title,k=e.onChange,x=(0,l.Z)(e,b),O=(0,d.useRef)(null),E=(0,d.useRef)(null),S=(0,u.Z)(void 0!==h&&h,{value:v}),w=(0,c.Z)(S,2),Z=w[0],P=w[1];(0,d.useImperativeHandle)(n,function(){return{focus:function(e){var n;null===(n=O.current)||void 0===n||n.focus(e)},blur:function(){var e;null===(e=O.current)||void 0===e||e.blur()},input:O.current,nativeElement:E.current}});var N=s()(i,p,(0,r.Z)((0,r.Z)({},"".concat(i,"-checked"),Z),"".concat(i,"-disabled"),m));return d.createElement("span",{className:N,title:y,style:f,ref:E},d.createElement("input",(0,o.Z)({},x,{className:"".concat(i,"-input"),ref:O,onChange:function(n){m||("checked"in e||P(n.target.checked),null==k||k({target:(0,a.Z)((0,a.Z)({},e),{},{type:C,checked:n.target.checked}),stopPropagation:function(){n.stopPropagation()},preventDefault:function(){n.preventDefault()},nativeEvent:n.nativeEvent}))},disabled:m,checked:!!Z,type:C})),d.createElement("span",{className:"".concat(i,"-inner")}))});n.Z=p}}]); \ No newline at end of file diff --git a/litellm/proxy/_experimental/out/_next/static/chunks/2004-294ce010a90069b4.js b/litellm/proxy/_experimental/out/_next/static/chunks/2004-294ce010a90069b4.js new file mode 100644 index 00000000000..280fafea373 --- /dev/null +++ b/litellm/proxy/_experimental/out/_next/static/chunks/2004-294ce010a90069b4.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[2004],{22004:function(e,l,s){s.d(l,{Z:function(){return ee},g:function(){return X}});var i=s(57437),a=s(2265),r=s(41649),t=s(78489),n=s(12514),o=s(49804),d=s(67101),c=s(47323),m=s(12485),u=s(18135),x=s(35242),h=s(29706),_=s(77991),g=s(21626),j=s(97214),p=s(28241),v=s(58834),Z=s(69552),b=s(71876),f=s(84264),w=s(24199),z=s(4260),y=s(10032),N=s(99981),C=s(22116),S=s(37592),O=s(15424),M=s(23628),k=s(86462),I=s(47686),P=s(53410),A=s(74998),T=s(31283),D=s(46468),F=s(59872),L=s(10900),R=s(49566),U=s(96761),E=s(5545),B=s(30401),V=s(78867),q=s(33860),G=s(95920),W=s(9114),$=s(19250),J=s(98015),Q=s(10901),Y=s(97415),H=e=>{var l,s,o,N,C;let{organizationId:O,onClose:M,accessToken:k,is_org_admin:I,is_proxy_admin:T,userModels:H,editOrg:K}=e,[X,ee]=(0,a.useState)(null),[el,es]=(0,a.useState)(!0),[ei]=y.Z.useForm(),[ea,er]=(0,a.useState)(!1),[et,en]=(0,a.useState)(!1),[eo,ed]=(0,a.useState)(!1),[ec,em]=(0,a.useState)(null),[eu,ex]=(0,a.useState)({}),[eh,e_]=(0,a.useState)(!1),eg=I||T,ej=async()=>{try{if(es(!0),!k)return;let e=await (0,$.organizationInfoCall)(k,O);ee(e)}catch(e){W.Z.fromBackend("Failed to load organization information"),console.error("Error fetching organization info:",e)}finally{es(!1)}};(0,a.useEffect)(()=>{ej()},[O,k]);let ep=async e=>{try{if(null==k)return;let l={user_email:e.user_email,user_id:e.user_id,role:e.role};await (0,$.organizationMemberAddCall)(k,O,l),W.Z.success("Organization member added successfully"),en(!1),ei.resetFields(),ej()}catch(e){W.Z.fromBackend("Failed to add organization member"),console.error("Error adding organization member:",e)}},ev=async e=>{try{if(!k)return;let l={user_email:e.user_email,user_id:e.user_id,role:e.role};await (0,$.organizationMemberUpdateCall)(k,O,l),W.Z.success("Organization member updated successfully"),ed(!1),ei.resetFields(),ej()}catch(e){W.Z.fromBackend("Failed to update organization member"),console.error("Error updating organization member:",e)}},eZ=async e=>{try{if(!k)return;await (0,$.organizationMemberDeleteCall)(k,O,e.user_id),W.Z.success("Organization member deleted successfully"),ed(!1),ei.resetFields(),ej()}catch(e){W.Z.fromBackend("Failed to delete organization member"),console.error("Error deleting organization member:",e)}},eb=async e=>{try{if(!k)return;e_(!0);let l={organization_id:O,organization_alias:e.organization_alias,models:e.models,litellm_budget_table:{tpm_limit:e.tpm_limit,rpm_limit:e.rpm_limit,max_budget:e.max_budget,budget_duration:e.budget_duration},metadata:e.metadata?JSON.parse(e.metadata):null};if((void 0!==e.vector_stores||void 0!==e.mcp_servers_and_groups)&&(l.object_permission={...null==X?void 0:X.object_permission,vector_stores:e.vector_stores||[]},void 0!==e.mcp_servers_and_groups)){let{servers:s,accessGroups:i}=e.mcp_servers_and_groups||{servers:[],accessGroups:[]};s&&s.length>0&&(l.object_permission.mcp_servers=s),i&&i.length>0&&(l.object_permission.mcp_access_groups=i)}await (0,$.organizationUpdateCall)(k,l),W.Z.success("Organization settings updated successfully"),er(!1),ej()}catch(e){W.Z.fromBackend("Failed to update organization settings"),console.error("Error updating organization:",e)}finally{e_(!1)}};if(el)return(0,i.jsx)("div",{className:"p-4",children:"Loading..."});if(!X)return(0,i.jsx)("div",{className:"p-4",children:"Organization not found"});let ef=async(e,l)=>{await (0,F.vQ)(e)&&(ex(e=>({...e,[l]:!0})),setTimeout(()=>{ex(e=>({...e,[l]:!1}))},2e3))};return(0,i.jsxs)("div",{className:"w-full h-screen p-4 bg-white",children:[(0,i.jsx)("div",{className:"flex justify-between items-center mb-6",children:(0,i.jsxs)("div",{children:[(0,i.jsx)(t.Z,{icon:L.Z,onClick:M,variant:"light",className:"mb-4",children:"Back to Organizations"}),(0,i.jsx)(U.Z,{children:X.organization_alias}),(0,i.jsxs)("div",{className:"flex items-center cursor-pointer",children:[(0,i.jsx)(f.Z,{className:"text-gray-500 font-mono",children:X.organization_id}),(0,i.jsx)(E.ZP,{type:"text",size:"small",icon:eu["org-id"]?(0,i.jsx)(B.Z,{size:12}):(0,i.jsx)(V.Z,{size:12}),onClick:()=>ef(X.organization_id,"org-id"),className:"left-2 z-10 transition-all duration-200 ".concat(eu["org-id"]?"text-green-600 bg-green-50 border-green-200":"text-gray-500 hover:text-gray-700 hover:bg-gray-100")})]})]})}),(0,i.jsxs)(u.Z,{defaultIndex:K?2:0,children:[(0,i.jsxs)(x.Z,{className:"mb-4",children:[(0,i.jsx)(m.Z,{children:"Overview"}),(0,i.jsx)(m.Z,{children:"Members"}),(0,i.jsx)(m.Z,{children:"Settings"})]}),(0,i.jsxs)(_.Z,{children:[(0,i.jsx)(h.Z,{children:(0,i.jsxs)(d.Z,{numItems:1,numItemsSm:2,numItemsLg:3,className:"gap-6",children:[(0,i.jsxs)(n.Z,{children:[(0,i.jsx)(f.Z,{children:"Organization Details"}),(0,i.jsxs)("div",{className:"mt-2",children:[(0,i.jsxs)(f.Z,{children:["Created: ",new Date(X.created_at).toLocaleDateString()]}),(0,i.jsxs)(f.Z,{children:["Updated: ",new Date(X.updated_at).toLocaleDateString()]}),(0,i.jsxs)(f.Z,{children:["Created By: ",X.created_by]})]})]}),(0,i.jsxs)(n.Z,{children:[(0,i.jsx)(f.Z,{children:"Budget Status"}),(0,i.jsxs)("div",{className:"mt-2",children:[(0,i.jsxs)(U.Z,{children:["$",(0,F.pw)(X.spend,4)]}),(0,i.jsxs)(f.Z,{children:["of"," ",null===X.litellm_budget_table.max_budget?"Unlimited":"$".concat((0,F.pw)(X.litellm_budget_table.max_budget,4))]}),X.litellm_budget_table.budget_duration&&(0,i.jsxs)(f.Z,{className:"text-gray-500",children:["Reset: ",X.litellm_budget_table.budget_duration]})]})]}),(0,i.jsxs)(n.Z,{children:[(0,i.jsx)(f.Z,{children:"Rate Limits"}),(0,i.jsxs)("div",{className:"mt-2",children:[(0,i.jsxs)(f.Z,{children:["TPM: ",X.litellm_budget_table.tpm_limit||"Unlimited"]}),(0,i.jsxs)(f.Z,{children:["RPM: ",X.litellm_budget_table.rpm_limit||"Unlimited"]}),X.litellm_budget_table.max_parallel_requests&&(0,i.jsxs)(f.Z,{children:["Max Parallel Requests: ",X.litellm_budget_table.max_parallel_requests]})]})]}),(0,i.jsxs)(n.Z,{children:[(0,i.jsx)(f.Z,{children:"Models"}),(0,i.jsx)("div",{className:"mt-2 flex flex-wrap gap-2",children:0===X.models.length?(0,i.jsx)(r.Z,{color:"red",children:"All proxy models"}):X.models.map((e,l)=>(0,i.jsx)(r.Z,{color:"red",children:e},l))})]}),(0,i.jsxs)(n.Z,{children:[(0,i.jsx)(f.Z,{children:"Teams"}),(0,i.jsx)("div",{className:"mt-2 flex flex-wrap gap-2",children:null===(l=X.teams)||void 0===l?void 0:l.map((e,l)=>(0,i.jsx)(r.Z,{color:"red",children:e.team_id},l))})]}),(0,i.jsx)(J.Z,{objectPermission:X.object_permission,variant:"card",accessToken:k})]})}),(0,i.jsx)(h.Z,{children:(0,i.jsxs)("div",{className:"space-y-4",children:[(0,i.jsx)(n.Z,{className:"w-full mx-auto flex-auto overflow-y-auto max-h-[75vh]",children:(0,i.jsxs)(g.Z,{children:[(0,i.jsx)(v.Z,{children:(0,i.jsxs)(b.Z,{children:[(0,i.jsx)(Z.Z,{children:"User ID"}),(0,i.jsx)(Z.Z,{children:"Role"}),(0,i.jsx)(Z.Z,{children:"Spend"}),(0,i.jsx)(Z.Z,{children:"Created At"}),(0,i.jsx)(Z.Z,{})]})}),(0,i.jsx)(j.Z,{children:null===(s=X.members)||void 0===s?void 0:s.map((e,l)=>(0,i.jsxs)(b.Z,{children:[(0,i.jsx)(p.Z,{children:(0,i.jsx)(f.Z,{className:"font-mono",children:e.user_id})}),(0,i.jsx)(p.Z,{children:(0,i.jsx)(f.Z,{className:"font-mono",children:e.user_role})}),(0,i.jsx)(p.Z,{children:(0,i.jsxs)(f.Z,{children:["$",(0,F.pw)(e.spend,4)]})}),(0,i.jsx)(p.Z,{children:(0,i.jsx)(f.Z,{children:new Date(e.created_at).toLocaleString()})}),(0,i.jsx)(p.Z,{children:eg&&(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(c.Z,{icon:P.Z,size:"sm",onClick:()=>{em({role:e.user_role,user_email:e.user_email,user_id:e.user_id}),ed(!0)}}),(0,i.jsx)(c.Z,{icon:A.Z,size:"sm",onClick:()=>{eZ(e)}})]})})]},l))})]})}),eg&&(0,i.jsx)(t.Z,{onClick:()=>{en(!0)},children:"Add Member"})]})}),(0,i.jsx)(h.Z,{children:(0,i.jsxs)(n.Z,{className:"overflow-y-auto max-h-[65vh]",children:[(0,i.jsxs)("div",{className:"flex justify-between items-center mb-4",children:[(0,i.jsx)(U.Z,{children:"Organization Settings"}),eg&&!ea&&(0,i.jsx)(t.Z,{onClick:()=>er(!0),children:"Edit Settings"})]}),ea?(0,i.jsxs)(y.Z,{form:ei,onFinish:eb,initialValues:{organization_alias:X.organization_alias,models:X.models,tpm_limit:X.litellm_budget_table.tpm_limit,rpm_limit:X.litellm_budget_table.rpm_limit,max_budget:X.litellm_budget_table.max_budget,budget_duration:X.litellm_budget_table.budget_duration,metadata:X.metadata?JSON.stringify(X.metadata,null,2):"",vector_stores:(null===(o=X.object_permission)||void 0===o?void 0:o.vector_stores)||[],mcp_servers_and_groups:{servers:(null===(N=X.object_permission)||void 0===N?void 0:N.mcp_servers)||[],accessGroups:(null===(C=X.object_permission)||void 0===C?void 0:C.mcp_access_groups)||[]}},layout:"vertical",children:[(0,i.jsx)(y.Z.Item,{label:"Organization Name",name:"organization_alias",rules:[{required:!0,message:"Please input an organization name"}],children:(0,i.jsx)(R.Z,{})}),(0,i.jsx)(y.Z.Item,{label:"Models",name:"models",children:(0,i.jsxs)(S.default,{mode:"multiple",placeholder:"Select models",children:[(0,i.jsx)(S.default.Option,{value:"all-proxy-models",children:"All Proxy Models"},"all-proxy-models"),H.map(e=>(0,i.jsx)(S.default.Option,{value:e,children:(0,D.W0)(e)},e))]})}),(0,i.jsx)(y.Z.Item,{label:"Max Budget (USD)",name:"max_budget",children:(0,i.jsx)(w.Z,{step:.01,precision:2,style:{width:"100%"}})}),(0,i.jsx)(y.Z.Item,{label:"Reset Budget",name:"budget_duration",children:(0,i.jsxs)(S.default,{placeholder:"n/a",children:[(0,i.jsx)(S.default.Option,{value:"24h",children:"daily"}),(0,i.jsx)(S.default.Option,{value:"7d",children:"weekly"}),(0,i.jsx)(S.default.Option,{value:"30d",children:"monthly"})]})}),(0,i.jsx)(y.Z.Item,{label:"Tokens per minute Limit (TPM)",name:"tpm_limit",children:(0,i.jsx)(w.Z,{step:1,style:{width:"100%"}})}),(0,i.jsx)(y.Z.Item,{label:"Requests per minute Limit (RPM)",name:"rpm_limit",children:(0,i.jsx)(w.Z,{step:1,style:{width:"100%"}})}),(0,i.jsx)(y.Z.Item,{label:"Vector Stores",name:"vector_stores",children:(0,i.jsx)(Y.Z,{onChange:e=>ei.setFieldValue("vector_stores",e),value:ei.getFieldValue("vector_stores"),accessToken:k||"",placeholder:"Select vector stores"})}),(0,i.jsx)(y.Z.Item,{label:"MCP Servers & Access Groups",name:"mcp_servers_and_groups",children:(0,i.jsx)(G.Z,{onChange:e=>ei.setFieldValue("mcp_servers_and_groups",e),value:ei.getFieldValue("mcp_servers_and_groups"),accessToken:k||"",placeholder:"Select MCP servers and access groups"})}),(0,i.jsx)(y.Z.Item,{label:"Metadata",name:"metadata",children:(0,i.jsx)(z.default.TextArea,{rows:4})}),(0,i.jsx)("div",{className:"sticky z-10 bg-white p-4 border-t border-gray-200 bottom-[-1.5rem] inset-x-[-1.5rem]",children:(0,i.jsxs)("div",{className:"flex justify-end items-center gap-2",children:[(0,i.jsx)(t.Z,{variant:"secondary",onClick:()=>er(!1),disabled:eh,children:"Cancel"}),(0,i.jsx)(t.Z,{type:"submit",loading:eh,children:"Save Changes"})]})})]}):(0,i.jsxs)("div",{className:"space-y-4",children:[(0,i.jsxs)("div",{children:[(0,i.jsx)(f.Z,{className:"font-medium",children:"Organization Name"}),(0,i.jsx)("div",{children:X.organization_alias})]}),(0,i.jsxs)("div",{children:[(0,i.jsx)(f.Z,{className:"font-medium",children:"Organization ID"}),(0,i.jsx)("div",{className:"font-mono",children:X.organization_id})]}),(0,i.jsxs)("div",{children:[(0,i.jsx)(f.Z,{className:"font-medium",children:"Created At"}),(0,i.jsx)("div",{children:new Date(X.created_at).toLocaleString()})]}),(0,i.jsxs)("div",{children:[(0,i.jsx)(f.Z,{className:"font-medium",children:"Models"}),(0,i.jsx)("div",{className:"flex flex-wrap gap-2 mt-1",children:X.models.map((e,l)=>(0,i.jsx)(r.Z,{color:"red",children:e},l))})]}),(0,i.jsxs)("div",{children:[(0,i.jsx)(f.Z,{className:"font-medium",children:"Rate Limits"}),(0,i.jsxs)("div",{children:["TPM: ",X.litellm_budget_table.tpm_limit||"Unlimited"]}),(0,i.jsxs)("div",{children:["RPM: ",X.litellm_budget_table.rpm_limit||"Unlimited"]})]}),(0,i.jsxs)("div",{children:[(0,i.jsx)(f.Z,{className:"font-medium",children:"Budget"}),(0,i.jsxs)("div",{children:["Max:"," ",null!==X.litellm_budget_table.max_budget?"$".concat((0,F.pw)(X.litellm_budget_table.max_budget,4)):"No Limit"]}),(0,i.jsxs)("div",{children:["Reset: ",X.litellm_budget_table.budget_duration||"Never"]})]}),(0,i.jsx)(J.Z,{objectPermission:X.object_permission,variant:"inline",className:"pt-4 border-t border-gray-200",accessToken:k})]})]})})]})]}),(0,i.jsx)(q.Z,{isVisible:et,onCancel:()=>en(!1),onSubmit:ep,accessToken:k,title:"Add Organization Member",roles:[{label:"org_admin",value:"org_admin",description:"Can add and remove members, and change their roles."},{label:"internal_user",value:"internal_user",description:"Can view/create keys for themselves within organization."},{label:"internal_user_viewer",value:"internal_user_viewer",description:"Can only view their keys within organization."}],defaultRole:"internal_user"}),(0,i.jsx)(Q.Z,{visible:eo,onCancel:()=>ed(!1),onSubmit:ev,initialData:ec,mode:"edit",config:{title:"Edit Member",showEmail:!0,showUserId:!0,roleOptions:[{label:"Org Admin",value:"org_admin"},{label:"Internal User",value:"internal_user"},{label:"Internal User Viewer",value:"internal_user_viewer"}]}})]})},K=s(21609);let X=async(e,l)=>{l(await (0,$.organizationListCall)(e))};var ee=e=>{let{organizations:l,userRole:s,userModels:L,accessToken:R,lastRefreshed:U,handleRefreshClick:E,currentOrg:B,guardrailsList:V=[],setOrganizations:q,premiumUser:J}=e,[Q,ee]=(0,a.useState)(null),[el,es]=(0,a.useState)(!1),[ei,ea]=(0,a.useState)(!1),[er,et]=(0,a.useState)(null),[en,eo]=(0,a.useState)(!1),[ed,ec]=(0,a.useState)(!1),[em]=y.Z.useForm(),[eu,ex]=(0,a.useState)({});(0,a.useEffect)(()=>{R&&X(R,q)},[R]);let eh=e=>{e&&(et(e),ea(!0))},e_=async()=>{if(er&&R)try{eo(!0),await (0,$.organizationDeleteCall)(R,er),W.Z.success("Organization deleted successfully"),ea(!1),et(null),await X(R,q)}catch(e){console.error("Error deleting organization:",e)}finally{eo(!1)}},eg=async e=>{try{var l,s,i,a;if(!R)return;console.log("values in organizations new create call: ".concat(JSON.stringify(e))),(e.allowed_vector_store_ids&&e.allowed_vector_store_ids.length>0||e.allowed_mcp_servers_and_groups&&((null===(l=e.allowed_mcp_servers_and_groups.servers)||void 0===l?void 0:l.length)>0||(null===(s=e.allowed_mcp_servers_and_groups.accessGroups)||void 0===s?void 0:s.length)>0))&&(e.object_permission={},e.allowed_vector_store_ids&&e.allowed_vector_store_ids.length>0&&(e.object_permission.vector_stores=e.allowed_vector_store_ids,delete e.allowed_vector_store_ids),e.allowed_mcp_servers_and_groups&&((null===(i=e.allowed_mcp_servers_and_groups.servers)||void 0===i?void 0:i.length)>0&&(e.object_permission.mcp_servers=e.allowed_mcp_servers_and_groups.servers),(null===(a=e.allowed_mcp_servers_and_groups.accessGroups)||void 0===a?void 0:a.length)>0&&(e.object_permission.mcp_access_groups=e.allowed_mcp_servers_and_groups.accessGroups),delete e.allowed_mcp_servers_and_groups)),await (0,$.organizationCreateCall)(R,e),W.Z.success("Organization created successfully"),ec(!1),em.resetFields(),X(R,q)}catch(e){console.error("Error creating organization:",e)}};return J?(0,i.jsxs)("div",{className:"w-full mx-4 h-[75vh]",children:[(0,i.jsx)(d.Z,{numItems:1,className:"gap-2 p-8 w-full mt-2",children:(0,i.jsxs)(o.Z,{numColSpan:1,className:"flex flex-col gap-2",children:[("Admin"===s||"Org Admin"===s)&&(0,i.jsx)(t.Z,{className:"w-fit",onClick:()=>ec(!0),children:"+ Create New Organization"}),Q?(0,i.jsx)(H,{organizationId:Q,onClose:()=>{ee(null),es(!1)},accessToken:R,is_org_admin:!0,is_proxy_admin:"Admin"===s,userModels:L,editOrg:el}):(0,i.jsxs)(u.Z,{className:"gap-2 h-[75vh] w-full",children:[(0,i.jsxs)(x.Z,{className:"flex justify-between mt-2 w-full items-center",children:[(0,i.jsx)("div",{className:"flex",children:(0,i.jsx)(m.Z,{children:"Your Organizations"})}),(0,i.jsxs)("div",{className:"flex items-center space-x-2",children:[U&&(0,i.jsxs)(f.Z,{children:["Last Refreshed: ",U]}),(0,i.jsx)(c.Z,{icon:M.Z,variant:"shadow",size:"xs",className:"self-center",onClick:E})]})]}),(0,i.jsx)(_.Z,{children:(0,i.jsxs)(h.Z,{children:[(0,i.jsx)(f.Z,{children:"Click on ā€œOrganization IDā€ to view organization details."}),(0,i.jsx)(d.Z,{numItems:1,className:"gap-2 pt-2 pb-2 h-[75vh] w-full mt-2",children:(0,i.jsx)(o.Z,{numColSpan:1,children:(0,i.jsx)(n.Z,{className:"w-full mx-auto flex-auto overflow-y-auto max-h-[50vh]",children:(0,i.jsxs)(g.Z,{children:[(0,i.jsx)(v.Z,{children:(0,i.jsxs)(b.Z,{children:[(0,i.jsx)(Z.Z,{children:"Organization ID"}),(0,i.jsx)(Z.Z,{children:"Organization Name"}),(0,i.jsx)(Z.Z,{children:"Created"}),(0,i.jsx)(Z.Z,{children:"Spend (USD)"}),(0,i.jsx)(Z.Z,{children:"Budget (USD)"}),(0,i.jsx)(Z.Z,{children:"Models"}),(0,i.jsx)(Z.Z,{children:"TPM / RPM Limits"}),(0,i.jsx)(Z.Z,{children:"Info"}),(0,i.jsx)(Z.Z,{children:"Actions"})]})}),(0,i.jsx)(j.Z,{children:l&&l.length>0?l.sort((e,l)=>new Date(l.created_at).getTime()-new Date(e.created_at).getTime()).map(e=>{var l,a,n,o,d,m,u,x,h;return(0,i.jsxs)(b.Z,{children:[(0,i.jsx)(p.Z,{children:(0,i.jsx)("div",{className:"overflow-hidden",children:(0,i.jsx)(N.Z,{title:e.organization_id,children:(0,i.jsxs)(t.Z,{size:"xs",variant:"light",className:"font-mono text-blue-500 bg-blue-50 hover:bg-blue-100 text-xs font-normal px-2 py-0.5 text-left overflow-hidden truncate max-w-[200px]",onClick:()=>ee(e.organization_id),children:[null===(l=e.organization_id)||void 0===l?void 0:l.slice(0,7),"..."]})})})}),(0,i.jsx)(p.Z,{children:e.organization_alias}),(0,i.jsx)(p.Z,{children:e.created_at?new Date(e.created_at).toLocaleDateString():"N/A"}),(0,i.jsx)(p.Z,{children:(0,F.pw)(e.spend,4)}),(0,i.jsx)(p.Z,{children:(null===(a=e.litellm_budget_table)||void 0===a?void 0:a.max_budget)!==null&&(null===(n=e.litellm_budget_table)||void 0===n?void 0:n.max_budget)!==void 0?null===(o=e.litellm_budget_table)||void 0===o?void 0:o.max_budget:"No limit"}),(0,i.jsx)(p.Z,{style:{maxWidth:"8-x",whiteSpace:"pre-wrap",overflow:"hidden"},className:e.models.length>3?"px-0":"",children:(0,i.jsx)("div",{className:"flex flex-col",children:Array.isArray(e.models)?(0,i.jsx)("div",{className:"flex flex-col",children:0===e.models.length?(0,i.jsx)(r.Z,{size:"xs",className:"mb-1",color:"red",children:(0,i.jsx)(f.Z,{children:"All Proxy Models"})}):(0,i.jsx)(i.Fragment,{children:(0,i.jsxs)("div",{className:"flex items-start",children:[e.models.length>3&&(0,i.jsx)("div",{children:(0,i.jsx)(c.Z,{icon:eu[e.organization_id||""]?k.Z:I.Z,className:"cursor-pointer",size:"xs",onClick:()=>{ex(l=>({...l,[e.organization_id||""]:!l[e.organization_id||""]}))}})}),(0,i.jsxs)("div",{className:"flex flex-wrap gap-1",children:[e.models.slice(0,3).map((e,l)=>"all-proxy-models"===e?(0,i.jsx)(r.Z,{size:"xs",color:"red",children:(0,i.jsx)(f.Z,{children:"All Proxy Models"})},l):(0,i.jsx)(r.Z,{size:"xs",color:"blue",children:(0,i.jsx)(f.Z,{children:e.length>30?"".concat((0,D.W0)(e).slice(0,30),"..."):(0,D.W0)(e)})},l)),e.models.length>3&&!eu[e.organization_id||""]&&(0,i.jsx)(r.Z,{size:"xs",color:"gray",className:"cursor-pointer",children:(0,i.jsxs)(f.Z,{children:["+",e.models.length-3," ",e.models.length-3==1?"more model":"more models"]})}),eu[e.organization_id||""]&&(0,i.jsx)("div",{className:"flex flex-wrap gap-1",children:e.models.slice(3).map((e,l)=>"all-proxy-models"===e?(0,i.jsx)(r.Z,{size:"xs",color:"red",children:(0,i.jsx)(f.Z,{children:"All Proxy Models"})},l+3):(0,i.jsx)(r.Z,{size:"xs",color:"blue",children:(0,i.jsx)(f.Z,{children:e.length>30?"".concat((0,D.W0)(e).slice(0,30),"..."):(0,D.W0)(e)})},l+3))})]})]})})}):null})}),(0,i.jsx)(p.Z,{children:(0,i.jsxs)(f.Z,{children:["TPM:"," ",(null===(d=e.litellm_budget_table)||void 0===d?void 0:d.tpm_limit)?null===(m=e.litellm_budget_table)||void 0===m?void 0:m.tpm_limit:"Unlimited",(0,i.jsx)("br",{}),"RPM:"," ",(null===(u=e.litellm_budget_table)||void 0===u?void 0:u.rpm_limit)?null===(x=e.litellm_budget_table)||void 0===x?void 0:x.rpm_limit:"Unlimited"]})}),(0,i.jsx)(p.Z,{children:(0,i.jsxs)(f.Z,{children:[(null===(h=e.members)||void 0===h?void 0:h.length)||0," Members"]})}),(0,i.jsx)(p.Z,{children:"Admin"===s&&(0,i.jsxs)(i.Fragment,{children:[(0,i.jsxs)(N.Z,{title:"Edit organization",children:[" ",(0,i.jsx)(c.Z,{icon:P.Z,size:"sm",className:"cursor-pointer hover:text-blue-600",onClick:()=>{ee(e.organization_id),es(!0)}})]}),(0,i.jsxs)(N.Z,{title:"Delete organization",children:[" ",(0,i.jsx)(c.Z,{onClick:()=>eh(e.organization_id),icon:A.Z,size:"sm",className:"cursor-pointer hover:text-red-600"})]})]})})]},e.organization_id)}):null})]})})})})]})})]})]})}),(0,i.jsx)(C.Z,{title:"Create Organization",visible:ed,width:800,footer:null,onCancel:()=>{ec(!1),em.resetFields()},children:(0,i.jsxs)(y.Z,{form:em,onFinish:eg,labelCol:{span:8},wrapperCol:{span:16},labelAlign:"left",children:[(0,i.jsx)(y.Z.Item,{label:"Organization Name",name:"organization_alias",rules:[{required:!0,message:"Please input an organization name"}],children:(0,i.jsx)(T.o,{placeholder:""})}),(0,i.jsx)(y.Z.Item,{label:"Models",name:"models",children:(0,i.jsxs)(S.default,{mode:"multiple",placeholder:"Select models",style:{width:"100%"},children:[(0,i.jsx)(S.default.Option,{value:"all-proxy-models",children:"All Proxy Models"},"all-proxy-models"),L&&L.length>0&&L.map(e=>(0,i.jsx)(S.default.Option,{value:e,children:(0,D.W0)(e)},e))]})}),(0,i.jsx)(y.Z.Item,{label:"Max Budget (USD)",name:"max_budget",children:(0,i.jsx)(w.Z,{step:.01,precision:2,width:200})}),(0,i.jsx)(y.Z.Item,{label:"Reset Budget",name:"budget_duration",children:(0,i.jsxs)(S.default,{defaultValue:null,placeholder:"n/a",children:[(0,i.jsx)(S.default.Option,{value:"24h",children:"daily"}),(0,i.jsx)(S.default.Option,{value:"7d",children:"weekly"}),(0,i.jsx)(S.default.Option,{value:"30d",children:"monthly"})]})}),(0,i.jsx)(y.Z.Item,{label:"Tokens per minute Limit (TPM)",name:"tpm_limit",children:(0,i.jsx)(w.Z,{step:1,width:400})}),(0,i.jsx)(y.Z.Item,{label:"Requests per minute Limit (RPM)",name:"rpm_limit",children:(0,i.jsx)(w.Z,{step:1,width:400})}),(0,i.jsx)(y.Z.Item,{label:(0,i.jsxs)("span",{children:["Allowed Vector Stores"," ",(0,i.jsx)(N.Z,{title:"Select which vector stores this organization can access by default. Leave empty for access to all vector stores",children:(0,i.jsx)(O.Z,{style:{marginLeft:"4px"}})})]}),name:"allowed_vector_store_ids",className:"mt-4",help:"Select vector stores this organization can access. Leave empty for access to all vector stores",children:(0,i.jsx)(Y.Z,{onChange:e=>em.setFieldValue("allowed_vector_store_ids",e),value:em.getFieldValue("allowed_vector_store_ids"),accessToken:R||"",placeholder:"Select vector stores (optional)"})}),(0,i.jsx)(y.Z.Item,{label:(0,i.jsxs)("span",{children:["Allowed MCP Servers"," ",(0,i.jsx)(N.Z,{title:"Select which MCP servers and access groups this organization can access by default.",children:(0,i.jsx)(O.Z,{style:{marginLeft:"4px"}})})]}),name:"allowed_mcp_servers_and_groups",className:"mt-4",help:"Select MCP servers and access groups this organization can access.",children:(0,i.jsx)(G.Z,{onChange:e=>em.setFieldValue("allowed_mcp_servers_and_groups",e),value:em.getFieldValue("allowed_mcp_servers_and_groups"),accessToken:R||"",placeholder:"Select MCP servers and access groups (optional)"})}),(0,i.jsx)(y.Z.Item,{label:"Metadata",name:"metadata",children:(0,i.jsx)(z.default.TextArea,{rows:4})}),(0,i.jsx)("div",{style:{textAlign:"right",marginTop:"10px"},children:(0,i.jsx)(t.Z,{type:"submit",children:"Create Organization"})})]})}),(0,i.jsx)(K.Z,{isOpen:ei,title:"Delete Organization?",message:"Are you sure you want to delete this organization? This action cannot be undone.",resourceInformationTitle:"Organization Information",resourceInformation:[{label:"Organization ID",value:er,code:!0}],onCancel:()=>{ea(!1),et(null)},onOk:e_,confirmLoading:en})]}):(0,i.jsx)("div",{children:(0,i.jsxs)(f.Z,{children:["This is a LiteLLM Enterprise feature, and requires a valid key to use. Get a trial key"," ",(0,i.jsx)("a",{href:"https://www.litellm.ai/#pricing",target:"_blank",rel:"noopener noreferrer",children:"here"}),"."]})})}}}]); \ No newline at end of file diff --git a/litellm/proxy/_experimental/out/_next/static/chunks/2004-8b1ad3d8c195646a.js b/litellm/proxy/_experimental/out/_next/static/chunks/2004-8b1ad3d8c195646a.js deleted file mode 100644 index 59075bdbc32..00000000000 --- a/litellm/proxy/_experimental/out/_next/static/chunks/2004-8b1ad3d8c195646a.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[2004],{22004:function(e,l,s){s.d(l,{Z:function(){return ee},g:function(){return X}});var i=s(57437),a=s(2265),r=s(41649),t=s(78489),n=s(12514),o=s(49804),d=s(67101),c=s(47323),m=s(12485),u=s(18135),x=s(35242),h=s(29706),_=s(77991),g=s(21626),j=s(97214),p=s(28241),v=s(58834),Z=s(69552),b=s(71876),f=s(84264),w=s(24199),z=s(4260),y=s(10032),N=s(99981),C=s(22116),S=s(37592),O=s(15424),M=s(23628),k=s(86462),I=s(47686),P=s(53410),A=s(74998),T=s(31283),D=s(46468),F=s(59872),L=s(10900),R=s(49566),U=s(96761),E=s(5545),B=s(30401),V=s(78867),q=s(33860),G=s(95920),W=s(9114),$=s(19250),J=s(98015),Q=s(10901),Y=s(97415),H=e=>{var l,s,o,N,C;let{organizationId:O,onClose:M,accessToken:k,is_org_admin:I,is_proxy_admin:T,userModels:H,editOrg:K}=e,[X,ee]=(0,a.useState)(null),[el,es]=(0,a.useState)(!0),[ei]=y.Z.useForm(),[ea,er]=(0,a.useState)(!1),[et,en]=(0,a.useState)(!1),[eo,ed]=(0,a.useState)(!1),[ec,em]=(0,a.useState)(null),[eu,ex]=(0,a.useState)({}),eh=I||T,e_=async()=>{try{if(es(!0),!k)return;let e=await (0,$.organizationInfoCall)(k,O);ee(e)}catch(e){W.Z.fromBackend("Failed to load organization information"),console.error("Error fetching organization info:",e)}finally{es(!1)}};(0,a.useEffect)(()=>{e_()},[O,k]);let eg=async e=>{try{if(null==k)return;let l={user_email:e.user_email,user_id:e.user_id,role:e.role};await (0,$.organizationMemberAddCall)(k,O,l),W.Z.success("Organization member added successfully"),en(!1),ei.resetFields(),e_()}catch(e){W.Z.fromBackend("Failed to add organization member"),console.error("Error adding organization member:",e)}},ej=async e=>{try{if(!k)return;let l={user_email:e.user_email,user_id:e.user_id,role:e.role};await (0,$.organizationMemberUpdateCall)(k,O,l),W.Z.success("Organization member updated successfully"),ed(!1),ei.resetFields(),e_()}catch(e){W.Z.fromBackend("Failed to update organization member"),console.error("Error updating organization member:",e)}},ep=async e=>{try{if(!k)return;await (0,$.organizationMemberDeleteCall)(k,O,e.user_id),W.Z.success("Organization member deleted successfully"),ed(!1),ei.resetFields(),e_()}catch(e){W.Z.fromBackend("Failed to delete organization member"),console.error("Error deleting organization member:",e)}},ev=async e=>{try{if(!k)return;let l={organization_id:O,organization_alias:e.organization_alias,models:e.models,litellm_budget_table:{tpm_limit:e.tpm_limit,rpm_limit:e.rpm_limit,max_budget:e.max_budget,budget_duration:e.budget_duration},metadata:e.metadata?JSON.parse(e.metadata):null};if((void 0!==e.vector_stores||void 0!==e.mcp_servers_and_groups)&&(l.object_permission={...null==X?void 0:X.object_permission,vector_stores:e.vector_stores||[]},void 0!==e.mcp_servers_and_groups)){let{servers:s,accessGroups:i}=e.mcp_servers_and_groups||{servers:[],accessGroups:[]};s&&s.length>0&&(l.object_permission.mcp_servers=s),i&&i.length>0&&(l.object_permission.mcp_access_groups=i)}await (0,$.organizationUpdateCall)(k,l),W.Z.success("Organization settings updated successfully"),er(!1),e_()}catch(e){W.Z.fromBackend("Failed to update organization settings"),console.error("Error updating organization:",e)}};if(el)return(0,i.jsx)("div",{className:"p-4",children:"Loading..."});if(!X)return(0,i.jsx)("div",{className:"p-4",children:"Organization not found"});let eZ=async(e,l)=>{await (0,F.vQ)(e)&&(ex(e=>({...e,[l]:!0})),setTimeout(()=>{ex(e=>({...e,[l]:!1}))},2e3))};return(0,i.jsxs)("div",{className:"w-full h-screen p-4 bg-white",children:[(0,i.jsx)("div",{className:"flex justify-between items-center mb-6",children:(0,i.jsxs)("div",{children:[(0,i.jsx)(t.Z,{icon:L.Z,onClick:M,variant:"light",className:"mb-4",children:"Back to Organizations"}),(0,i.jsx)(U.Z,{children:X.organization_alias}),(0,i.jsxs)("div",{className:"flex items-center cursor-pointer",children:[(0,i.jsx)(f.Z,{className:"text-gray-500 font-mono",children:X.organization_id}),(0,i.jsx)(E.ZP,{type:"text",size:"small",icon:eu["org-id"]?(0,i.jsx)(B.Z,{size:12}):(0,i.jsx)(V.Z,{size:12}),onClick:()=>eZ(X.organization_id,"org-id"),className:"left-2 z-10 transition-all duration-200 ".concat(eu["org-id"]?"text-green-600 bg-green-50 border-green-200":"text-gray-500 hover:text-gray-700 hover:bg-gray-100")})]})]})}),(0,i.jsxs)(u.Z,{defaultIndex:K?2:0,children:[(0,i.jsxs)(x.Z,{className:"mb-4",children:[(0,i.jsx)(m.Z,{children:"Overview"}),(0,i.jsx)(m.Z,{children:"Members"}),(0,i.jsx)(m.Z,{children:"Settings"})]}),(0,i.jsxs)(_.Z,{children:[(0,i.jsx)(h.Z,{children:(0,i.jsxs)(d.Z,{numItems:1,numItemsSm:2,numItemsLg:3,className:"gap-6",children:[(0,i.jsxs)(n.Z,{children:[(0,i.jsx)(f.Z,{children:"Organization Details"}),(0,i.jsxs)("div",{className:"mt-2",children:[(0,i.jsxs)(f.Z,{children:["Created: ",new Date(X.created_at).toLocaleDateString()]}),(0,i.jsxs)(f.Z,{children:["Updated: ",new Date(X.updated_at).toLocaleDateString()]}),(0,i.jsxs)(f.Z,{children:["Created By: ",X.created_by]})]})]}),(0,i.jsxs)(n.Z,{children:[(0,i.jsx)(f.Z,{children:"Budget Status"}),(0,i.jsxs)("div",{className:"mt-2",children:[(0,i.jsxs)(U.Z,{children:["$",(0,F.pw)(X.spend,4)]}),(0,i.jsxs)(f.Z,{children:["of"," ",null===X.litellm_budget_table.max_budget?"Unlimited":"$".concat((0,F.pw)(X.litellm_budget_table.max_budget,4))]}),X.litellm_budget_table.budget_duration&&(0,i.jsxs)(f.Z,{className:"text-gray-500",children:["Reset: ",X.litellm_budget_table.budget_duration]})]})]}),(0,i.jsxs)(n.Z,{children:[(0,i.jsx)(f.Z,{children:"Rate Limits"}),(0,i.jsxs)("div",{className:"mt-2",children:[(0,i.jsxs)(f.Z,{children:["TPM: ",X.litellm_budget_table.tpm_limit||"Unlimited"]}),(0,i.jsxs)(f.Z,{children:["RPM: ",X.litellm_budget_table.rpm_limit||"Unlimited"]}),X.litellm_budget_table.max_parallel_requests&&(0,i.jsxs)(f.Z,{children:["Max Parallel Requests: ",X.litellm_budget_table.max_parallel_requests]})]})]}),(0,i.jsxs)(n.Z,{children:[(0,i.jsx)(f.Z,{children:"Models"}),(0,i.jsx)("div",{className:"mt-2 flex flex-wrap gap-2",children:0===X.models.length?(0,i.jsx)(r.Z,{color:"red",children:"All proxy models"}):X.models.map((e,l)=>(0,i.jsx)(r.Z,{color:"red",children:e},l))})]}),(0,i.jsxs)(n.Z,{children:[(0,i.jsx)(f.Z,{children:"Teams"}),(0,i.jsx)("div",{className:"mt-2 flex flex-wrap gap-2",children:null===(l=X.teams)||void 0===l?void 0:l.map((e,l)=>(0,i.jsx)(r.Z,{color:"red",children:e.team_id},l))})]}),(0,i.jsx)(J.Z,{objectPermission:X.object_permission,variant:"card",accessToken:k})]})}),(0,i.jsx)(h.Z,{children:(0,i.jsxs)("div",{className:"space-y-4",children:[(0,i.jsx)(n.Z,{className:"w-full mx-auto flex-auto overflow-y-auto max-h-[75vh]",children:(0,i.jsxs)(g.Z,{children:[(0,i.jsx)(v.Z,{children:(0,i.jsxs)(b.Z,{children:[(0,i.jsx)(Z.Z,{children:"User ID"}),(0,i.jsx)(Z.Z,{children:"Role"}),(0,i.jsx)(Z.Z,{children:"Spend"}),(0,i.jsx)(Z.Z,{children:"Created At"}),(0,i.jsx)(Z.Z,{})]})}),(0,i.jsx)(j.Z,{children:null===(s=X.members)||void 0===s?void 0:s.map((e,l)=>(0,i.jsxs)(b.Z,{children:[(0,i.jsx)(p.Z,{children:(0,i.jsx)(f.Z,{className:"font-mono",children:e.user_id})}),(0,i.jsx)(p.Z,{children:(0,i.jsx)(f.Z,{className:"font-mono",children:e.user_role})}),(0,i.jsx)(p.Z,{children:(0,i.jsxs)(f.Z,{children:["$",(0,F.pw)(e.spend,4)]})}),(0,i.jsx)(p.Z,{children:(0,i.jsx)(f.Z,{children:new Date(e.created_at).toLocaleString()})}),(0,i.jsx)(p.Z,{children:eh&&(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(c.Z,{icon:P.Z,size:"sm",onClick:()=>{em({role:e.user_role,user_email:e.user_email,user_id:e.user_id}),ed(!0)}}),(0,i.jsx)(c.Z,{icon:A.Z,size:"sm",onClick:()=>{ep(e)}})]})})]},l))})]})}),eh&&(0,i.jsx)(t.Z,{onClick:()=>{en(!0)},children:"Add Member"})]})}),(0,i.jsx)(h.Z,{children:(0,i.jsxs)(n.Z,{className:"overflow-y-auto max-h-[65vh]",children:[(0,i.jsxs)("div",{className:"flex justify-between items-center mb-4",children:[(0,i.jsx)(U.Z,{children:"Organization Settings"}),eh&&!ea&&(0,i.jsx)(t.Z,{onClick:()=>er(!0),children:"Edit Settings"})]}),ea?(0,i.jsxs)(y.Z,{form:ei,onFinish:ev,initialValues:{organization_alias:X.organization_alias,models:X.models,tpm_limit:X.litellm_budget_table.tpm_limit,rpm_limit:X.litellm_budget_table.rpm_limit,max_budget:X.litellm_budget_table.max_budget,budget_duration:X.litellm_budget_table.budget_duration,metadata:X.metadata?JSON.stringify(X.metadata,null,2):"",vector_stores:(null===(o=X.object_permission)||void 0===o?void 0:o.vector_stores)||[],mcp_servers_and_groups:{servers:(null===(N=X.object_permission)||void 0===N?void 0:N.mcp_servers)||[],accessGroups:(null===(C=X.object_permission)||void 0===C?void 0:C.mcp_access_groups)||[]}},layout:"vertical",children:[(0,i.jsx)(y.Z.Item,{label:"Organization Name",name:"organization_alias",rules:[{required:!0,message:"Please input an organization name"}],children:(0,i.jsx)(R.Z,{})}),(0,i.jsx)(y.Z.Item,{label:"Models",name:"models",children:(0,i.jsxs)(S.default,{mode:"multiple",placeholder:"Select models",children:[(0,i.jsx)(S.default.Option,{value:"all-proxy-models",children:"All Proxy Models"},"all-proxy-models"),H.map(e=>(0,i.jsx)(S.default.Option,{value:e,children:(0,D.W0)(e)},e))]})}),(0,i.jsx)(y.Z.Item,{label:"Max Budget (USD)",name:"max_budget",children:(0,i.jsx)(w.Z,{step:.01,precision:2,style:{width:"100%"}})}),(0,i.jsx)(y.Z.Item,{label:"Reset Budget",name:"budget_duration",children:(0,i.jsxs)(S.default,{placeholder:"n/a",children:[(0,i.jsx)(S.default.Option,{value:"24h",children:"daily"}),(0,i.jsx)(S.default.Option,{value:"7d",children:"weekly"}),(0,i.jsx)(S.default.Option,{value:"30d",children:"monthly"})]})}),(0,i.jsx)(y.Z.Item,{label:"Tokens per minute Limit (TPM)",name:"tpm_limit",children:(0,i.jsx)(w.Z,{step:1,style:{width:"100%"}})}),(0,i.jsx)(y.Z.Item,{label:"Requests per minute Limit (RPM)",name:"rpm_limit",children:(0,i.jsx)(w.Z,{step:1,style:{width:"100%"}})}),(0,i.jsx)(y.Z.Item,{label:"Vector Stores",name:"vector_stores",children:(0,i.jsx)(Y.Z,{onChange:e=>ei.setFieldValue("vector_stores",e),value:ei.getFieldValue("vector_stores"),accessToken:k||"",placeholder:"Select vector stores"})}),(0,i.jsx)(y.Z.Item,{label:"MCP Servers & Access Groups",name:"mcp_servers_and_groups",children:(0,i.jsx)(G.Z,{onChange:e=>ei.setFieldValue("mcp_servers_and_groups",e),value:ei.getFieldValue("mcp_servers_and_groups"),accessToken:k||"",placeholder:"Select MCP servers and access groups"})}),(0,i.jsx)(y.Z.Item,{label:"Metadata",name:"metadata",children:(0,i.jsx)(z.default.TextArea,{rows:4})}),(0,i.jsx)("div",{className:"sticky z-10 bg-white p-4 border-t border-gray-200 bottom-[-1.5rem] inset-x-[-1.5rem]",children:(0,i.jsxs)("div",{className:"flex justify-end items-center gap-2",children:[(0,i.jsx)(t.Z,{variant:"secondary",onClick:()=>er(!1),children:"Cancel"}),(0,i.jsx)(t.Z,{type:"submit",children:"Save Changes"})]})})]}):(0,i.jsxs)("div",{className:"space-y-4",children:[(0,i.jsxs)("div",{children:[(0,i.jsx)(f.Z,{className:"font-medium",children:"Organization Name"}),(0,i.jsx)("div",{children:X.organization_alias})]}),(0,i.jsxs)("div",{children:[(0,i.jsx)(f.Z,{className:"font-medium",children:"Organization ID"}),(0,i.jsx)("div",{className:"font-mono",children:X.organization_id})]}),(0,i.jsxs)("div",{children:[(0,i.jsx)(f.Z,{className:"font-medium",children:"Created At"}),(0,i.jsx)("div",{children:new Date(X.created_at).toLocaleString()})]}),(0,i.jsxs)("div",{children:[(0,i.jsx)(f.Z,{className:"font-medium",children:"Models"}),(0,i.jsx)("div",{className:"flex flex-wrap gap-2 mt-1",children:X.models.map((e,l)=>(0,i.jsx)(r.Z,{color:"red",children:e},l))})]}),(0,i.jsxs)("div",{children:[(0,i.jsx)(f.Z,{className:"font-medium",children:"Rate Limits"}),(0,i.jsxs)("div",{children:["TPM: ",X.litellm_budget_table.tpm_limit||"Unlimited"]}),(0,i.jsxs)("div",{children:["RPM: ",X.litellm_budget_table.rpm_limit||"Unlimited"]})]}),(0,i.jsxs)("div",{children:[(0,i.jsx)(f.Z,{className:"font-medium",children:"Budget"}),(0,i.jsxs)("div",{children:["Max:"," ",null!==X.litellm_budget_table.max_budget?"$".concat((0,F.pw)(X.litellm_budget_table.max_budget,4)):"No Limit"]}),(0,i.jsxs)("div",{children:["Reset: ",X.litellm_budget_table.budget_duration||"Never"]})]}),(0,i.jsx)(J.Z,{objectPermission:X.object_permission,variant:"inline",className:"pt-4 border-t border-gray-200",accessToken:k})]})]})})]})]}),(0,i.jsx)(q.Z,{isVisible:et,onCancel:()=>en(!1),onSubmit:eg,accessToken:k,title:"Add Organization Member",roles:[{label:"org_admin",value:"org_admin",description:"Can add and remove members, and change their roles."},{label:"internal_user",value:"internal_user",description:"Can view/create keys for themselves within organization."},{label:"internal_user_viewer",value:"internal_user_viewer",description:"Can only view their keys within organization."}],defaultRole:"internal_user"}),(0,i.jsx)(Q.Z,{visible:eo,onCancel:()=>ed(!1),onSubmit:ej,initialData:ec,mode:"edit",config:{title:"Edit Member",showEmail:!0,showUserId:!0,roleOptions:[{label:"Org Admin",value:"org_admin"},{label:"Internal User",value:"internal_user"},{label:"Internal User Viewer",value:"internal_user_viewer"}]}})]})},K=s(21609);let X=async(e,l)=>{l(await (0,$.organizationListCall)(e))};var ee=e=>{let{organizations:l,userRole:s,userModels:L,accessToken:R,lastRefreshed:U,handleRefreshClick:E,currentOrg:B,guardrailsList:V=[],setOrganizations:q,premiumUser:J}=e,[Q,ee]=(0,a.useState)(null),[el,es]=(0,a.useState)(!1),[ei,ea]=(0,a.useState)(!1),[er,et]=(0,a.useState)(null),[en,eo]=(0,a.useState)(!1),[ed,ec]=(0,a.useState)(!1),[em]=y.Z.useForm(),[eu,ex]=(0,a.useState)({});(0,a.useEffect)(()=>{R&&X(R,q)},[R]);let eh=e=>{e&&(et(e),ea(!0))},e_=async()=>{if(er&&R)try{eo(!0),await (0,$.organizationDeleteCall)(R,er),W.Z.success("Organization deleted successfully"),ea(!1),et(null),await X(R,q)}catch(e){console.error("Error deleting organization:",e)}finally{eo(!1)}},eg=async e=>{try{var l,s,i,a;if(!R)return;console.log("values in organizations new create call: ".concat(JSON.stringify(e))),(e.allowed_vector_store_ids&&e.allowed_vector_store_ids.length>0||e.allowed_mcp_servers_and_groups&&((null===(l=e.allowed_mcp_servers_and_groups.servers)||void 0===l?void 0:l.length)>0||(null===(s=e.allowed_mcp_servers_and_groups.accessGroups)||void 0===s?void 0:s.length)>0))&&(e.object_permission={},e.allowed_vector_store_ids&&e.allowed_vector_store_ids.length>0&&(e.object_permission.vector_stores=e.allowed_vector_store_ids,delete e.allowed_vector_store_ids),e.allowed_mcp_servers_and_groups&&((null===(i=e.allowed_mcp_servers_and_groups.servers)||void 0===i?void 0:i.length)>0&&(e.object_permission.mcp_servers=e.allowed_mcp_servers_and_groups.servers),(null===(a=e.allowed_mcp_servers_and_groups.accessGroups)||void 0===a?void 0:a.length)>0&&(e.object_permission.mcp_access_groups=e.allowed_mcp_servers_and_groups.accessGroups),delete e.allowed_mcp_servers_and_groups)),await (0,$.organizationCreateCall)(R,e),W.Z.success("Organization created successfully"),ec(!1),em.resetFields(),X(R,q)}catch(e){console.error("Error creating organization:",e)}};return J?(0,i.jsxs)("div",{className:"w-full mx-4 h-[75vh]",children:[(0,i.jsx)(d.Z,{numItems:1,className:"gap-2 p-8 w-full mt-2",children:(0,i.jsxs)(o.Z,{numColSpan:1,className:"flex flex-col gap-2",children:[("Admin"===s||"Org Admin"===s)&&(0,i.jsx)(t.Z,{className:"w-fit",onClick:()=>ec(!0),children:"+ Create New Organization"}),Q?(0,i.jsx)(H,{organizationId:Q,onClose:()=>{ee(null),es(!1)},accessToken:R,is_org_admin:!0,is_proxy_admin:"Admin"===s,userModels:L,editOrg:el}):(0,i.jsxs)(u.Z,{className:"gap-2 h-[75vh] w-full",children:[(0,i.jsxs)(x.Z,{className:"flex justify-between mt-2 w-full items-center",children:[(0,i.jsx)("div",{className:"flex",children:(0,i.jsx)(m.Z,{children:"Your Organizations"})}),(0,i.jsxs)("div",{className:"flex items-center space-x-2",children:[U&&(0,i.jsxs)(f.Z,{children:["Last Refreshed: ",U]}),(0,i.jsx)(c.Z,{icon:M.Z,variant:"shadow",size:"xs",className:"self-center",onClick:E})]})]}),(0,i.jsx)(_.Z,{children:(0,i.jsxs)(h.Z,{children:[(0,i.jsx)(f.Z,{children:"Click on ā€œOrganization IDā€ to view organization details."}),(0,i.jsx)(d.Z,{numItems:1,className:"gap-2 pt-2 pb-2 h-[75vh] w-full mt-2",children:(0,i.jsx)(o.Z,{numColSpan:1,children:(0,i.jsx)(n.Z,{className:"w-full mx-auto flex-auto overflow-y-auto max-h-[50vh]",children:(0,i.jsxs)(g.Z,{children:[(0,i.jsx)(v.Z,{children:(0,i.jsxs)(b.Z,{children:[(0,i.jsx)(Z.Z,{children:"Organization ID"}),(0,i.jsx)(Z.Z,{children:"Organization Name"}),(0,i.jsx)(Z.Z,{children:"Created"}),(0,i.jsx)(Z.Z,{children:"Spend (USD)"}),(0,i.jsx)(Z.Z,{children:"Budget (USD)"}),(0,i.jsx)(Z.Z,{children:"Models"}),(0,i.jsx)(Z.Z,{children:"TPM / RPM Limits"}),(0,i.jsx)(Z.Z,{children:"Info"}),(0,i.jsx)(Z.Z,{children:"Actions"})]})}),(0,i.jsx)(j.Z,{children:l&&l.length>0?l.sort((e,l)=>new Date(l.created_at).getTime()-new Date(e.created_at).getTime()).map(e=>{var l,a,n,o,d,m,u,x,h;return(0,i.jsxs)(b.Z,{children:[(0,i.jsx)(p.Z,{children:(0,i.jsx)("div",{className:"overflow-hidden",children:(0,i.jsx)(N.Z,{title:e.organization_id,children:(0,i.jsxs)(t.Z,{size:"xs",variant:"light",className:"font-mono text-blue-500 bg-blue-50 hover:bg-blue-100 text-xs font-normal px-2 py-0.5 text-left overflow-hidden truncate max-w-[200px]",onClick:()=>ee(e.organization_id),children:[null===(l=e.organization_id)||void 0===l?void 0:l.slice(0,7),"..."]})})})}),(0,i.jsx)(p.Z,{children:e.organization_alias}),(0,i.jsx)(p.Z,{children:e.created_at?new Date(e.created_at).toLocaleDateString():"N/A"}),(0,i.jsx)(p.Z,{children:(0,F.pw)(e.spend,4)}),(0,i.jsx)(p.Z,{children:(null===(a=e.litellm_budget_table)||void 0===a?void 0:a.max_budget)!==null&&(null===(n=e.litellm_budget_table)||void 0===n?void 0:n.max_budget)!==void 0?null===(o=e.litellm_budget_table)||void 0===o?void 0:o.max_budget:"No limit"}),(0,i.jsx)(p.Z,{style:{maxWidth:"8-x",whiteSpace:"pre-wrap",overflow:"hidden"},className:e.models.length>3?"px-0":"",children:(0,i.jsx)("div",{className:"flex flex-col",children:Array.isArray(e.models)?(0,i.jsx)("div",{className:"flex flex-col",children:0===e.models.length?(0,i.jsx)(r.Z,{size:"xs",className:"mb-1",color:"red",children:(0,i.jsx)(f.Z,{children:"All Proxy Models"})}):(0,i.jsx)(i.Fragment,{children:(0,i.jsxs)("div",{className:"flex items-start",children:[e.models.length>3&&(0,i.jsx)("div",{children:(0,i.jsx)(c.Z,{icon:eu[e.organization_id||""]?k.Z:I.Z,className:"cursor-pointer",size:"xs",onClick:()=>{ex(l=>({...l,[e.organization_id||""]:!l[e.organization_id||""]}))}})}),(0,i.jsxs)("div",{className:"flex flex-wrap gap-1",children:[e.models.slice(0,3).map((e,l)=>"all-proxy-models"===e?(0,i.jsx)(r.Z,{size:"xs",color:"red",children:(0,i.jsx)(f.Z,{children:"All Proxy Models"})},l):(0,i.jsx)(r.Z,{size:"xs",color:"blue",children:(0,i.jsx)(f.Z,{children:e.length>30?"".concat((0,D.W0)(e).slice(0,30),"..."):(0,D.W0)(e)})},l)),e.models.length>3&&!eu[e.organization_id||""]&&(0,i.jsx)(r.Z,{size:"xs",color:"gray",className:"cursor-pointer",children:(0,i.jsxs)(f.Z,{children:["+",e.models.length-3," ",e.models.length-3==1?"more model":"more models"]})}),eu[e.organization_id||""]&&(0,i.jsx)("div",{className:"flex flex-wrap gap-1",children:e.models.slice(3).map((e,l)=>"all-proxy-models"===e?(0,i.jsx)(r.Z,{size:"xs",color:"red",children:(0,i.jsx)(f.Z,{children:"All Proxy Models"})},l+3):(0,i.jsx)(r.Z,{size:"xs",color:"blue",children:(0,i.jsx)(f.Z,{children:e.length>30?"".concat((0,D.W0)(e).slice(0,30),"..."):(0,D.W0)(e)})},l+3))})]})]})})}):null})}),(0,i.jsx)(p.Z,{children:(0,i.jsxs)(f.Z,{children:["TPM:"," ",(null===(d=e.litellm_budget_table)||void 0===d?void 0:d.tpm_limit)?null===(m=e.litellm_budget_table)||void 0===m?void 0:m.tpm_limit:"Unlimited",(0,i.jsx)("br",{}),"RPM:"," ",(null===(u=e.litellm_budget_table)||void 0===u?void 0:u.rpm_limit)?null===(x=e.litellm_budget_table)||void 0===x?void 0:x.rpm_limit:"Unlimited"]})}),(0,i.jsx)(p.Z,{children:(0,i.jsxs)(f.Z,{children:[(null===(h=e.members)||void 0===h?void 0:h.length)||0," Members"]})}),(0,i.jsx)(p.Z,{children:"Admin"===s&&(0,i.jsxs)(i.Fragment,{children:[(0,i.jsxs)(N.Z,{title:"Edit organization",children:[" ",(0,i.jsx)(c.Z,{icon:P.Z,size:"sm",className:"cursor-pointer hover:text-blue-600",onClick:()=>{ee(e.organization_id),es(!0)}})]}),(0,i.jsxs)(N.Z,{title:"Delete organization",children:[" ",(0,i.jsx)(c.Z,{onClick:()=>eh(e.organization_id),icon:A.Z,size:"sm",className:"cursor-pointer hover:text-red-600"})]})]})})]},e.organization_id)}):null})]})})})})]})})]})]})}),(0,i.jsx)(C.Z,{title:"Create Organization",visible:ed,width:800,footer:null,onCancel:()=>{ec(!1),em.resetFields()},children:(0,i.jsxs)(y.Z,{form:em,onFinish:eg,labelCol:{span:8},wrapperCol:{span:16},labelAlign:"left",children:[(0,i.jsx)(y.Z.Item,{label:"Organization Name",name:"organization_alias",rules:[{required:!0,message:"Please input an organization name"}],children:(0,i.jsx)(T.o,{placeholder:""})}),(0,i.jsx)(y.Z.Item,{label:"Models",name:"models",children:(0,i.jsxs)(S.default,{mode:"multiple",placeholder:"Select models",style:{width:"100%"},children:[(0,i.jsx)(S.default.Option,{value:"all-proxy-models",children:"All Proxy Models"},"all-proxy-models"),L&&L.length>0&&L.map(e=>(0,i.jsx)(S.default.Option,{value:e,children:(0,D.W0)(e)},e))]})}),(0,i.jsx)(y.Z.Item,{label:"Max Budget (USD)",name:"max_budget",children:(0,i.jsx)(w.Z,{step:.01,precision:2,width:200})}),(0,i.jsx)(y.Z.Item,{label:"Reset Budget",name:"budget_duration",children:(0,i.jsxs)(S.default,{defaultValue:null,placeholder:"n/a",children:[(0,i.jsx)(S.default.Option,{value:"24h",children:"daily"}),(0,i.jsx)(S.default.Option,{value:"7d",children:"weekly"}),(0,i.jsx)(S.default.Option,{value:"30d",children:"monthly"})]})}),(0,i.jsx)(y.Z.Item,{label:"Tokens per minute Limit (TPM)",name:"tpm_limit",children:(0,i.jsx)(w.Z,{step:1,width:400})}),(0,i.jsx)(y.Z.Item,{label:"Requests per minute Limit (RPM)",name:"rpm_limit",children:(0,i.jsx)(w.Z,{step:1,width:400})}),(0,i.jsx)(y.Z.Item,{label:(0,i.jsxs)("span",{children:["Allowed Vector Stores"," ",(0,i.jsx)(N.Z,{title:"Select which vector stores this organization can access by default. Leave empty for access to all vector stores",children:(0,i.jsx)(O.Z,{style:{marginLeft:"4px"}})})]}),name:"allowed_vector_store_ids",className:"mt-4",help:"Select vector stores this organization can access. Leave empty for access to all vector stores",children:(0,i.jsx)(Y.Z,{onChange:e=>em.setFieldValue("allowed_vector_store_ids",e),value:em.getFieldValue("allowed_vector_store_ids"),accessToken:R||"",placeholder:"Select vector stores (optional)"})}),(0,i.jsx)(y.Z.Item,{label:(0,i.jsxs)("span",{children:["Allowed MCP Servers"," ",(0,i.jsx)(N.Z,{title:"Select which MCP servers and access groups this organization can access by default.",children:(0,i.jsx)(O.Z,{style:{marginLeft:"4px"}})})]}),name:"allowed_mcp_servers_and_groups",className:"mt-4",help:"Select MCP servers and access groups this organization can access.",children:(0,i.jsx)(G.Z,{onChange:e=>em.setFieldValue("allowed_mcp_servers_and_groups",e),value:em.getFieldValue("allowed_mcp_servers_and_groups"),accessToken:R||"",placeholder:"Select MCP servers and access groups (optional)"})}),(0,i.jsx)(y.Z.Item,{label:"Metadata",name:"metadata",children:(0,i.jsx)(z.default.TextArea,{rows:4})}),(0,i.jsx)("div",{style:{textAlign:"right",marginTop:"10px"},children:(0,i.jsx)(t.Z,{type:"submit",children:"Create Organization"})})]})}),(0,i.jsx)(K.Z,{isOpen:ei,title:"Delete Organization?",message:"Are you sure you want to delete this organization? This action cannot be undone.",resourceInformationTitle:"Organization Information",resourceInformation:[{label:"Organization ID",value:er,code:!0}],onCancel:()=>{ea(!1),et(null)},onOk:e_,confirmLoading:en})]}):(0,i.jsx)("div",{children:(0,i.jsxs)(f.Z,{children:["This is a LiteLLM Enterprise feature, and requires a valid key to use. Get a trial key"," ",(0,i.jsx)("a",{href:"https://www.litellm.ai/#pricing",target:"_blank",rel:"noopener noreferrer",children:"here"}),"."]})})}}}]); \ No newline at end of file diff --git a/litellm/proxy/_experimental/out/_next/static/chunks/2012-9200c205d5b0405a.js b/litellm/proxy/_experimental/out/_next/static/chunks/2012-9200c205d5b0405a.js deleted file mode 100644 index 600ee05936e..00000000000 --- a/litellm/proxy/_experimental/out/_next/static/chunks/2012-9200c205d5b0405a.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[2012],{26210:function(e,l,s){s.d(l,{UQ:function(){return t.Z},X1:function(){return i.Z},_m:function(){return a.Z},oi:function(){return n.Z},xv:function(){return r.Z}});var t=s(87452),i=s(88829),a=s(72208),r=s(84264),n=s(49566)},30078:function(e,l,s){s.d(l,{Ct:function(){return t.Z},Dx:function(){return x.Z},OK:function(){return n.Z},Zb:function(){return a.Z},nP:function(){return c.Z},oi:function(){return h.Z},rj:function(){return r.Z},td:function(){return d.Z},v0:function(){return m.Z},x4:function(){return o.Z},xv:function(){return u.Z},zx:function(){return i.Z}});var t=s(41649),i=s(78489),a=s(12514),r=s(67101),n=s(12485),m=s(18135),d=s(35242),o=s(29706),c=s(77991),u=s(84264),h=s(49566),x=s(96761)},62490:function(e,l,s){s.d(l,{Ct:function(){return t.Z},RM:function(){return n.Z},SC:function(){return c.Z},Zb:function(){return a.Z},iA:function(){return r.Z},pj:function(){return m.Z},ss:function(){return d.Z},xs:function(){return o.Z},xv:function(){return u.Z},zx:function(){return i.Z}});var t=s(41649),i=s(78489),a=s(12514),r=s(21626),n=s(97214),m=s(28241),d=s(58834),o=s(69552),c=s(71876),u=s(84264)},11318:function(e,l,s){s.d(l,{Z:function(){return n}});var t=s(2265),i=s(80443),a=s(19250);let r=async(e,l,s,t)=>"Admin"!=s&&"Admin Viewer"!=s?await (0,a.teamListCall)(e,(null==t?void 0:t.organization_id)||null,l):await (0,a.teamListCall)(e,(null==t?void 0:t.organization_id)||null);var n=()=>{let[e,l]=(0,t.useState)([]),{accessToken:s,userId:a,userRole:n}=(0,i.Z)();return(0,t.useEffect)(()=>{(async()=>{l(await r(s,a,n,null))})()},[s,a,n]),{teams:e,setTeams:l}}},21609:function(e,l,s){s.d(l,{Z:function(){return o}});var t=s(57437),i=s(57840),a=s(22116),r=s(51653),n=s(76188),m=s(4260),d=s(2265);function o(e){let{isOpen:l,title:s,alertMessage:o,message:c,resourceInformationTitle:u,resourceInformation:h,onCancel:x,onOk:b,confirmLoading:p,requiredConfirmation:g}=e,{Title:_,Text:v}=i.default,[j,f]=(0,d.useState)("");return(0,d.useEffect)(()=>{l&&f("")},[l]),(0,t.jsx)(a.Z,{title:s,open:l,onOk:b,onCancel:x,confirmLoading:p,okText:p?"Deleting...":"Delete",cancelText:"Cancel",okButtonProps:{danger:!0,disabled:!!g&&j!==g||p},cancelButtonProps:{disabled:p},children:(0,t.jsxs)("div",{className:"space-y-4",children:[o&&(0,t.jsx)(r.Z,{message:o,type:"warning"}),(0,t.jsxs)("div",{className:"mt-4 p-4 bg-red-50 rounded-lg border border-red-200",children:[(0,t.jsx)(_,{level:5,className:"mb-3 text-gray-900",children:u}),(0,t.jsx)(n.Z,{column:1,size:"small",children:h&&h.map(e=>{let{label:l,value:s,...i}=e;return(0,t.jsx)(n.Z.Item,{label:(0,t.jsx)("span",{className:"font-semibold text-gray-700",children:l}),children:(0,t.jsx)(v,{...i,children:null!=s?s:"-"})},l)})})]}),(0,t.jsx)("div",{children:(0,t.jsx)(v,{children:c})}),g&&(0,t.jsxs)("div",{className:"mb-6 mt-4 pt-4 border-t border-gray-200",children:[(0,t.jsxs)(v,{className:"block text-base font-medium text-gray-700 mb-2",children:[(0,t.jsx)(v,{children:"Type "}),(0,t.jsx)(v,{strong:!0,type:"danger",children:g}),(0,t.jsx)(v,{children:" to confirm deletion:"})]}),(0,t.jsx)(m.default,{value:j,onChange:e=>f(e.target.value),placeholder:g,className:"rounded-md text-base border-gray-200",autoFocus:!0})]})]})})}},33860:function(e,l,s){var t=s(57437),i=s(2265),a=s(10032),r=s(22116),n=s(37592),m=s(99981),d=s(5545),o=s(7310),c=s.n(o),u=s(19250);l.Z=e=>{let{isVisible:l,onCancel:s,onSubmit:o,accessToken:h,title:x="Add Team Member",roles:b=[{label:"admin",value:"admin",description:"Admin role. Can create team keys, add members, and manage settings."},{label:"user",value:"user",description:"User role. Can view team info, but not manage it."}],defaultRole:p="user"}=e,[g]=a.Z.useForm(),[_,v]=(0,i.useState)([]),[j,f]=(0,i.useState)(!1),[Z,y]=(0,i.useState)("user_email"),N=async(e,l)=>{if(!e){v([]);return}f(!0);try{let s=new URLSearchParams;if(s.append(l,e),null==h)return;let t=(await (0,u.userFilterUICall)(h,s)).map(e=>({label:"user_email"===l?"".concat(e.user_email):"".concat(e.user_id),value:"user_email"===l?e.user_email:e.user_id,user:e}));v(t)}catch(e){console.error("Error fetching users:",e)}finally{f(!1)}},w=(0,i.useCallback)(c()((e,l)=>N(e,l),300),[]),k=(e,l)=>{y(l),w(e,l)},M=(e,l)=>{let s=l.user;g.setFieldsValue({user_email:s.user_email,user_id:s.user_id,role:g.getFieldValue("role")})};return(0,t.jsx)(r.Z,{title:x,open:l,onCancel:()=>{g.resetFields(),v([]),s()},footer:null,width:800,children:(0,t.jsxs)(a.Z,{form:g,onFinish:o,labelCol:{span:8},wrapperCol:{span:16},labelAlign:"left",initialValues:{role:p},children:[(0,t.jsx)(a.Z.Item,{label:"Email",name:"user_email",className:"mb-4",children:(0,t.jsx)(n.default,{showSearch:!0,className:"w-full",placeholder:"Search by email",filterOption:!1,onSearch:e=>k(e,"user_email"),onSelect:(e,l)=>M(e,l),options:"user_email"===Z?_:[],loading:j,allowClear:!0})}),(0,t.jsx)("div",{className:"text-center mb-4",children:"OR"}),(0,t.jsx)(a.Z.Item,{label:"User ID",name:"user_id",className:"mb-4",children:(0,t.jsx)(n.default,{showSearch:!0,className:"w-full",placeholder:"Search by user ID",filterOption:!1,onSearch:e=>k(e,"user_id"),onSelect:(e,l)=>M(e,l),options:"user_id"===Z?_:[],loading:j,allowClear:!0})}),(0,t.jsx)(a.Z.Item,{label:"Member Role",name:"role",className:"mb-4",children:(0,t.jsx)(n.default,{defaultValue:p,children:b.map(e=>(0,t.jsx)(n.default.Option,{value:e.value,children:(0,t.jsxs)(m.Z,{title:e.description,children:[(0,t.jsx)("span",{className:"font-medium",children:e.label}),(0,t.jsxs)("span",{className:"ml-2 text-gray-500 text-sm",children:["- ",e.description]})]})},e.value))})}),(0,t.jsx)("div",{className:"text-right mt-4",children:(0,t.jsx)(d.ZP,{type:"default",htmlType:"submit",children:"Add Member"})})]})})}},10901:function(e,l,s){s.d(l,{Z:function(){return h}});var t=s(57437),i=s(2265),a=s(10032),r=s(22116),n=s(5545),m=s(27281),d=s(57365),o=s(49566),c=s(92280),u=s(24199),h=e=>{var l,s,h;let{visible:x,onCancel:b,onSubmit:p,initialData:g,mode:_,config:v}=e,[j]=a.Z.useForm();console.log("Initial Data:",g),(0,i.useEffect)(()=>{if(x){if("edit"===_&&g){let e={...g,role:g.role||v.defaultRole,max_budget_in_team:g.max_budget_in_team||null,tpm_limit:g.tpm_limit||null,rpm_limit:g.rpm_limit||null};console.log("Setting form values:",e),j.setFieldsValue(e)}else{var e;j.resetFields(),j.setFieldsValue({role:v.defaultRole||(null===(e=v.roleOptions[0])||void 0===e?void 0:e.value)})}}},[x,g,_,j,v.defaultRole,v.roleOptions]);let f=async e=>{try{let l=Object.entries(e).reduce((e,l)=>{let[s,t]=l;if("string"==typeof t){let l=t.trim();return""===l&&("max_budget_in_team"===s||"tpm_limit"===s||"rpm_limit"===s)?{...e,[s]:null}:{...e,[s]:l}}return{...e,[s]:t}},{});console.log("Submitting form data:",l),p(l),j.resetFields()}catch(e){console.error("Form submission error:",e)}},Z=e=>{switch(e.type){case"input":return(0,t.jsx)(o.Z,{placeholder:e.placeholder});case"numerical":return(0,t.jsx)(u.Z,{step:e.step||1,min:e.min||0,style:{width:"100%"},placeholder:e.placeholder||"Enter a numerical value"});case"select":var l;return(0,t.jsx)(m.Z,{children:null===(l=e.options)||void 0===l?void 0:l.map(e=>(0,t.jsx)(d.Z,{value:e.value,children:e.label},e.value))});default:return null}};return(0,t.jsx)(r.Z,{title:v.title||("add"===_?"Add Member":"Edit Member"),open:x,width:1e3,footer:null,onCancel:b,children:(0,t.jsxs)(a.Z,{form:j,onFinish:f,labelCol:{span:8},wrapperCol:{span:16},labelAlign:"left",children:[v.showEmail&&(0,t.jsx)(a.Z.Item,{label:"Email",name:"user_email",className:"mb-4",rules:[{type:"email",message:"Please enter a valid email!"}],children:(0,t.jsx)(o.Z,{placeholder:"user@example.com"})}),v.showEmail&&v.showUserId&&(0,t.jsx)("div",{className:"text-center mb-4",children:(0,t.jsx)(c.x,{children:"OR"})}),v.showUserId&&(0,t.jsx)(a.Z.Item,{label:"User ID",name:"user_id",className:"mb-4",children:(0,t.jsx)(o.Z,{placeholder:"user_123"})}),(0,t.jsx)(a.Z.Item,{label:(0,t.jsxs)("div",{className:"flex items-center gap-2",children:[(0,t.jsx)("span",{children:"Role"}),"edit"===_&&g&&(0,t.jsxs)("span",{className:"text-gray-500 text-sm",children:["(Current: ",(s=g.role,(null===(h=v.roleOptions.find(e=>e.value===s))||void 0===h?void 0:h.label)||s),")"]})]}),name:"role",className:"mb-4",rules:[{required:!0,message:"Please select a role!"}],children:(0,t.jsx)(m.Z,{children:"edit"===_&&g?[...v.roleOptions.filter(e=>e.value===g.role),...v.roleOptions.filter(e=>e.value!==g.role)].map(e=>(0,t.jsx)(d.Z,{value:e.value,children:e.label},e.value)):v.roleOptions.map(e=>(0,t.jsx)(d.Z,{value:e.value,children:e.label},e.value))})}),null===(l=v.additionalFields)||void 0===l?void 0:l.map(e=>(0,t.jsx)(a.Z.Item,{label:e.label,name:e.name,className:"mb-4",rules:e.rules,children:Z(e)},e.name)),(0,t.jsxs)("div",{className:"text-right mt-6",children:[(0,t.jsx)(n.ZP,{onClick:b,className:"mr-2",children:"Cancel"}),(0,t.jsx)(n.ZP,{type:"default",htmlType:"submit",children:"add"===_?"Add Member":"Save Changes"})]})]})})}},33293:function(e,l,s){s.d(l,{Z:function(){return el}});var t=s(57437),i=s(33860),a=s(19250),r=s(59872),n=s(33304),m=s(15424),d=s(10900),o=s(30078),c=s(10032),u=s(42264),h=s(5545),x=s(4260),b=s(37592),p=s(99981),g=s(63709),_=s(30401),v=s(78867),j=s(2265),f=s(21609),Z=s(95096),y=s(46468),N=s(27799),w=s(95920),k=s(68473),M=s(9114),C=s(98015),S=s(24199),T=s(97415),I=s(10901),P=s(21425),L=s(78489),F=s(12514),E=s(21626),O=s(97214),D=s(28241),A=s(58834),R=s(69552),U=s(71876),z=s(84264),B=s(96761),V=s(4156),q=s(85180),G=s(89245),K=s(78355);let $={"/key/generate":"Member can generate a virtual key for this team","/key/service-account/generate":"Member can generate a service account key (not belonging to any user) for this team","/key/update":"Member can update a virtual key belonging to this team","/key/delete":"Member can delete a virtual key belonging to this team","/key/info":"Member can get info about a virtual key belonging to this team","/key/regenerate":"Member can regenerate a virtual key belonging to this team","/key/{key_id}/regenerate":"Member can regenerate a virtual key belonging to this team","/key/list":"Member can list virtual keys belonging to this team","/key/block":"Member can block a virtual key belonging to this team","/key/unblock":"Member can unblock a virtual key belonging to this team"},J=e=>e.includes("/info")||e.includes("/list")?"GET":"POST",Q=e=>{let l=J(e),s=$[e];if(!s){for(let[l,t]of Object.entries($))if(e.includes(l)){s=t;break}}return s||(s="Access ".concat(e)),{method:l,endpoint:e,description:s,route:e}};var W=e=>{let{teamId:l,accessToken:s,canEditTeam:i}=e,[r,n]=(0,j.useState)([]),[m,d]=(0,j.useState)([]),[o,c]=(0,j.useState)(!0),[u,x]=(0,j.useState)(!1),[b,p]=(0,j.useState)(!1),g=async()=>{try{if(c(!0),!s)return;let e=await (0,a.getTeamPermissionsCall)(s,l),t=e.all_available_permissions||[];n(t);let i=e.team_member_permissions||[];d(i),p(!1)}catch(e){M.Z.fromBackend("Failed to load permissions"),console.error("Error fetching permissions:",e)}finally{c(!1)}};(0,j.useEffect)(()=>{g()},[l,s]);let _=(e,l)=>{d(l?[...m,e]:m.filter(l=>l!==e)),p(!0)},v=async()=>{try{if(!s)return;x(!0),await (0,a.teamPermissionsUpdateCall)(s,l,m),M.Z.success("Permissions updated successfully"),p(!1)}catch(e){M.Z.fromBackend("Failed to update permissions"),console.error("Error updating permissions:",e)}finally{x(!1)}};if(o)return(0,t.jsx)("div",{className:"p-6 text-center",children:"Loading permissions..."});let f=r.length>0;return(0,t.jsxs)(F.Z,{className:"bg-white shadow-md rounded-md p-6",children:[(0,t.jsxs)("div",{className:"flex flex-col sm:flex-row justify-between items-start sm:items-center border-b pb-4 mb-6",children:[(0,t.jsx)(B.Z,{className:"mb-2 sm:mb-0",children:"Member Permissions"}),i&&b&&(0,t.jsxs)("div",{className:"flex gap-3",children:[(0,t.jsx)(h.ZP,{icon:(0,t.jsx)(G.Z,{}),onClick:()=>{g()},children:"Reset"}),(0,t.jsxs)(L.Z,{onClick:v,loading:u,className:"flex items-center gap-2",children:[(0,t.jsx)(K.Z,{})," Save Changes"]})]})]}),(0,t.jsx)(z.Z,{className:"mb-6 text-gray-600",children:"Control what team members can do when they are not team admins."}),f?(0,t.jsx)("div",{className:"overflow-x-auto",children:(0,t.jsxs)(E.Z,{className:" min-w-full",children:[(0,t.jsx)(A.Z,{children:(0,t.jsxs)(U.Z,{children:[(0,t.jsx)(R.Z,{children:"Method"}),(0,t.jsx)(R.Z,{children:"Endpoint"}),(0,t.jsx)(R.Z,{children:"Description"}),(0,t.jsx)(R.Z,{className:"sticky right-0 bg-white shadow-[-4px_0_4px_-4px_rgba(0,0,0,0.1)] text-center",children:"Allow Access"})]})}),(0,t.jsx)(O.Z,{children:r.map(e=>{let l=Q(e);return(0,t.jsxs)(U.Z,{className:"hover:bg-gray-50 transition-colors",children:[(0,t.jsx)(D.Z,{children:(0,t.jsx)("span",{className:"px-2 py-1 rounded text-xs font-medium ".concat("GET"===l.method?"bg-blue-100 text-blue-800":"bg-green-100 text-green-800"),children:l.method})}),(0,t.jsx)(D.Z,{children:(0,t.jsx)("span",{className:"font-mono text-sm text-gray-800",children:l.endpoint})}),(0,t.jsx)(D.Z,{className:"text-gray-700",children:l.description}),(0,t.jsx)(D.Z,{className:"sticky right-0 bg-white shadow-[-4px_0_4px_-4px_rgba(0,0,0,0.1)] text-center",children:(0,t.jsx)(V.Z,{checked:m.includes(e),onChange:l=>_(e,l.target.checked),disabled:!i})})]},e)})})]})}):(0,t.jsx)("div",{className:"py-12",children:(0,t.jsx)(q.Z,{description:"No permissions available"})})]})},X=s(47323),Y=s(53410),H=s(74998),ee=e=>{let{teamData:l,canEditTeam:s,handleMemberDelete:i,setSelectedEditMember:a,setIsEditMemberModalVisible:n,setIsAddMemberModalVisible:d}=e,o=e=>{if(null==e)return"0";if("number"==typeof e){let l=Number(e);return l===Math.floor(l)?l.toString():(0,r.pw)(l,8).replace(/\.?0+$/,"")}return"0"},c=e=>{if(!e)return 0;let s=l.team_memberships.find(l=>l.user_id===e);return(null==s?void 0:s.spend)||0},u=e=>{var s;if(!e)return null;let t=l.team_memberships.find(l=>l.user_id===e);console.log("membership=".concat(t));let i=null==t?void 0:null===(s=t.litellm_budget_table)||void 0===s?void 0:s.max_budget;return null==i?null:o(i)},h=e=>{var s,t;if(!e)return"No Limits";let i=l.team_memberships.find(l=>l.user_id===e),a=null==i?void 0:null===(s=i.litellm_budget_table)||void 0===s?void 0:s.rpm_limit,r=null==i?void 0:null===(t=i.litellm_budget_table)||void 0===t?void 0:t.tpm_limit,n=[a?"".concat(o(a)," RPM"):null,r?"".concat(o(r)," TPM"):null].filter(Boolean);return n.length>0?n.join(" / "):"No Limits"};return(0,t.jsxs)("div",{className:"space-y-4",children:[(0,t.jsx)(F.Z,{className:"w-full mx-auto flex-auto overflow-auto max-h-[50vh]",children:(0,t.jsx)("div",{className:"overflow-x-auto",children:(0,t.jsxs)(E.Z,{className:"min-w-full",children:[(0,t.jsx)(A.Z,{children:(0,t.jsxs)(U.Z,{children:[(0,t.jsx)(R.Z,{children:"User ID"}),(0,t.jsx)(R.Z,{children:"User Email"}),(0,t.jsx)(R.Z,{children:"Role"}),(0,t.jsxs)(R.Z,{children:["Team Member Spend (USD)"," ",(0,t.jsx)(p.Z,{title:"This is the amount spent by a user in the team.",children:(0,t.jsx)(m.Z,{})})]}),(0,t.jsx)(R.Z,{children:"Team Member Budget (USD)"}),(0,t.jsxs)(R.Z,{children:["Team Member Rate Limits"," ",(0,t.jsx)(p.Z,{title:"Rate limits for this member's usage within this team.",children:(0,t.jsx)(m.Z,{})})]}),(0,t.jsx)(R.Z,{className:"sticky right-0 bg-white z-10 border-l border-gray-200",children:"Actions"})]})}),(0,t.jsx)(O.Z,{children:l.team_info.members_with_roles.map((e,m)=>(0,t.jsxs)(U.Z,{children:[(0,t.jsx)(D.Z,{children:(0,t.jsx)(z.Z,{className:"font-mono",children:e.user_id})}),(0,t.jsx)(D.Z,{children:(0,t.jsx)(z.Z,{className:"font-mono",children:e.user_email?e.user_email:"No Email"})}),(0,t.jsx)(D.Z,{children:(0,t.jsx)(z.Z,{className:"font-mono",children:e.role})}),(0,t.jsx)(D.Z,{children:(0,t.jsxs)(z.Z,{className:"font-mono",children:["$",(0,r.pw)(c(e.user_id),4)]})}),(0,t.jsx)(D.Z,{children:(0,t.jsx)(z.Z,{className:"font-mono",children:u(e.user_id)?"$".concat((0,r.pw)(Number(u(e.user_id)),4)):"No Limit"})}),(0,t.jsx)(D.Z,{children:(0,t.jsx)(z.Z,{className:"font-mono",children:h(e.user_id)})}),(0,t.jsx)(D.Z,{className:"sticky right-0 bg-white z-10 border-l border-gray-200",children:s&&(0,t.jsxs)("div",{className:"flex gap-2",children:[(0,t.jsx)(X.Z,{icon:Y.Z,size:"sm",onClick:()=>{var s,t,i;let r=l.team_memberships.find(l=>l.user_id===e.user_id);a({...e,max_budget_in_team:(null==r?void 0:null===(s=r.litellm_budget_table)||void 0===s?void 0:s.max_budget)||null,tpm_limit:(null==r?void 0:null===(t=r.litellm_budget_table)||void 0===t?void 0:t.tpm_limit)||null,rpm_limit:(null==r?void 0:null===(i=r.litellm_budget_table)||void 0===i?void 0:i.rpm_limit)||null}),n(!0)},className:"cursor-pointer hover:text-blue-600"}),(0,t.jsx)(X.Z,{icon:H.Z,size:"sm",onClick:()=>i(e),className:"cursor-pointer hover:text-red-600"})]})})]},m))})]})})}),(0,t.jsx)(L.Z,{onClick:()=>d(!0),children:"Add Member"})]})},el=e=>{var l,s,L,F,E,O,D,A,R,U,z,B,V,q,G,K,$,J;let{teamId:Q,onClose:X,accessToken:Y,is_team_admin:H,is_proxy_admin:el,userModels:es,editTeam:et,premiumUser:ei=!1,onUpdate:ea}=e,[er,en]=(0,j.useState)(null),[em,ed]=(0,j.useState)(!0),[eo,ec]=(0,j.useState)(!1),[eu]=c.Z.useForm(),[eh,ex]=(0,j.useState)(!1),[eb,ep]=(0,j.useState)(null),[eg,e_]=(0,j.useState)(!1),[ev,ej]=(0,j.useState)([]),[ef,eZ]=(0,j.useState)(!1),[ey,eN]=(0,j.useState)({}),[ew,ek]=(0,j.useState)([]),[eM,eC]=(0,j.useState)(null),[eS,eT]=(0,j.useState)(!1),[eI,eP]=(0,j.useState)(!1);console.log("userModels in team info",es);let eL=H||el,eF=async()=>{try{if(ed(!0),!Y)return;let e=await (0,a.teamInfoCall)(Y,Q);en(e)}catch(e){M.Z.fromBackend("Failed to load team information"),console.error("Error fetching team info:",e)}finally{ed(!1)}};(0,j.useEffect)(()=>{eF()},[Q,Y]),(0,j.useEffect)(()=>{(async()=>{try{if(!Y)return;let e=(await (0,a.getGuardrailsList)(Y)).guardrails.map(e=>e.guardrail_name);ek(e)}catch(e){console.error("Failed to fetch guardrails:",e)}})()},[Y]);let eE=async e=>{try{if(null==Y)return;let l={user_email:e.user_email,user_id:e.user_id,role:e.role};await (0,a.teamMemberAddCall)(Y,Q,l),M.Z.success("Team member added successfully"),ec(!1),eu.resetFields();let s=await (0,a.teamInfoCall)(Y,Q);en(s),ea(s)}catch(i){var l,s,t;let e="Failed to add team member";(null==i?void 0:null===(t=i.raw)||void 0===t?void 0:null===(s=t.detail)||void 0===s?void 0:null===(l=s.error)||void 0===l?void 0:l.includes("Assigning team admins is a premium feature"))?e="Assigning admins is an enterprise-only feature. Please upgrade your LiteLLM plan to enable this.":(null==i?void 0:i.message)&&(e=i.message),M.Z.fromBackend(e),console.error("Error adding team member:",i)}},eO=async e=>{try{if(null==Y)return;let l={user_email:e.user_email,user_id:e.user_id,role:e.role,max_budget_in_team:e.max_budget_in_team,tpm_limit:e.tpm_limit,rpm_limit:e.rpm_limit};console.log("Updating member with values:",l),u.ZP.destroy(),await (0,a.teamMemberUpdateCall)(Y,Q,l),M.Z.success("Team member updated successfully"),ex(!1);let s=await (0,a.teamInfoCall)(Y,Q);en(s),ea(s)}catch(t){var l,s;let e="Failed to update team member";(null==t?void 0:null===(s=t.raw)||void 0===s?void 0:null===(l=s.detail)||void 0===l?void 0:l.includes("Assigning team admins is a premium feature"))?e="Assigning admins is an enterprise-only feature. Please upgrade your LiteLLM plan to enable this.":(null==t?void 0:t.message)&&(e=t.message),ex(!1),u.ZP.destroy(),M.Z.fromBackend(e),console.error("Error updating team member:",t)}},eD=async()=>{if(eM&&Y){eP(!0);try{await (0,a.teamMemberDeleteCall)(Y,Q,eM),M.Z.success("Team member removed successfully");let e=await (0,a.teamInfoCall)(Y,Q);en(e),ea(e)}catch(e){M.Z.fromBackend("Failed to remove team member"),console.error("Error removing team member:",e)}finally{eP(!1),eT(!1),eC(null)}}},eA=async e=>{try{if(!Y)return;let l={};try{l=e.metadata?JSON.parse(e.metadata):{}}catch(e){M.Z.fromBackend("Invalid JSON in metadata field");return}let s=e=>null==e||"string"==typeof e&&""===e.trim()||"number"==typeof e&&Number.isNaN(e)?null:e,t={team_id:Q,team_alias:e.team_alias,models:e.models,tpm_limit:s(e.tpm_limit),rpm_limit:s(e.rpm_limit),max_budget:e.max_budget,budget_duration:e.budget_duration,metadata:{...l,guardrails:e.guardrails||[],logging:e.logging_settings||[]},organization_id:e.organization_id};t.max_budget=(0,n.C)(t.max_budget),void 0!==e.team_member_budget&&(t.team_member_budget=Number(e.team_member_budget)),void 0!==e.team_member_key_duration&&(t.team_member_key_duration=e.team_member_key_duration),(void 0!==e.team_member_tpm_limit||void 0!==e.team_member_rpm_limit)&&(t.team_member_tpm_limit=s(e.team_member_tpm_limit),t.team_member_rpm_limit=s(e.team_member_rpm_limit));let{servers:i,accessGroups:r}=e.mcp_servers_and_groups||{servers:[],accessGroups:[]},m=new Set(i||[]),d=Object.fromEntries(Object.entries(e.mcp_tool_permissions||{}).filter(e=>{let[l]=e;return m.has(l)}));t.object_permission={},i&&(t.object_permission.mcp_servers=i),r&&(t.object_permission.mcp_access_groups=r),d&&(t.object_permission.mcp_tool_permissions=d),delete e.mcp_servers_and_groups,delete e.mcp_tool_permissions,await (0,a.teamUpdateCall)(Y,t),M.Z.success("Team settings updated successfully"),e_(!1),eF()}catch(e){console.error("Error updating team:",e)}};if(em)return(0,t.jsx)("div",{className:"p-4",children:"Loading..."});if(!(null==er?void 0:er.team_info))return(0,t.jsx)("div",{className:"p-4",children:"Team not found"});let{team_info:eR}=er,eU=async(e,l)=>{await (0,r.vQ)(e)&&(eN(e=>({...e,[l]:!0})),setTimeout(()=>{eN(e=>({...e,[l]:!1}))},2e3))};return(0,t.jsxs)("div",{className:"p-4",children:[(0,t.jsx)("div",{className:"flex justify-between items-center mb-6",children:(0,t.jsxs)("div",{children:[(0,t.jsx)(o.zx,{icon:d.Z,variant:"light",onClick:X,className:"mb-4",children:"Back to Teams"}),(0,t.jsx)(o.Dx,{children:eR.team_alias}),(0,t.jsxs)("div",{className:"flex items-center cursor-pointer",children:[(0,t.jsx)(o.xv,{className:"text-gray-500 font-mono",children:eR.team_id}),(0,t.jsx)(h.ZP,{type:"text",size:"small",icon:ey["team-id"]?(0,t.jsx)(_.Z,{size:12}):(0,t.jsx)(v.Z,{size:12}),onClick:()=>eU(eR.team_id,"team-id"),className:"left-2 z-10 transition-all duration-200 ".concat(ey["team-id"]?"text-green-600 bg-green-50 border-green-200":"text-gray-500 hover:text-gray-700 hover:bg-gray-100")})]})]})}),(0,t.jsxs)(o.v0,{defaultIndex:et?3:0,children:[(0,t.jsx)(o.td,{className:"mb-4",children:[(0,t.jsx)(o.OK,{children:"Overview"},"overview"),...eL?[(0,t.jsx)(o.OK,{children:"Members"},"members"),(0,t.jsx)(o.OK,{children:"Member Permissions"},"member-permissions"),(0,t.jsx)(o.OK,{children:"Settings"},"settings")]:[]]}),(0,t.jsxs)(o.nP,{children:[(0,t.jsx)(o.x4,{children:(0,t.jsxs)(o.rj,{numItems:1,numItemsSm:2,numItemsLg:3,className:"gap-6",children:[(0,t.jsxs)(o.Zb,{children:[(0,t.jsx)(o.xv,{children:"Budget Status"}),(0,t.jsxs)("div",{className:"mt-2",children:[(0,t.jsxs)(o.Dx,{children:["$",(0,r.pw)(eR.spend,4)]}),(0,t.jsxs)(o.xv,{children:["of ",null===eR.max_budget?"Unlimited":"$".concat((0,r.pw)(eR.max_budget,4))]}),eR.budget_duration&&(0,t.jsxs)(o.xv,{className:"text-gray-500",children:["Reset: ",eR.budget_duration]}),(0,t.jsx)("br",{}),eR.team_member_budget_table&&(0,t.jsxs)(o.xv,{className:"text-gray-500",children:["Team Member Budget: $",(0,r.pw)(eR.team_member_budget_table.max_budget,4)]})]})]}),(0,t.jsxs)(o.Zb,{children:[(0,t.jsx)(o.xv,{children:"Rate Limits"}),(0,t.jsxs)("div",{className:"mt-2",children:[(0,t.jsxs)(o.xv,{children:["TPM: ",eR.tpm_limit||"Unlimited"]}),(0,t.jsxs)(o.xv,{children:["RPM: ",eR.rpm_limit||"Unlimited"]}),eR.max_parallel_requests&&(0,t.jsxs)(o.xv,{children:["Max Parallel Requests: ",eR.max_parallel_requests]})]})]}),(0,t.jsxs)(o.Zb,{children:[(0,t.jsx)(o.xv,{children:"Models"}),(0,t.jsx)("div",{className:"mt-2 flex flex-wrap gap-2",children:0===eR.models.length?(0,t.jsx)(o.Ct,{color:"red",children:"All proxy models"}):eR.models.map((e,l)=>(0,t.jsx)(o.Ct,{color:"red",children:e},l))})]}),(0,t.jsxs)(o.Zb,{children:[(0,t.jsx)(o.xv,{className:"font-semibold text-gray-900",children:"Virtual Keys"}),(0,t.jsxs)("div",{className:"mt-2",children:[(0,t.jsxs)(o.xv,{children:["User Keys: ",er.keys.filter(e=>e.user_id).length]}),(0,t.jsxs)(o.xv,{children:["Service Account Keys: ",er.keys.filter(e=>!e.user_id).length]}),(0,t.jsxs)(o.xv,{className:"text-gray-500",children:["Total: ",er.keys.length]})]})]}),(0,t.jsx)(C.Z,{objectPermission:eR.object_permission,variant:"card",accessToken:Y}),(0,t.jsx)(N.Z,{loggingConfigs:(null===(l=eR.metadata)||void 0===l?void 0:l.logging)||[],disabledCallbacks:[],variant:"card"})]})}),(0,t.jsx)(o.x4,{children:(0,t.jsx)(ee,{teamData:er,canEditTeam:eL,handleMemberDelete:e=>{eC(e),eT(!0)},setSelectedEditMember:ep,setIsEditMemberModalVisible:ex,setIsAddMemberModalVisible:ec})}),eL&&(0,t.jsx)(o.x4,{children:(0,t.jsx)(W,{teamId:Q,accessToken:Y,canEditTeam:eL})}),(0,t.jsx)(o.x4,{children:(0,t.jsxs)(o.Zb,{className:"overflow-y-auto max-h-[65vh]",children:[(0,t.jsxs)("div",{className:"flex justify-between items-center mb-4",children:[(0,t.jsx)(o.Dx,{children:"Team Settings"}),eL&&!eg&&(0,t.jsx)(o.zx,{onClick:()=>e_(!0),children:"Edit Settings"})]}),eg?(0,t.jsxs)(c.Z,{form:eu,onFinish:eA,initialValues:{...eR,team_alias:eR.team_alias,models:eR.models,tpm_limit:eR.tpm_limit,rpm_limit:eR.rpm_limit,max_budget:eR.max_budget,budget_duration:eR.budget_duration,team_member_tpm_limit:null===(s=eR.team_member_budget_table)||void 0===s?void 0:s.tpm_limit,team_member_rpm_limit:null===(L=eR.team_member_budget_table)||void 0===L?void 0:L.rpm_limit,guardrails:(null===(F=eR.metadata)||void 0===F?void 0:F.guardrails)||[],disable_global_guardrails:(null===(E=eR.metadata)||void 0===E?void 0:E.disable_global_guardrails)||!1,metadata:eR.metadata?JSON.stringify((e=>{let{logging:l,...s}=e;return s})(eR.metadata),null,2):"",logging_settings:(null===(O=eR.metadata)||void 0===O?void 0:O.logging)||[],organization_id:eR.organization_id,vector_stores:(null===(D=eR.object_permission)||void 0===D?void 0:D.vector_stores)||[],mcp_servers:(null===(A=eR.object_permission)||void 0===A?void 0:A.mcp_servers)||[],mcp_access_groups:(null===(R=eR.object_permission)||void 0===R?void 0:R.mcp_access_groups)||[],mcp_servers_and_groups:{servers:(null===(U=eR.object_permission)||void 0===U?void 0:U.mcp_servers)||[],accessGroups:(null===(z=eR.object_permission)||void 0===z?void 0:z.mcp_access_groups)||[]},mcp_tool_permissions:(null===(B=eR.object_permission)||void 0===B?void 0:B.mcp_tool_permissions)||{}},layout:"vertical",children:[(0,t.jsx)(c.Z.Item,{label:"Team Name",name:"team_alias",rules:[{required:!0,message:"Please input a team name"}],children:(0,t.jsx)(x.default,{type:""})}),(0,t.jsx)(c.Z.Item,{label:"Models",name:"models",rules:[{required:!0,message:"Please select at least one model"}],children:(0,t.jsxs)(b.default,{mode:"multiple",placeholder:"Select models",children:[(el||es.includes("all-proxy-models"))&&(0,t.jsx)(b.default.Option,{value:"all-proxy-models",children:"All Proxy Models"},"all-proxy-models"),(0,t.jsx)(b.default.Option,{value:"no-default-models",children:"No Default Models"},"no-default-models"),Array.from(new Set(es)).map((e,l)=>(0,t.jsx)(b.default.Option,{value:e,children:(0,y.W0)(e)},l))]})}),(0,t.jsx)(c.Z.Item,{label:"Max Budget (USD)",name:"max_budget",children:(0,t.jsx)(S.Z,{step:.01,precision:2,style:{width:"100%"}})}),(0,t.jsx)(c.Z.Item,{label:"Team Member Budget (USD)",name:"team_member_budget",tooltip:"This is the individual budget for a user in the team.",children:(0,t.jsx)(S.Z,{step:.01,precision:2,style:{width:"100%"}})}),(0,t.jsx)(c.Z.Item,{label:"Team Member Key Duration (eg: 1d, 1mo)",name:"team_member_key_duration",tooltip:"Set a limit to the duration of a team member's key. Format: 30s (seconds), 30m (minutes), 30h (hours), 30d (days), 1mo (month)",children:(0,t.jsx)(o.oi,{placeholder:"e.g., 30d"})}),(0,t.jsx)(c.Z.Item,{label:"Team Member TPM Limit",name:"team_member_tpm_limit",tooltip:"Default tokens per minute limit for an individual team member. This limit applies to all requests the user makes within this team. Can be overridden per member.",children:(0,t.jsx)(S.Z,{step:1,style:{width:"100%"},placeholder:"e.g., 1000"})}),(0,t.jsx)(c.Z.Item,{label:"Team Member RPM Limit",name:"team_member_rpm_limit",tooltip:"Default requests per minute limit for an individual team member. This limit applies to all requests the user makes within this team. Can be overridden per member.",children:(0,t.jsx)(S.Z,{step:1,style:{width:"100%"},placeholder:"e.g., 100"})}),(0,t.jsx)(c.Z.Item,{label:"Reset Budget",name:"budget_duration",children:(0,t.jsxs)(b.default,{placeholder:"n/a",children:[(0,t.jsx)(b.default.Option,{value:"24h",children:"daily"}),(0,t.jsx)(b.default.Option,{value:"7d",children:"weekly"}),(0,t.jsx)(b.default.Option,{value:"30d",children:"monthly"})]})}),(0,t.jsx)(c.Z.Item,{label:"Tokens per minute Limit (TPM)",name:"tpm_limit",children:(0,t.jsx)(S.Z,{step:1,style:{width:"100%"}})}),(0,t.jsx)(c.Z.Item,{label:"Requests per minute Limit (RPM)",name:"rpm_limit",children:(0,t.jsx)(S.Z,{step:1,style:{width:"100%"}})}),(0,t.jsx)(c.Z.Item,{label:(0,t.jsxs)("span",{children:["Guardrails"," ",(0,t.jsx)(p.Z,{title:"Setup your first guardrail",children:(0,t.jsx)("a",{href:"https://docs.litellm.ai/docs/proxy/guardrails/quick_start",target:"_blank",rel:"noopener noreferrer",onClick:e=>e.stopPropagation(),children:(0,t.jsx)(m.Z,{style:{marginLeft:"4px"}})})})]}),name:"guardrails",help:"Select existing guardrails or enter new ones",children:(0,t.jsx)(b.default,{mode:"tags",placeholder:"Select or enter guardrails",options:ew.map(e=>({value:e,label:e}))})}),(0,t.jsx)(c.Z.Item,{label:(0,t.jsxs)("span",{children:["Disable Global Guardrails"," ",(0,t.jsx)(p.Z,{title:"When enabled, this team will bypass any guardrails configured to run on every request (global guardrails)",children:(0,t.jsx)(m.Z,{style:{marginLeft:"4px"}})})]}),name:"disable_global_guardrails",valuePropName:"checked",help:"Bypass global guardrails for this team",children:(0,t.jsx)(g.Z,{checkedChildren:"Yes",unCheckedChildren:"No"})}),(0,t.jsx)(c.Z.Item,{label:"Vector Stores",name:"vector_stores",children:(0,t.jsx)(T.Z,{onChange:e=>eu.setFieldValue("vector_stores",e),value:eu.getFieldValue("vector_stores"),accessToken:Y||"",placeholder:"Select vector stores"})}),(0,t.jsx)(c.Z.Item,{label:"Allowed Pass Through Routes",name:"allowed_passthrough_routes",children:(0,t.jsx)(Z.Z,{onChange:e=>eu.setFieldValue("allowed_passthrough_routes",e),value:eu.getFieldValue("allowed_passthrough_routes"),accessToken:Y||"",placeholder:"Select pass through routes"})}),(0,t.jsx)(c.Z.Item,{label:"MCP Servers / Access Groups",name:"mcp_servers_and_groups",children:(0,t.jsx)(w.Z,{onChange:e=>eu.setFieldValue("mcp_servers_and_groups",e),value:eu.getFieldValue("mcp_servers_and_groups"),accessToken:Y||"",placeholder:"Select MCP servers or access groups (optional)"})}),(0,t.jsx)(c.Z.Item,{name:"mcp_tool_permissions",initialValue:{},hidden:!0,children:(0,t.jsx)(x.default,{type:"hidden"})}),(0,t.jsx)(c.Z.Item,{noStyle:!0,shouldUpdate:(e,l)=>e.mcp_servers_and_groups!==l.mcp_servers_and_groups||e.mcp_tool_permissions!==l.mcp_tool_permissions,children:()=>{var e;return(0,t.jsx)("div",{className:"mb-6",children:(0,t.jsx)(k.Z,{accessToken:Y||"",selectedServers:(null===(e=eu.getFieldValue("mcp_servers_and_groups"))||void 0===e?void 0:e.servers)||[],toolPermissions:eu.getFieldValue("mcp_tool_permissions")||{},onChange:e=>eu.setFieldsValue({mcp_tool_permissions:e})})})}}),(0,t.jsx)(c.Z.Item,{label:"Organization ID",name:"organization_id",children:(0,t.jsx)(x.default,{type:""})}),(0,t.jsx)(c.Z.Item,{label:"Logging Settings",name:"logging_settings",children:(0,t.jsx)(P.Z,{value:eu.getFieldValue("logging_settings"),onChange:e=>eu.setFieldValue("logging_settings",e)})}),(0,t.jsx)(c.Z.Item,{label:"Metadata",name:"metadata",children:(0,t.jsx)(x.default.TextArea,{rows:10})}),(0,t.jsx)("div",{className:"sticky z-10 bg-white p-4 border-t border-gray-200 bottom-[-1.5rem] inset-x-[-1.5rem]",children:(0,t.jsxs)("div",{className:"flex justify-end items-center gap-2",children:[(0,t.jsx)(o.zx,{variant:"secondary",onClick:()=>e_(!1),children:"Cancel"}),(0,t.jsx)(o.zx,{type:"submit",children:"Save Changes"})]})})]}):(0,t.jsxs)("div",{className:"space-y-4",children:[(0,t.jsxs)("div",{children:[(0,t.jsx)(o.xv,{className:"font-medium",children:"Team Name"}),(0,t.jsx)("div",{children:eR.team_alias})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)(o.xv,{className:"font-medium",children:"Team ID"}),(0,t.jsx)("div",{className:"font-mono",children:eR.team_id})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)(o.xv,{className:"font-medium",children:"Created At"}),(0,t.jsx)("div",{children:new Date(eR.created_at).toLocaleString()})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)(o.xv,{className:"font-medium",children:"Models"}),(0,t.jsx)("div",{className:"flex flex-wrap gap-2 mt-1",children:eR.models.map((e,l)=>(0,t.jsx)(o.Ct,{color:"red",children:e},l))})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)(o.xv,{className:"font-medium",children:"Rate Limits"}),(0,t.jsxs)("div",{children:["TPM: ",eR.tpm_limit||"Unlimited"]}),(0,t.jsxs)("div",{children:["RPM: ",eR.rpm_limit||"Unlimited"]})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)(o.xv,{className:"font-medium",children:"Team Budget"}),(0,t.jsxs)("div",{children:["Max Budget:"," ",null!==eR.max_budget?"$".concat((0,r.pw)(eR.max_budget,4)):"No Limit"]}),(0,t.jsxs)("div",{children:["Budget Reset: ",eR.budget_duration||"Never"]})]}),(0,t.jsxs)("div",{children:[(0,t.jsxs)(o.xv,{className:"font-medium",children:["Team Member Settings"," ",(0,t.jsx)(p.Z,{title:"These are limits on individual team members",children:(0,t.jsx)(m.Z,{style:{marginLeft:"4px"}})})]}),(0,t.jsxs)("div",{children:["Max Budget: ",(null===(V=eR.team_member_budget_table)||void 0===V?void 0:V.max_budget)||"No Limit"]}),(0,t.jsxs)("div",{children:["Key Duration: ",(null===(q=eR.metadata)||void 0===q?void 0:q.team_member_key_duration)||"No Limit"]}),(0,t.jsxs)("div",{children:["TPM Limit: ",(null===(G=eR.team_member_budget_table)||void 0===G?void 0:G.tpm_limit)||"No Limit"]}),(0,t.jsxs)("div",{children:["RPM Limit: ",(null===(K=eR.team_member_budget_table)||void 0===K?void 0:K.rpm_limit)||"No Limit"]})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)(o.xv,{className:"font-medium",children:"Organization ID"}),(0,t.jsx)("div",{children:eR.organization_id})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)(o.xv,{className:"font-medium",children:"Status"}),(0,t.jsx)(o.Ct,{color:eR.blocked?"red":"green",children:eR.blocked?"Blocked":"Active"})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)(o.xv,{className:"font-medium",children:"Disable Global Guardrails"}),(0,t.jsx)("div",{children:(null===($=eR.metadata)||void 0===$?void 0:$.disable_global_guardrails)===!0?(0,t.jsx)(o.Ct,{color:"yellow",children:"Enabled - Global guardrails bypassed"}):(0,t.jsx)(o.Ct,{color:"green",children:"Disabled - Global guardrails active"})})]}),(0,t.jsx)(C.Z,{objectPermission:eR.object_permission,variant:"inline",className:"pt-4 border-t border-gray-200",accessToken:Y}),(0,t.jsx)(N.Z,{loggingConfigs:(null===(J=eR.metadata)||void 0===J?void 0:J.logging)||[],disabledCallbacks:[],variant:"inline",className:"pt-4 border-t border-gray-200"})]})]})})]})]}),(0,t.jsx)(I.Z,{visible:eh,onCancel:()=>ex(!1),onSubmit:eO,initialData:eb,mode:"edit",config:{title:"Edit Member",showEmail:!0,showUserId:!0,roleOptions:[{label:"Admin",value:"admin"},{label:"User",value:"user"}],additionalFields:[{name:"max_budget_in_team",label:(0,t.jsxs)("span",{children:["Team Member Budget (USD)"," ",(0,t.jsx)(p.Z,{title:"Maximum amount in USD this member can spend within this team. This is separate from any global user budget limits",children:(0,t.jsx)(m.Z,{style:{marginLeft:"4px"}})})]}),type:"numerical",step:.01,min:0,placeholder:"Budget limit for this member within this team"},{name:"tpm_limit",label:(0,t.jsxs)("span",{children:["Team Member TPM Limit"," ",(0,t.jsx)(p.Z,{title:"Maximum tokens per minute this member can use within this team. This is separate from any global user TPM limit",children:(0,t.jsx)(m.Z,{style:{marginLeft:"4px"}})})]}),type:"numerical",step:1,min:0,placeholder:"Tokens per minute limit for this member in this team"},{name:"rpm_limit",label:(0,t.jsxs)("span",{children:["Team Member RPM Limit"," ",(0,t.jsx)(p.Z,{title:"Maximum requests per minute this member can make within this team. This is separate from any global user RPM limit",children:(0,t.jsx)(m.Z,{style:{marginLeft:"4px"}})})]}),type:"numerical",step:1,min:0,placeholder:"Requests per minute limit for this member in this team"}]}}),(0,t.jsx)(i.Z,{isVisible:eo,onCancel:()=>ec(!1),onSubmit:eE,accessToken:Y}),(0,t.jsx)(f.Z,{isOpen:eS,title:"Delete Team Member",alertMessage:"Removing team members will also delete any keys created by or created for this member.",message:"Are you sure you want to remove this member from the team? This action cannot be undone.",resourceInformationTitle:"Team Member Information",resourceInformation:[{label:"User ID",value:null==eM?void 0:eM.user_id,code:!0},{label:"Email",value:null==eM?void 0:eM.user_email},{label:"Role",value:null==eM?void 0:eM.role}],onCancel:()=>{eT(!1),eC(null)},onOk:eD,confirmLoading:eI})]})}}}]); \ No newline at end of file diff --git a/litellm/proxy/_experimental/out/_next/static/chunks/2012-c09fa25a9cbf6028.js b/litellm/proxy/_experimental/out/_next/static/chunks/2012-c09fa25a9cbf6028.js new file mode 100644 index 00000000000..fc87e2ebf09 --- /dev/null +++ b/litellm/proxy/_experimental/out/_next/static/chunks/2012-c09fa25a9cbf6028.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[2012],{26210:function(e,l,s){s.d(l,{UQ:function(){return t.Z},X1:function(){return i.Z},_m:function(){return a.Z},oi:function(){return n.Z},xv:function(){return r.Z}});var t=s(87452),i=s(88829),a=s(72208),r=s(84264),n=s(49566)},30078:function(e,l,s){s.d(l,{Ct:function(){return t.Z},Dx:function(){return x.Z},OK:function(){return n.Z},Zb:function(){return a.Z},nP:function(){return c.Z},oi:function(){return h.Z},rj:function(){return r.Z},td:function(){return d.Z},v0:function(){return m.Z},x4:function(){return o.Z},xv:function(){return u.Z},zx:function(){return i.Z}});var t=s(41649),i=s(78489),a=s(12514),r=s(67101),n=s(12485),m=s(18135),d=s(35242),o=s(29706),c=s(77991),u=s(84264),h=s(49566),x=s(96761)},62490:function(e,l,s){s.d(l,{Ct:function(){return t.Z},RM:function(){return n.Z},SC:function(){return c.Z},Zb:function(){return a.Z},iA:function(){return r.Z},pj:function(){return m.Z},ss:function(){return d.Z},xs:function(){return o.Z},xv:function(){return u.Z},zx:function(){return i.Z}});var t=s(41649),i=s(78489),a=s(12514),r=s(21626),n=s(97214),m=s(28241),d=s(58834),o=s(69552),c=s(71876),u=s(84264)},11318:function(e,l,s){s.d(l,{Z:function(){return n}});var t=s(2265),i=s(39760),a=s(19250);let r=async(e,l,s,t)=>"Admin"!=s&&"Admin Viewer"!=s?await (0,a.teamListCall)(e,(null==t?void 0:t.organization_id)||null,l):await (0,a.teamListCall)(e,(null==t?void 0:t.organization_id)||null);var n=()=>{let[e,l]=(0,t.useState)([]),{accessToken:s,userId:a,userRole:n}=(0,i.Z)();return(0,t.useEffect)(()=>{(async()=>{l(await r(s,a,n,null))})()},[s,a,n]),{teams:e,setTeams:l}}},21609:function(e,l,s){s.d(l,{Z:function(){return o}});var t=s(57437),i=s(57840),a=s(22116),r=s(51653),n=s(76188),m=s(4260),d=s(2265);function o(e){let{isOpen:l,title:s,alertMessage:o,message:c,resourceInformationTitle:u,resourceInformation:h,onCancel:x,onOk:b,confirmLoading:p,requiredConfirmation:g}=e,{Title:_,Text:v}=i.default,[j,f]=(0,d.useState)("");return(0,d.useEffect)(()=>{l&&f("")},[l]),(0,t.jsx)(a.Z,{title:s,open:l,onOk:b,onCancel:x,confirmLoading:p,okText:p?"Deleting...":"Delete",cancelText:"Cancel",okButtonProps:{danger:!0,disabled:!!g&&j!==g||p},cancelButtonProps:{disabled:p},children:(0,t.jsxs)("div",{className:"space-y-4",children:[o&&(0,t.jsx)(r.Z,{message:o,type:"warning"}),(0,t.jsxs)("div",{className:"mt-4 p-4 bg-red-50 rounded-lg border border-red-200",children:[(0,t.jsx)(_,{level:5,className:"mb-3 text-gray-900",children:u}),(0,t.jsx)(n.Z,{column:1,size:"small",children:h&&h.map(e=>{let{label:l,value:s,...i}=e;return(0,t.jsx)(n.Z.Item,{label:(0,t.jsx)("span",{className:"font-semibold text-gray-700",children:l}),children:(0,t.jsx)(v,{...i,children:null!=s?s:"-"})},l)})})]}),(0,t.jsx)("div",{children:(0,t.jsx)(v,{children:c})}),g&&(0,t.jsxs)("div",{className:"mb-6 mt-4 pt-4 border-t border-gray-200",children:[(0,t.jsxs)(v,{className:"block text-base font-medium text-gray-700 mb-2",children:[(0,t.jsx)(v,{children:"Type "}),(0,t.jsx)(v,{strong:!0,type:"danger",children:g}),(0,t.jsx)(v,{children:" to confirm deletion:"})]}),(0,t.jsx)(m.default,{value:j,onChange:e=>f(e.target.value),placeholder:g,className:"rounded-md text-base border-gray-200",autoFocus:!0})]})]})})}},33860:function(e,l,s){var t=s(57437),i=s(2265),a=s(10032),r=s(22116),n=s(37592),m=s(99981),d=s(5545),o=s(7310),c=s.n(o),u=s(19250);l.Z=e=>{let{isVisible:l,onCancel:s,onSubmit:o,accessToken:h,title:x="Add Team Member",roles:b=[{label:"admin",value:"admin",description:"Admin role. Can create team keys, add members, and manage settings."},{label:"user",value:"user",description:"User role. Can view team info, but not manage it."}],defaultRole:p="user"}=e,[g]=a.Z.useForm(),[_,v]=(0,i.useState)([]),[j,f]=(0,i.useState)(!1),[Z,y]=(0,i.useState)("user_email"),N=async(e,l)=>{if(!e){v([]);return}f(!0);try{let s=new URLSearchParams;if(s.append(l,e),null==h)return;let t=(await (0,u.userFilterUICall)(h,s)).map(e=>({label:"user_email"===l?"".concat(e.user_email):"".concat(e.user_id),value:"user_email"===l?e.user_email:e.user_id,user:e}));v(t)}catch(e){console.error("Error fetching users:",e)}finally{f(!1)}},w=(0,i.useCallback)(c()((e,l)=>N(e,l),300),[]),k=(e,l)=>{y(l),w(e,l)},M=(e,l)=>{let s=l.user;g.setFieldsValue({user_email:s.user_email,user_id:s.user_id,role:g.getFieldValue("role")})};return(0,t.jsx)(r.Z,{title:x,open:l,onCancel:()=>{g.resetFields(),v([]),s()},footer:null,width:800,children:(0,t.jsxs)(a.Z,{form:g,onFinish:o,labelCol:{span:8},wrapperCol:{span:16},labelAlign:"left",initialValues:{role:p},children:[(0,t.jsx)(a.Z.Item,{label:"Email",name:"user_email",className:"mb-4",children:(0,t.jsx)(n.default,{showSearch:!0,className:"w-full",placeholder:"Search by email",filterOption:!1,onSearch:e=>k(e,"user_email"),onSelect:(e,l)=>M(e,l),options:"user_email"===Z?_:[],loading:j,allowClear:!0})}),(0,t.jsx)("div",{className:"text-center mb-4",children:"OR"}),(0,t.jsx)(a.Z.Item,{label:"User ID",name:"user_id",className:"mb-4",children:(0,t.jsx)(n.default,{showSearch:!0,className:"w-full",placeholder:"Search by user ID",filterOption:!1,onSearch:e=>k(e,"user_id"),onSelect:(e,l)=>M(e,l),options:"user_id"===Z?_:[],loading:j,allowClear:!0})}),(0,t.jsx)(a.Z.Item,{label:"Member Role",name:"role",className:"mb-4",children:(0,t.jsx)(n.default,{defaultValue:p,children:b.map(e=>(0,t.jsx)(n.default.Option,{value:e.value,children:(0,t.jsxs)(m.Z,{title:e.description,children:[(0,t.jsx)("span",{className:"font-medium",children:e.label}),(0,t.jsxs)("span",{className:"ml-2 text-gray-500 text-sm",children:["- ",e.description]})]})},e.value))})}),(0,t.jsx)("div",{className:"text-right mt-4",children:(0,t.jsx)(d.ZP,{type:"default",htmlType:"submit",children:"Add Member"})})]})})}},10901:function(e,l,s){s.d(l,{Z:function(){return h}});var t=s(57437),i=s(2265),a=s(10032),r=s(22116),n=s(5545),m=s(27281),d=s(57365),o=s(49566),c=s(92280),u=s(24199),h=e=>{var l,s,h;let{visible:x,onCancel:b,onSubmit:p,initialData:g,mode:_,config:v}=e,[j]=a.Z.useForm();console.log("Initial Data:",g),(0,i.useEffect)(()=>{if(x){if("edit"===_&&g){let e={...g,role:g.role||v.defaultRole,max_budget_in_team:g.max_budget_in_team||null,tpm_limit:g.tpm_limit||null,rpm_limit:g.rpm_limit||null};console.log("Setting form values:",e),j.setFieldsValue(e)}else{var e;j.resetFields(),j.setFieldsValue({role:v.defaultRole||(null===(e=v.roleOptions[0])||void 0===e?void 0:e.value)})}}},[x,g,_,j,v.defaultRole,v.roleOptions]);let f=async e=>{try{let l=Object.entries(e).reduce((e,l)=>{let[s,t]=l;if("string"==typeof t){let l=t.trim();return""===l&&("max_budget_in_team"===s||"tpm_limit"===s||"rpm_limit"===s)?{...e,[s]:null}:{...e,[s]:l}}return{...e,[s]:t}},{});console.log("Submitting form data:",l),p(l),j.resetFields()}catch(e){console.error("Form submission error:",e)}},Z=e=>{switch(e.type){case"input":return(0,t.jsx)(o.Z,{placeholder:e.placeholder});case"numerical":return(0,t.jsx)(u.Z,{step:e.step||1,min:e.min||0,style:{width:"100%"},placeholder:e.placeholder||"Enter a numerical value"});case"select":var l;return(0,t.jsx)(m.Z,{children:null===(l=e.options)||void 0===l?void 0:l.map(e=>(0,t.jsx)(d.Z,{value:e.value,children:e.label},e.value))});default:return null}};return(0,t.jsx)(r.Z,{title:v.title||("add"===_?"Add Member":"Edit Member"),open:x,width:1e3,footer:null,onCancel:b,children:(0,t.jsxs)(a.Z,{form:j,onFinish:f,labelCol:{span:8},wrapperCol:{span:16},labelAlign:"left",children:[v.showEmail&&(0,t.jsx)(a.Z.Item,{label:"Email",name:"user_email",className:"mb-4",rules:[{type:"email",message:"Please enter a valid email!"}],children:(0,t.jsx)(o.Z,{placeholder:"user@example.com"})}),v.showEmail&&v.showUserId&&(0,t.jsx)("div",{className:"text-center mb-4",children:(0,t.jsx)(c.x,{children:"OR"})}),v.showUserId&&(0,t.jsx)(a.Z.Item,{label:"User ID",name:"user_id",className:"mb-4",children:(0,t.jsx)(o.Z,{placeholder:"user_123"})}),(0,t.jsx)(a.Z.Item,{label:(0,t.jsxs)("div",{className:"flex items-center gap-2",children:[(0,t.jsx)("span",{children:"Role"}),"edit"===_&&g&&(0,t.jsxs)("span",{className:"text-gray-500 text-sm",children:["(Current: ",(s=g.role,(null===(h=v.roleOptions.find(e=>e.value===s))||void 0===h?void 0:h.label)||s),")"]})]}),name:"role",className:"mb-4",rules:[{required:!0,message:"Please select a role!"}],children:(0,t.jsx)(m.Z,{children:"edit"===_&&g?[...v.roleOptions.filter(e=>e.value===g.role),...v.roleOptions.filter(e=>e.value!==g.role)].map(e=>(0,t.jsx)(d.Z,{value:e.value,children:e.label},e.value)):v.roleOptions.map(e=>(0,t.jsx)(d.Z,{value:e.value,children:e.label},e.value))})}),null===(l=v.additionalFields)||void 0===l?void 0:l.map(e=>(0,t.jsx)(a.Z.Item,{label:e.label,name:e.name,className:"mb-4",rules:e.rules,children:Z(e)},e.name)),(0,t.jsxs)("div",{className:"text-right mt-6",children:[(0,t.jsx)(n.ZP,{onClick:b,className:"mr-2",children:"Cancel"}),(0,t.jsx)(n.ZP,{type:"default",htmlType:"submit",children:"add"===_?"Add Member":"Save Changes"})]})]})})}},33293:function(e,l,s){s.d(l,{Z:function(){return el}});var t=s(57437),i=s(33860),a=s(19250),r=s(59872),n=s(33304),m=s(15424),d=s(10900),o=s(30078),c=s(10032),u=s(42264),h=s(5545),x=s(4260),b=s(37592),p=s(99981),g=s(63709),_=s(30401),v=s(78867),j=s(2265),f=s(21609),Z=s(95096),y=s(46468),N=s(27799),w=s(95920),k=s(68473),M=s(9114),C=s(98015),S=s(24199),T=s(97415),I=s(10901),P=s(21425),L=s(78489),F=s(12514),E=s(21626),O=s(97214),D=s(28241),A=s(58834),R=s(69552),U=s(71876),z=s(84264),B=s(96761),V=s(61994),q=s(85180),G=s(89245),K=s(78355);let $={"/key/generate":"Member can generate a virtual key for this team","/key/service-account/generate":"Member can generate a service account key (not belonging to any user) for this team","/key/update":"Member can update a virtual key belonging to this team","/key/delete":"Member can delete a virtual key belonging to this team","/key/info":"Member can get info about a virtual key belonging to this team","/key/regenerate":"Member can regenerate a virtual key belonging to this team","/key/{key_id}/regenerate":"Member can regenerate a virtual key belonging to this team","/key/list":"Member can list virtual keys belonging to this team","/key/block":"Member can block a virtual key belonging to this team","/key/unblock":"Member can unblock a virtual key belonging to this team"},J=e=>e.includes("/info")||e.includes("/list")?"GET":"POST",Q=e=>{let l=J(e),s=$[e];if(!s){for(let[l,t]of Object.entries($))if(e.includes(l)){s=t;break}}return s||(s="Access ".concat(e)),{method:l,endpoint:e,description:s,route:e}};var W=e=>{let{teamId:l,accessToken:s,canEditTeam:i}=e,[r,n]=(0,j.useState)([]),[m,d]=(0,j.useState)([]),[o,c]=(0,j.useState)(!0),[u,x]=(0,j.useState)(!1),[b,p]=(0,j.useState)(!1),g=async()=>{try{if(c(!0),!s)return;let e=await (0,a.getTeamPermissionsCall)(s,l),t=e.all_available_permissions||[];n(t);let i=e.team_member_permissions||[];d(i),p(!1)}catch(e){M.Z.fromBackend("Failed to load permissions"),console.error("Error fetching permissions:",e)}finally{c(!1)}};(0,j.useEffect)(()=>{g()},[l,s]);let _=(e,l)=>{d(l?[...m,e]:m.filter(l=>l!==e)),p(!0)},v=async()=>{try{if(!s)return;x(!0),await (0,a.teamPermissionsUpdateCall)(s,l,m),M.Z.success("Permissions updated successfully"),p(!1)}catch(e){M.Z.fromBackend("Failed to update permissions"),console.error("Error updating permissions:",e)}finally{x(!1)}};if(o)return(0,t.jsx)("div",{className:"p-6 text-center",children:"Loading permissions..."});let f=r.length>0;return(0,t.jsxs)(F.Z,{className:"bg-white shadow-md rounded-md p-6",children:[(0,t.jsxs)("div",{className:"flex flex-col sm:flex-row justify-between items-start sm:items-center border-b pb-4 mb-6",children:[(0,t.jsx)(B.Z,{className:"mb-2 sm:mb-0",children:"Member Permissions"}),i&&b&&(0,t.jsxs)("div",{className:"flex gap-3",children:[(0,t.jsx)(h.ZP,{icon:(0,t.jsx)(G.Z,{}),onClick:()=>{g()},children:"Reset"}),(0,t.jsxs)(L.Z,{onClick:v,loading:u,className:"flex items-center gap-2",children:[(0,t.jsx)(K.Z,{})," Save Changes"]})]})]}),(0,t.jsx)(z.Z,{className:"mb-6 text-gray-600",children:"Control what team members can do when they are not team admins."}),f?(0,t.jsx)("div",{className:"overflow-x-auto",children:(0,t.jsxs)(E.Z,{className:" min-w-full",children:[(0,t.jsx)(A.Z,{children:(0,t.jsxs)(U.Z,{children:[(0,t.jsx)(R.Z,{children:"Method"}),(0,t.jsx)(R.Z,{children:"Endpoint"}),(0,t.jsx)(R.Z,{children:"Description"}),(0,t.jsx)(R.Z,{className:"sticky right-0 bg-white shadow-[-4px_0_4px_-4px_rgba(0,0,0,0.1)] text-center",children:"Allow Access"})]})}),(0,t.jsx)(O.Z,{children:r.map(e=>{let l=Q(e);return(0,t.jsxs)(U.Z,{className:"hover:bg-gray-50 transition-colors",children:[(0,t.jsx)(D.Z,{children:(0,t.jsx)("span",{className:"px-2 py-1 rounded text-xs font-medium ".concat("GET"===l.method?"bg-blue-100 text-blue-800":"bg-green-100 text-green-800"),children:l.method})}),(0,t.jsx)(D.Z,{children:(0,t.jsx)("span",{className:"font-mono text-sm text-gray-800",children:l.endpoint})}),(0,t.jsx)(D.Z,{className:"text-gray-700",children:l.description}),(0,t.jsx)(D.Z,{className:"sticky right-0 bg-white shadow-[-4px_0_4px_-4px_rgba(0,0,0,0.1)] text-center",children:(0,t.jsx)(V.Z,{checked:m.includes(e),onChange:l=>_(e,l.target.checked),disabled:!i})})]},e)})})]})}):(0,t.jsx)("div",{className:"py-12",children:(0,t.jsx)(q.Z,{description:"No permissions available"})})]})},X=s(47323),Y=s(53410),H=s(74998),ee=e=>{let{teamData:l,canEditTeam:s,handleMemberDelete:i,setSelectedEditMember:a,setIsEditMemberModalVisible:n,setIsAddMemberModalVisible:d}=e,o=e=>{if(null==e)return"0";if("number"==typeof e){let l=Number(e);return l===Math.floor(l)?l.toString():(0,r.pw)(l,8).replace(/\.?0+$/,"")}return"0"},c=e=>{if(!e)return 0;let s=l.team_memberships.find(l=>l.user_id===e);return(null==s?void 0:s.spend)||0},u=e=>{var s;if(!e)return null;let t=l.team_memberships.find(l=>l.user_id===e);console.log("membership=".concat(t));let i=null==t?void 0:null===(s=t.litellm_budget_table)||void 0===s?void 0:s.max_budget;return null==i?null:o(i)},h=e=>{var s,t;if(!e)return"No Limits";let i=l.team_memberships.find(l=>l.user_id===e),a=null==i?void 0:null===(s=i.litellm_budget_table)||void 0===s?void 0:s.rpm_limit,r=null==i?void 0:null===(t=i.litellm_budget_table)||void 0===t?void 0:t.tpm_limit,n=[a?"".concat(o(a)," RPM"):null,r?"".concat(o(r)," TPM"):null].filter(Boolean);return n.length>0?n.join(" / "):"No Limits"};return(0,t.jsxs)("div",{className:"space-y-4",children:[(0,t.jsx)(F.Z,{className:"w-full mx-auto flex-auto overflow-auto max-h-[50vh]",children:(0,t.jsx)("div",{className:"overflow-x-auto",children:(0,t.jsxs)(E.Z,{className:"min-w-full",children:[(0,t.jsx)(A.Z,{children:(0,t.jsxs)(U.Z,{children:[(0,t.jsx)(R.Z,{children:"User ID"}),(0,t.jsx)(R.Z,{children:"User Email"}),(0,t.jsx)(R.Z,{children:"Role"}),(0,t.jsxs)(R.Z,{children:["Team Member Spend (USD)"," ",(0,t.jsx)(p.Z,{title:"This is the amount spent by a user in the team.",children:(0,t.jsx)(m.Z,{})})]}),(0,t.jsx)(R.Z,{children:"Team Member Budget (USD)"}),(0,t.jsxs)(R.Z,{children:["Team Member Rate Limits"," ",(0,t.jsx)(p.Z,{title:"Rate limits for this member's usage within this team.",children:(0,t.jsx)(m.Z,{})})]}),(0,t.jsx)(R.Z,{className:"sticky right-0 bg-white z-10 border-l border-gray-200",children:"Actions"})]})}),(0,t.jsx)(O.Z,{children:l.team_info.members_with_roles.map((e,m)=>(0,t.jsxs)(U.Z,{children:[(0,t.jsx)(D.Z,{children:(0,t.jsx)(z.Z,{className:"font-mono",children:e.user_id})}),(0,t.jsx)(D.Z,{children:(0,t.jsx)(z.Z,{className:"font-mono",children:e.user_email?e.user_email:"No Email"})}),(0,t.jsx)(D.Z,{children:(0,t.jsx)(z.Z,{className:"font-mono",children:e.role})}),(0,t.jsx)(D.Z,{children:(0,t.jsxs)(z.Z,{className:"font-mono",children:["$",(0,r.pw)(c(e.user_id),4)]})}),(0,t.jsx)(D.Z,{children:(0,t.jsx)(z.Z,{className:"font-mono",children:u(e.user_id)?"$".concat((0,r.pw)(Number(u(e.user_id)),4)):"No Limit"})}),(0,t.jsx)(D.Z,{children:(0,t.jsx)(z.Z,{className:"font-mono",children:h(e.user_id)})}),(0,t.jsx)(D.Z,{className:"sticky right-0 bg-white z-10 border-l border-gray-200",children:s&&(0,t.jsxs)("div",{className:"flex gap-2",children:[(0,t.jsx)(X.Z,{icon:Y.Z,size:"sm",onClick:()=>{var s,t,i;let r=l.team_memberships.find(l=>l.user_id===e.user_id);a({...e,max_budget_in_team:(null==r?void 0:null===(s=r.litellm_budget_table)||void 0===s?void 0:s.max_budget)||null,tpm_limit:(null==r?void 0:null===(t=r.litellm_budget_table)||void 0===t?void 0:t.tpm_limit)||null,rpm_limit:(null==r?void 0:null===(i=r.litellm_budget_table)||void 0===i?void 0:i.rpm_limit)||null}),n(!0)},className:"cursor-pointer hover:text-blue-600"}),(0,t.jsx)(X.Z,{icon:H.Z,size:"sm",onClick:()=>i(e),className:"cursor-pointer hover:text-red-600"})]})})]},m))})]})})}),(0,t.jsx)(L.Z,{onClick:()=>d(!0),children:"Add Member"})]})},el=e=>{var l,s,L,F,E,O,D,A,R,U,z,B,V,q,G,K,$,J;let{teamId:Q,onClose:X,accessToken:Y,is_team_admin:H,is_proxy_admin:el,userModels:es,editTeam:et,premiumUser:ei=!1,onUpdate:ea}=e,[er,en]=(0,j.useState)(null),[em,ed]=(0,j.useState)(!0),[eo,ec]=(0,j.useState)(!1),[eu]=c.Z.useForm(),[eh,ex]=(0,j.useState)(!1),[eb,ep]=(0,j.useState)(null),[eg,e_]=(0,j.useState)(!1),[ev,ej]=(0,j.useState)([]),[ef,eZ]=(0,j.useState)(!1),[ey,eN]=(0,j.useState)({}),[ew,ek]=(0,j.useState)([]),[eM,eC]=(0,j.useState)(null),[eS,eT]=(0,j.useState)(!1),[eI,eP]=(0,j.useState)(!1),[eL,eF]=(0,j.useState)(!1);console.log("userModels in team info",es);let eE=H||el,eO=async()=>{try{if(ed(!0),!Y)return;let e=await (0,a.teamInfoCall)(Y,Q);en(e)}catch(e){M.Z.fromBackend("Failed to load team information"),console.error("Error fetching team info:",e)}finally{ed(!1)}};(0,j.useEffect)(()=>{eO()},[Q,Y]),(0,j.useEffect)(()=>{(async()=>{try{if(!Y)return;let e=(await (0,a.getGuardrailsList)(Y)).guardrails.map(e=>e.guardrail_name);ek(e)}catch(e){console.error("Failed to fetch guardrails:",e)}})()},[Y]);let eD=async e=>{try{if(null==Y)return;let l={user_email:e.user_email,user_id:e.user_id,role:e.role};await (0,a.teamMemberAddCall)(Y,Q,l),M.Z.success("Team member added successfully"),ec(!1),eu.resetFields();let s=await (0,a.teamInfoCall)(Y,Q);en(s),ea(s)}catch(i){var l,s,t;let e="Failed to add team member";(null==i?void 0:null===(t=i.raw)||void 0===t?void 0:null===(s=t.detail)||void 0===s?void 0:null===(l=s.error)||void 0===l?void 0:l.includes("Assigning team admins is a premium feature"))?e="Assigning admins is an enterprise-only feature. Please upgrade your LiteLLM plan to enable this.":(null==i?void 0:i.message)&&(e=i.message),M.Z.fromBackend(e),console.error("Error adding team member:",i)}},eA=async e=>{try{if(null==Y)return;let l={user_email:e.user_email,user_id:e.user_id,role:e.role,max_budget_in_team:e.max_budget_in_team,tpm_limit:e.tpm_limit,rpm_limit:e.rpm_limit};console.log("Updating member with values:",l),u.ZP.destroy(),await (0,a.teamMemberUpdateCall)(Y,Q,l),M.Z.success("Team member updated successfully"),ex(!1);let s=await (0,a.teamInfoCall)(Y,Q);en(s),ea(s)}catch(t){var l,s;let e="Failed to update team member";(null==t?void 0:null===(s=t.raw)||void 0===s?void 0:null===(l=s.detail)||void 0===l?void 0:l.includes("Assigning team admins is a premium feature"))?e="Assigning admins is an enterprise-only feature. Please upgrade your LiteLLM plan to enable this.":(null==t?void 0:t.message)&&(e=t.message),ex(!1),u.ZP.destroy(),M.Z.fromBackend(e),console.error("Error updating team member:",t)}},eR=async()=>{if(eM&&Y){eP(!0);try{await (0,a.teamMemberDeleteCall)(Y,Q,eM),M.Z.success("Team member removed successfully");let e=await (0,a.teamInfoCall)(Y,Q);en(e),ea(e)}catch(e){M.Z.fromBackend("Failed to remove team member"),console.error("Error removing team member:",e)}finally{eP(!1),eT(!1),eC(null)}}},eU=async e=>{try{if(!Y)return;eF(!0);let l={};try{l=e.metadata?JSON.parse(e.metadata):{}}catch(e){M.Z.fromBackend("Invalid JSON in metadata field");return}let s=e=>null==e||"string"==typeof e&&""===e.trim()||"number"==typeof e&&Number.isNaN(e)?null:e,t={team_id:Q,team_alias:e.team_alias,models:e.models,tpm_limit:s(e.tpm_limit),rpm_limit:s(e.rpm_limit),max_budget:e.max_budget,budget_duration:e.budget_duration,metadata:{...l,guardrails:e.guardrails||[],logging:e.logging_settings||[]},organization_id:e.organization_id};t.max_budget=(0,n.C)(t.max_budget),void 0!==e.team_member_budget&&(t.team_member_budget=Number(e.team_member_budget)),void 0!==e.team_member_key_duration&&(t.team_member_key_duration=e.team_member_key_duration),(void 0!==e.team_member_tpm_limit||void 0!==e.team_member_rpm_limit)&&(t.team_member_tpm_limit=s(e.team_member_tpm_limit),t.team_member_rpm_limit=s(e.team_member_rpm_limit));let{servers:i,accessGroups:r}=e.mcp_servers_and_groups||{servers:[],accessGroups:[]},m=new Set(i||[]),d=Object.fromEntries(Object.entries(e.mcp_tool_permissions||{}).filter(e=>{let[l]=e;return m.has(l)}));t.object_permission={},i&&(t.object_permission.mcp_servers=i),r&&(t.object_permission.mcp_access_groups=r),d&&(t.object_permission.mcp_tool_permissions=d),delete e.mcp_servers_and_groups,delete e.mcp_tool_permissions,await (0,a.teamUpdateCall)(Y,t),M.Z.success("Team settings updated successfully"),e_(!1),eO()}catch(e){console.error("Error updating team:",e)}finally{eF(!1)}};if(em)return(0,t.jsx)("div",{className:"p-4",children:"Loading..."});if(!(null==er?void 0:er.team_info))return(0,t.jsx)("div",{className:"p-4",children:"Team not found"});let{team_info:ez}=er,eB=async(e,l)=>{await (0,r.vQ)(e)&&(eN(e=>({...e,[l]:!0})),setTimeout(()=>{eN(e=>({...e,[l]:!1}))},2e3))};return(0,t.jsxs)("div",{className:"p-4",children:[(0,t.jsx)("div",{className:"flex justify-between items-center mb-6",children:(0,t.jsxs)("div",{children:[(0,t.jsx)(o.zx,{icon:d.Z,variant:"light",onClick:X,className:"mb-4",children:"Back to Teams"}),(0,t.jsx)(o.Dx,{children:ez.team_alias}),(0,t.jsxs)("div",{className:"flex items-center cursor-pointer",children:[(0,t.jsx)(o.xv,{className:"text-gray-500 font-mono",children:ez.team_id}),(0,t.jsx)(h.ZP,{type:"text",size:"small",icon:ey["team-id"]?(0,t.jsx)(_.Z,{size:12}):(0,t.jsx)(v.Z,{size:12}),onClick:()=>eB(ez.team_id,"team-id"),className:"left-2 z-10 transition-all duration-200 ".concat(ey["team-id"]?"text-green-600 bg-green-50 border-green-200":"text-gray-500 hover:text-gray-700 hover:bg-gray-100")})]})]})}),(0,t.jsxs)(o.v0,{defaultIndex:et?3:0,children:[(0,t.jsx)(o.td,{className:"mb-4",children:[(0,t.jsx)(o.OK,{children:"Overview"},"overview"),...eE?[(0,t.jsx)(o.OK,{children:"Members"},"members"),(0,t.jsx)(o.OK,{children:"Member Permissions"},"member-permissions"),(0,t.jsx)(o.OK,{children:"Settings"},"settings")]:[]]}),(0,t.jsxs)(o.nP,{children:[(0,t.jsx)(o.x4,{children:(0,t.jsxs)(o.rj,{numItems:1,numItemsSm:2,numItemsLg:3,className:"gap-6",children:[(0,t.jsxs)(o.Zb,{children:[(0,t.jsx)(o.xv,{children:"Budget Status"}),(0,t.jsxs)("div",{className:"mt-2",children:[(0,t.jsxs)(o.Dx,{children:["$",(0,r.pw)(ez.spend,4)]}),(0,t.jsxs)(o.xv,{children:["of ",null===ez.max_budget?"Unlimited":"$".concat((0,r.pw)(ez.max_budget,4))]}),ez.budget_duration&&(0,t.jsxs)(o.xv,{className:"text-gray-500",children:["Reset: ",ez.budget_duration]}),(0,t.jsx)("br",{}),ez.team_member_budget_table&&(0,t.jsxs)(o.xv,{className:"text-gray-500",children:["Team Member Budget: $",(0,r.pw)(ez.team_member_budget_table.max_budget,4)]})]})]}),(0,t.jsxs)(o.Zb,{children:[(0,t.jsx)(o.xv,{children:"Rate Limits"}),(0,t.jsxs)("div",{className:"mt-2",children:[(0,t.jsxs)(o.xv,{children:["TPM: ",ez.tpm_limit||"Unlimited"]}),(0,t.jsxs)(o.xv,{children:["RPM: ",ez.rpm_limit||"Unlimited"]}),ez.max_parallel_requests&&(0,t.jsxs)(o.xv,{children:["Max Parallel Requests: ",ez.max_parallel_requests]})]})]}),(0,t.jsxs)(o.Zb,{children:[(0,t.jsx)(o.xv,{children:"Models"}),(0,t.jsx)("div",{className:"mt-2 flex flex-wrap gap-2",children:0===ez.models.length?(0,t.jsx)(o.Ct,{color:"red",children:"All proxy models"}):ez.models.map((e,l)=>(0,t.jsx)(o.Ct,{color:"red",children:e},l))})]}),(0,t.jsxs)(o.Zb,{children:[(0,t.jsx)(o.xv,{className:"font-semibold text-gray-900",children:"Virtual Keys"}),(0,t.jsxs)("div",{className:"mt-2",children:[(0,t.jsxs)(o.xv,{children:["User Keys: ",er.keys.filter(e=>e.user_id).length]}),(0,t.jsxs)(o.xv,{children:["Service Account Keys: ",er.keys.filter(e=>!e.user_id).length]}),(0,t.jsxs)(o.xv,{className:"text-gray-500",children:["Total: ",er.keys.length]})]})]}),(0,t.jsx)(C.Z,{objectPermission:ez.object_permission,variant:"card",accessToken:Y}),(0,t.jsx)(N.Z,{loggingConfigs:(null===(l=ez.metadata)||void 0===l?void 0:l.logging)||[],disabledCallbacks:[],variant:"card"})]})}),(0,t.jsx)(o.x4,{children:(0,t.jsx)(ee,{teamData:er,canEditTeam:eE,handleMemberDelete:e=>{eC(e),eT(!0)},setSelectedEditMember:ep,setIsEditMemberModalVisible:ex,setIsAddMemberModalVisible:ec})}),eE&&(0,t.jsx)(o.x4,{children:(0,t.jsx)(W,{teamId:Q,accessToken:Y,canEditTeam:eE})}),(0,t.jsx)(o.x4,{children:(0,t.jsxs)(o.Zb,{className:"overflow-y-auto max-h-[65vh]",children:[(0,t.jsxs)("div",{className:"flex justify-between items-center mb-4",children:[(0,t.jsx)(o.Dx,{children:"Team Settings"}),eE&&!eg&&(0,t.jsx)(o.zx,{onClick:()=>e_(!0),children:"Edit Settings"})]}),eg?(0,t.jsxs)(c.Z,{form:eu,onFinish:eU,initialValues:{...ez,team_alias:ez.team_alias,models:ez.models,tpm_limit:ez.tpm_limit,rpm_limit:ez.rpm_limit,max_budget:ez.max_budget,budget_duration:ez.budget_duration,team_member_tpm_limit:null===(s=ez.team_member_budget_table)||void 0===s?void 0:s.tpm_limit,team_member_rpm_limit:null===(L=ez.team_member_budget_table)||void 0===L?void 0:L.rpm_limit,guardrails:(null===(F=ez.metadata)||void 0===F?void 0:F.guardrails)||[],disable_global_guardrails:(null===(E=ez.metadata)||void 0===E?void 0:E.disable_global_guardrails)||!1,metadata:ez.metadata?JSON.stringify((e=>{let{logging:l,...s}=e;return s})(ez.metadata),null,2):"",logging_settings:(null===(O=ez.metadata)||void 0===O?void 0:O.logging)||[],organization_id:ez.organization_id,vector_stores:(null===(D=ez.object_permission)||void 0===D?void 0:D.vector_stores)||[],mcp_servers:(null===(A=ez.object_permission)||void 0===A?void 0:A.mcp_servers)||[],mcp_access_groups:(null===(R=ez.object_permission)||void 0===R?void 0:R.mcp_access_groups)||[],mcp_servers_and_groups:{servers:(null===(U=ez.object_permission)||void 0===U?void 0:U.mcp_servers)||[],accessGroups:(null===(z=ez.object_permission)||void 0===z?void 0:z.mcp_access_groups)||[]},mcp_tool_permissions:(null===(B=ez.object_permission)||void 0===B?void 0:B.mcp_tool_permissions)||{}},layout:"vertical",children:[(0,t.jsx)(c.Z.Item,{label:"Team Name",name:"team_alias",rules:[{required:!0,message:"Please input a team name"}],children:(0,t.jsx)(x.default,{type:""})}),(0,t.jsx)(c.Z.Item,{label:"Models",name:"models",rules:[{required:!0,message:"Please select at least one model"}],children:(0,t.jsxs)(b.default,{mode:"multiple",placeholder:"Select models",children:[(el||es.includes("all-proxy-models"))&&(0,t.jsx)(b.default.Option,{value:"all-proxy-models",children:"All Proxy Models"},"all-proxy-models"),(0,t.jsx)(b.default.Option,{value:"no-default-models",children:"No Default Models"},"no-default-models"),Array.from(new Set(es)).map((e,l)=>(0,t.jsx)(b.default.Option,{value:e,children:(0,y.W0)(e)},l))]})}),(0,t.jsx)(c.Z.Item,{label:"Max Budget (USD)",name:"max_budget",children:(0,t.jsx)(S.Z,{step:.01,precision:2,style:{width:"100%"}})}),(0,t.jsx)(c.Z.Item,{label:"Team Member Budget (USD)",name:"team_member_budget",tooltip:"This is the individual budget for a user in the team.",children:(0,t.jsx)(S.Z,{step:.01,precision:2,style:{width:"100%"}})}),(0,t.jsx)(c.Z.Item,{label:"Team Member Key Duration (eg: 1d, 1mo)",name:"team_member_key_duration",tooltip:"Set a limit to the duration of a team member's key. Format: 30s (seconds), 30m (minutes), 30h (hours), 30d (days), 1mo (month)",children:(0,t.jsx)(o.oi,{placeholder:"e.g., 30d"})}),(0,t.jsx)(c.Z.Item,{label:"Team Member TPM Limit",name:"team_member_tpm_limit",tooltip:"Default tokens per minute limit for an individual team member. This limit applies to all requests the user makes within this team. Can be overridden per member.",children:(0,t.jsx)(S.Z,{step:1,style:{width:"100%"},placeholder:"e.g., 1000"})}),(0,t.jsx)(c.Z.Item,{label:"Team Member RPM Limit",name:"team_member_rpm_limit",tooltip:"Default requests per minute limit for an individual team member. This limit applies to all requests the user makes within this team. Can be overridden per member.",children:(0,t.jsx)(S.Z,{step:1,style:{width:"100%"},placeholder:"e.g., 100"})}),(0,t.jsx)(c.Z.Item,{label:"Reset Budget",name:"budget_duration",children:(0,t.jsxs)(b.default,{placeholder:"n/a",children:[(0,t.jsx)(b.default.Option,{value:"24h",children:"daily"}),(0,t.jsx)(b.default.Option,{value:"7d",children:"weekly"}),(0,t.jsx)(b.default.Option,{value:"30d",children:"monthly"})]})}),(0,t.jsx)(c.Z.Item,{label:"Tokens per minute Limit (TPM)",name:"tpm_limit",children:(0,t.jsx)(S.Z,{step:1,style:{width:"100%"}})}),(0,t.jsx)(c.Z.Item,{label:"Requests per minute Limit (RPM)",name:"rpm_limit",children:(0,t.jsx)(S.Z,{step:1,style:{width:"100%"}})}),(0,t.jsx)(c.Z.Item,{label:(0,t.jsxs)("span",{children:["Guardrails"," ",(0,t.jsx)(p.Z,{title:"Setup your first guardrail",children:(0,t.jsx)("a",{href:"https://docs.litellm.ai/docs/proxy/guardrails/quick_start",target:"_blank",rel:"noopener noreferrer",onClick:e=>e.stopPropagation(),children:(0,t.jsx)(m.Z,{style:{marginLeft:"4px"}})})})]}),name:"guardrails",help:"Select existing guardrails or enter new ones",children:(0,t.jsx)(b.default,{mode:"tags",placeholder:"Select or enter guardrails",options:ew.map(e=>({value:e,label:e}))})}),(0,t.jsx)(c.Z.Item,{label:(0,t.jsxs)("span",{children:["Disable Global Guardrails"," ",(0,t.jsx)(p.Z,{title:"When enabled, this team will bypass any guardrails configured to run on every request (global guardrails)",children:(0,t.jsx)(m.Z,{style:{marginLeft:"4px"}})})]}),name:"disable_global_guardrails",valuePropName:"checked",help:"Bypass global guardrails for this team",children:(0,t.jsx)(g.Z,{checkedChildren:"Yes",unCheckedChildren:"No"})}),(0,t.jsx)(c.Z.Item,{label:"Vector Stores",name:"vector_stores",children:(0,t.jsx)(T.Z,{onChange:e=>eu.setFieldValue("vector_stores",e),value:eu.getFieldValue("vector_stores"),accessToken:Y||"",placeholder:"Select vector stores"})}),(0,t.jsx)(c.Z.Item,{label:"Allowed Pass Through Routes",name:"allowed_passthrough_routes",children:(0,t.jsx)(Z.Z,{onChange:e=>eu.setFieldValue("allowed_passthrough_routes",e),value:eu.getFieldValue("allowed_passthrough_routes"),accessToken:Y||"",placeholder:"Select pass through routes"})}),(0,t.jsx)(c.Z.Item,{label:"MCP Servers / Access Groups",name:"mcp_servers_and_groups",children:(0,t.jsx)(w.Z,{onChange:e=>eu.setFieldValue("mcp_servers_and_groups",e),value:eu.getFieldValue("mcp_servers_and_groups"),accessToken:Y||"",placeholder:"Select MCP servers or access groups (optional)"})}),(0,t.jsx)(c.Z.Item,{name:"mcp_tool_permissions",initialValue:{},hidden:!0,children:(0,t.jsx)(x.default,{type:"hidden"})}),(0,t.jsx)(c.Z.Item,{noStyle:!0,shouldUpdate:(e,l)=>e.mcp_servers_and_groups!==l.mcp_servers_and_groups||e.mcp_tool_permissions!==l.mcp_tool_permissions,children:()=>{var e;return(0,t.jsx)("div",{className:"mb-6",children:(0,t.jsx)(k.Z,{accessToken:Y||"",selectedServers:(null===(e=eu.getFieldValue("mcp_servers_and_groups"))||void 0===e?void 0:e.servers)||[],toolPermissions:eu.getFieldValue("mcp_tool_permissions")||{},onChange:e=>eu.setFieldsValue({mcp_tool_permissions:e})})})}}),(0,t.jsx)(c.Z.Item,{label:"Organization ID",name:"organization_id",children:(0,t.jsx)(x.default,{type:""})}),(0,t.jsx)(c.Z.Item,{label:"Logging Settings",name:"logging_settings",children:(0,t.jsx)(P.Z,{value:eu.getFieldValue("logging_settings"),onChange:e=>eu.setFieldValue("logging_settings",e)})}),(0,t.jsx)(c.Z.Item,{label:"Metadata",name:"metadata",children:(0,t.jsx)(x.default.TextArea,{rows:10})}),(0,t.jsx)("div",{className:"sticky z-10 bg-white p-4 border-t border-gray-200 bottom-[-1.5rem] inset-x-[-1.5rem]",children:(0,t.jsxs)("div",{className:"flex justify-end items-center gap-2",children:[(0,t.jsx)(o.zx,{variant:"secondary",onClick:()=>e_(!1),disabled:eL,children:"Cancel"}),(0,t.jsx)(o.zx,{type:"submit",loading:eL,children:"Save Changes"})]})})]}):(0,t.jsxs)("div",{className:"space-y-4",children:[(0,t.jsxs)("div",{children:[(0,t.jsx)(o.xv,{className:"font-medium",children:"Team Name"}),(0,t.jsx)("div",{children:ez.team_alias})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)(o.xv,{className:"font-medium",children:"Team ID"}),(0,t.jsx)("div",{className:"font-mono",children:ez.team_id})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)(o.xv,{className:"font-medium",children:"Created At"}),(0,t.jsx)("div",{children:new Date(ez.created_at).toLocaleString()})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)(o.xv,{className:"font-medium",children:"Models"}),(0,t.jsx)("div",{className:"flex flex-wrap gap-2 mt-1",children:ez.models.map((e,l)=>(0,t.jsx)(o.Ct,{color:"red",children:e},l))})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)(o.xv,{className:"font-medium",children:"Rate Limits"}),(0,t.jsxs)("div",{children:["TPM: ",ez.tpm_limit||"Unlimited"]}),(0,t.jsxs)("div",{children:["RPM: ",ez.rpm_limit||"Unlimited"]})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)(o.xv,{className:"font-medium",children:"Team Budget"}),(0,t.jsxs)("div",{children:["Max Budget:"," ",null!==ez.max_budget?"$".concat((0,r.pw)(ez.max_budget,4)):"No Limit"]}),(0,t.jsxs)("div",{children:["Budget Reset: ",ez.budget_duration||"Never"]})]}),(0,t.jsxs)("div",{children:[(0,t.jsxs)(o.xv,{className:"font-medium",children:["Team Member Settings"," ",(0,t.jsx)(p.Z,{title:"These are limits on individual team members",children:(0,t.jsx)(m.Z,{style:{marginLeft:"4px"}})})]}),(0,t.jsxs)("div",{children:["Max Budget: ",(null===(V=ez.team_member_budget_table)||void 0===V?void 0:V.max_budget)||"No Limit"]}),(0,t.jsxs)("div",{children:["Key Duration: ",(null===(q=ez.metadata)||void 0===q?void 0:q.team_member_key_duration)||"No Limit"]}),(0,t.jsxs)("div",{children:["TPM Limit: ",(null===(G=ez.team_member_budget_table)||void 0===G?void 0:G.tpm_limit)||"No Limit"]}),(0,t.jsxs)("div",{children:["RPM Limit: ",(null===(K=ez.team_member_budget_table)||void 0===K?void 0:K.rpm_limit)||"No Limit"]})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)(o.xv,{className:"font-medium",children:"Organization ID"}),(0,t.jsx)("div",{children:ez.organization_id})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)(o.xv,{className:"font-medium",children:"Status"}),(0,t.jsx)(o.Ct,{color:ez.blocked?"red":"green",children:ez.blocked?"Blocked":"Active"})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)(o.xv,{className:"font-medium",children:"Disable Global Guardrails"}),(0,t.jsx)("div",{children:(null===($=ez.metadata)||void 0===$?void 0:$.disable_global_guardrails)===!0?(0,t.jsx)(o.Ct,{color:"yellow",children:"Enabled - Global guardrails bypassed"}):(0,t.jsx)(o.Ct,{color:"green",children:"Disabled - Global guardrails active"})})]}),(0,t.jsx)(C.Z,{objectPermission:ez.object_permission,variant:"inline",className:"pt-4 border-t border-gray-200",accessToken:Y}),(0,t.jsx)(N.Z,{loggingConfigs:(null===(J=ez.metadata)||void 0===J?void 0:J.logging)||[],disabledCallbacks:[],variant:"inline",className:"pt-4 border-t border-gray-200"})]})]})})]})]}),(0,t.jsx)(I.Z,{visible:eh,onCancel:()=>ex(!1),onSubmit:eA,initialData:eb,mode:"edit",config:{title:"Edit Member",showEmail:!0,showUserId:!0,roleOptions:[{label:"Admin",value:"admin"},{label:"User",value:"user"}],additionalFields:[{name:"max_budget_in_team",label:(0,t.jsxs)("span",{children:["Team Member Budget (USD)"," ",(0,t.jsx)(p.Z,{title:"Maximum amount in USD this member can spend within this team. This is separate from any global user budget limits",children:(0,t.jsx)(m.Z,{style:{marginLeft:"4px"}})})]}),type:"numerical",step:.01,min:0,placeholder:"Budget limit for this member within this team"},{name:"tpm_limit",label:(0,t.jsxs)("span",{children:["Team Member TPM Limit"," ",(0,t.jsx)(p.Z,{title:"Maximum tokens per minute this member can use within this team. This is separate from any global user TPM limit",children:(0,t.jsx)(m.Z,{style:{marginLeft:"4px"}})})]}),type:"numerical",step:1,min:0,placeholder:"Tokens per minute limit for this member in this team"},{name:"rpm_limit",label:(0,t.jsxs)("span",{children:["Team Member RPM Limit"," ",(0,t.jsx)(p.Z,{title:"Maximum requests per minute this member can make within this team. This is separate from any global user RPM limit",children:(0,t.jsx)(m.Z,{style:{marginLeft:"4px"}})})]}),type:"numerical",step:1,min:0,placeholder:"Requests per minute limit for this member in this team"}]}}),(0,t.jsx)(i.Z,{isVisible:eo,onCancel:()=>ec(!1),onSubmit:eD,accessToken:Y}),(0,t.jsx)(f.Z,{isOpen:eS,title:"Delete Team Member",alertMessage:"Removing team members will also delete any keys created by or created for this member.",message:"Are you sure you want to remove this member from the team? This action cannot be undone.",resourceInformationTitle:"Team Member Information",resourceInformation:[{label:"User ID",value:null==eM?void 0:eM.user_id,code:!0},{label:"Email",value:null==eM?void 0:eM.user_email},{label:"Role",value:null==eM?void 0:eM.role}],onCancel:()=>{eT(!1),eC(null)},onOk:eR,confirmLoading:eI})]})}}}]); \ No newline at end of file diff --git a/litellm/proxy/_experimental/out/_next/static/chunks/2019-15183fcc4c29249f.js b/litellm/proxy/_experimental/out/_next/static/chunks/2019-15183fcc4c29249f.js deleted file mode 100644 index 742a5c6e359..00000000000 --- a/litellm/proxy/_experimental/out/_next/static/chunks/2019-15183fcc4c29249f.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[2019],{92019:function(e,s,t){var a=t(57437),r=t(13817),l=t(18310),i=t(60985),n=t(92403),o=t(28595),c=t(68208),d=t(9775),m=t(41361),g=t(37527),u=t(15883),x=t(12660),y=t(88009),h=t(48231),p=t(57400),f=t(58630),b=t(44625),j=t(41169),N=t(38434),v=t(71891),L=t(55322),w=t(2265),k=t(99376),Z=t(20347),S=t(79262),_=t(19250);let{Sider:z}=r.default,O=()=>{let e="ui/".replace(/^\/+|\/+$/g,""),s=e?"/".concat(e,"/"):"/";if(_.serverRootPath&&"/"!==_.serverRootPath){let e=_.serverRootPath.replace(/\/+$/,""),t=s.replace(/^\/+/,"");return"".concat(e,"/").concat(t)}return s},P=e=>{switch(e){case"api-keys":return"virtual-keys";case"llm-playground":return"test-key";case"models":return"models-and-endpoints";case"new_usage":return"usage";case"teams":return"teams";case"organizations":return"organizations";case"users":return"users";case"api_ref":return"api-reference";case"model-hub-table":return"model-hub";case"logs":return"logs";case"guardrails":return"guardrails";case"mcp-servers":return"tools/mcp-servers";case"vector-stores":return"tools/vector-stores";case"caching":return"experimental/caching";case"prompts":return"experimental/prompts";case"budgets":return"experimental/budgets";case"transform-request":return"experimental/api-playground";case"tag-management":return"experimental/tag-management";case"usage":return"experimental/old-usage";case"general-settings":return"settings/router-settings";case"settings":return"settings/logging-and-alerts";case"admin-panel":return"settings/admin-settings";case"ui-theme":return"settings/ui-theme";default:return e.replace(/^\/+/,"")}},M=e=>{let s=O(),t=P(e).replace(/^\/+|\/+$/g,"");return"".concat(s).concat(t)},C=[{key:"1",page:"api-keys",label:"Virtual Keys",icon:(0,a.jsx)(n.Z,{style:{fontSize:18}})},{key:"3",page:"llm-playground",label:"Test Key",icon:(0,a.jsx)(o.Z,{style:{fontSize:18}}),roles:Z.LQ},{key:"2",page:"models",label:"Models + Endpoints",icon:(0,a.jsx)(c.Z,{style:{fontSize:18}}),roles:Z.LQ},{key:"12",page:"new_usage",label:"Usage",icon:(0,a.jsx)(d.Z,{style:{fontSize:18}}),roles:[...Z.ZL,...Z.lo]},{key:"6",page:"teams",label:"Teams",icon:(0,a.jsx)(m.Z,{style:{fontSize:18}})},{key:"17",page:"organizations",label:"Organizations",icon:(0,a.jsx)(g.Z,{style:{fontSize:18}}),roles:Z.ZL},{key:"5",page:"users",label:"Internal Users",icon:(0,a.jsx)(u.Z,{style:{fontSize:18}}),roles:Z.ZL},{key:"14",page:"api_ref",label:"API Reference",icon:(0,a.jsx)(x.Z,{style:{fontSize:18}})},{key:"16",page:"model-hub-table",label:"Model Hub",icon:(0,a.jsx)(y.Z,{style:{fontSize:18}})},{key:"15",page:"logs",label:"Logs",icon:(0,a.jsx)(h.Z,{style:{fontSize:18}})},{key:"11",page:"guardrails",label:"Guardrails",icon:(0,a.jsx)(p.Z,{style:{fontSize:18}}),roles:Z.ZL},{key:"26",page:"tools",label:"Tools",icon:(0,a.jsx)(f.Z,{style:{fontSize:18}}),children:[{key:"18",page:"mcp-servers",label:"MCP Servers",icon:(0,a.jsx)(f.Z,{style:{fontSize:18}})},{key:"21",page:"vector-stores",label:"Vector Stores",icon:(0,a.jsx)(b.Z,{style:{fontSize:18}}),roles:Z.ZL}]},{key:"experimental",page:"experimental",label:"Experimental",icon:(0,a.jsx)(j.Z,{style:{fontSize:18}}),children:[{key:"9",page:"caching",label:"Caching",icon:(0,a.jsx)(b.Z,{style:{fontSize:18}}),roles:Z.ZL},{key:"25",page:"prompts",label:"Prompts",icon:(0,a.jsx)(N.Z,{style:{fontSize:18}}),roles:Z.ZL},{key:"10",page:"budgets",label:"Budgets",icon:(0,a.jsx)(g.Z,{style:{fontSize:18}}),roles:Z.ZL},{key:"20",page:"transform-request",label:"API Playground",icon:(0,a.jsx)(x.Z,{style:{fontSize:18}}),roles:[...Z.ZL,...Z.lo]},{key:"19",page:"tag-management",label:"Tag Management",icon:(0,a.jsx)(v.Z,{style:{fontSize:18}}),roles:Z.ZL},{key:"4",page:"usage",label:"Old Usage",icon:(0,a.jsx)(d.Z,{style:{fontSize:18}})}]},{key:"settings",page:"settings",label:"Settings",icon:(0,a.jsx)(L.Z,{style:{fontSize:18}}),roles:Z.ZL,children:[{key:"11",page:"general-settings",label:"Router Settings",icon:(0,a.jsx)(L.Z,{style:{fontSize:18}}),roles:Z.ZL},{key:"8",page:"settings",label:"Logging & Alerts",icon:(0,a.jsx)(L.Z,{style:{fontSize:18}}),roles:Z.ZL},{key:"13",page:"admin-panel",label:"Admin Settings",icon:(0,a.jsx)(L.Z,{style:{fontSize:18}}),roles:Z.ZL},{key:"14",page:"ui-theme",label:"UI Theme",icon:(0,a.jsx)(L.Z,{style:{fontSize:18}}),roles:Z.ZL}]}];s.Z=e=>{let{accessToken:s,userRole:t,defaultSelectedKey:n,collapsed:o=!1}=e,c=(0,k.useRouter)(),d=(0,k.usePathname)()||"/",m=w.useMemo(()=>C.filter(e=>!e.roles||e.roles.includes(t)).map(e=>({...e,children:e.children?e.children.filter(e=>!e.roles||e.roles.includes(t)):void 0})),[t]),g=w.useMemo(()=>{var e,s;let t=O(),a=(d.startsWith(t)?d.slice(t.length):d.replace(/^\/+/,"")).toLowerCase(),r=e=>{let s=P(e).toLowerCase();return a===s||a.startsWith("".concat(s,"/"))};for(let e of m){if(!e.children&&r(e.page))return e.key;if(e.children){for(let s of e.children)if(r(s.page))return s.key}}let l=null===(e=m.find(e=>e.page===n))||void 0===e?void 0:e.key;if(l)return l;for(let e of m)if(null===(s=e.children)||void 0===s?void 0:s.some(e=>e.page===n))return e.children.find(e=>e.page===n).key;return"1"},[d,m,n]),u=e=>{let s=M(e);c.push(s)};return(0,a.jsx)(r.default,{style:{minHeight:"100vh"},children:(0,a.jsxs)(z,{theme:"light",width:220,collapsed:o,collapsedWidth:80,collapsible:!0,trigger:null,style:{transition:"all 0.3s cubic-bezier(0.4, 0, 0.2, 1)",position:"relative"},children:[(0,a.jsx)(l.ZP,{theme:{components:{Menu:{iconSize:18,fontSize:14}}},children:(0,a.jsx)(i.Z,{mode:"inline",selectedKeys:[g],defaultOpenKeys:o?[]:["llm-tools"],inlineCollapsed:o,className:"custom-sidebar-menu",style:{borderRight:0,backgroundColor:"transparent",fontSize:"14px"},items:m.map(e=>{var s;return{key:e.key,icon:e.icon,label:e.label,children:null===(s=e.children)||void 0===s?void 0:s.map(e=>({key:e.key,icon:e.icon,label:e.label,onClick:()=>u(e.page)})),onClick:e.children?void 0:()=>u(e.page)}})})}),(0,Z.tY)(t)&&!o&&(0,a.jsx)(S.Z,{accessToken:s,width:220})]})})}},79262:function(e,s,t){t.d(s,{Z:function(){return u}});var a=t(57437);t(1309);var r=t(76865),l=t(70525),i=t(95805),n=t(51817),o=t(21047);t(22135),t(40875);var c=t(49663),d=t(2265),m=t(19250);let g=function(){for(var e=arguments.length,s=Array(e),t=0;t{(async()=>{if(s){j(!0),v(null);try{let e=await (0,m.getRemainingUsers)(s);f(e)}catch(e){console.error("Failed to fetch usage data:",e),v("Failed to load usage data")}finally{j(!1)}}})()},[s]);let{isOverLimit:L,isNearLimit:w,usagePercentage:k,userMetrics:Z,teamMetrics:S}=(e=>{if(!e)return{isOverLimit:!1,isNearLimit:!1,usagePercentage:0,userMetrics:{isOverLimit:!1,isNearLimit:!1,usagePercentage:0},teamMetrics:{isOverLimit:!1,isNearLimit:!1,usagePercentage:0}};let s=e.total_users?e.total_users_used/e.total_users*100:0,t=s>100,a=s>=80&&s<=100,r=e.total_teams?e.total_teams_used/e.total_teams*100:0,l=r>100,i=r>=80&&r<=100,n=t||l;return{isOverLimit:n,isNearLimit:(a||i)&&!n,usagePercentage:Math.max(s,r),userMetrics:{isOverLimit:t,isNearLimit:a,usagePercentage:s},teamMetrics:{isOverLimit:l,isNearLimit:i,usagePercentage:r}}})(p),_=()=>L?(0,a.jsx)(r.Z,{className:"h-3 w-3"}):w?(0,a.jsx)(l.Z,{className:"h-3 w-3"}):null;return s&&((null==p?void 0:p.total_users)!==null||(null==p?void 0:p.total_teams)!==null)?(0,a.jsx)("div",{className:"fixed bottom-4 left-4 z-50",style:{width:"".concat(Math.min(t,220),"px")},children:(0,a.jsx)(()=>y?(0,a.jsx)("button",{onClick:()=>h(!1),className:g("bg-white border border-gray-200 rounded-lg shadow-sm p-3 hover:shadow-md transition-all w-full"),title:"Show usage details",children:(0,a.jsxs)("div",{className:"flex items-center gap-2",children:[(0,a.jsx)(i.Z,{className:"h-4 w-4 flex-shrink-0"}),(L||w)&&(0,a.jsx)("span",{className:"flex-shrink-0",children:_()}),(0,a.jsxs)("div",{className:"flex items-center gap-2 text-sm font-medium truncate",children:[p&&null!==p.total_users&&(0,a.jsxs)("span",{className:g("flex-shrink-0 px-1.5 py-0.5 rounded text-xs border",Z.isOverLimit&&"bg-red-50 text-red-700 border-red-200",Z.isNearLimit&&"bg-yellow-50 text-yellow-700 border-yellow-200",!Z.isOverLimit&&!Z.isNearLimit&&"bg-gray-50 text-gray-700 border-gray-200"),children:["U: ",p.total_users_used,"/",p.total_users]}),p&&null!==p.total_teams&&(0,a.jsxs)("span",{className:g("flex-shrink-0 px-1.5 py-0.5 rounded text-xs border",S.isOverLimit&&"bg-red-50 text-red-700 border-red-200",S.isNearLimit&&"bg-yellow-50 text-yellow-700 border-yellow-200",!S.isOverLimit&&!S.isNearLimit&&"bg-gray-50 text-gray-700 border-gray-200"),children:["T: ",p.total_teams_used,"/",p.total_teams]}),!p||null===p.total_users&&null===p.total_teams&&(0,a.jsx)("span",{className:"truncate",children:"Usage"})]})]})}):b?(0,a.jsx)("div",{className:"bg-white border border-gray-200 rounded-lg shadow-sm p-4 w-full",children:(0,a.jsxs)("div",{className:"flex items-center justify-center gap-2 py-2",children:[(0,a.jsx)(n.Z,{className:"h-4 w-4 animate-spin"}),(0,a.jsx)("span",{className:"text-sm text-gray-500 truncate",children:"Loading..."})]})}):N||!p?(0,a.jsx)("div",{className:"bg-white border border-gray-200 rounded-lg shadow-sm p-4 group w-full",children:(0,a.jsxs)("div",{className:"flex items-center justify-between gap-2",children:[(0,a.jsx)("div",{className:"flex-1 min-w-0",children:(0,a.jsx)("span",{className:"text-sm text-gray-500 truncate block",children:N||"No data"})}),(0,a.jsx)("button",{onClick:()=>h(!0),className:"opacity-0 group-hover:opacity-100 p-1 hover:bg-gray-100 rounded transition-all flex-shrink-0",title:"Minimize",children:(0,a.jsx)(o.Z,{className:"h-3 w-3 text-gray-400"})})]})}):(0,a.jsxs)("div",{className:g("bg-white border rounded-lg shadow-sm p-3 transition-all duration-200 group w-full"),children:[(0,a.jsxs)("div",{className:"flex items-center justify-between gap-2 mb-3",children:[(0,a.jsxs)("div",{className:"flex items-center gap-2 min-w-0 flex-1",children:[(0,a.jsx)(i.Z,{className:"h-4 w-4 flex-shrink-0"}),(0,a.jsx)("span",{className:"font-medium text-sm truncate",children:"Usage"})]}),(0,a.jsx)("button",{onClick:()=>h(!0),className:"opacity-0 group-hover:opacity-100 p-1 hover:bg-gray-100 rounded transition-all flex-shrink-0",title:"Minimize",children:(0,a.jsx)(o.Z,{className:"h-3 w-3 text-gray-400"})})]}),(0,a.jsxs)("div",{className:"space-y-3 text-sm",children:[null!==p.total_users&&(0,a.jsxs)("div",{className:g("space-y-1 border rounded-md p-2",Z.isOverLimit&&"border-red-200 bg-red-50",Z.isNearLimit&&"border-yellow-200 bg-yellow-50"),children:[(0,a.jsxs)("div",{className:"flex items-center gap-2 text-xs text-gray-600 mb-1",children:[(0,a.jsx)(i.Z,{className:"h-3 w-3"}),(0,a.jsx)("span",{className:"font-medium",children:"Users"}),(0,a.jsx)("span",{className:g("ml-1 px-1.5 py-0.5 rounded border",Z.isOverLimit&&"bg-red-50 text-red-700 border-red-200",Z.isNearLimit&&"bg-yellow-50 text-yellow-700 border-yellow-200",!Z.isOverLimit&&!Z.isNearLimit&&"bg-gray-50 text-gray-600 border-gray-200"),children:Z.isOverLimit?"Over limit":Z.isNearLimit?"Near limit":"OK"})]}),(0,a.jsxs)("div",{className:"flex justify-between items-center",children:[(0,a.jsx)("span",{className:"text-gray-600 text-xs",children:"Used:"}),(0,a.jsxs)("span",{className:"font-medium text-right",children:[p.total_users_used,"/",p.total_users]})]}),(0,a.jsxs)("div",{className:"flex justify-between items-center",children:[(0,a.jsx)("span",{className:"text-gray-600 text-xs",children:"Remaining:"}),(0,a.jsx)("span",{className:g("font-medium text-right",Z.isOverLimit&&"text-red-600",Z.isNearLimit&&"text-yellow-600"),children:p.total_users_remaining})]}),(0,a.jsxs)("div",{className:"flex justify-between items-center",children:[(0,a.jsx)("span",{className:"text-gray-600 text-xs",children:"Usage:"}),(0,a.jsxs)("span",{className:"font-medium text-right",children:[Math.round(Z.usagePercentage),"%"]})]}),(0,a.jsx)("div",{className:"w-full bg-gray-200 rounded-full h-2",children:(0,a.jsx)("div",{className:g("h-2 rounded-full transition-all duration-300",Z.isOverLimit&&"bg-red-500",Z.isNearLimit&&"bg-yellow-500",!Z.isOverLimit&&!Z.isNearLimit&&"bg-green-500"),style:{width:"".concat(Math.min(Z.usagePercentage,100),"%")}})})]}),null!==p.total_teams&&(0,a.jsxs)("div",{className:g("space-y-1 border rounded-md p-2",S.isOverLimit&&"border-red-200 bg-red-50",S.isNearLimit&&"border-yellow-200 bg-yellow-50"),children:[(0,a.jsxs)("div",{className:"flex items-center gap-2 text-xs text-gray-600 mb-1",children:[(0,a.jsx)(c.Z,{className:"h-3 w-3"}),(0,a.jsx)("span",{className:"font-medium",children:"Teams"}),(0,a.jsx)("span",{className:g("ml-1 px-1.5 py-0.5 rounded border",S.isOverLimit&&"bg-red-50 text-red-700 border-red-200",S.isNearLimit&&"bg-yellow-50 text-yellow-700 border-yellow-200",!S.isOverLimit&&!S.isNearLimit&&"bg-gray-50 text-gray-600 border-gray-200"),children:S.isOverLimit?"Over limit":S.isNearLimit?"Near limit":"OK"})]}),(0,a.jsxs)("div",{className:"flex justify-between items-center",children:[(0,a.jsx)("span",{className:"text-gray-600 text-xs",children:"Used:"}),(0,a.jsxs)("span",{className:"font-medium text-right",children:[p.total_teams_used,"/",p.total_teams]})]}),(0,a.jsxs)("div",{className:"flex justify-between items-center",children:[(0,a.jsx)("span",{className:"text-gray-600 text-xs",children:"Remaining:"}),(0,a.jsx)("span",{className:g("font-medium text-right",S.isOverLimit&&"text-red-600",S.isNearLimit&&"text-yellow-600"),children:p.total_teams_remaining})]}),(0,a.jsxs)("div",{className:"flex justify-between items-center",children:[(0,a.jsx)("span",{className:"text-gray-600 text-xs",children:"Usage:"}),(0,a.jsxs)("span",{className:"font-medium text-right",children:[Math.round(S.usagePercentage),"%"]})]}),(0,a.jsx)("div",{className:"w-full bg-gray-200 rounded-full h-2",children:(0,a.jsx)("div",{className:g("h-2 rounded-full transition-all duration-300",S.isOverLimit&&"bg-red-500",S.isNearLimit&&"bg-yellow-500",!S.isOverLimit&&!S.isNearLimit&&"bg-green-500"),style:{width:"".concat(Math.min(S.usagePercentage,100),"%")}})})]})]})]}),{})}):null}}}]); \ No newline at end of file diff --git a/litellm/proxy/_experimental/out/_next/static/chunks/2249-01a36f26b1cecba3.js b/litellm/proxy/_experimental/out/_next/static/chunks/2249-01a36f26b1cecba3.js new file mode 100644 index 00000000000..b21241beec0 --- /dev/null +++ b/litellm/proxy/_experimental/out/_next/static/chunks/2249-01a36f26b1cecba3.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[2249],{64748:function(e,s,l){l.d(s,{Ct:function(){return a.Z},Dx:function(){return m.Z},OK:function(){return n.Z},Zb:function(){return r.Z},nP:function(){return o.Z},td:function(){return c.Z},v0:function(){return i.Z},x4:function(){return d.Z},xv:function(){return x.Z},zx:function(){return t.Z}});var a=l(41649),t=l(78489),r=l(12514),n=l(12485),i=l(18135),c=l(35242),d=l(29706),o=l(77991),x=l(84264),m=l(96761)},78801:function(e,s,l){l.d(s,{Z:function(){return a.Z},x:function(){return t.Z}});var a=l(12514),t=l(84264)},92249:function(e,s,l){l.d(s,{Z:function(){return W}});var a=l(57437),t=l(23639),r=l(64748),n=l(22116),i=l(78867),c=l(99376),d=l(2265),o=l(17906),x=l(20347),m=l(41649),h=l(78489),u=l(84264),p=l(99981),g=l(3810),j=l(15424),v=l(15690),b=l(10032),N=l(61994),f=l(5545),y=l(96761),_=l(19250),k=l(9114);let{Step:w}=v.default;var Z=e=>{let{visible:s,onClose:l,accessToken:t,agentHubData:r,onSuccess:i}=e,[c,o]=(0,d.useState)(0),[x,h]=(0,d.useState)(new Set),[p,g]=(0,d.useState)(!1),[j]=b.Z.useForm(),Z=()=>{o(0),h(new Set),j.resetFields(),l()},C=(e,s)=>{let l=new Set(x);s?l.add(e):l.delete(e),h(l)},S=e=>{e?h(new Set(r.map(e=>e.agent_id||e.name))):h(new Set)};(0,d.useEffect)(()=>{s&&r.length>0&&h(new Set(r.filter(e=>!0===e.is_public).map(e=>e.agent_id||e.name)))},[s,r]);let P=async()=>{if(0===x.size){k.Z.fromBackend("Please select at least one agent to make public");return}g(!0);try{let e=Array.from(x);await (0,_.makeAgentsPublicCall)(t,e),k.Z.success("Successfully made ".concat(e.length," agent(s) public!")),Z(),i()}catch(e){console.error("Error making agents public:",e),k.Z.fromBackend("Failed to make agents public. Please try again.")}finally{g(!1)}},M=()=>{let e=r.length>0&&r.every(e=>x.has(e.agent_id||e.name)),s=x.size>0&&!e;return(0,a.jsxs)("div",{className:"space-y-4",children:[(0,a.jsxs)("div",{className:"flex items-center justify-between",children:[(0,a.jsx)(y.Z,{children:"Select Agents to Make Public"}),(0,a.jsx)("div",{className:"flex items-center space-x-2",children:(0,a.jsxs)(N.Z,{checked:e,indeterminate:s,onChange:e=>S(e.target.checked),disabled:0===r.length,children:["Select All ",r.length>0&&"(".concat(r.length,")")]})})]}),(0,a.jsx)(u.Z,{className:"text-sm text-gray-600",children:"Select the agents you want to be visible on the public model hub. Users will still require a valid API key to use these agents."}),(0,a.jsx)("div",{className:"max-h-96 overflow-y-auto border rounded-lg p-4",children:(0,a.jsx)("div",{className:"space-y-3",children:0===r.length?(0,a.jsx)("div",{className:"text-center py-8 text-gray-500",children:(0,a.jsx)(u.Z,{children:"No agents available."})}):r.map(e=>{let s=e.agent_id||e.name;return(0,a.jsxs)("div",{className:"flex items-center space-x-3 p-3 border rounded-lg hover:bg-gray-50",children:[(0,a.jsx)(N.Z,{checked:x.has(s),onChange:e=>C(s,e.target.checked)}),(0,a.jsxs)("div",{className:"flex-1",children:[(0,a.jsxs)("div",{className:"flex items-center space-x-2",children:[(0,a.jsx)(u.Z,{className:"font-medium",children:e.name}),(0,a.jsxs)(m.Z,{color:"blue",size:"sm",children:["v",e.version]})]}),(0,a.jsx)(u.Z,{className:"text-xs text-gray-600 mt-1",children:e.description}),e.skills&&e.skills.length>0&&(0,a.jsxs)("div",{className:"flex flex-wrap gap-1 mt-1",children:[e.skills.slice(0,3).map(e=>(0,a.jsx)(m.Z,{color:"purple",size:"xs",children:e.name},e.id)),e.skills.length>3&&(0,a.jsxs)(u.Z,{className:"text-xs text-gray-500",children:["+",e.skills.length-3," more"]})]})]})]},s)})})}),x.size>0&&(0,a.jsx)("div",{className:"bg-blue-50 border border-blue-200 rounded-lg p-3",children:(0,a.jsxs)(u.Z,{className:"text-sm text-blue-800",children:[(0,a.jsx)("strong",{children:x.size})," agent",1!==x.size?"s":""," selected"]})})]})},z=()=>(0,a.jsxs)("div",{className:"space-y-4",children:[(0,a.jsx)(y.Z,{children:"Confirm Making Agents Public"}),(0,a.jsx)("div",{className:"bg-yellow-50 border border-yellow-200 rounded-lg p-4",children:(0,a.jsxs)(u.Z,{className:"text-sm text-yellow-800",children:[(0,a.jsx)("strong",{children:"Warning:"})," Once you make these agents public, anyone who can go to the"," ",(0,a.jsx)("code",{children:"/ui/model_hub_table"})," will be able to know they exist on the proxy."]})}),(0,a.jsxs)("div",{className:"space-y-3",children:[(0,a.jsx)(u.Z,{className:"font-medium",children:"Agents to be made public:"}),(0,a.jsx)("div",{className:"max-h-48 overflow-y-auto border rounded-lg p-3",children:(0,a.jsx)("div",{className:"space-y-2",children:Array.from(x).map(e=>{let s=r.find(s=>(s.agent_id||s.name)===e);return(0,a.jsx)("div",{className:"flex items-center justify-between p-2 bg-gray-50 rounded",children:(0,a.jsxs)("div",{className:"flex-1",children:[(0,a.jsxs)("div",{className:"flex items-center space-x-2",children:[(0,a.jsx)(u.Z,{className:"font-medium",children:(null==s?void 0:s.name)||e}),s&&(0,a.jsxs)(m.Z,{color:"blue",size:"xs",children:["v",s.version]})]}),(null==s?void 0:s.description)&&(0,a.jsx)(u.Z,{className:"text-xs text-gray-600 mt-1",children:s.description})]})},e)})})})]}),(0,a.jsx)("div",{className:"bg-blue-50 border border-blue-200 rounded-lg p-3",children:(0,a.jsxs)(u.Z,{className:"text-sm text-blue-800",children:["Total: ",(0,a.jsx)("strong",{children:x.size})," agent",1!==x.size?"s":""," will be made public"]})})]});return(0,a.jsx)(n.Z,{title:"Make Agents Public",open:s,onCancel:Z,footer:null,width:1200,maskClosable:!1,children:(0,a.jsxs)(b.Z,{form:j,layout:"vertical",children:[(0,a.jsxs)(v.default,{current:c,className:"mb-6",children:[(0,a.jsx)(w,{title:"Select Agents"}),(0,a.jsx)(w,{title:"Confirm"})]}),(()=>{switch(c){case 0:return M();case 1:return z();default:return null}})(),(0,a.jsxs)("div",{className:"flex justify-between mt-6",children:[(0,a.jsx)(f.ZP,{onClick:0===c?Z:()=>{1===c&&o(0)},children:0===c?"Cancel":"Previous"}),(0,a.jsxs)("div",{className:"flex space-x-2",children:[0===c&&(0,a.jsx)(f.ZP,{onClick:()=>{if(0===c){if(0===x.size){k.Z.fromBackend("Please select at least one agent to make public");return}o(1)}},disabled:0===x.size,children:"Next"}),1===c&&(0,a.jsx)(f.ZP,{onClick:P,loading:p,children:"Make Public"})]})]})]})})};let{Step:C}=v.default;var S=e=>{let{visible:s,onClose:l,accessToken:t,mcpHubData:r,onSuccess:i}=e,[c,o]=(0,d.useState)(0),[x,h]=(0,d.useState)(new Set),[p,g]=(0,d.useState)(!1),[j]=b.Z.useForm(),w=()=>{o(0),h(new Set),j.resetFields(),l()},Z=(e,s)=>{let l=new Set(x);s?l.add(e):l.delete(e),h(l)},S=e=>{e?h(new Set(r.map(e=>e.server_id))):h(new Set)};(0,d.useEffect)(()=>{s&&r.length>0&&h(new Set(r.filter(e=>{var s;return(null===(s=e.mcp_info)||void 0===s?void 0:s.is_public)===!0}).map(e=>e.server_id)))},[s]);let P=async()=>{if(0===x.size){k.Z.fromBackend("Please select at least one MCP server to make public");return}g(!0);try{let e=Array.from(x);await (0,_.makeMCPPublicCall)(t,e),k.Z.success("Successfully made ".concat(e.length," MCP server(s) public!")),w(),i()}catch(e){console.error("Error making MCP servers public:",e),k.Z.fromBackend("Failed to make MCP servers public. Please try again.")}finally{g(!1)}},M=()=>{let e=r.length>0&&r.every(e=>x.has(e.server_id)),s=x.size>0&&!e;return(0,a.jsxs)("div",{className:"space-y-4",children:[(0,a.jsxs)("div",{className:"flex items-center justify-between",children:[(0,a.jsx)(y.Z,{children:"Select MCP Servers to Make Public"}),(0,a.jsx)("div",{className:"flex items-center space-x-2",children:(0,a.jsxs)(N.Z,{checked:e,indeterminate:s,onChange:e=>S(e.target.checked),disabled:0===r.length,children:["Select All ",r.length>0&&"(".concat(r.length,")")]})})]}),(0,a.jsx)(u.Z,{className:"text-sm text-gray-600",children:"Select the MCP servers you want to be visible on the public model hub. Users will still require a valid API key to use these servers."}),(0,a.jsx)("div",{className:"max-h-96 overflow-y-auto border rounded-lg p-4",children:(0,a.jsx)("div",{className:"space-y-3",children:0===r.length?(0,a.jsx)("div",{className:"text-center py-8 text-gray-500",children:(0,a.jsx)(u.Z,{children:"No MCP servers available."})}):r.map(e=>{var s;let l=(null===(s=e.mcp_info)||void 0===s?void 0:s.is_public)===!0;return(0,a.jsxs)("div",{className:"flex items-center space-x-3 p-3 border rounded-lg hover:bg-gray-50",children:[(0,a.jsx)(N.Z,{checked:x.has(e.server_id),onChange:s=>Z(e.server_id,s.target.checked)}),(0,a.jsxs)("div",{className:"flex-1",children:[(0,a.jsxs)("div",{className:"flex items-center space-x-2",children:[(0,a.jsx)(u.Z,{className:"font-medium",children:e.server_name}),l&&(0,a.jsx)(m.Z,{color:"emerald",size:"sm",children:"Public"}),(0,a.jsx)(m.Z,{color:"blue",size:"sm",children:e.transport}),(0,a.jsx)(m.Z,{color:"active"===e.status||"healthy"===e.status?"green":"inactive"===e.status||"unhealthy"===e.status?"red":"gray",size:"sm",children:e.status||"unknown"})]}),(0,a.jsx)(u.Z,{className:"text-xs text-gray-600 mt-1",children:e.description||e.url}),e.allowed_tools&&e.allowed_tools.length>0&&(0,a.jsxs)("div",{className:"flex flex-wrap gap-1 mt-1",children:[e.allowed_tools.slice(0,3).map((e,s)=>(0,a.jsx)(m.Z,{color:"purple",size:"xs",children:e},s)),e.allowed_tools.length>3&&(0,a.jsxs)(u.Z,{className:"text-xs text-gray-500",children:["+",e.allowed_tools.length-3," more"]})]})]})]},e.server_id)})})}),x.size>0&&(0,a.jsx)("div",{className:"bg-blue-50 border border-blue-200 rounded-lg p-3",children:(0,a.jsxs)(u.Z,{className:"text-sm text-blue-800",children:[(0,a.jsx)("strong",{children:x.size})," MCP server",1!==x.size?"s":""," selected"]})})]})},z=()=>(0,a.jsxs)("div",{className:"space-y-4",children:[(0,a.jsx)(y.Z,{children:"Confirm Making MCP Servers Public"}),(0,a.jsx)("div",{className:"bg-yellow-50 border border-yellow-200 rounded-lg p-4",children:(0,a.jsxs)(u.Z,{className:"text-sm text-yellow-800",children:[(0,a.jsx)("strong",{children:"Warning:"})," Once you make these MCP servers public, anyone who can go to the"," ",(0,a.jsx)("code",{children:"/ui/model_hub_table"})," will be able to know they exist on the proxy."]})}),(0,a.jsxs)("div",{className:"space-y-3",children:[(0,a.jsx)(u.Z,{className:"font-medium",children:"MCP Servers to be made public:"}),(0,a.jsx)("div",{className:"max-h-48 overflow-y-auto border rounded-lg p-3",children:(0,a.jsx)("div",{className:"space-y-2",children:Array.from(x).map(e=>{let s=r.find(s=>s.server_id===e);return(0,a.jsx)("div",{className:"flex items-center justify-between p-2 bg-gray-50 rounded",children:(0,a.jsxs)("div",{className:"flex-1",children:[(0,a.jsxs)("div",{className:"flex items-center space-x-2",children:[(0,a.jsx)(u.Z,{className:"font-medium",children:(null==s?void 0:s.server_name)||e}),s&&(0,a.jsxs)(a.Fragment,{children:[(0,a.jsx)(m.Z,{color:"blue",size:"xs",children:s.transport}),(0,a.jsx)(m.Z,{color:"active"===s.status||"healthy"===s.status?"green":"inactive"===s.status||"unhealthy"===s.status?"red":"gray",size:"xs",children:s.status||"unknown"})]})]}),(null==s?void 0:s.description)&&(0,a.jsx)(u.Z,{className:"text-xs text-gray-600 mt-1",children:s.description}),(null==s?void 0:s.url)&&(0,a.jsx)(u.Z,{className:"text-xs text-gray-500 mt-1",children:s.url})]})},e)})})})]}),(0,a.jsx)("div",{className:"bg-blue-50 border border-blue-200 rounded-lg p-3",children:(0,a.jsxs)(u.Z,{className:"text-sm text-blue-800",children:["Total: ",(0,a.jsx)("strong",{children:x.size})," MCP server",1!==x.size?"s":""," will be made public"]})})]});return(0,a.jsx)(n.Z,{title:"Make MCP Servers Public",open:s,onCancel:w,footer:null,width:1200,maskClosable:!1,children:(0,a.jsxs)(b.Z,{form:j,layout:"vertical",children:[(0,a.jsxs)(v.default,{current:c,className:"mb-6",children:[(0,a.jsx)(C,{title:"Select Servers"}),(0,a.jsx)(C,{title:"Confirm"})]}),(()=>{switch(c){case 0:return M();case 1:return z();default:return null}})(),(0,a.jsxs)("div",{className:"flex justify-between mt-6",children:[(0,a.jsx)(f.ZP,{onClick:0===c?w:()=>{1===c&&o(0)},children:0===c?"Cancel":"Previous"}),(0,a.jsxs)("div",{className:"flex space-x-2",children:[0===c&&(0,a.jsx)(f.ZP,{onClick:()=>{if(0===c){if(0===x.size){k.Z.fromBackend("Please select at least one MCP server to make public");return}o(1)}},disabled:0===x.size,children:"Next"}),1===c&&(0,a.jsx)(f.ZP,{onClick:P,loading:p,children:"Make Public"})]})]})]})})},P=l(78801),M=e=>{let{modelHubData:s,onFilteredDataChange:l,showFiltersCard:t=!0,className:r=""}=e,[n,i]=(0,d.useState)(""),[c,o]=(0,d.useState)(""),[x,m]=(0,d.useState)(""),[h,u]=(0,d.useState)(""),p=(0,d.useRef)([]),g=(0,d.useMemo)(()=>(null==s?void 0:s.filter(e=>{let s=e.model_group.toLowerCase().includes(n.toLowerCase()),l=""===c||e.providers.includes(c),a=""===x||e.mode===x,t=""===h||Object.entries(e).filter(e=>{let[s,l]=e;return s.startsWith("supports_")&&!0===l}).some(e=>{let[s]=e;return s.replace(/^supports_/,"").split("_").map(e=>e.charAt(0).toUpperCase()+e.slice(1)).join(" ")===h});return s&&l&&a&&t}))||[],[s,n,c,x,h]);(0,d.useEffect)(()=>{(g.length!==p.current.length||g.some((e,s)=>{var l;return e.model_group!==(null===(l=p.current[s])||void 0===l?void 0:l.model_group)}))&&(p.current=g,l(g))},[g,l]);let j=(0,a.jsxs)("div",{className:"flex flex-wrap gap-4 items-center",children:[(0,a.jsxs)("div",{children:[(0,a.jsx)(P.x,{className:"text-sm font-medium mb-2",children:"Search Models:"}),(0,a.jsx)("input",{type:"text",placeholder:"Search model names...",value:n,onChange:e=>i(e.target.value),className:"border rounded px-3 py-2 w-64 h-10 text-sm"})]}),(0,a.jsxs)("div",{children:[(0,a.jsx)(P.x,{className:"text-sm font-medium mb-2",children:"Provider:"}),(0,a.jsxs)("select",{value:c,onChange:e=>o(e.target.value),className:"border rounded px-3 py-2 text-sm text-gray-600 w-40 h-10",children:[(0,a.jsx)("option",{value:"",className:"text-sm text-gray-600",children:"All Providers"}),s&&(e=>{let s=new Set;return e.forEach(e=>{e.providers.forEach(e=>s.add(e))}),Array.from(s)})(s).map(e=>(0,a.jsx)("option",{value:e,className:"text-sm text-gray-800",children:e},e))]})]}),(0,a.jsxs)("div",{children:[(0,a.jsx)(P.x,{className:"text-sm font-medium mb-2",children:"Mode:"}),(0,a.jsxs)("select",{value:x,onChange:e=>m(e.target.value),className:"border rounded px-3 py-2 text-sm text-gray-600 w-32 h-10",children:[(0,a.jsx)("option",{value:"",className:"text-sm text-gray-600",children:"All Modes"}),s&&(e=>{let s=new Set;return e.forEach(e=>{e.mode&&s.add(e.mode)}),Array.from(s)})(s).map(e=>(0,a.jsx)("option",{value:e,className:"text-sm text-gray-800",children:e},e))]})]}),(0,a.jsxs)("div",{children:[(0,a.jsx)(P.x,{className:"text-sm font-medium mb-2",children:"Features:"}),(0,a.jsxs)("select",{value:h,onChange:e=>u(e.target.value),className:"border rounded px-3 py-2 text-sm text-gray-600 w-48 h-10",children:[(0,a.jsx)("option",{value:"",className:"text-sm text-gray-600",children:"All Features"}),s&&(e=>{let s=new Set;return e.forEach(e=>{Object.entries(e).filter(e=>{let[s,l]=e;return s.startsWith("supports_")&&!0===l}).forEach(e=>{let[l]=e,a=l.replace(/^supports_/,"").split("_").map(e=>e.charAt(0).toUpperCase()+e.slice(1)).join(" ");s.add(a)})}),Array.from(s).sort()})(s).map(e=>(0,a.jsx)("option",{value:e,className:"text-sm text-gray-800",children:e},e))]})]}),(n||c||x||h)&&(0,a.jsx)("div",{className:"flex items-end",children:(0,a.jsx)("button",{onClick:()=>{i(""),o(""),m(""),u("")},className:"text-blue-600 hover:text-blue-800 text-sm underline h-10 flex items-center",children:"Clear Filters"})})]});return t?(0,a.jsx)(P.Z,{className:"mb-6 ".concat(r),children:j}):(0,a.jsx)("div",{className:r,children:j})};let{Step:z}=v.default;var A=e=>{let{visible:s,onClose:l,accessToken:t,modelHubData:r,onSuccess:i}=e,[c,o]=(0,d.useState)(0),[x,h]=(0,d.useState)(new Set),[p,g]=(0,d.useState)([]),[j,w]=(0,d.useState)(!1),[Z]=b.Z.useForm(),C=()=>{o(0),h(new Set),g([]),Z.resetFields(),l()},S=(e,s)=>{let l=new Set(x);s?l.add(e):l.delete(e),h(l)},P=e=>{e?h(new Set(p.map(e=>e.model_group))):h(new Set)},A=(0,d.useCallback)(e=>{g(e)},[]);(0,d.useEffect)(()=>{s&&r.length>0&&(g(r),h(new Set(r.filter(e=>!0===e.is_public_model_group).map(e=>e.model_group))))},[s,r]);let F=async()=>{if(0===x.size){k.Z.fromBackend("Please select at least one model to make public");return}w(!0);try{let e=Array.from(x);await (0,_.makeModelGroupPublic)(t,e),k.Z.success("Successfully made ".concat(e.length," model group(s) public!")),C(),i()}catch(e){console.error("Error making model groups public:",e),k.Z.fromBackend("Failed to make model groups public. Please try again.")}finally{w(!1)}},L=()=>{let e=p.length>0&&p.every(e=>x.has(e.model_group)),s=x.size>0&&!e;return(0,a.jsxs)("div",{className:"space-y-4",children:[(0,a.jsxs)("div",{className:"flex items-center justify-between",children:[(0,a.jsx)(y.Z,{children:"Select Models to Make Public"}),(0,a.jsx)("div",{className:"flex items-center space-x-2",children:(0,a.jsxs)(N.Z,{checked:e,indeterminate:s,onChange:e=>P(e.target.checked),disabled:0===p.length,children:["Select All ",p.length>0&&"(".concat(p.length,")")]})})]}),(0,a.jsx)(u.Z,{className:"text-sm text-gray-600",children:"Select the models you want to be visible on the public model hub. Users will still require a valid API key to use these models."}),(0,a.jsx)(M,{modelHubData:r,onFilteredDataChange:A,showFiltersCard:!1,className:"border rounded-lg p-4 bg-gray-50"}),(0,a.jsx)("div",{className:"max-h-96 overflow-y-auto border rounded-lg p-4",children:(0,a.jsx)("div",{className:"space-y-3",children:0===p.length?(0,a.jsx)("div",{className:"text-center py-8 text-gray-500",children:(0,a.jsx)(u.Z,{children:"No models match the current filters."})}):p.map(e=>(0,a.jsxs)("div",{className:"flex items-center space-x-3 p-3 border rounded-lg hover:bg-gray-50",children:[(0,a.jsx)(N.Z,{checked:x.has(e.model_group),onChange:s=>S(e.model_group,s.target.checked)}),(0,a.jsxs)("div",{className:"flex-1",children:[(0,a.jsxs)("div",{className:"flex items-center space-x-2",children:[(0,a.jsx)(u.Z,{className:"font-medium",children:e.model_group}),e.mode&&(0,a.jsx)(m.Z,{color:"green",size:"sm",children:e.mode})]}),(0,a.jsx)("div",{className:"flex flex-wrap gap-1 mt-1",children:e.providers.map(e=>(0,a.jsx)(m.Z,{color:"blue",size:"xs",children:e},e))})]})]},e.model_group))})}),x.size>0&&(0,a.jsx)("div",{className:"bg-blue-50 border border-blue-200 rounded-lg p-3",children:(0,a.jsxs)(u.Z,{className:"text-sm text-blue-800",children:[(0,a.jsx)("strong",{children:x.size})," model",1!==x.size?"s":""," selected"]})})]})},D=()=>(0,a.jsxs)("div",{className:"space-y-4",children:[(0,a.jsx)(y.Z,{children:"Confirm Making Models Public"}),(0,a.jsx)("div",{className:"bg-yellow-50 border border-yellow-200 rounded-lg p-4",children:(0,a.jsxs)(u.Z,{className:"text-sm text-yellow-800",children:[(0,a.jsx)("strong",{children:"Warning:"})," Once you make these models public, anyone who can go to the"," ",(0,a.jsx)("code",{children:"/ui/model_hub_table"})," will be able to know they exist on the proxy."]})}),(0,a.jsxs)("div",{className:"space-y-3",children:[(0,a.jsx)(u.Z,{className:"font-medium",children:"Models to be made public:"}),(0,a.jsx)("div",{className:"max-h-48 overflow-y-auto border rounded-lg p-3",children:(0,a.jsx)("div",{className:"space-y-2",children:Array.from(x).map(e=>{let s=r.find(s=>s.model_group===e);return(0,a.jsx)("div",{className:"flex items-center justify-between p-2 bg-gray-50 rounded",children:(0,a.jsxs)("div",{children:[(0,a.jsx)(u.Z,{className:"font-medium",children:e}),s&&(0,a.jsx)("div",{className:"flex flex-wrap gap-1 mt-1",children:s.providers.map(e=>(0,a.jsx)(m.Z,{color:"blue",size:"xs",children:e},e))})]})},e)})})})]}),(0,a.jsx)("div",{className:"bg-blue-50 border border-blue-200 rounded-lg p-3",children:(0,a.jsxs)(u.Z,{className:"text-sm text-blue-800",children:["Total: ",(0,a.jsx)("strong",{children:x.size})," model",1!==x.size?"s":""," will be made public"]})})]});return(0,a.jsx)(n.Z,{title:"Make Models Public",open:s,onCancel:C,footer:null,width:1200,maskClosable:!1,children:(0,a.jsxs)(b.Z,{form:Z,layout:"vertical",children:[(0,a.jsxs)(v.default,{current:c,className:"mb-6",children:[(0,a.jsx)(z,{title:"Select Models"}),(0,a.jsx)(z,{title:"Confirm"})]}),(()=>{switch(c){case 0:return L();case 1:return D();default:return null}})(),(0,a.jsxs)("div",{className:"flex justify-between mt-6",children:[(0,a.jsx)(f.ZP,{onClick:0===c?C:()=>{1===c&&o(0)},children:0===c?"Cancel":"Previous"}),(0,a.jsxs)("div",{className:"flex space-x-2",children:[0===c&&(0,a.jsx)(f.ZP,{onClick:()=>{if(0===c){if(0===x.size){k.Z.fromBackend("Please select at least one model to make public");return}o(1)}},disabled:0===x.size,children:"Next"}),1===c&&(0,a.jsx)(f.ZP,{onClick:F,loading:j,children:"Make Public"})]})]})]})})},F=l(8048);let L=e=>e.replace(/^supports_/,"").split("_").map(e=>e.charAt(0).toUpperCase()+e.slice(1)).join(" "),D=e=>Object.entries(e).filter(e=>{let[s,l]=e;return s.startsWith("supports_")&&!0===l}).map(e=>{let[s]=e;return s}),O=e=>"$".concat((1e6*e).toFixed(2)),U=e=>e>=1e6?"".concat((e/1e6).toFixed(1),"M"):e>=1e3?"".concat((e/1e3).toFixed(1),"K"):e.toString(),E=function(e,s){let l=arguments.length>2&&void 0!==arguments[2]&&arguments[2],r=[{header:"Public Model Name",accessorKey:"model_group",enableSorting:!0,sortingFn:"alphanumeric",cell:e=>{let{row:l}=e,r=l.original;return(0,a.jsxs)("div",{className:"space-y-1",children:[(0,a.jsxs)("div",{className:"flex items-center space-x-2",children:[(0,a.jsx)(u.Z,{className:"font-medium text-sm",children:r.model_group}),(0,a.jsx)(p.Z,{title:"Copy model name",children:(0,a.jsx)(t.Z,{onClick:()=>s(r.model_group),className:"cursor-pointer text-gray-500 hover:text-blue-500 text-xs"})})]}),(0,a.jsx)("div",{className:"md:hidden",children:(0,a.jsx)(u.Z,{className:"text-xs text-gray-600",children:r.providers.join(", ")})})]})}},{header:"Provider",accessorKey:"providers",enableSorting:!0,sortingFn:(e,s)=>{let l=e.original.providers.join(", "),a=s.original.providers.join(", ");return l.localeCompare(a)},cell:e=>{let{row:s}=e,l=s.original;return(0,a.jsxs)("div",{className:"flex flex-wrap gap-1",children:[l.providers.slice(0,2).map(e=>(0,a.jsx)(g.Z,{color:"blue",className:"text-xs",children:e},e)),l.providers.length>2&&(0,a.jsxs)(u.Z,{className:"text-xs text-gray-500",children:["+",l.providers.length-2]})]})},meta:{className:"hidden md:table-cell"}},{header:"Mode",accessorKey:"mode",enableSorting:!0,sortingFn:"alphanumeric",cell:e=>{let{row:s}=e,l=s.original;return l.mode?(0,a.jsx)(m.Z,{color:"green",size:"sm",children:l.mode}):(0,a.jsx)(u.Z,{className:"text-gray-500",children:"-"})},meta:{className:"hidden lg:table-cell"}},{header:"Tokens",accessorKey:"max_input_tokens",enableSorting:!0,sortingFn:(e,s)=>(e.original.max_input_tokens||0)+(e.original.max_output_tokens||0)-((s.original.max_input_tokens||0)+(s.original.max_output_tokens||0)),cell:e=>{let{row:s}=e,l=s.original;return(0,a.jsx)("div",{className:"space-y-1",children:(0,a.jsxs)(u.Z,{className:"text-xs",children:[l.max_input_tokens?U(l.max_input_tokens):"-"," /"," ",l.max_output_tokens?U(l.max_output_tokens):"-"]})})},meta:{className:"hidden lg:table-cell"}},{header:"Cost/1M",accessorKey:"input_cost_per_token",enableSorting:!0,sortingFn:(e,s)=>(e.original.input_cost_per_token||0)+(e.original.output_cost_per_token||0)-((s.original.input_cost_per_token||0)+(s.original.output_cost_per_token||0)),cell:e=>{let{row:s}=e,l=s.original;return(0,a.jsxs)("div",{className:"space-y-1",children:[(0,a.jsx)(u.Z,{className:"text-xs",children:l.input_cost_per_token?O(l.input_cost_per_token):"-"}),(0,a.jsx)(u.Z,{className:"text-xs text-gray-500",children:l.output_cost_per_token?O(l.output_cost_per_token):"-"})]})}},{header:"Features",accessorKey:"capabilities",enableSorting:!1,cell:e=>{let{row:s}=e,l=D(s.original),t=["green","blue","purple","orange","red","yellow"];return(0,a.jsx)("div",{className:"flex flex-wrap gap-1",children:0===l.length?(0,a.jsx)(u.Z,{className:"text-gray-500 text-xs",children:"-"}):l.map((e,s)=>(0,a.jsx)(m.Z,{color:t[s%t.length],size:"xs",children:L(e)},e))})}},{header:"Public",accessorKey:"is_public_model_group",enableSorting:!0,sortingFn:(e,s)=>(!0===e.original.is_public_model_group?1:0)-(!0===s.original.is_public_model_group?1:0),cell:e=>{let{row:s}=e;return!0===s.original.is_public_model_group?(0,a.jsx)(m.Z,{color:"green",size:"xs",children:"Yes"}):(0,a.jsx)(m.Z,{color:"gray",size:"xs",children:"No"})},meta:{className:"hidden md:table-cell"}},{header:"Details",id:"details",enableSorting:!1,cell:s=>{let{row:l}=s,t=l.original;return(0,a.jsxs)(h.Z,{size:"xs",variant:"secondary",onClick:()=>e(t),icon:j.Z,children:[(0,a.jsx)("span",{className:"hidden lg:inline",children:"Details"}),(0,a.jsx)("span",{className:"lg:hidden",children:"Info"})]})}}];return l?r.filter(e=>!("accessorKey"in e)||"is_public_model_group"!==e.accessorKey):r};var K=l(87526),T=l(86462),H=l(47686),I=l(77355),R=l(93416),B=l(74998),Y=l(95704),V=e=>{let{accessToken:s,userRole:l}=e,[t,r]=(0,d.useState)([]),[i,c]=(0,d.useState)({url:"",displayName:""}),[o,m]=(0,d.useState)(null),[h,u]=(0,d.useState)(!1),[p,g]=(0,d.useState)(!0),j=async()=>{if(s)try{u(!0);let e=await (0,_.getPublicModelHubInfo)();if(e&&e.useful_links){let s=e.useful_links||{},l=Object.entries(s).map((e,s)=>{let[l,a]=e;return{id:"".concat(s,"-").concat(l),displayName:l,url:a}});r(l)}else r([])}catch(e){console.error("Error fetching useful links:",e),r([])}finally{u(!1)}};if((0,d.useEffect)(()=>{j()},[s]),!(0,x.tY)(l||""))return null;let v=async e=>{if(!s)return!1;try{let l={};return e.forEach(e=>{l[e.displayName]=e.url}),await (0,_.updateUsefulLinksCall)(s,l),n.Z.success({title:"Links Saved Successfully",content:(0,a.jsxs)("div",{className:"py-4",children:[(0,a.jsx)("p",{className:"text-gray-600 mb-4",children:"Your useful links have been saved and are now visible on the public model hub."}),(0,a.jsxs)("div",{className:"bg-blue-50 border border-blue-200 rounded-lg p-4",children:[(0,a.jsx)("p",{className:"text-sm text-blue-800 mb-2 font-medium",children:"View your updated model hub:"}),(0,a.jsx)("a",{href:"".concat((0,_.getProxyBaseUrl)(),"/ui/model_hub_table"),target:"_blank",rel:"noopener noreferrer",className:"inline-flex items-center text-blue-600 hover:text-blue-800 underline text-sm font-medium",children:"Open Public Model Hub →"})]})]}),width:500,okText:"Close",maskClosable:!0,keyboard:!0}),!0}catch(e){return console.error("Error saving links:",e),k.Z.fromBackend("Failed to save links - ".concat(e)),!1}},b=async()=>{if(!i.url||!i.displayName)return;try{new URL(i.url)}catch(e){k.Z.fromBackend("Please enter a valid URL");return}if(t.some(e=>e.displayName===i.displayName)){k.Z.fromBackend("A link with this display name already exists");return}let e=[...t,{id:"".concat(Date.now(),"-").concat(i.displayName),displayName:i.displayName,url:i.url}];await v(e)&&(r(e),c({url:"",displayName:""}),k.Z.success("Link added successfully"))},N=e=>{m({...e})},f=async()=>{if(!o)return;try{new URL(o.url)}catch(e){k.Z.fromBackend("Please enter a valid URL");return}if(t.some(e=>e.id!==o.id&&e.displayName===o.displayName)){k.Z.fromBackend("A link with this display name already exists");return}let e=t.map(e=>e.id===o.id?o:e);await v(e)&&(r(e),m(null),k.Z.success("Link updated successfully"))},y=()=>{m(null)},w=async e=>{let s=t.filter(s=>s.id!==e);await v(s)&&(r(s),k.Z.success("Link deleted successfully"))},Z=e=>{window.open(e,"_blank")};return(0,a.jsxs)(Y.Zb,{className:"mb-6",children:[(0,a.jsxs)("div",{className:"flex items-center justify-between cursor-pointer",onClick:()=>g(!p),children:[(0,a.jsxs)("div",{className:"flex flex-col",children:[(0,a.jsx)(Y.Dx,{className:"mb-0",children:"Link Management"}),(0,a.jsx)("p",{className:"text-sm text-gray-500",children:"Manage the links that are displayed under 'Useful Links' on the public model hub."})]}),(0,a.jsx)("div",{className:"flex items-center",children:p?(0,a.jsx)(T.Z,{className:"w-5 h-5 text-gray-500"}):(0,a.jsx)(H.Z,{className:"w-5 h-5 text-gray-500"})})]}),p&&(0,a.jsxs)("div",{className:"mt-4",children:[(0,a.jsxs)("div",{className:"mb-6",children:[(0,a.jsx)(Y.xv,{className:"text-sm font-medium text-gray-700 mb-2",children:"Add New Link"}),(0,a.jsxs)("div",{className:"grid grid-cols-3 gap-4",children:[(0,a.jsxs)("div",{children:[(0,a.jsx)("label",{className:"block text-xs text-gray-500 mb-1",children:"URL"}),(0,a.jsx)("input",{type:"text",value:i.url,onChange:e=>c({...i,url:e.target.value}),placeholder:"https://example.com",className:"w-full px-3 py-2 border border-gray-300 rounded-md text-sm"})]}),(0,a.jsxs)("div",{children:[(0,a.jsx)("label",{className:"block text-xs text-gray-500 mb-1",children:"Display Name"}),(0,a.jsx)("input",{type:"text",value:i.displayName,onChange:e=>c({...i,displayName:e.target.value}),placeholder:"Friendly name",className:"w-full px-3 py-2 border border-gray-300 rounded-md text-sm"})]}),(0,a.jsx)("div",{className:"flex items-end",children:(0,a.jsxs)("button",{onClick:b,disabled:!i.url||!i.displayName,className:"flex items-center px-4 py-2 rounded-md text-sm ".concat(i.url&&i.displayName?"bg-green-600 text-white hover:bg-green-700":"bg-gray-300 text-gray-500 cursor-not-allowed"),children:[(0,a.jsx)(I.Z,{className:"w-4 h-4 mr-1"}),"Add Link"]})})]})]}),(0,a.jsx)(Y.xv,{className:"text-sm font-medium text-gray-700 mb-2",children:"Manage Existing Links"}),(0,a.jsx)("div",{className:"rounded-lg custom-border relative",children:(0,a.jsx)("div",{className:"overflow-x-auto",children:(0,a.jsxs)(Y.iA,{className:"[&_td]:py-0.5 [&_th]:py-1",children:[(0,a.jsx)(Y.ss,{children:(0,a.jsxs)(Y.SC,{children:[(0,a.jsx)(Y.xs,{className:"py-1 h-8",children:"Display Name"}),(0,a.jsx)(Y.xs,{className:"py-1 h-8",children:"URL"}),(0,a.jsx)(Y.xs,{className:"py-1 h-8",children:"Actions"})]})}),(0,a.jsxs)(Y.RM,{children:[t.map(e=>(0,a.jsx)(Y.SC,{className:"h-8",children:o&&o.id===e.id?(0,a.jsxs)(a.Fragment,{children:[(0,a.jsx)(Y.pj,{className:"py-0.5",children:(0,a.jsx)("input",{type:"text",value:o.displayName,onChange:e=>m({...o,displayName:e.target.value}),className:"w-full px-2 py-1 border border-gray-300 rounded-md text-sm"})}),(0,a.jsx)(Y.pj,{className:"py-0.5",children:(0,a.jsx)("input",{type:"text",value:o.url,onChange:e=>m({...o,url:e.target.value}),className:"w-full px-2 py-1 border border-gray-300 rounded-md text-sm"})}),(0,a.jsx)(Y.pj,{className:"py-0.5 whitespace-nowrap",children:(0,a.jsxs)("div",{className:"flex space-x-2",children:[(0,a.jsx)("button",{onClick:f,className:"text-xs bg-blue-50 text-blue-600 px-2 py-1 rounded hover:bg-blue-100",children:"Save"}),(0,a.jsx)("button",{onClick:y,className:"text-xs bg-gray-50 text-gray-600 px-2 py-1 rounded hover:bg-gray-100",children:"Cancel"})]})})]}):(0,a.jsxs)(a.Fragment,{children:[(0,a.jsx)(Y.pj,{className:"py-0.5 text-sm text-gray-900",children:e.displayName}),(0,a.jsx)(Y.pj,{className:"py-0.5 text-sm text-gray-500",children:e.url}),(0,a.jsx)(Y.pj,{className:"py-0.5 whitespace-nowrap",children:(0,a.jsxs)("div",{className:"flex space-x-2",children:[(0,a.jsx)("button",{onClick:()=>Z(e.url),className:"text-xs bg-green-50 text-green-600 px-2 py-1 rounded hover:bg-green-100",children:"Use"}),(0,a.jsx)("button",{onClick:()=>N(e),className:"text-xs bg-blue-50 text-blue-600 px-2 py-1 rounded hover:bg-blue-100",children:(0,a.jsx)(R.Z,{className:"w-3 h-3"})}),(0,a.jsx)("button",{onClick:()=>w(e.id),className:"text-xs bg-red-50 text-red-600 px-2 py-1 rounded hover:bg-red-100",children:(0,a.jsx)(B.Z,{className:"w-3 h-3"})})]})})]})},e.id)),0===t.length&&(0,a.jsx)(Y.SC,{children:(0,a.jsx)(Y.pj,{colSpan:3,className:"py-0.5 text-sm text-gray-500 text-center",children:"No links added yet. Add a new link above."})})]})]})})})]})]})},W=e=>{var s,l,v,b;let{accessToken:N,publicPage:f,premiumUser:y,userRole:w}=e,[C,P]=(0,d.useState)(!1),[z,L]=(0,d.useState)(null),[D,O]=(0,d.useState)(!0),[U,T]=(0,d.useState)(!1),[H,I]=(0,d.useState)(!1),[R,B]=(0,d.useState)(null),[Y,W]=(0,d.useState)([]),[q,G]=(0,d.useState)(!1),[J,$]=(0,d.useState)(null),[Q,X]=(0,d.useState)(!1),[ee,es]=(0,d.useState)(!0),[el,ea]=(0,d.useState)(null),[et,er]=(0,d.useState)(!1),[en,ei]=(0,d.useState)(null),[ec,ed]=(0,d.useState)(!0),[eo,ex]=(0,d.useState)(null),[em,eh]=(0,d.useState)(!1),[eu,ep]=(0,d.useState)(!1),eg=(0,c.useRouter)(),ej=(0,d.useRef)(null),ev=(0,d.useRef)(null),eb=(0,d.useRef)(null);(0,d.useEffect)(()=>{let e=async e=>{try{O(!0);let s=await (0,_.modelHubCall)(e);console.log("ModelHubData:",s),L(s.data),(0,_.getConfigFieldSetting)(e,"enable_public_model_hub").then(e=>{console.log("data: ".concat(JSON.stringify(e))),!0==e.field_value&&P(!0)}).catch(e=>{})}catch(e){console.error("There was an error fetching the model data",e)}finally{O(!1)}},s=async()=>{try{var e,s;O(!0),await (0,_.getUiConfig)();let l=await (0,_.modelHubPublicModelsCall)();console.log("ModelHubData:",l),console.log("First model structure:",l[0]),console.log("Model has model_group?",null===(e=l[0])||void 0===e?void 0:e.model_group),console.log("Model has providers?",null===(s=l[0])||void 0===s?void 0:s.providers),L(l),P(!0)}catch(e){console.error("There was an error fetching the public model data",e)}finally{O(!1)}};N?e(N):f&&s()},[N,f]),(0,d.useEffect)(()=>{let e=async()=>{if(N)try{es(!0);let e=await (0,_.getAgentsList)(N);console.log("AgentHubData:",e);let s=e.agents.map(e=>({agent_id:e.agent_id,...e.agent_card_params,is_public:e.litellm_params.is_public}));$(s)}catch(e){console.error("There was an error fetching the agent data",e)}finally{es(!1)}};f||e()},[f,N]),(0,d.useEffect)(()=>{let e=async()=>{if(N)try{ed(!0);let e=await (0,_.fetchMCPServers)(N);console.log("MCPHubData:",e),ei(e)}catch(e){console.error("There was an error fetching the MCP server data",e)}finally{ed(!1)}};f||e()},[f,N]);let eN=()=>{N&&G(!0)},ef=()=>{N&&X(!0)},ey=()=>{N&&ep(!0)},e_=()=>{T(!1),I(!1),B(null),er(!1),ea(null),eh(!1),ex(null)},ek=()=>{T(!1),I(!1),B(null),er(!1),ea(null),eh(!1),ex(null)},ew=e=>{navigator.clipboard.writeText(e),k.Z.success("Copied to clipboard!")},eZ=e=>e.replace(/^supports_/,"").split("_").map(e=>e.charAt(0).toUpperCase()+e.slice(1)).join(" "),eC=e=>Object.entries(e).filter(e=>{let[s,l]=e;return s.startsWith("supports_")&&!0===l}).map(e=>{let[s]=e;return s}),eS=e=>"$".concat((1e6*e).toFixed(2)),eP=(0,d.useCallback)(e=>{W(e)},[]);return(console.log("publicPage: ",f),console.log("publicPageAllowed: ",C),f&&C)?(0,a.jsx)(K.Z,{accessToken:N}):(0,a.jsxs)("div",{className:"w-full mx-4 h-[75vh]",children:[!1==f?(0,a.jsxs)("div",{className:"w-full m-2 mt-2 p-8",children:[(0,a.jsxs)("div",{className:"flex justify-between items-center mb-6",children:[(0,a.jsxs)("div",{className:"flex flex-col items-start",children:[(0,a.jsx)(r.Dx,{className:"text-center",children:"AI Hub"}),(0,x.tY)(w||"")?(0,a.jsx)("p",{className:"text-sm text-gray-600",children:"Make models, agents, and MCP servers public for developers to know what's available."}):(0,a.jsx)("p",{className:"text-sm text-gray-600",children:"A list of all public model names personally available to you."})]}),(0,a.jsxs)("div",{className:"flex items-center space-x-4",children:[(0,a.jsx)(r.xv,{children:"Model Hub URL:"}),(0,a.jsxs)("div",{className:"flex items-center bg-gray-200 px-2 py-1 rounded",children:[(0,a.jsx)(r.xv,{className:"mr-2",children:"".concat((0,_.getProxyBaseUrl)(),"/ui/model_hub_table")}),(0,a.jsx)("button",{onClick:()=>ew("".concat((0,_.getProxyBaseUrl)(),"/ui/model_hub_table")),className:"p-1 hover:bg-gray-300 rounded transition-colors",title:"Copy URL",children:(0,a.jsx)(i.Z,{size:16,className:"text-gray-600"})})]})]})]}),(0,x.tY)(w||"")&&(0,a.jsx)("div",{className:"mt-8 mb-2",children:(0,a.jsx)(V,{accessToken:N,userRole:w})}),(0,a.jsxs)(r.v0,{children:[(0,a.jsxs)(r.td,{className:"mb-4",children:[(0,a.jsx)(r.OK,{children:"Model Hub"}),(0,a.jsx)(r.OK,{children:"Agent Hub"}),(0,a.jsx)(r.OK,{children:"MCP Hub"})]}),(0,a.jsxs)(r.nP,{children:[(0,a.jsxs)(r.x4,{children:[(0,a.jsxs)(r.Zb,{children:[!1==f&&(0,x.tY)(w||"")&&(0,a.jsx)("div",{className:"flex justify-end mb-4",children:(0,a.jsx)(r.zx,{onClick:()=>eN(),children:"Select Models to Make Public"})}),(0,a.jsx)(M,{modelHubData:z||[],onFilteredDataChange:eP}),(0,a.jsx)(F.C,{columns:E(e=>{B(e),T(!0)},ew,f),data:Y,isLoading:D,table:ej,defaultSorting:[{id:"model_group",desc:!1}]})]}),(0,a.jsx)("div",{className:"mt-4 text-center space-y-2",children:(0,a.jsxs)(r.xv,{className:"text-sm text-gray-600",children:["Showing ",Y.length," of ",(null==z?void 0:z.length)||0," models"]})})]}),(0,a.jsxs)(r.x4,{children:[(0,a.jsxs)(r.Zb,{children:[!1==f&&(0,x.tY)(w||"")&&(0,a.jsx)("div",{className:"flex justify-end mb-4",children:(0,a.jsx)(r.zx,{onClick:()=>ef(),children:"Select Agents to Make Public"})}),(0,a.jsx)(F.C,{columns:function(e,s){return arguments.length>2&&void 0!==arguments[2]&&arguments[2],[{header:"Agent Name",accessorKey:"name",enableSorting:!0,sortingFn:"alphanumeric",cell:e=>{let{row:l}=e,r=l.original;return(0,a.jsxs)("div",{className:"space-y-1",children:[(0,a.jsxs)("div",{className:"flex items-center space-x-2",children:[(0,a.jsx)(u.Z,{className:"font-medium text-sm",children:r.name}),(0,a.jsx)(p.Z,{title:"Copy agent name",children:(0,a.jsx)(t.Z,{onClick:()=>s(r.name),className:"cursor-pointer text-gray-500 hover:text-blue-500 text-xs"})})]}),(0,a.jsx)("div",{className:"md:hidden",children:(0,a.jsx)(u.Z,{className:"text-xs text-gray-600",children:r.description})})]})}},{header:"Description",accessorKey:"description",enableSorting:!0,sortingFn:"alphanumeric",cell:e=>{let{row:s}=e,l=s.original;return(0,a.jsx)(u.Z,{className:"text-xs line-clamp-2",children:l.description||"-"})},meta:{className:"hidden md:table-cell"}},{header:"Version",accessorKey:"version",enableSorting:!0,sortingFn:"alphanumeric",cell:e=>{let{row:s}=e,l=s.original;return(0,a.jsxs)(m.Z,{color:"blue",size:"sm",children:["v",l.version]})},meta:{className:"hidden lg:table-cell"}},{header:"Protocol",accessorKey:"protocolVersion",enableSorting:!0,sortingFn:"alphanumeric",cell:e=>{let{row:s}=e,l=s.original;return(0,a.jsx)(u.Z,{className:"text-xs",children:l.protocolVersion||"-"})},meta:{className:"hidden lg:table-cell"}},{header:"Skills",accessorKey:"skills",enableSorting:!1,cell:e=>{let{row:s}=e,l=s.original.skills||[];return(0,a.jsxs)("div",{className:"space-y-1",children:[(0,a.jsxs)(u.Z,{className:"text-xs font-medium",children:[l.length," skill",1!==l.length?"s":""]}),l.length>0&&(0,a.jsxs)("div",{className:"flex flex-wrap gap-1",children:[l.slice(0,2).map(e=>(0,a.jsx)(g.Z,{color:"purple",className:"text-xs",children:e.name},e.id)),l.length>2&&(0,a.jsxs)(u.Z,{className:"text-xs text-gray-500",children:["+",l.length-2]})]})]})}},{header:"Capabilities",accessorKey:"capabilities",enableSorting:!1,cell:e=>{let{row:s}=e,l=Object.entries(s.original.capabilities||{}).filter(e=>{let[s,l]=e;return!0===l}).map(e=>{let[s]=e;return s});return(0,a.jsx)("div",{className:"flex flex-wrap gap-1",children:0===l.length?(0,a.jsx)(u.Z,{className:"text-gray-500 text-xs",children:"-"}):l.map(e=>(0,a.jsx)(m.Z,{color:"green",size:"xs",children:e},e))})}},{header:"I/O Modes",accessorKey:"defaultInputModes",enableSorting:!1,cell:e=>{let{row:s}=e,l=s.original,t=l.defaultInputModes||[],r=l.defaultOutputModes||[];return(0,a.jsxs)("div",{className:"space-y-1",children:[(0,a.jsxs)(u.Z,{className:"text-xs",children:[(0,a.jsx)("span",{className:"font-medium",children:"In:"})," ",t.join(", ")||"-"]}),(0,a.jsxs)(u.Z,{className:"text-xs",children:[(0,a.jsx)("span",{className:"font-medium",children:"Out:"})," ",r.join(", ")||"-"]})]})},meta:{className:"hidden xl:table-cell"}},{header:"Public",accessorKey:"is_public",enableSorting:!0,sortingFn:(e,s)=>(!0===e.original.is_public?1:0)-(!0===s.original.is_public?1:0),cell:e=>{let{row:s}=e;return console.log("CHECKPOINT 1: ".concat(JSON.stringify(s.original))),!0===s.original.is_public?(0,a.jsx)(m.Z,{color:"green",size:"xs",children:"Yes"}):(0,a.jsx)(m.Z,{color:"gray",size:"xs",children:"No"})},meta:{className:"hidden md:table-cell"}},{header:"Details",id:"details",enableSorting:!1,cell:s=>{let{row:l}=s,t=l.original;return(0,a.jsxs)(h.Z,{size:"xs",variant:"secondary",onClick:()=>e(t),icon:j.Z,children:[(0,a.jsx)("span",{className:"hidden lg:inline",children:"Details"}),(0,a.jsx)("span",{className:"lg:hidden",children:"Info"})]})}}]}(e=>{ea(e),er(!0)},ew,f),data:J||[],isLoading:ee,table:ev,defaultSorting:[{id:"name",desc:!1}]})]}),(0,a.jsx)("div",{className:"mt-4 text-center space-y-2",children:(0,a.jsxs)(r.xv,{className:"text-sm text-gray-600",children:["Showing ",(null==J?void 0:J.length)||0," agent",(null==J?void 0:J.length)!==1?"s":""]})})]}),(0,a.jsxs)(r.x4,{children:[(0,a.jsxs)(r.Zb,{children:[!1==f&&(0,x.tY)(w||"")&&(0,a.jsx)("div",{className:"flex justify-end mb-4",children:(0,a.jsx)(r.zx,{onClick:()=>ey(),children:"Select MCP Servers to Make Public"})}),(0,a.jsx)(F.C,{columns:function(e,s){return arguments.length>2&&void 0!==arguments[2]&&arguments[2],[{header:"Server Name",accessorKey:"server_name",enableSorting:!0,sortingFn:"alphanumeric",cell:e=>{let{row:l}=e,r=l.original;return(0,a.jsxs)("div",{className:"space-y-1",children:[(0,a.jsxs)("div",{className:"flex items-center space-x-2",children:[(0,a.jsx)(u.Z,{className:"font-medium text-sm",children:r.server_name}),(0,a.jsx)(p.Z,{title:"Copy server name",children:(0,a.jsx)(t.Z,{onClick:()=>s(r.server_name),className:"cursor-pointer text-gray-500 hover:text-blue-500 text-xs"})})]}),(0,a.jsx)("div",{className:"md:hidden",children:(0,a.jsx)(u.Z,{className:"text-xs text-gray-600",children:r.description||"-"})})]})}},{header:"Description",accessorKey:"description",enableSorting:!0,sortingFn:"alphanumeric",cell:e=>{let{row:s}=e,l=s.original;return(0,a.jsx)(u.Z,{className:"text-xs line-clamp-2",children:l.description||"-"})},meta:{className:"hidden md:table-cell"}},{header:"URL",accessorKey:"url",enableSorting:!0,sortingFn:"alphanumeric",cell:e=>{let{row:l}=e,r=l.original;return(0,a.jsxs)("div",{className:"flex items-center space-x-2",children:[(0,a.jsx)(u.Z,{className:"text-xs truncate max-w-xs",children:r.url}),(0,a.jsx)(p.Z,{title:"Copy URL",children:(0,a.jsx)(t.Z,{onClick:()=>s(r.url),className:"cursor-pointer text-gray-500 hover:text-blue-500 text-xs flex-shrink-0"})})]})},meta:{className:"hidden lg:table-cell"}},{header:"Transport",accessorKey:"transport",enableSorting:!0,sortingFn:"alphanumeric",cell:e=>{let{row:s}=e,l=s.original;return(0,a.jsx)(m.Z,{color:"blue",size:"sm",children:l.transport})},meta:{className:"hidden md:table-cell"}},{header:"Auth Type",accessorKey:"auth_type",enableSorting:!0,sortingFn:"alphanumeric",cell:e=>{let{row:s}=e,l=s.original,t="none"===l.auth_type?"gray":"green";return(0,a.jsx)(m.Z,{color:t,size:"sm",children:l.auth_type})},meta:{className:"hidden md:table-cell"}},{header:"Status",accessorKey:"status",enableSorting:!0,sortingFn:"alphanumeric",cell:e=>{let{row:s}=e,l=s.original,t={active:"green",inactive:"red",unknown:"gray",healthy:"green",unhealthy:"red"}[l.status]||"gray";return(0,a.jsx)(m.Z,{color:t,size:"sm",children:l.status||"unknown"})}},{header:"Tools",accessorKey:"allowed_tools",enableSorting:!1,cell:e=>{let{row:s}=e,l=s.original.allowed_tools||[];return(0,a.jsxs)("div",{className:"space-y-1",children:[(0,a.jsx)(u.Z,{className:"text-xs font-medium",children:l.length>0?"".concat(l.length," tool").concat(1!==l.length?"s":""):"All tools"}),l.length>0&&(0,a.jsxs)("div",{className:"flex flex-wrap gap-1",children:[l.slice(0,2).map((e,s)=>(0,a.jsx)(g.Z,{color:"purple",className:"text-xs",children:e},s)),l.length>2&&(0,a.jsxs)(u.Z,{className:"text-xs text-gray-500",children:["+",l.length-2]})]})]})},meta:{className:"hidden lg:table-cell"}},{header:"Created By",accessorKey:"created_by",enableSorting:!0,sortingFn:"alphanumeric",cell:e=>{let{row:s}=e,l=s.original;return(0,a.jsx)(u.Z,{className:"text-xs",children:l.created_by||"-"})},meta:{className:"hidden xl:table-cell"}},{header:"Public",accessorKey:"mcp_info.is_public",enableSorting:!0,sortingFn:(e,s)=>{var l,a;return((null===(l=e.original.mcp_info)||void 0===l?void 0:l.is_public)===!0?1:0)-((null===(a=s.original.mcp_info)||void 0===a?void 0:a.is_public)===!0?1:0)},cell:e=>{var s;let{row:l}=e;return(null===(s=l.original.mcp_info)||void 0===s?void 0:s.is_public)===!0?(0,a.jsx)(m.Z,{color:"green",size:"xs",children:"Yes"}):(0,a.jsx)(m.Z,{color:"gray",size:"xs",children:"No"})},meta:{className:"hidden md:table-cell"}},{header:"Details",id:"details",enableSorting:!1,cell:s=>{let{row:l}=s,t=l.original;return(0,a.jsxs)(h.Z,{size:"xs",variant:"secondary",onClick:()=>e(t),icon:j.Z,children:[(0,a.jsx)("span",{className:"hidden lg:inline",children:"Details"}),(0,a.jsx)("span",{className:"lg:hidden",children:"Info"})]})}}]}(e=>{ex(e),eh(!0)},ew,f),data:en||[],isLoading:ec,table:eb,defaultSorting:[{id:"server_name",desc:!1}]})]}),(0,a.jsx)("div",{className:"mt-4 text-center space-y-2",children:(0,a.jsxs)(r.xv,{className:"text-sm text-gray-600",children:["Showing ",(null==en?void 0:en.length)||0," MCP server",(null==en?void 0:en.length)!==1?"s":""]})})]})]})]})]}):(0,a.jsxs)(r.Zb,{className:"mx-auto max-w-xl mt-10",children:[(0,a.jsx)(r.xv,{className:"text-xl text-center mb-2 text-black",children:"Public Model Hub not enabled."}),(0,a.jsx)("p",{className:"text-base text-center text-slate-800",children:"Ask your proxy admin to enable this on their Admin UI."})]}),(0,a.jsx)(n.Z,{title:"Public Model Hub",width:600,visible:H,footer:null,onOk:e_,onCancel:ek,children:(0,a.jsxs)("div",{className:"pt-5 pb-5",children:[(0,a.jsxs)("div",{className:"flex justify-between mb-4",children:[(0,a.jsx)(r.xv,{className:"text-base mr-2",children:"Shareable Link:"}),(0,a.jsx)(r.xv,{className:"max-w-sm ml-2 bg-gray-200 pr-2 pl-2 pt-1 pb-1 text-center rounded",children:"".concat((0,_.getProxyBaseUrl)(),"/ui/model_hub_table")})]}),(0,a.jsx)("div",{className:"flex justify-end",children:(0,a.jsx)(r.zx,{onClick:()=>{eg.replace("/model_hub_table?key=".concat(N))},children:"See Page"})})]})}),(0,a.jsx)(n.Z,{title:(null==R?void 0:R.model_group)||"Model Details",width:1e3,visible:U,footer:null,onOk:e_,onCancel:ek,children:R&&(0,a.jsxs)("div",{className:"space-y-6",children:[(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"text-lg font-semibold mb-4",children:"Model Overview"}),(0,a.jsxs)("div",{className:"grid grid-cols-2 gap-4 mb-4",children:[(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"font-medium",children:"Model Group:"}),(0,a.jsx)(r.xv,{children:R.model_group})]}),(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"font-medium",children:"Mode:"}),(0,a.jsx)(r.xv,{children:R.mode||"Not specified"})]}),(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"font-medium",children:"Providers:"}),(0,a.jsx)("div",{className:"flex flex-wrap gap-1 mt-1",children:R.providers.map(e=>(0,a.jsx)(r.Ct,{color:"blue",children:e},e))})]})]})]}),(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"text-lg font-semibold mb-4",children:"Token & Cost Information"}),(0,a.jsxs)("div",{className:"grid grid-cols-2 gap-4",children:[(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"font-medium",children:"Max Input Tokens:"}),(0,a.jsx)(r.xv,{children:(null===(s=R.max_input_tokens)||void 0===s?void 0:s.toLocaleString())||"Not specified"})]}),(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"font-medium",children:"Max Output Tokens:"}),(0,a.jsx)(r.xv,{children:(null===(l=R.max_output_tokens)||void 0===l?void 0:l.toLocaleString())||"Not specified"})]}),(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"font-medium",children:"Input Cost per 1M Tokens:"}),(0,a.jsx)(r.xv,{children:R.input_cost_per_token?eS(R.input_cost_per_token):"Not specified"})]}),(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"font-medium",children:"Output Cost per 1M Tokens:"}),(0,a.jsx)(r.xv,{children:R.output_cost_per_token?eS(R.output_cost_per_token):"Not specified"})]})]})]}),(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"text-lg font-semibold mb-4",children:"Capabilities"}),(0,a.jsx)("div",{className:"flex flex-wrap gap-2",children:(()=>{let e=eC(R),s=["green","blue","purple","orange","red","yellow"];return 0===e.length?(0,a.jsx)(r.xv,{className:"text-gray-500",children:"No special capabilities listed"}):e.map((e,l)=>(0,a.jsx)(r.Ct,{color:s[l%s.length],children:eZ(e)},e))})()})]}),(R.tpm||R.rpm)&&(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"text-lg font-semibold mb-4",children:"Rate Limits"}),(0,a.jsxs)("div",{className:"grid grid-cols-2 gap-4",children:[R.tpm&&(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"font-medium",children:"Tokens per Minute:"}),(0,a.jsx)(r.xv,{children:R.tpm.toLocaleString()})]}),R.rpm&&(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"font-medium",children:"Requests per Minute:"}),(0,a.jsx)(r.xv,{children:R.rpm.toLocaleString()})]})]})]}),R.supported_openai_params&&(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"text-lg font-semibold mb-4",children:"Supported OpenAI Parameters"}),(0,a.jsx)("div",{className:"flex flex-wrap gap-2",children:R.supported_openai_params.map(e=>(0,a.jsx)(r.Ct,{color:"green",children:e},e))})]}),(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"text-lg font-semibold mb-4",children:"Usage Example"}),(0,a.jsx)(o.Z,{language:"python",className:"text-sm",children:'import openai\n\nclient = openai.OpenAI(\n api_key="your_api_key",\n base_url="http://0.0.0.0:4000" # Your LiteLLM Proxy URL\n)\n\nresponse = client.chat.completions.create(\n model="'.concat(R.model_group,'",\n messages=[\n {\n "role": "user",\n "content": "Hello, how are you?"\n }\n ]\n)\n\nprint(response.choices[0].message.content)')})]})]})}),(0,a.jsx)(n.Z,{title:(null==el?void 0:el.name)||"Agent Details",width:1e3,visible:et,footer:null,onOk:e_,onCancel:ek,children:el&&(0,a.jsxs)("div",{className:"space-y-6",children:[(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"text-lg font-semibold mb-4",children:"Agent Overview"}),(0,a.jsxs)("div",{className:"grid grid-cols-2 gap-4 mb-4",children:[(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"font-medium",children:"Name:"}),(0,a.jsx)(r.xv,{children:el.name})]}),(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"font-medium",children:"Version:"}),(0,a.jsxs)(r.Ct,{color:"blue",children:["v",el.version]})]}),(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"font-medium",children:"Protocol Version:"}),(0,a.jsx)(r.xv,{children:el.protocolVersion})]}),(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"font-medium",children:"URL:"}),(0,a.jsxs)("div",{className:"flex items-center space-x-2",children:[(0,a.jsx)(r.xv,{className:"truncate",children:el.url}),(0,a.jsx)(t.Z,{onClick:()=>ew(el.url),className:"cursor-pointer text-gray-500 hover:text-blue-500"})]})]})]}),(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"font-medium",children:"Description:"}),(0,a.jsx)(r.xv,{className:"mt-1",children:el.description})]})]}),el.capabilities&&Object.keys(el.capabilities).length>0&&(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"text-lg font-semibold mb-4",children:"Capabilities"}),(0,a.jsx)("div",{className:"flex flex-wrap gap-2",children:Object.entries(el.capabilities).filter(e=>{let[s,l]=e;return!0===l}).map(e=>{let[s]=e;return(0,a.jsx)(r.Ct,{color:"green",children:s},s)})})]}),(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"text-lg font-semibold mb-4",children:"Input/Output Modes"}),(0,a.jsxs)("div",{className:"grid grid-cols-2 gap-4",children:[(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"font-medium",children:"Input Modes:"}),(0,a.jsx)("div",{className:"flex flex-wrap gap-1 mt-1",children:(null===(v=el.defaultInputModes)||void 0===v?void 0:v.map(e=>(0,a.jsx)(r.Ct,{color:"blue",children:e},e)))||(0,a.jsx)(r.xv,{children:"Not specified"})})]}),(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"font-medium",children:"Output Modes:"}),(0,a.jsx)("div",{className:"flex flex-wrap gap-1 mt-1",children:(null===(b=el.defaultOutputModes)||void 0===b?void 0:b.map(e=>(0,a.jsx)(r.Ct,{color:"purple",children:e},e)))||(0,a.jsx)(r.xv,{children:"Not specified"})})]})]})]}),el.skills&&el.skills.length>0&&(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"text-lg font-semibold mb-4",children:"Skills"}),(0,a.jsx)("div",{className:"space-y-4",children:el.skills.map(e=>(0,a.jsxs)("div",{className:"border border-gray-200 rounded p-4",children:[(0,a.jsxs)("div",{className:"flex justify-between items-start mb-2",children:[(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"font-medium text-base",children:e.name}),(0,a.jsxs)(r.xv,{className:"text-xs text-gray-500",children:["ID: ",e.id]})]}),e.tags&&e.tags.length>0&&(0,a.jsx)("div",{className:"flex flex-wrap gap-1",children:e.tags.map(e=>(0,a.jsx)(r.Ct,{color:"purple",size:"xs",children:e},e))})]}),(0,a.jsx)(r.xv,{className:"text-sm mb-2",children:e.description}),e.examples&&e.examples.length>0&&(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"text-xs font-medium text-gray-700",children:"Examples:"}),(0,a.jsx)("div",{className:"flex flex-wrap gap-1 mt-1",children:e.examples.map((e,s)=>(0,a.jsx)(r.Ct,{color:"gray",size:"xs",children:e},s))})]})]},e.id))})]}),el.supportsAuthenticatedExtendedCard&&(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"text-lg font-semibold mb-4",children:"Additional Features"}),(0,a.jsx)(r.Ct,{color:"green",children:"Supports Authenticated Extended Card"})]})]})}),(0,a.jsx)(n.Z,{title:(null==eo?void 0:eo.server_name)||"MCP Server Details",width:1e3,visible:em,footer:null,onOk:e_,onCancel:ek,children:eo&&(0,a.jsxs)("div",{className:"space-y-6",children:[(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"text-lg font-semibold mb-4",children:"Server Overview"}),(0,a.jsxs)("div",{className:"grid grid-cols-2 gap-4 mb-4",children:[(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"font-medium",children:"Server Name:"}),(0,a.jsx)(r.xv,{children:eo.server_name})]}),(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"font-medium",children:"Server ID:"}),(0,a.jsxs)("div",{className:"flex items-center space-x-2",children:[(0,a.jsx)(r.xv,{className:"text-xs truncate",children:eo.server_id}),(0,a.jsx)(t.Z,{onClick:()=>ew(eo.server_id),className:"cursor-pointer text-gray-500 hover:text-blue-500"})]})]}),eo.alias&&(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"font-medium",children:"Alias:"}),(0,a.jsx)(r.xv,{children:eo.alias})]}),(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"font-medium",children:"Transport:"}),(0,a.jsx)(r.Ct,{color:"blue",children:eo.transport})]}),(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"font-medium",children:"Auth Type:"}),(0,a.jsx)(r.Ct,{color:"none"===eo.auth_type?"gray":"green",children:eo.auth_type})]}),(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"font-medium",children:"Status:"}),(0,a.jsx)(r.Ct,{color:"active"===eo.status||"healthy"===eo.status?"green":"inactive"===eo.status||"unhealthy"===eo.status?"red":"gray",children:eo.status||"unknown"})]})]}),eo.description&&(0,a.jsxs)("div",{className:"mt-2",children:[(0,a.jsx)(r.xv,{className:"font-medium",children:"Description:"}),(0,a.jsx)(r.xv,{className:"mt-1",children:eo.description})]})]}),(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"text-lg font-semibold mb-4",children:"Connection Details"}),(0,a.jsxs)("div",{className:"space-y-2",children:[(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"font-medium",children:"URL:"}),(0,a.jsxs)("div",{className:"flex items-center space-x-2 mt-1",children:[(0,a.jsx)(r.xv,{className:"text-sm break-all bg-gray-100 p-2 rounded flex-1",children:eo.url}),(0,a.jsx)(t.Z,{onClick:()=>ew(eo.url),className:"cursor-pointer text-gray-500 hover:text-blue-500 flex-shrink-0"})]})]}),eo.command&&(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"font-medium",children:"Command:"}),(0,a.jsx)(r.xv,{className:"text-sm bg-gray-100 p-2 rounded mt-1 font-mono",children:eo.command})]})]})]}),eo.allowed_tools&&eo.allowed_tools.length>0&&(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"text-lg font-semibold mb-4",children:"Allowed Tools"}),(0,a.jsx)("div",{className:"flex flex-wrap gap-2",children:eo.allowed_tools.map((e,s)=>(0,a.jsx)(r.Ct,{color:"purple",children:e},s))})]}),eo.teams&&eo.teams.length>0&&(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"text-lg font-semibold mb-4",children:"Teams"}),(0,a.jsx)("div",{className:"flex flex-wrap gap-2",children:eo.teams.map((e,s)=>(0,a.jsx)(r.Ct,{color:"blue",children:e},s))})]}),eo.mcp_access_groups&&eo.mcp_access_groups.length>0&&(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"text-lg font-semibold mb-4",children:"Access Groups"}),(0,a.jsx)("div",{className:"flex flex-wrap gap-2",children:eo.mcp_access_groups.map((e,s)=>(0,a.jsx)(r.Ct,{color:"green",children:e},s))})]}),(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"text-lg font-semibold mb-4",children:"Metadata"}),(0,a.jsxs)("div",{className:"grid grid-cols-2 gap-4",children:[(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"font-medium",children:"Created By:"}),(0,a.jsx)(r.xv,{children:eo.created_by})]}),(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"font-medium",children:"Updated By:"}),(0,a.jsx)(r.xv,{children:eo.updated_by})]}),(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"font-medium",children:"Created At:"}),(0,a.jsx)(r.xv,{className:"text-sm",children:new Date(eo.created_at).toLocaleString()})]}),(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"font-medium",children:"Updated At:"}),(0,a.jsx)(r.xv,{className:"text-sm",children:new Date(eo.updated_at).toLocaleString()})]}),eo.last_health_check&&(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"font-medium",children:"Last Health Check:"}),(0,a.jsx)(r.xv,{className:"text-sm",children:new Date(eo.last_health_check).toLocaleString()})]})]}),eo.health_check_error&&(0,a.jsxs)("div",{className:"mt-2 p-2 bg-red-50 rounded",children:[(0,a.jsx)(r.xv,{className:"font-medium text-red-700",children:"Health Check Error:"}),(0,a.jsx)(r.xv,{className:"text-sm text-red-600 mt-1",children:eo.health_check_error})]})]}),(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"text-lg font-semibold mb-4",children:"Usage Example"}),(0,a.jsx)(o.Z,{language:"python",className:"text-sm",children:'from fastmcp import Client\nimport asyncio\n\n# Standard MCP configuration\nconfig = {\n "mcpServers": {\n "'.concat(eo.server_name,'": {\n "url": "http://localhost:4000/').concat(eo.server_name,'/mcp",\n "headers": {\n "x-litellm-api-key": "Bearer sk-1234"\n }\n }\n }\n}\n\n# Create a client that connects to the server\nclient = Client(config)\n\nasync def main():\n async with client:\n # List available tools\n tools = await client.list_tools()\n print(f"Available tools: {[tool.name for tool in tools]}")\n\n # Call a tool\n response = await client.call_tool(\n name="tool_name", \n arguments={"arg": "value"}\n )\n print(f"Response: {response}")\n\nif __name__ == "__main__":\n asyncio.run(main())')})]})]})}),(0,a.jsx)(A,{visible:q,onClose:()=>G(!1),accessToken:N||"",modelHubData:z||[],onSuccess:()=>{N&&(async()=>{try{let e=await (0,_.modelHubCall)(N);L(e.data)}catch(e){console.error("Error refreshing model data:",e)}})()}}),(0,a.jsx)(Z,{visible:Q,onClose:()=>X(!1),accessToken:N||"",agentHubData:J||[],onSuccess:()=>{N&&(async()=>{try{let e=(await (0,_.getAgentsList)(N)).agents.map(e=>({agent_id:e.agent_id,...e.agent_card_params,is_public:e.is_public}));$(e)}catch(e){console.error("Error refreshing agent data:",e)}})()}}),(0,a.jsx)(S,{visible:eu,onClose:()=>ep(!1),accessToken:N||"",mcpHubData:en||[],onSuccess:()=>{N&&(async()=>{try{let e=await (0,_.fetchMCPServers)(N);ei(e)}catch(e){console.error("Error refreshing MCP server data:",e)}})()}})]})}}}]); \ No newline at end of file diff --git a/litellm/proxy/_experimental/out/_next/static/chunks/2249-3e3c0a9e241e35dc.js b/litellm/proxy/_experimental/out/_next/static/chunks/2249-3e3c0a9e241e35dc.js deleted file mode 100644 index 1c6e112db48..00000000000 --- a/litellm/proxy/_experimental/out/_next/static/chunks/2249-3e3c0a9e241e35dc.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[2249],{64748:function(e,s,l){l.d(s,{Ct:function(){return a.Z},Dx:function(){return m.Z},OK:function(){return n.Z},Zb:function(){return r.Z},nP:function(){return o.Z},td:function(){return c.Z},v0:function(){return i.Z},x4:function(){return d.Z},xv:function(){return x.Z},zx:function(){return t.Z}});var a=l(41649),t=l(78489),r=l(12514),n=l(12485),i=l(18135),c=l(35242),d=l(29706),o=l(77991),x=l(84264),m=l(96761)},78801:function(e,s,l){l.d(s,{Z:function(){return a.Z},x:function(){return t.Z}});var a=l(12514),t=l(84264)},92249:function(e,s,l){l.d(s,{Z:function(){return W}});var a=l(57437),t=l(23639),r=l(64748),n=l(22116),i=l(78867),c=l(99376),d=l(2265),o=l(17906),x=l(20347),m=l(41649),h=l(78489),u=l(84264),p=l(99981),g=l(3810),j=l(15424),v=l(15690),b=l(10032),N=l(4156),f=l(5545),y=l(96761),_=l(19250),k=l(9114);let{Step:w}=v.default;var Z=e=>{let{visible:s,onClose:l,accessToken:t,agentHubData:r,onSuccess:i}=e,[c,o]=(0,d.useState)(0),[x,h]=(0,d.useState)(new Set),[p,g]=(0,d.useState)(!1),[j]=b.Z.useForm(),Z=()=>{o(0),h(new Set),j.resetFields(),l()},C=(e,s)=>{let l=new Set(x);s?l.add(e):l.delete(e),h(l)},S=e=>{e?h(new Set(r.map(e=>e.agent_id||e.name))):h(new Set)};(0,d.useEffect)(()=>{s&&r.length>0&&h(new Set(r.filter(e=>!0===e.is_public).map(e=>e.agent_id||e.name)))},[s,r]);let P=async()=>{if(0===x.size){k.Z.fromBackend("Please select at least one agent to make public");return}g(!0);try{let e=Array.from(x);await (0,_.makeAgentsPublicCall)(t,e),k.Z.success("Successfully made ".concat(e.length," agent(s) public!")),Z(),i()}catch(e){console.error("Error making agents public:",e),k.Z.fromBackend("Failed to make agents public. Please try again.")}finally{g(!1)}},M=()=>{let e=r.length>0&&r.every(e=>x.has(e.agent_id||e.name)),s=x.size>0&&!e;return(0,a.jsxs)("div",{className:"space-y-4",children:[(0,a.jsxs)("div",{className:"flex items-center justify-between",children:[(0,a.jsx)(y.Z,{children:"Select Agents to Make Public"}),(0,a.jsx)("div",{className:"flex items-center space-x-2",children:(0,a.jsxs)(N.Z,{checked:e,indeterminate:s,onChange:e=>S(e.target.checked),disabled:0===r.length,children:["Select All ",r.length>0&&"(".concat(r.length,")")]})})]}),(0,a.jsx)(u.Z,{className:"text-sm text-gray-600",children:"Select the agents you want to be visible on the public model hub. Users will still require a valid API key to use these agents."}),(0,a.jsx)("div",{className:"max-h-96 overflow-y-auto border rounded-lg p-4",children:(0,a.jsx)("div",{className:"space-y-3",children:0===r.length?(0,a.jsx)("div",{className:"text-center py-8 text-gray-500",children:(0,a.jsx)(u.Z,{children:"No agents available."})}):r.map(e=>{let s=e.agent_id||e.name;return(0,a.jsxs)("div",{className:"flex items-center space-x-3 p-3 border rounded-lg hover:bg-gray-50",children:[(0,a.jsx)(N.Z,{checked:x.has(s),onChange:e=>C(s,e.target.checked)}),(0,a.jsxs)("div",{className:"flex-1",children:[(0,a.jsxs)("div",{className:"flex items-center space-x-2",children:[(0,a.jsx)(u.Z,{className:"font-medium",children:e.name}),(0,a.jsxs)(m.Z,{color:"blue",size:"sm",children:["v",e.version]})]}),(0,a.jsx)(u.Z,{className:"text-xs text-gray-600 mt-1",children:e.description}),e.skills&&e.skills.length>0&&(0,a.jsxs)("div",{className:"flex flex-wrap gap-1 mt-1",children:[e.skills.slice(0,3).map(e=>(0,a.jsx)(m.Z,{color:"purple",size:"xs",children:e.name},e.id)),e.skills.length>3&&(0,a.jsxs)(u.Z,{className:"text-xs text-gray-500",children:["+",e.skills.length-3," more"]})]})]})]},s)})})}),x.size>0&&(0,a.jsx)("div",{className:"bg-blue-50 border border-blue-200 rounded-lg p-3",children:(0,a.jsxs)(u.Z,{className:"text-sm text-blue-800",children:[(0,a.jsx)("strong",{children:x.size})," agent",1!==x.size?"s":""," selected"]})})]})},z=()=>(0,a.jsxs)("div",{className:"space-y-4",children:[(0,a.jsx)(y.Z,{children:"Confirm Making Agents Public"}),(0,a.jsx)("div",{className:"bg-yellow-50 border border-yellow-200 rounded-lg p-4",children:(0,a.jsxs)(u.Z,{className:"text-sm text-yellow-800",children:[(0,a.jsx)("strong",{children:"Warning:"})," Once you make these agents public, anyone who can go to the"," ",(0,a.jsx)("code",{children:"/ui/model_hub_table"})," will be able to know they exist on the proxy."]})}),(0,a.jsxs)("div",{className:"space-y-3",children:[(0,a.jsx)(u.Z,{className:"font-medium",children:"Agents to be made public:"}),(0,a.jsx)("div",{className:"max-h-48 overflow-y-auto border rounded-lg p-3",children:(0,a.jsx)("div",{className:"space-y-2",children:Array.from(x).map(e=>{let s=r.find(s=>(s.agent_id||s.name)===e);return(0,a.jsx)("div",{className:"flex items-center justify-between p-2 bg-gray-50 rounded",children:(0,a.jsxs)("div",{className:"flex-1",children:[(0,a.jsxs)("div",{className:"flex items-center space-x-2",children:[(0,a.jsx)(u.Z,{className:"font-medium",children:(null==s?void 0:s.name)||e}),s&&(0,a.jsxs)(m.Z,{color:"blue",size:"xs",children:["v",s.version]})]}),(null==s?void 0:s.description)&&(0,a.jsx)(u.Z,{className:"text-xs text-gray-600 mt-1",children:s.description})]})},e)})})})]}),(0,a.jsx)("div",{className:"bg-blue-50 border border-blue-200 rounded-lg p-3",children:(0,a.jsxs)(u.Z,{className:"text-sm text-blue-800",children:["Total: ",(0,a.jsx)("strong",{children:x.size})," agent",1!==x.size?"s":""," will be made public"]})})]});return(0,a.jsx)(n.Z,{title:"Make Agents Public",open:s,onCancel:Z,footer:null,width:1200,maskClosable:!1,children:(0,a.jsxs)(b.Z,{form:j,layout:"vertical",children:[(0,a.jsxs)(v.default,{current:c,className:"mb-6",children:[(0,a.jsx)(w,{title:"Select Agents"}),(0,a.jsx)(w,{title:"Confirm"})]}),(()=>{switch(c){case 0:return M();case 1:return z();default:return null}})(),(0,a.jsxs)("div",{className:"flex justify-between mt-6",children:[(0,a.jsx)(f.ZP,{onClick:0===c?Z:()=>{1===c&&o(0)},children:0===c?"Cancel":"Previous"}),(0,a.jsxs)("div",{className:"flex space-x-2",children:[0===c&&(0,a.jsx)(f.ZP,{onClick:()=>{if(0===c){if(0===x.size){k.Z.fromBackend("Please select at least one agent to make public");return}o(1)}},disabled:0===x.size,children:"Next"}),1===c&&(0,a.jsx)(f.ZP,{onClick:P,loading:p,children:"Make Public"})]})]})]})})};let{Step:C}=v.default;var S=e=>{let{visible:s,onClose:l,accessToken:t,mcpHubData:r,onSuccess:i}=e,[c,o]=(0,d.useState)(0),[x,h]=(0,d.useState)(new Set),[p,g]=(0,d.useState)(!1),[j]=b.Z.useForm(),w=()=>{o(0),h(new Set),j.resetFields(),l()},Z=(e,s)=>{let l=new Set(x);s?l.add(e):l.delete(e),h(l)},S=e=>{e?h(new Set(r.map(e=>e.server_id))):h(new Set)};(0,d.useEffect)(()=>{s&&r.length>0&&h(new Set(r.filter(e=>{var s;return(null===(s=e.mcp_info)||void 0===s?void 0:s.is_public)===!0}).map(e=>e.server_id)))},[s]);let P=async()=>{if(0===x.size){k.Z.fromBackend("Please select at least one MCP server to make public");return}g(!0);try{let e=Array.from(x);await (0,_.makeMCPPublicCall)(t,e),k.Z.success("Successfully made ".concat(e.length," MCP server(s) public!")),w(),i()}catch(e){console.error("Error making MCP servers public:",e),k.Z.fromBackend("Failed to make MCP servers public. Please try again.")}finally{g(!1)}},M=()=>{let e=r.length>0&&r.every(e=>x.has(e.server_id)),s=x.size>0&&!e;return(0,a.jsxs)("div",{className:"space-y-4",children:[(0,a.jsxs)("div",{className:"flex items-center justify-between",children:[(0,a.jsx)(y.Z,{children:"Select MCP Servers to Make Public"}),(0,a.jsx)("div",{className:"flex items-center space-x-2",children:(0,a.jsxs)(N.Z,{checked:e,indeterminate:s,onChange:e=>S(e.target.checked),disabled:0===r.length,children:["Select All ",r.length>0&&"(".concat(r.length,")")]})})]}),(0,a.jsx)(u.Z,{className:"text-sm text-gray-600",children:"Select the MCP servers you want to be visible on the public model hub. Users will still require a valid API key to use these servers."}),(0,a.jsx)("div",{className:"max-h-96 overflow-y-auto border rounded-lg p-4",children:(0,a.jsx)("div",{className:"space-y-3",children:0===r.length?(0,a.jsx)("div",{className:"text-center py-8 text-gray-500",children:(0,a.jsx)(u.Z,{children:"No MCP servers available."})}):r.map(e=>{var s;let l=(null===(s=e.mcp_info)||void 0===s?void 0:s.is_public)===!0;return(0,a.jsxs)("div",{className:"flex items-center space-x-3 p-3 border rounded-lg hover:bg-gray-50",children:[(0,a.jsx)(N.Z,{checked:x.has(e.server_id),onChange:s=>Z(e.server_id,s.target.checked)}),(0,a.jsxs)("div",{className:"flex-1",children:[(0,a.jsxs)("div",{className:"flex items-center space-x-2",children:[(0,a.jsx)(u.Z,{className:"font-medium",children:e.server_name}),l&&(0,a.jsx)(m.Z,{color:"emerald",size:"sm",children:"Public"}),(0,a.jsx)(m.Z,{color:"blue",size:"sm",children:e.transport}),(0,a.jsx)(m.Z,{color:"active"===e.status||"healthy"===e.status?"green":"inactive"===e.status||"unhealthy"===e.status?"red":"gray",size:"sm",children:e.status||"unknown"})]}),(0,a.jsx)(u.Z,{className:"text-xs text-gray-600 mt-1",children:e.description||e.url}),e.allowed_tools&&e.allowed_tools.length>0&&(0,a.jsxs)("div",{className:"flex flex-wrap gap-1 mt-1",children:[e.allowed_tools.slice(0,3).map((e,s)=>(0,a.jsx)(m.Z,{color:"purple",size:"xs",children:e},s)),e.allowed_tools.length>3&&(0,a.jsxs)(u.Z,{className:"text-xs text-gray-500",children:["+",e.allowed_tools.length-3," more"]})]})]})]},e.server_id)})})}),x.size>0&&(0,a.jsx)("div",{className:"bg-blue-50 border border-blue-200 rounded-lg p-3",children:(0,a.jsxs)(u.Z,{className:"text-sm text-blue-800",children:[(0,a.jsx)("strong",{children:x.size})," MCP server",1!==x.size?"s":""," selected"]})})]})},z=()=>(0,a.jsxs)("div",{className:"space-y-4",children:[(0,a.jsx)(y.Z,{children:"Confirm Making MCP Servers Public"}),(0,a.jsx)("div",{className:"bg-yellow-50 border border-yellow-200 rounded-lg p-4",children:(0,a.jsxs)(u.Z,{className:"text-sm text-yellow-800",children:[(0,a.jsx)("strong",{children:"Warning:"})," Once you make these MCP servers public, anyone who can go to the"," ",(0,a.jsx)("code",{children:"/ui/model_hub_table"})," will be able to know they exist on the proxy."]})}),(0,a.jsxs)("div",{className:"space-y-3",children:[(0,a.jsx)(u.Z,{className:"font-medium",children:"MCP Servers to be made public:"}),(0,a.jsx)("div",{className:"max-h-48 overflow-y-auto border rounded-lg p-3",children:(0,a.jsx)("div",{className:"space-y-2",children:Array.from(x).map(e=>{let s=r.find(s=>s.server_id===e);return(0,a.jsx)("div",{className:"flex items-center justify-between p-2 bg-gray-50 rounded",children:(0,a.jsxs)("div",{className:"flex-1",children:[(0,a.jsxs)("div",{className:"flex items-center space-x-2",children:[(0,a.jsx)(u.Z,{className:"font-medium",children:(null==s?void 0:s.server_name)||e}),s&&(0,a.jsxs)(a.Fragment,{children:[(0,a.jsx)(m.Z,{color:"blue",size:"xs",children:s.transport}),(0,a.jsx)(m.Z,{color:"active"===s.status||"healthy"===s.status?"green":"inactive"===s.status||"unhealthy"===s.status?"red":"gray",size:"xs",children:s.status||"unknown"})]})]}),(null==s?void 0:s.description)&&(0,a.jsx)(u.Z,{className:"text-xs text-gray-600 mt-1",children:s.description}),(null==s?void 0:s.url)&&(0,a.jsx)(u.Z,{className:"text-xs text-gray-500 mt-1",children:s.url})]})},e)})})})]}),(0,a.jsx)("div",{className:"bg-blue-50 border border-blue-200 rounded-lg p-3",children:(0,a.jsxs)(u.Z,{className:"text-sm text-blue-800",children:["Total: ",(0,a.jsx)("strong",{children:x.size})," MCP server",1!==x.size?"s":""," will be made public"]})})]});return(0,a.jsx)(n.Z,{title:"Make MCP Servers Public",open:s,onCancel:w,footer:null,width:1200,maskClosable:!1,children:(0,a.jsxs)(b.Z,{form:j,layout:"vertical",children:[(0,a.jsxs)(v.default,{current:c,className:"mb-6",children:[(0,a.jsx)(C,{title:"Select Servers"}),(0,a.jsx)(C,{title:"Confirm"})]}),(()=>{switch(c){case 0:return M();case 1:return z();default:return null}})(),(0,a.jsxs)("div",{className:"flex justify-between mt-6",children:[(0,a.jsx)(f.ZP,{onClick:0===c?w:()=>{1===c&&o(0)},children:0===c?"Cancel":"Previous"}),(0,a.jsxs)("div",{className:"flex space-x-2",children:[0===c&&(0,a.jsx)(f.ZP,{onClick:()=>{if(0===c){if(0===x.size){k.Z.fromBackend("Please select at least one MCP server to make public");return}o(1)}},disabled:0===x.size,children:"Next"}),1===c&&(0,a.jsx)(f.ZP,{onClick:P,loading:p,children:"Make Public"})]})]})]})})},P=l(78801),M=e=>{let{modelHubData:s,onFilteredDataChange:l,showFiltersCard:t=!0,className:r=""}=e,[n,i]=(0,d.useState)(""),[c,o]=(0,d.useState)(""),[x,m]=(0,d.useState)(""),[h,u]=(0,d.useState)(""),p=(0,d.useRef)([]),g=(0,d.useMemo)(()=>(null==s?void 0:s.filter(e=>{let s=e.model_group.toLowerCase().includes(n.toLowerCase()),l=""===c||e.providers.includes(c),a=""===x||e.mode===x,t=""===h||Object.entries(e).filter(e=>{let[s,l]=e;return s.startsWith("supports_")&&!0===l}).some(e=>{let[s]=e;return s.replace(/^supports_/,"").split("_").map(e=>e.charAt(0).toUpperCase()+e.slice(1)).join(" ")===h});return s&&l&&a&&t}))||[],[s,n,c,x,h]);(0,d.useEffect)(()=>{(g.length!==p.current.length||g.some((e,s)=>{var l;return e.model_group!==(null===(l=p.current[s])||void 0===l?void 0:l.model_group)}))&&(p.current=g,l(g))},[g,l]);let j=(0,a.jsxs)("div",{className:"flex flex-wrap gap-4 items-center",children:[(0,a.jsxs)("div",{children:[(0,a.jsx)(P.x,{className:"text-sm font-medium mb-2",children:"Search Models:"}),(0,a.jsx)("input",{type:"text",placeholder:"Search model names...",value:n,onChange:e=>i(e.target.value),className:"border rounded px-3 py-2 w-64 h-10 text-sm"})]}),(0,a.jsxs)("div",{children:[(0,a.jsx)(P.x,{className:"text-sm font-medium mb-2",children:"Provider:"}),(0,a.jsxs)("select",{value:c,onChange:e=>o(e.target.value),className:"border rounded px-3 py-2 text-sm text-gray-600 w-40 h-10",children:[(0,a.jsx)("option",{value:"",className:"text-sm text-gray-600",children:"All Providers"}),s&&(e=>{let s=new Set;return e.forEach(e=>{e.providers.forEach(e=>s.add(e))}),Array.from(s)})(s).map(e=>(0,a.jsx)("option",{value:e,className:"text-sm text-gray-800",children:e},e))]})]}),(0,a.jsxs)("div",{children:[(0,a.jsx)(P.x,{className:"text-sm font-medium mb-2",children:"Mode:"}),(0,a.jsxs)("select",{value:x,onChange:e=>m(e.target.value),className:"border rounded px-3 py-2 text-sm text-gray-600 w-32 h-10",children:[(0,a.jsx)("option",{value:"",className:"text-sm text-gray-600",children:"All Modes"}),s&&(e=>{let s=new Set;return e.forEach(e=>{e.mode&&s.add(e.mode)}),Array.from(s)})(s).map(e=>(0,a.jsx)("option",{value:e,className:"text-sm text-gray-800",children:e},e))]})]}),(0,a.jsxs)("div",{children:[(0,a.jsx)(P.x,{className:"text-sm font-medium mb-2",children:"Features:"}),(0,a.jsxs)("select",{value:h,onChange:e=>u(e.target.value),className:"border rounded px-3 py-2 text-sm text-gray-600 w-48 h-10",children:[(0,a.jsx)("option",{value:"",className:"text-sm text-gray-600",children:"All Features"}),s&&(e=>{let s=new Set;return e.forEach(e=>{Object.entries(e).filter(e=>{let[s,l]=e;return s.startsWith("supports_")&&!0===l}).forEach(e=>{let[l]=e,a=l.replace(/^supports_/,"").split("_").map(e=>e.charAt(0).toUpperCase()+e.slice(1)).join(" ");s.add(a)})}),Array.from(s).sort()})(s).map(e=>(0,a.jsx)("option",{value:e,className:"text-sm text-gray-800",children:e},e))]})]}),(n||c||x||h)&&(0,a.jsx)("div",{className:"flex items-end",children:(0,a.jsx)("button",{onClick:()=>{i(""),o(""),m(""),u("")},className:"text-blue-600 hover:text-blue-800 text-sm underline h-10 flex items-center",children:"Clear Filters"})})]});return t?(0,a.jsx)(P.Z,{className:"mb-6 ".concat(r),children:j}):(0,a.jsx)("div",{className:r,children:j})};let{Step:z}=v.default;var A=e=>{let{visible:s,onClose:l,accessToken:t,modelHubData:r,onSuccess:i}=e,[c,o]=(0,d.useState)(0),[x,h]=(0,d.useState)(new Set),[p,g]=(0,d.useState)([]),[j,w]=(0,d.useState)(!1),[Z]=b.Z.useForm(),C=()=>{o(0),h(new Set),g([]),Z.resetFields(),l()},S=(e,s)=>{let l=new Set(x);s?l.add(e):l.delete(e),h(l)},P=e=>{e?h(new Set(p.map(e=>e.model_group))):h(new Set)},A=(0,d.useCallback)(e=>{g(e)},[]);(0,d.useEffect)(()=>{s&&r.length>0&&(g(r),h(new Set(r.filter(e=>!0===e.is_public_model_group).map(e=>e.model_group))))},[s,r]);let F=async()=>{if(0===x.size){k.Z.fromBackend("Please select at least one model to make public");return}w(!0);try{let e=Array.from(x);await (0,_.makeModelGroupPublic)(t,e),k.Z.success("Successfully made ".concat(e.length," model group(s) public!")),C(),i()}catch(e){console.error("Error making model groups public:",e),k.Z.fromBackend("Failed to make model groups public. Please try again.")}finally{w(!1)}},L=()=>{let e=p.length>0&&p.every(e=>x.has(e.model_group)),s=x.size>0&&!e;return(0,a.jsxs)("div",{className:"space-y-4",children:[(0,a.jsxs)("div",{className:"flex items-center justify-between",children:[(0,a.jsx)(y.Z,{children:"Select Models to Make Public"}),(0,a.jsx)("div",{className:"flex items-center space-x-2",children:(0,a.jsxs)(N.Z,{checked:e,indeterminate:s,onChange:e=>P(e.target.checked),disabled:0===p.length,children:["Select All ",p.length>0&&"(".concat(p.length,")")]})})]}),(0,a.jsx)(u.Z,{className:"text-sm text-gray-600",children:"Select the models you want to be visible on the public model hub. Users will still require a valid API key to use these models."}),(0,a.jsx)(M,{modelHubData:r,onFilteredDataChange:A,showFiltersCard:!1,className:"border rounded-lg p-4 bg-gray-50"}),(0,a.jsx)("div",{className:"max-h-96 overflow-y-auto border rounded-lg p-4",children:(0,a.jsx)("div",{className:"space-y-3",children:0===p.length?(0,a.jsx)("div",{className:"text-center py-8 text-gray-500",children:(0,a.jsx)(u.Z,{children:"No models match the current filters."})}):p.map(e=>(0,a.jsxs)("div",{className:"flex items-center space-x-3 p-3 border rounded-lg hover:bg-gray-50",children:[(0,a.jsx)(N.Z,{checked:x.has(e.model_group),onChange:s=>S(e.model_group,s.target.checked)}),(0,a.jsxs)("div",{className:"flex-1",children:[(0,a.jsxs)("div",{className:"flex items-center space-x-2",children:[(0,a.jsx)(u.Z,{className:"font-medium",children:e.model_group}),e.mode&&(0,a.jsx)(m.Z,{color:"green",size:"sm",children:e.mode})]}),(0,a.jsx)("div",{className:"flex flex-wrap gap-1 mt-1",children:e.providers.map(e=>(0,a.jsx)(m.Z,{color:"blue",size:"xs",children:e},e))})]})]},e.model_group))})}),x.size>0&&(0,a.jsx)("div",{className:"bg-blue-50 border border-blue-200 rounded-lg p-3",children:(0,a.jsxs)(u.Z,{className:"text-sm text-blue-800",children:[(0,a.jsx)("strong",{children:x.size})," model",1!==x.size?"s":""," selected"]})})]})},D=()=>(0,a.jsxs)("div",{className:"space-y-4",children:[(0,a.jsx)(y.Z,{children:"Confirm Making Models Public"}),(0,a.jsx)("div",{className:"bg-yellow-50 border border-yellow-200 rounded-lg p-4",children:(0,a.jsxs)(u.Z,{className:"text-sm text-yellow-800",children:[(0,a.jsx)("strong",{children:"Warning:"})," Once you make these models public, anyone who can go to the"," ",(0,a.jsx)("code",{children:"/ui/model_hub_table"})," will be able to know they exist on the proxy."]})}),(0,a.jsxs)("div",{className:"space-y-3",children:[(0,a.jsx)(u.Z,{className:"font-medium",children:"Models to be made public:"}),(0,a.jsx)("div",{className:"max-h-48 overflow-y-auto border rounded-lg p-3",children:(0,a.jsx)("div",{className:"space-y-2",children:Array.from(x).map(e=>{let s=r.find(s=>s.model_group===e);return(0,a.jsx)("div",{className:"flex items-center justify-between p-2 bg-gray-50 rounded",children:(0,a.jsxs)("div",{children:[(0,a.jsx)(u.Z,{className:"font-medium",children:e}),s&&(0,a.jsx)("div",{className:"flex flex-wrap gap-1 mt-1",children:s.providers.map(e=>(0,a.jsx)(m.Z,{color:"blue",size:"xs",children:e},e))})]})},e)})})})]}),(0,a.jsx)("div",{className:"bg-blue-50 border border-blue-200 rounded-lg p-3",children:(0,a.jsxs)(u.Z,{className:"text-sm text-blue-800",children:["Total: ",(0,a.jsx)("strong",{children:x.size})," model",1!==x.size?"s":""," will be made public"]})})]});return(0,a.jsx)(n.Z,{title:"Make Models Public",open:s,onCancel:C,footer:null,width:1200,maskClosable:!1,children:(0,a.jsxs)(b.Z,{form:Z,layout:"vertical",children:[(0,a.jsxs)(v.default,{current:c,className:"mb-6",children:[(0,a.jsx)(z,{title:"Select Models"}),(0,a.jsx)(z,{title:"Confirm"})]}),(()=>{switch(c){case 0:return L();case 1:return D();default:return null}})(),(0,a.jsxs)("div",{className:"flex justify-between mt-6",children:[(0,a.jsx)(f.ZP,{onClick:0===c?C:()=>{1===c&&o(0)},children:0===c?"Cancel":"Previous"}),(0,a.jsxs)("div",{className:"flex space-x-2",children:[0===c&&(0,a.jsx)(f.ZP,{onClick:()=>{if(0===c){if(0===x.size){k.Z.fromBackend("Please select at least one model to make public");return}o(1)}},disabled:0===x.size,children:"Next"}),1===c&&(0,a.jsx)(f.ZP,{onClick:F,loading:j,children:"Make Public"})]})]})]})})},F=l(8048);let L=e=>e.replace(/^supports_/,"").split("_").map(e=>e.charAt(0).toUpperCase()+e.slice(1)).join(" "),D=e=>Object.entries(e).filter(e=>{let[s,l]=e;return s.startsWith("supports_")&&!0===l}).map(e=>{let[s]=e;return s}),O=e=>"$".concat((1e6*e).toFixed(2)),U=e=>e>=1e6?"".concat((e/1e6).toFixed(1),"M"):e>=1e3?"".concat((e/1e3).toFixed(1),"K"):e.toString(),E=function(e,s){let l=arguments.length>2&&void 0!==arguments[2]&&arguments[2],r=[{header:"Public Model Name",accessorKey:"model_group",enableSorting:!0,sortingFn:"alphanumeric",cell:e=>{let{row:l}=e,r=l.original;return(0,a.jsxs)("div",{className:"space-y-1",children:[(0,a.jsxs)("div",{className:"flex items-center space-x-2",children:[(0,a.jsx)(u.Z,{className:"font-medium text-sm",children:r.model_group}),(0,a.jsx)(p.Z,{title:"Copy model name",children:(0,a.jsx)(t.Z,{onClick:()=>s(r.model_group),className:"cursor-pointer text-gray-500 hover:text-blue-500 text-xs"})})]}),(0,a.jsx)("div",{className:"md:hidden",children:(0,a.jsx)(u.Z,{className:"text-xs text-gray-600",children:r.providers.join(", ")})})]})}},{header:"Provider",accessorKey:"providers",enableSorting:!0,sortingFn:(e,s)=>{let l=e.original.providers.join(", "),a=s.original.providers.join(", ");return l.localeCompare(a)},cell:e=>{let{row:s}=e,l=s.original;return(0,a.jsxs)("div",{className:"flex flex-wrap gap-1",children:[l.providers.slice(0,2).map(e=>(0,a.jsx)(g.Z,{color:"blue",className:"text-xs",children:e},e)),l.providers.length>2&&(0,a.jsxs)(u.Z,{className:"text-xs text-gray-500",children:["+",l.providers.length-2]})]})},meta:{className:"hidden md:table-cell"}},{header:"Mode",accessorKey:"mode",enableSorting:!0,sortingFn:"alphanumeric",cell:e=>{let{row:s}=e,l=s.original;return l.mode?(0,a.jsx)(m.Z,{color:"green",size:"sm",children:l.mode}):(0,a.jsx)(u.Z,{className:"text-gray-500",children:"-"})},meta:{className:"hidden lg:table-cell"}},{header:"Tokens",accessorKey:"max_input_tokens",enableSorting:!0,sortingFn:(e,s)=>(e.original.max_input_tokens||0)+(e.original.max_output_tokens||0)-((s.original.max_input_tokens||0)+(s.original.max_output_tokens||0)),cell:e=>{let{row:s}=e,l=s.original;return(0,a.jsx)("div",{className:"space-y-1",children:(0,a.jsxs)(u.Z,{className:"text-xs",children:[l.max_input_tokens?U(l.max_input_tokens):"-"," /"," ",l.max_output_tokens?U(l.max_output_tokens):"-"]})})},meta:{className:"hidden lg:table-cell"}},{header:"Cost/1M",accessorKey:"input_cost_per_token",enableSorting:!0,sortingFn:(e,s)=>(e.original.input_cost_per_token||0)+(e.original.output_cost_per_token||0)-((s.original.input_cost_per_token||0)+(s.original.output_cost_per_token||0)),cell:e=>{let{row:s}=e,l=s.original;return(0,a.jsxs)("div",{className:"space-y-1",children:[(0,a.jsx)(u.Z,{className:"text-xs",children:l.input_cost_per_token?O(l.input_cost_per_token):"-"}),(0,a.jsx)(u.Z,{className:"text-xs text-gray-500",children:l.output_cost_per_token?O(l.output_cost_per_token):"-"})]})}},{header:"Features",accessorKey:"capabilities",enableSorting:!1,cell:e=>{let{row:s}=e,l=D(s.original),t=["green","blue","purple","orange","red","yellow"];return(0,a.jsx)("div",{className:"flex flex-wrap gap-1",children:0===l.length?(0,a.jsx)(u.Z,{className:"text-gray-500 text-xs",children:"-"}):l.map((e,s)=>(0,a.jsx)(m.Z,{color:t[s%t.length],size:"xs",children:L(e)},e))})}},{header:"Public",accessorKey:"is_public_model_group",enableSorting:!0,sortingFn:(e,s)=>(!0===e.original.is_public_model_group?1:0)-(!0===s.original.is_public_model_group?1:0),cell:e=>{let{row:s}=e;return!0===s.original.is_public_model_group?(0,a.jsx)(m.Z,{color:"green",size:"xs",children:"Yes"}):(0,a.jsx)(m.Z,{color:"gray",size:"xs",children:"No"})},meta:{className:"hidden md:table-cell"}},{header:"Details",id:"details",enableSorting:!1,cell:s=>{let{row:l}=s,t=l.original;return(0,a.jsxs)(h.Z,{size:"xs",variant:"secondary",onClick:()=>e(t),icon:j.Z,children:[(0,a.jsx)("span",{className:"hidden lg:inline",children:"Details"}),(0,a.jsx)("span",{className:"lg:hidden",children:"Info"})]})}}];return l?r.filter(e=>!("accessorKey"in e)||"is_public_model_group"!==e.accessorKey):r};var K=l(87526),T=l(86462),H=l(47686),I=l(77355),R=l(93416),B=l(74998),Y=l(95704),V=e=>{let{accessToken:s,userRole:l}=e,[t,r]=(0,d.useState)([]),[i,c]=(0,d.useState)({url:"",displayName:""}),[o,m]=(0,d.useState)(null),[h,u]=(0,d.useState)(!1),[p,g]=(0,d.useState)(!0),j=async()=>{if(s)try{u(!0);let e=await (0,_.getPublicModelHubInfo)();if(e&&e.useful_links){let s=e.useful_links||{},l=Object.entries(s).map((e,s)=>{let[l,a]=e;return{id:"".concat(s,"-").concat(l),displayName:l,url:a}});r(l)}else r([])}catch(e){console.error("Error fetching useful links:",e),r([])}finally{u(!1)}};if((0,d.useEffect)(()=>{j()},[s]),!(0,x.tY)(l||""))return null;let v=async e=>{if(!s)return!1;try{let l={};return e.forEach(e=>{l[e.displayName]=e.url}),await (0,_.updateUsefulLinksCall)(s,l),n.Z.success({title:"Links Saved Successfully",content:(0,a.jsxs)("div",{className:"py-4",children:[(0,a.jsx)("p",{className:"text-gray-600 mb-4",children:"Your useful links have been saved and are now visible on the public model hub."}),(0,a.jsxs)("div",{className:"bg-blue-50 border border-blue-200 rounded-lg p-4",children:[(0,a.jsx)("p",{className:"text-sm text-blue-800 mb-2 font-medium",children:"View your updated model hub:"}),(0,a.jsx)("a",{href:"".concat((0,_.getProxyBaseUrl)(),"/ui/model_hub_table"),target:"_blank",rel:"noopener noreferrer",className:"inline-flex items-center text-blue-600 hover:text-blue-800 underline text-sm font-medium",children:"Open Public Model Hub →"})]})]}),width:500,okText:"Close",maskClosable:!0,keyboard:!0}),!0}catch(e){return console.error("Error saving links:",e),k.Z.fromBackend("Failed to save links - ".concat(e)),!1}},b=async()=>{if(!i.url||!i.displayName)return;try{new URL(i.url)}catch(e){k.Z.fromBackend("Please enter a valid URL");return}if(t.some(e=>e.displayName===i.displayName)){k.Z.fromBackend("A link with this display name already exists");return}let e=[...t,{id:"".concat(Date.now(),"-").concat(i.displayName),displayName:i.displayName,url:i.url}];await v(e)&&(r(e),c({url:"",displayName:""}),k.Z.success("Link added successfully"))},N=e=>{m({...e})},f=async()=>{if(!o)return;try{new URL(o.url)}catch(e){k.Z.fromBackend("Please enter a valid URL");return}if(t.some(e=>e.id!==o.id&&e.displayName===o.displayName)){k.Z.fromBackend("A link with this display name already exists");return}let e=t.map(e=>e.id===o.id?o:e);await v(e)&&(r(e),m(null),k.Z.success("Link updated successfully"))},y=()=>{m(null)},w=async e=>{let s=t.filter(s=>s.id!==e);await v(s)&&(r(s),k.Z.success("Link deleted successfully"))},Z=e=>{window.open(e,"_blank")};return(0,a.jsxs)(Y.Zb,{className:"mb-6",children:[(0,a.jsxs)("div",{className:"flex items-center justify-between cursor-pointer",onClick:()=>g(!p),children:[(0,a.jsxs)("div",{className:"flex flex-col",children:[(0,a.jsx)(Y.Dx,{className:"mb-0",children:"Link Management"}),(0,a.jsx)("p",{className:"text-sm text-gray-500",children:"Manage the links that are displayed under 'Useful Links' on the public model hub."})]}),(0,a.jsx)("div",{className:"flex items-center",children:p?(0,a.jsx)(T.Z,{className:"w-5 h-5 text-gray-500"}):(0,a.jsx)(H.Z,{className:"w-5 h-5 text-gray-500"})})]}),p&&(0,a.jsxs)("div",{className:"mt-4",children:[(0,a.jsxs)("div",{className:"mb-6",children:[(0,a.jsx)(Y.xv,{className:"text-sm font-medium text-gray-700 mb-2",children:"Add New Link"}),(0,a.jsxs)("div",{className:"grid grid-cols-3 gap-4",children:[(0,a.jsxs)("div",{children:[(0,a.jsx)("label",{className:"block text-xs text-gray-500 mb-1",children:"URL"}),(0,a.jsx)("input",{type:"text",value:i.url,onChange:e=>c({...i,url:e.target.value}),placeholder:"https://example.com",className:"w-full px-3 py-2 border border-gray-300 rounded-md text-sm"})]}),(0,a.jsxs)("div",{children:[(0,a.jsx)("label",{className:"block text-xs text-gray-500 mb-1",children:"Display Name"}),(0,a.jsx)("input",{type:"text",value:i.displayName,onChange:e=>c({...i,displayName:e.target.value}),placeholder:"Friendly name",className:"w-full px-3 py-2 border border-gray-300 rounded-md text-sm"})]}),(0,a.jsx)("div",{className:"flex items-end",children:(0,a.jsxs)("button",{onClick:b,disabled:!i.url||!i.displayName,className:"flex items-center px-4 py-2 rounded-md text-sm ".concat(i.url&&i.displayName?"bg-green-600 text-white hover:bg-green-700":"bg-gray-300 text-gray-500 cursor-not-allowed"),children:[(0,a.jsx)(I.Z,{className:"w-4 h-4 mr-1"}),"Add Link"]})})]})]}),(0,a.jsx)(Y.xv,{className:"text-sm font-medium text-gray-700 mb-2",children:"Manage Existing Links"}),(0,a.jsx)("div",{className:"rounded-lg custom-border relative",children:(0,a.jsx)("div",{className:"overflow-x-auto",children:(0,a.jsxs)(Y.iA,{className:"[&_td]:py-0.5 [&_th]:py-1",children:[(0,a.jsx)(Y.ss,{children:(0,a.jsxs)(Y.SC,{children:[(0,a.jsx)(Y.xs,{className:"py-1 h-8",children:"Display Name"}),(0,a.jsx)(Y.xs,{className:"py-1 h-8",children:"URL"}),(0,a.jsx)(Y.xs,{className:"py-1 h-8",children:"Actions"})]})}),(0,a.jsxs)(Y.RM,{children:[t.map(e=>(0,a.jsx)(Y.SC,{className:"h-8",children:o&&o.id===e.id?(0,a.jsxs)(a.Fragment,{children:[(0,a.jsx)(Y.pj,{className:"py-0.5",children:(0,a.jsx)("input",{type:"text",value:o.displayName,onChange:e=>m({...o,displayName:e.target.value}),className:"w-full px-2 py-1 border border-gray-300 rounded-md text-sm"})}),(0,a.jsx)(Y.pj,{className:"py-0.5",children:(0,a.jsx)("input",{type:"text",value:o.url,onChange:e=>m({...o,url:e.target.value}),className:"w-full px-2 py-1 border border-gray-300 rounded-md text-sm"})}),(0,a.jsx)(Y.pj,{className:"py-0.5 whitespace-nowrap",children:(0,a.jsxs)("div",{className:"flex space-x-2",children:[(0,a.jsx)("button",{onClick:f,className:"text-xs bg-blue-50 text-blue-600 px-2 py-1 rounded hover:bg-blue-100",children:"Save"}),(0,a.jsx)("button",{onClick:y,className:"text-xs bg-gray-50 text-gray-600 px-2 py-1 rounded hover:bg-gray-100",children:"Cancel"})]})})]}):(0,a.jsxs)(a.Fragment,{children:[(0,a.jsx)(Y.pj,{className:"py-0.5 text-sm text-gray-900",children:e.displayName}),(0,a.jsx)(Y.pj,{className:"py-0.5 text-sm text-gray-500",children:e.url}),(0,a.jsx)(Y.pj,{className:"py-0.5 whitespace-nowrap",children:(0,a.jsxs)("div",{className:"flex space-x-2",children:[(0,a.jsx)("button",{onClick:()=>Z(e.url),className:"text-xs bg-green-50 text-green-600 px-2 py-1 rounded hover:bg-green-100",children:"Use"}),(0,a.jsx)("button",{onClick:()=>N(e),className:"text-xs bg-blue-50 text-blue-600 px-2 py-1 rounded hover:bg-blue-100",children:(0,a.jsx)(R.Z,{className:"w-3 h-3"})}),(0,a.jsx)("button",{onClick:()=>w(e.id),className:"text-xs bg-red-50 text-red-600 px-2 py-1 rounded hover:bg-red-100",children:(0,a.jsx)(B.Z,{className:"w-3 h-3"})})]})})]})},e.id)),0===t.length&&(0,a.jsx)(Y.SC,{children:(0,a.jsx)(Y.pj,{colSpan:3,className:"py-0.5 text-sm text-gray-500 text-center",children:"No links added yet. Add a new link above."})})]})]})})})]})]})},W=e=>{var s,l,v,b;let{accessToken:N,publicPage:f,premiumUser:y,userRole:w}=e,[C,P]=(0,d.useState)(!1),[z,L]=(0,d.useState)(null),[D,O]=(0,d.useState)(!0),[U,T]=(0,d.useState)(!1),[H,I]=(0,d.useState)(!1),[R,B]=(0,d.useState)(null),[Y,W]=(0,d.useState)([]),[q,G]=(0,d.useState)(!1),[J,$]=(0,d.useState)(null),[Q,X]=(0,d.useState)(!1),[ee,es]=(0,d.useState)(!0),[el,ea]=(0,d.useState)(null),[et,er]=(0,d.useState)(!1),[en,ei]=(0,d.useState)(null),[ec,ed]=(0,d.useState)(!0),[eo,ex]=(0,d.useState)(null),[em,eh]=(0,d.useState)(!1),[eu,ep]=(0,d.useState)(!1),eg=(0,c.useRouter)(),ej=(0,d.useRef)(null),ev=(0,d.useRef)(null),eb=(0,d.useRef)(null);(0,d.useEffect)(()=>{let e=async e=>{try{O(!0);let s=await (0,_.modelHubCall)(e);console.log("ModelHubData:",s),L(s.data),(0,_.getConfigFieldSetting)(e,"enable_public_model_hub").then(e=>{console.log("data: ".concat(JSON.stringify(e))),!0==e.field_value&&P(!0)}).catch(e=>{})}catch(e){console.error("There was an error fetching the model data",e)}finally{O(!1)}},s=async()=>{try{var e,s;O(!0),await (0,_.getUiConfig)();let l=await (0,_.modelHubPublicModelsCall)();console.log("ModelHubData:",l),console.log("First model structure:",l[0]),console.log("Model has model_group?",null===(e=l[0])||void 0===e?void 0:e.model_group),console.log("Model has providers?",null===(s=l[0])||void 0===s?void 0:s.providers),L(l),P(!0)}catch(e){console.error("There was an error fetching the public model data",e)}finally{O(!1)}};N?e(N):f&&s()},[N,f]),(0,d.useEffect)(()=>{let e=async()=>{if(N)try{es(!0);let e=await (0,_.getAgentsList)(N);console.log("AgentHubData:",e);let s=e.agents.map(e=>({agent_id:e.agent_id,...e.agent_card_params,is_public:e.litellm_params.is_public}));$(s)}catch(e){console.error("There was an error fetching the agent data",e)}finally{es(!1)}};f||e()},[f,N]),(0,d.useEffect)(()=>{let e=async()=>{if(N)try{ed(!0);let e=await (0,_.fetchMCPServers)(N);console.log("MCPHubData:",e),ei(e)}catch(e){console.error("There was an error fetching the MCP server data",e)}finally{ed(!1)}};f||e()},[f,N]);let eN=()=>{N&&G(!0)},ef=()=>{N&&X(!0)},ey=()=>{N&&ep(!0)},e_=()=>{T(!1),I(!1),B(null),er(!1),ea(null),eh(!1),ex(null)},ek=()=>{T(!1),I(!1),B(null),er(!1),ea(null),eh(!1),ex(null)},ew=e=>{navigator.clipboard.writeText(e),k.Z.success("Copied to clipboard!")},eZ=e=>e.replace(/^supports_/,"").split("_").map(e=>e.charAt(0).toUpperCase()+e.slice(1)).join(" "),eC=e=>Object.entries(e).filter(e=>{let[s,l]=e;return s.startsWith("supports_")&&!0===l}).map(e=>{let[s]=e;return s}),eS=e=>"$".concat((1e6*e).toFixed(2)),eP=(0,d.useCallback)(e=>{W(e)},[]);return(console.log("publicPage: ",f),console.log("publicPageAllowed: ",C),f&&C)?(0,a.jsx)(K.Z,{accessToken:N}):(0,a.jsxs)("div",{className:"w-full mx-4 h-[75vh]",children:[!1==f?(0,a.jsxs)("div",{className:"w-full m-2 mt-2 p-8",children:[(0,a.jsxs)("div",{className:"flex justify-between items-center mb-6",children:[(0,a.jsxs)("div",{className:"flex flex-col items-start",children:[(0,a.jsx)(r.Dx,{className:"text-center",children:"AI Hub"}),(0,x.tY)(w||"")?(0,a.jsx)("p",{className:"text-sm text-gray-600",children:"Make models, agents, and MCP servers public for developers to know what's available."}):(0,a.jsx)("p",{className:"text-sm text-gray-600",children:"A list of all public model names personally available to you."})]}),(0,a.jsxs)("div",{className:"flex items-center space-x-4",children:[(0,a.jsx)(r.xv,{children:"Model Hub URL:"}),(0,a.jsxs)("div",{className:"flex items-center bg-gray-200 px-2 py-1 rounded",children:[(0,a.jsx)(r.xv,{className:"mr-2",children:"".concat((0,_.getProxyBaseUrl)(),"/ui/model_hub_table")}),(0,a.jsx)("button",{onClick:()=>ew("".concat((0,_.getProxyBaseUrl)(),"/ui/model_hub_table")),className:"p-1 hover:bg-gray-300 rounded transition-colors",title:"Copy URL",children:(0,a.jsx)(i.Z,{size:16,className:"text-gray-600"})})]})]})]}),(0,x.tY)(w||"")&&(0,a.jsx)("div",{className:"mt-8 mb-2",children:(0,a.jsx)(V,{accessToken:N,userRole:w})}),(0,a.jsxs)(r.v0,{children:[(0,a.jsxs)(r.td,{className:"mb-4",children:[(0,a.jsx)(r.OK,{children:"Model Hub"}),(0,a.jsx)(r.OK,{children:"Agent Hub"}),(0,a.jsx)(r.OK,{children:"MCP Hub"})]}),(0,a.jsxs)(r.nP,{children:[(0,a.jsxs)(r.x4,{children:[(0,a.jsxs)(r.Zb,{children:[!1==f&&(0,x.tY)(w||"")&&(0,a.jsx)("div",{className:"flex justify-end mb-4",children:(0,a.jsx)(r.zx,{onClick:()=>eN(),children:"Select Models to Make Public"})}),(0,a.jsx)(M,{modelHubData:z||[],onFilteredDataChange:eP}),(0,a.jsx)(F.C,{columns:E(e=>{B(e),T(!0)},ew,f),data:Y,isLoading:D,table:ej,defaultSorting:[{id:"model_group",desc:!1}]})]}),(0,a.jsx)("div",{className:"mt-4 text-center space-y-2",children:(0,a.jsxs)(r.xv,{className:"text-sm text-gray-600",children:["Showing ",Y.length," of ",(null==z?void 0:z.length)||0," models"]})})]}),(0,a.jsxs)(r.x4,{children:[(0,a.jsxs)(r.Zb,{children:[!1==f&&(0,x.tY)(w||"")&&(0,a.jsx)("div",{className:"flex justify-end mb-4",children:(0,a.jsx)(r.zx,{onClick:()=>ef(),children:"Select Agents to Make Public"})}),(0,a.jsx)(F.C,{columns:function(e,s){return arguments.length>2&&void 0!==arguments[2]&&arguments[2],[{header:"Agent Name",accessorKey:"name",enableSorting:!0,sortingFn:"alphanumeric",cell:e=>{let{row:l}=e,r=l.original;return(0,a.jsxs)("div",{className:"space-y-1",children:[(0,a.jsxs)("div",{className:"flex items-center space-x-2",children:[(0,a.jsx)(u.Z,{className:"font-medium text-sm",children:r.name}),(0,a.jsx)(p.Z,{title:"Copy agent name",children:(0,a.jsx)(t.Z,{onClick:()=>s(r.name),className:"cursor-pointer text-gray-500 hover:text-blue-500 text-xs"})})]}),(0,a.jsx)("div",{className:"md:hidden",children:(0,a.jsx)(u.Z,{className:"text-xs text-gray-600",children:r.description})})]})}},{header:"Description",accessorKey:"description",enableSorting:!0,sortingFn:"alphanumeric",cell:e=>{let{row:s}=e,l=s.original;return(0,a.jsx)(u.Z,{className:"text-xs line-clamp-2",children:l.description||"-"})},meta:{className:"hidden md:table-cell"}},{header:"Version",accessorKey:"version",enableSorting:!0,sortingFn:"alphanumeric",cell:e=>{let{row:s}=e,l=s.original;return(0,a.jsxs)(m.Z,{color:"blue",size:"sm",children:["v",l.version]})},meta:{className:"hidden lg:table-cell"}},{header:"Protocol",accessorKey:"protocolVersion",enableSorting:!0,sortingFn:"alphanumeric",cell:e=>{let{row:s}=e,l=s.original;return(0,a.jsx)(u.Z,{className:"text-xs",children:l.protocolVersion||"-"})},meta:{className:"hidden lg:table-cell"}},{header:"Skills",accessorKey:"skills",enableSorting:!1,cell:e=>{let{row:s}=e,l=s.original.skills||[];return(0,a.jsxs)("div",{className:"space-y-1",children:[(0,a.jsxs)(u.Z,{className:"text-xs font-medium",children:[l.length," skill",1!==l.length?"s":""]}),l.length>0&&(0,a.jsxs)("div",{className:"flex flex-wrap gap-1",children:[l.slice(0,2).map(e=>(0,a.jsx)(g.Z,{color:"purple",className:"text-xs",children:e.name},e.id)),l.length>2&&(0,a.jsxs)(u.Z,{className:"text-xs text-gray-500",children:["+",l.length-2]})]})]})}},{header:"Capabilities",accessorKey:"capabilities",enableSorting:!1,cell:e=>{let{row:s}=e,l=Object.entries(s.original.capabilities||{}).filter(e=>{let[s,l]=e;return!0===l}).map(e=>{let[s]=e;return s});return(0,a.jsx)("div",{className:"flex flex-wrap gap-1",children:0===l.length?(0,a.jsx)(u.Z,{className:"text-gray-500 text-xs",children:"-"}):l.map(e=>(0,a.jsx)(m.Z,{color:"green",size:"xs",children:e},e))})}},{header:"I/O Modes",accessorKey:"defaultInputModes",enableSorting:!1,cell:e=>{let{row:s}=e,l=s.original,t=l.defaultInputModes||[],r=l.defaultOutputModes||[];return(0,a.jsxs)("div",{className:"space-y-1",children:[(0,a.jsxs)(u.Z,{className:"text-xs",children:[(0,a.jsx)("span",{className:"font-medium",children:"In:"})," ",t.join(", ")||"-"]}),(0,a.jsxs)(u.Z,{className:"text-xs",children:[(0,a.jsx)("span",{className:"font-medium",children:"Out:"})," ",r.join(", ")||"-"]})]})},meta:{className:"hidden xl:table-cell"}},{header:"Public",accessorKey:"is_public",enableSorting:!0,sortingFn:(e,s)=>(!0===e.original.is_public?1:0)-(!0===s.original.is_public?1:0),cell:e=>{let{row:s}=e;return console.log("CHECKPOINT 1: ".concat(JSON.stringify(s.original))),!0===s.original.is_public?(0,a.jsx)(m.Z,{color:"green",size:"xs",children:"Yes"}):(0,a.jsx)(m.Z,{color:"gray",size:"xs",children:"No"})},meta:{className:"hidden md:table-cell"}},{header:"Details",id:"details",enableSorting:!1,cell:s=>{let{row:l}=s,t=l.original;return(0,a.jsxs)(h.Z,{size:"xs",variant:"secondary",onClick:()=>e(t),icon:j.Z,children:[(0,a.jsx)("span",{className:"hidden lg:inline",children:"Details"}),(0,a.jsx)("span",{className:"lg:hidden",children:"Info"})]})}}]}(e=>{ea(e),er(!0)},ew,f),data:J||[],isLoading:ee,table:ev,defaultSorting:[{id:"name",desc:!1}]})]}),(0,a.jsx)("div",{className:"mt-4 text-center space-y-2",children:(0,a.jsxs)(r.xv,{className:"text-sm text-gray-600",children:["Showing ",(null==J?void 0:J.length)||0," agent",(null==J?void 0:J.length)!==1?"s":""]})})]}),(0,a.jsxs)(r.x4,{children:[(0,a.jsxs)(r.Zb,{children:[!1==f&&(0,x.tY)(w||"")&&(0,a.jsx)("div",{className:"flex justify-end mb-4",children:(0,a.jsx)(r.zx,{onClick:()=>ey(),children:"Select MCP Servers to Make Public"})}),(0,a.jsx)(F.C,{columns:function(e,s){return arguments.length>2&&void 0!==arguments[2]&&arguments[2],[{header:"Server Name",accessorKey:"server_name",enableSorting:!0,sortingFn:"alphanumeric",cell:e=>{let{row:l}=e,r=l.original;return(0,a.jsxs)("div",{className:"space-y-1",children:[(0,a.jsxs)("div",{className:"flex items-center space-x-2",children:[(0,a.jsx)(u.Z,{className:"font-medium text-sm",children:r.server_name}),(0,a.jsx)(p.Z,{title:"Copy server name",children:(0,a.jsx)(t.Z,{onClick:()=>s(r.server_name),className:"cursor-pointer text-gray-500 hover:text-blue-500 text-xs"})})]}),(0,a.jsx)("div",{className:"md:hidden",children:(0,a.jsx)(u.Z,{className:"text-xs text-gray-600",children:r.description||"-"})})]})}},{header:"Description",accessorKey:"description",enableSorting:!0,sortingFn:"alphanumeric",cell:e=>{let{row:s}=e,l=s.original;return(0,a.jsx)(u.Z,{className:"text-xs line-clamp-2",children:l.description||"-"})},meta:{className:"hidden md:table-cell"}},{header:"URL",accessorKey:"url",enableSorting:!0,sortingFn:"alphanumeric",cell:e=>{let{row:l}=e,r=l.original;return(0,a.jsxs)("div",{className:"flex items-center space-x-2",children:[(0,a.jsx)(u.Z,{className:"text-xs truncate max-w-xs",children:r.url}),(0,a.jsx)(p.Z,{title:"Copy URL",children:(0,a.jsx)(t.Z,{onClick:()=>s(r.url),className:"cursor-pointer text-gray-500 hover:text-blue-500 text-xs flex-shrink-0"})})]})},meta:{className:"hidden lg:table-cell"}},{header:"Transport",accessorKey:"transport",enableSorting:!0,sortingFn:"alphanumeric",cell:e=>{let{row:s}=e,l=s.original;return(0,a.jsx)(m.Z,{color:"blue",size:"sm",children:l.transport})},meta:{className:"hidden md:table-cell"}},{header:"Auth Type",accessorKey:"auth_type",enableSorting:!0,sortingFn:"alphanumeric",cell:e=>{let{row:s}=e,l=s.original,t="none"===l.auth_type?"gray":"green";return(0,a.jsx)(m.Z,{color:t,size:"sm",children:l.auth_type})},meta:{className:"hidden md:table-cell"}},{header:"Status",accessorKey:"status",enableSorting:!0,sortingFn:"alphanumeric",cell:e=>{let{row:s}=e,l=s.original,t={active:"green",inactive:"red",unknown:"gray",healthy:"green",unhealthy:"red"}[l.status]||"gray";return(0,a.jsx)(m.Z,{color:t,size:"sm",children:l.status||"unknown"})}},{header:"Tools",accessorKey:"allowed_tools",enableSorting:!1,cell:e=>{let{row:s}=e,l=s.original.allowed_tools||[];return(0,a.jsxs)("div",{className:"space-y-1",children:[(0,a.jsx)(u.Z,{className:"text-xs font-medium",children:l.length>0?"".concat(l.length," tool").concat(1!==l.length?"s":""):"All tools"}),l.length>0&&(0,a.jsxs)("div",{className:"flex flex-wrap gap-1",children:[l.slice(0,2).map((e,s)=>(0,a.jsx)(g.Z,{color:"purple",className:"text-xs",children:e},s)),l.length>2&&(0,a.jsxs)(u.Z,{className:"text-xs text-gray-500",children:["+",l.length-2]})]})]})},meta:{className:"hidden lg:table-cell"}},{header:"Created By",accessorKey:"created_by",enableSorting:!0,sortingFn:"alphanumeric",cell:e=>{let{row:s}=e,l=s.original;return(0,a.jsx)(u.Z,{className:"text-xs",children:l.created_by||"-"})},meta:{className:"hidden xl:table-cell"}},{header:"Public",accessorKey:"mcp_info.is_public",enableSorting:!0,sortingFn:(e,s)=>{var l,a;return((null===(l=e.original.mcp_info)||void 0===l?void 0:l.is_public)===!0?1:0)-((null===(a=s.original.mcp_info)||void 0===a?void 0:a.is_public)===!0?1:0)},cell:e=>{var s;let{row:l}=e;return(null===(s=l.original.mcp_info)||void 0===s?void 0:s.is_public)===!0?(0,a.jsx)(m.Z,{color:"green",size:"xs",children:"Yes"}):(0,a.jsx)(m.Z,{color:"gray",size:"xs",children:"No"})},meta:{className:"hidden md:table-cell"}},{header:"Details",id:"details",enableSorting:!1,cell:s=>{let{row:l}=s,t=l.original;return(0,a.jsxs)(h.Z,{size:"xs",variant:"secondary",onClick:()=>e(t),icon:j.Z,children:[(0,a.jsx)("span",{className:"hidden lg:inline",children:"Details"}),(0,a.jsx)("span",{className:"lg:hidden",children:"Info"})]})}}]}(e=>{ex(e),eh(!0)},ew,f),data:en||[],isLoading:ec,table:eb,defaultSorting:[{id:"server_name",desc:!1}]})]}),(0,a.jsx)("div",{className:"mt-4 text-center space-y-2",children:(0,a.jsxs)(r.xv,{className:"text-sm text-gray-600",children:["Showing ",(null==en?void 0:en.length)||0," MCP server",(null==en?void 0:en.length)!==1?"s":""]})})]})]})]})]}):(0,a.jsxs)(r.Zb,{className:"mx-auto max-w-xl mt-10",children:[(0,a.jsx)(r.xv,{className:"text-xl text-center mb-2 text-black",children:"Public Model Hub not enabled."}),(0,a.jsx)("p",{className:"text-base text-center text-slate-800",children:"Ask your proxy admin to enable this on their Admin UI."})]}),(0,a.jsx)(n.Z,{title:"Public Model Hub",width:600,visible:H,footer:null,onOk:e_,onCancel:ek,children:(0,a.jsxs)("div",{className:"pt-5 pb-5",children:[(0,a.jsxs)("div",{className:"flex justify-between mb-4",children:[(0,a.jsx)(r.xv,{className:"text-base mr-2",children:"Shareable Link:"}),(0,a.jsx)(r.xv,{className:"max-w-sm ml-2 bg-gray-200 pr-2 pl-2 pt-1 pb-1 text-center rounded",children:"".concat((0,_.getProxyBaseUrl)(),"/ui/model_hub_table")})]}),(0,a.jsx)("div",{className:"flex justify-end",children:(0,a.jsx)(r.zx,{onClick:()=>{eg.replace("/model_hub_table?key=".concat(N))},children:"See Page"})})]})}),(0,a.jsx)(n.Z,{title:(null==R?void 0:R.model_group)||"Model Details",width:1e3,visible:U,footer:null,onOk:e_,onCancel:ek,children:R&&(0,a.jsxs)("div",{className:"space-y-6",children:[(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"text-lg font-semibold mb-4",children:"Model Overview"}),(0,a.jsxs)("div",{className:"grid grid-cols-2 gap-4 mb-4",children:[(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"font-medium",children:"Model Group:"}),(0,a.jsx)(r.xv,{children:R.model_group})]}),(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"font-medium",children:"Mode:"}),(0,a.jsx)(r.xv,{children:R.mode||"Not specified"})]}),(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"font-medium",children:"Providers:"}),(0,a.jsx)("div",{className:"flex flex-wrap gap-1 mt-1",children:R.providers.map(e=>(0,a.jsx)(r.Ct,{color:"blue",children:e},e))})]})]})]}),(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"text-lg font-semibold mb-4",children:"Token & Cost Information"}),(0,a.jsxs)("div",{className:"grid grid-cols-2 gap-4",children:[(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"font-medium",children:"Max Input Tokens:"}),(0,a.jsx)(r.xv,{children:(null===(s=R.max_input_tokens)||void 0===s?void 0:s.toLocaleString())||"Not specified"})]}),(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"font-medium",children:"Max Output Tokens:"}),(0,a.jsx)(r.xv,{children:(null===(l=R.max_output_tokens)||void 0===l?void 0:l.toLocaleString())||"Not specified"})]}),(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"font-medium",children:"Input Cost per 1M Tokens:"}),(0,a.jsx)(r.xv,{children:R.input_cost_per_token?eS(R.input_cost_per_token):"Not specified"})]}),(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"font-medium",children:"Output Cost per 1M Tokens:"}),(0,a.jsx)(r.xv,{children:R.output_cost_per_token?eS(R.output_cost_per_token):"Not specified"})]})]})]}),(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"text-lg font-semibold mb-4",children:"Capabilities"}),(0,a.jsx)("div",{className:"flex flex-wrap gap-2",children:(()=>{let e=eC(R),s=["green","blue","purple","orange","red","yellow"];return 0===e.length?(0,a.jsx)(r.xv,{className:"text-gray-500",children:"No special capabilities listed"}):e.map((e,l)=>(0,a.jsx)(r.Ct,{color:s[l%s.length],children:eZ(e)},e))})()})]}),(R.tpm||R.rpm)&&(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"text-lg font-semibold mb-4",children:"Rate Limits"}),(0,a.jsxs)("div",{className:"grid grid-cols-2 gap-4",children:[R.tpm&&(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"font-medium",children:"Tokens per Minute:"}),(0,a.jsx)(r.xv,{children:R.tpm.toLocaleString()})]}),R.rpm&&(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"font-medium",children:"Requests per Minute:"}),(0,a.jsx)(r.xv,{children:R.rpm.toLocaleString()})]})]})]}),R.supported_openai_params&&(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"text-lg font-semibold mb-4",children:"Supported OpenAI Parameters"}),(0,a.jsx)("div",{className:"flex flex-wrap gap-2",children:R.supported_openai_params.map(e=>(0,a.jsx)(r.Ct,{color:"green",children:e},e))})]}),(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"text-lg font-semibold mb-4",children:"Usage Example"}),(0,a.jsx)(o.Z,{language:"python",className:"text-sm",children:'import openai\n\nclient = openai.OpenAI(\n api_key="your_api_key",\n base_url="http://0.0.0.0:4000" # Your LiteLLM Proxy URL\n)\n\nresponse = client.chat.completions.create(\n model="'.concat(R.model_group,'",\n messages=[\n {\n "role": "user",\n "content": "Hello, how are you?"\n }\n ]\n)\n\nprint(response.choices[0].message.content)')})]})]})}),(0,a.jsx)(n.Z,{title:(null==el?void 0:el.name)||"Agent Details",width:1e3,visible:et,footer:null,onOk:e_,onCancel:ek,children:el&&(0,a.jsxs)("div",{className:"space-y-6",children:[(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"text-lg font-semibold mb-4",children:"Agent Overview"}),(0,a.jsxs)("div",{className:"grid grid-cols-2 gap-4 mb-4",children:[(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"font-medium",children:"Name:"}),(0,a.jsx)(r.xv,{children:el.name})]}),(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"font-medium",children:"Version:"}),(0,a.jsxs)(r.Ct,{color:"blue",children:["v",el.version]})]}),(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"font-medium",children:"Protocol Version:"}),(0,a.jsx)(r.xv,{children:el.protocolVersion})]}),(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"font-medium",children:"URL:"}),(0,a.jsxs)("div",{className:"flex items-center space-x-2",children:[(0,a.jsx)(r.xv,{className:"truncate",children:el.url}),(0,a.jsx)(t.Z,{onClick:()=>ew(el.url),className:"cursor-pointer text-gray-500 hover:text-blue-500"})]})]})]}),(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"font-medium",children:"Description:"}),(0,a.jsx)(r.xv,{className:"mt-1",children:el.description})]})]}),el.capabilities&&Object.keys(el.capabilities).length>0&&(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"text-lg font-semibold mb-4",children:"Capabilities"}),(0,a.jsx)("div",{className:"flex flex-wrap gap-2",children:Object.entries(el.capabilities).filter(e=>{let[s,l]=e;return!0===l}).map(e=>{let[s]=e;return(0,a.jsx)(r.Ct,{color:"green",children:s},s)})})]}),(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"text-lg font-semibold mb-4",children:"Input/Output Modes"}),(0,a.jsxs)("div",{className:"grid grid-cols-2 gap-4",children:[(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"font-medium",children:"Input Modes:"}),(0,a.jsx)("div",{className:"flex flex-wrap gap-1 mt-1",children:(null===(v=el.defaultInputModes)||void 0===v?void 0:v.map(e=>(0,a.jsx)(r.Ct,{color:"blue",children:e},e)))||(0,a.jsx)(r.xv,{children:"Not specified"})})]}),(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"font-medium",children:"Output Modes:"}),(0,a.jsx)("div",{className:"flex flex-wrap gap-1 mt-1",children:(null===(b=el.defaultOutputModes)||void 0===b?void 0:b.map(e=>(0,a.jsx)(r.Ct,{color:"purple",children:e},e)))||(0,a.jsx)(r.xv,{children:"Not specified"})})]})]})]}),el.skills&&el.skills.length>0&&(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"text-lg font-semibold mb-4",children:"Skills"}),(0,a.jsx)("div",{className:"space-y-4",children:el.skills.map(e=>(0,a.jsxs)("div",{className:"border border-gray-200 rounded p-4",children:[(0,a.jsxs)("div",{className:"flex justify-between items-start mb-2",children:[(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"font-medium text-base",children:e.name}),(0,a.jsxs)(r.xv,{className:"text-xs text-gray-500",children:["ID: ",e.id]})]}),e.tags&&e.tags.length>0&&(0,a.jsx)("div",{className:"flex flex-wrap gap-1",children:e.tags.map(e=>(0,a.jsx)(r.Ct,{color:"purple",size:"xs",children:e},e))})]}),(0,a.jsx)(r.xv,{className:"text-sm mb-2",children:e.description}),e.examples&&e.examples.length>0&&(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"text-xs font-medium text-gray-700",children:"Examples:"}),(0,a.jsx)("div",{className:"flex flex-wrap gap-1 mt-1",children:e.examples.map((e,s)=>(0,a.jsx)(r.Ct,{color:"gray",size:"xs",children:e},s))})]})]},e.id))})]}),el.supportsAuthenticatedExtendedCard&&(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"text-lg font-semibold mb-4",children:"Additional Features"}),(0,a.jsx)(r.Ct,{color:"green",children:"Supports Authenticated Extended Card"})]})]})}),(0,a.jsx)(n.Z,{title:(null==eo?void 0:eo.server_name)||"MCP Server Details",width:1e3,visible:em,footer:null,onOk:e_,onCancel:ek,children:eo&&(0,a.jsxs)("div",{className:"space-y-6",children:[(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"text-lg font-semibold mb-4",children:"Server Overview"}),(0,a.jsxs)("div",{className:"grid grid-cols-2 gap-4 mb-4",children:[(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"font-medium",children:"Server Name:"}),(0,a.jsx)(r.xv,{children:eo.server_name})]}),(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"font-medium",children:"Server ID:"}),(0,a.jsxs)("div",{className:"flex items-center space-x-2",children:[(0,a.jsx)(r.xv,{className:"text-xs truncate",children:eo.server_id}),(0,a.jsx)(t.Z,{onClick:()=>ew(eo.server_id),className:"cursor-pointer text-gray-500 hover:text-blue-500"})]})]}),eo.alias&&(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"font-medium",children:"Alias:"}),(0,a.jsx)(r.xv,{children:eo.alias})]}),(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"font-medium",children:"Transport:"}),(0,a.jsx)(r.Ct,{color:"blue",children:eo.transport})]}),(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"font-medium",children:"Auth Type:"}),(0,a.jsx)(r.Ct,{color:"none"===eo.auth_type?"gray":"green",children:eo.auth_type})]}),(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"font-medium",children:"Status:"}),(0,a.jsx)(r.Ct,{color:"active"===eo.status||"healthy"===eo.status?"green":"inactive"===eo.status||"unhealthy"===eo.status?"red":"gray",children:eo.status||"unknown"})]})]}),eo.description&&(0,a.jsxs)("div",{className:"mt-2",children:[(0,a.jsx)(r.xv,{className:"font-medium",children:"Description:"}),(0,a.jsx)(r.xv,{className:"mt-1",children:eo.description})]})]}),(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"text-lg font-semibold mb-4",children:"Connection Details"}),(0,a.jsxs)("div",{className:"space-y-2",children:[(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"font-medium",children:"URL:"}),(0,a.jsxs)("div",{className:"flex items-center space-x-2 mt-1",children:[(0,a.jsx)(r.xv,{className:"text-sm break-all bg-gray-100 p-2 rounded flex-1",children:eo.url}),(0,a.jsx)(t.Z,{onClick:()=>ew(eo.url),className:"cursor-pointer text-gray-500 hover:text-blue-500 flex-shrink-0"})]})]}),eo.command&&(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"font-medium",children:"Command:"}),(0,a.jsx)(r.xv,{className:"text-sm bg-gray-100 p-2 rounded mt-1 font-mono",children:eo.command})]})]})]}),eo.allowed_tools&&eo.allowed_tools.length>0&&(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"text-lg font-semibold mb-4",children:"Allowed Tools"}),(0,a.jsx)("div",{className:"flex flex-wrap gap-2",children:eo.allowed_tools.map((e,s)=>(0,a.jsx)(r.Ct,{color:"purple",children:e},s))})]}),eo.teams&&eo.teams.length>0&&(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"text-lg font-semibold mb-4",children:"Teams"}),(0,a.jsx)("div",{className:"flex flex-wrap gap-2",children:eo.teams.map((e,s)=>(0,a.jsx)(r.Ct,{color:"blue",children:e},s))})]}),eo.mcp_access_groups&&eo.mcp_access_groups.length>0&&(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"text-lg font-semibold mb-4",children:"Access Groups"}),(0,a.jsx)("div",{className:"flex flex-wrap gap-2",children:eo.mcp_access_groups.map((e,s)=>(0,a.jsx)(r.Ct,{color:"green",children:e},s))})]}),(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"text-lg font-semibold mb-4",children:"Metadata"}),(0,a.jsxs)("div",{className:"grid grid-cols-2 gap-4",children:[(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"font-medium",children:"Created By:"}),(0,a.jsx)(r.xv,{children:eo.created_by})]}),(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"font-medium",children:"Updated By:"}),(0,a.jsx)(r.xv,{children:eo.updated_by})]}),(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"font-medium",children:"Created At:"}),(0,a.jsx)(r.xv,{className:"text-sm",children:new Date(eo.created_at).toLocaleString()})]}),(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"font-medium",children:"Updated At:"}),(0,a.jsx)(r.xv,{className:"text-sm",children:new Date(eo.updated_at).toLocaleString()})]}),eo.last_health_check&&(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"font-medium",children:"Last Health Check:"}),(0,a.jsx)(r.xv,{className:"text-sm",children:new Date(eo.last_health_check).toLocaleString()})]})]}),eo.health_check_error&&(0,a.jsxs)("div",{className:"mt-2 p-2 bg-red-50 rounded",children:[(0,a.jsx)(r.xv,{className:"font-medium text-red-700",children:"Health Check Error:"}),(0,a.jsx)(r.xv,{className:"text-sm text-red-600 mt-1",children:eo.health_check_error})]})]}),(0,a.jsxs)("div",{children:[(0,a.jsx)(r.xv,{className:"text-lg font-semibold mb-4",children:"Usage Example"}),(0,a.jsx)(o.Z,{language:"python",className:"text-sm",children:'from fastmcp import Client\nimport asyncio\n\n# Standard MCP configuration\nconfig = {\n "mcpServers": {\n "'.concat(eo.server_name,'": {\n "url": "http://localhost:4000/').concat(eo.server_name,'/mcp",\n "headers": {\n "x-litellm-api-key": "Bearer sk-1234"\n }\n }\n }\n}\n\n# Create a client that connects to the server\nclient = Client(config)\n\nasync def main():\n async with client:\n # List available tools\n tools = await client.list_tools()\n print(f"Available tools: {[tool.name for tool in tools]}")\n\n # Call a tool\n response = await client.call_tool(\n name="tool_name", \n arguments={"arg": "value"}\n )\n print(f"Response: {response}")\n\nif __name__ == "__main__":\n asyncio.run(main())')})]})]})}),(0,a.jsx)(A,{visible:q,onClose:()=>G(!1),accessToken:N||"",modelHubData:z||[],onSuccess:()=>{N&&(async()=>{try{let e=await (0,_.modelHubCall)(N);L(e.data)}catch(e){console.error("Error refreshing model data:",e)}})()}}),(0,a.jsx)(Z,{visible:Q,onClose:()=>X(!1),accessToken:N||"",agentHubData:J||[],onSuccess:()=>{N&&(async()=>{try{let e=(await (0,_.getAgentsList)(N)).agents.map(e=>({agent_id:e.agent_id,...e.agent_card_params,is_public:e.is_public}));$(e)}catch(e){console.error("Error refreshing agent data:",e)}})()}}),(0,a.jsx)(S,{visible:eu,onClose:()=>ep(!1),accessToken:N||"",mcpHubData:en||[],onSuccess:()=>{N&&(async()=>{try{let e=await (0,_.fetchMCPServers)(N);ei(e)}catch(e){console.error("Error refreshing MCP server data:",e)}})()}})]})}}}]); \ No newline at end of file diff --git a/litellm/proxy/_experimental/out/_next/static/chunks/3250-d3d70eb77e9f4fa7.js b/litellm/proxy/_experimental/out/_next/static/chunks/3250-6c57da6c11f342fa.js similarity index 99% rename from litellm/proxy/_experimental/out/_next/static/chunks/3250-d3d70eb77e9f4fa7.js rename to litellm/proxy/_experimental/out/_next/static/chunks/3250-6c57da6c11f342fa.js index 0416de210bc..c8c793ed69b 100644 --- a/litellm/proxy/_experimental/out/_next/static/chunks/3250-d3d70eb77e9f4fa7.js +++ b/litellm/proxy/_experimental/out/_next/static/chunks/3250-6c57da6c11f342fa.js @@ -1 +1 @@ -"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[3250],{49634:function(e,r,o){o.d(r,{Z:function(){return s}});var t=o(1119),l=o(2265),a={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M880 184H712v-64c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v64H384v-64c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v64H144c-17.7 0-32 14.3-32 32v664c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V216c0-17.7-14.3-32-32-32zm-40 656H184V460h656v380zM184 392V256h128v48c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-48h256v48c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-48h128v136H184z"}}]},name:"calendar",theme:"outlined"},n=o(55015),s=l.forwardRef(function(e,r){return l.createElement(n.Z,(0,t.Z)({},e,{ref:r,icon:a}))})},94789:function(e,r,o){o.d(r,{Z:function(){return d}});var t=o(5853),l=o(2265),a=o(26898),n=o(13241),s=o(1153);let i=(0,s.fn)("Callout"),d=l.forwardRef((e,r)=>{let{title:o,icon:d,color:c,className:m,children:p}=e,u=(0,t._T)(e,["title","icon","color","className","children"]);return l.createElement("div",Object.assign({ref:r,className:(0,n.q)(i("root"),"flex flex-col overflow-hidden rounded-tremor-default text-tremor-default border-l-4 py-3 pr-3 pl-4",c?(0,n.q)((0,s.bM)(c,a.K.background).bgColor,(0,s.bM)(c,a.K.darkBorder).borderColor,(0,s.bM)(c,a.K.darkText).textColor,"dark:bg-opacity-10 bg-opacity-10"):(0,n.q)("bg-tremor-brand-faint border-tremor-brand-emphasis text-tremor-brand-emphasis","dark:bg-dark-tremor-brand-muted/70 dark:border-dark-tremor-brand-emphasis dark:text-dark-tremor-brand-emphasis"),m)},u),l.createElement("div",{className:(0,n.q)(i("header"),"flex items-start")},d?l.createElement(d,{className:(0,n.q)(i("icon"),"flex-none h-5 w-5 mr-1.5")}):null,l.createElement("h4",{className:(0,n.q)(i("title"),"font-semibold")},o)),l.createElement("p",{className:(0,n.q)(i("body"),"overflow-y-auto",p?"mt-2":"")},p))});d.displayName="Callout"},35829:function(e,r,o){o.d(r,{Z:function(){return i}});var t=o(5853),l=o(26898),a=o(13241),n=o(1153),s=o(2265);let i=s.forwardRef((e,r)=>{let{color:o,children:i,className:d}=e,c=(0,t._T)(e,["color","children","className"]);return s.createElement("p",Object.assign({ref:r,className:(0,a.q)("font-semibold text-tremor-metric",o?(0,n.bM)(o,l.K.darkText).textColor:"text-tremor-content-strong dark:text-dark-tremor-content-strong",d)},c),i)});i.displayName="Metric"},49096:function(e,r,o){o.d(r,{ZD:function(){return a}});var t=o(61994);let l=e=>"boolean"==typeof e?`${e}`:0===e?"0":e,a=e=>{let r=function(){for(var r,o,l=arguments.length,a=Array(l),n=0;n{let t=Object.fromEntries(Object.entries(e||{}).filter(e=>{let[r]=e;return!["class","className"].includes(r)}));return r(o.map(e=>e(t)),null==e?void 0:e.class,null==e?void 0:e.className)}},cva:e=>o=>{var t;if((null==e?void 0:e.variants)==null)return r(null==e?void 0:e.base,null==o?void 0:o.class,null==o?void 0:o.className);let{variants:a,defaultVariants:n}=e,s=Object.keys(a).map(e=>{let r=null==o?void 0:o[e],t=null==n?void 0:n[e],s=l(r)||l(t);return a[e][s]}),i={...n,...o&&Object.entries(o).reduce((e,r)=>{let[o,t]=r;return void 0===t?e:{...e,[o]:t}},{})},d=null==e?void 0:null===(t=e.compoundVariants)||void 0===t?void 0:t.reduce((e,r)=>{let{class:o,className:t,...l}=r;return Object.entries(l).every(e=>{let[r,o]=e,t=i[r];return Array.isArray(o)?o.includes(t):t===o})?[...e,o,t]:e},[]);return r(null==e?void 0:e.base,s,d,null==o?void 0:o.class,null==o?void 0:o.className)},cx:r}},{compose:n,cva:s,cx:i}=a()},53335:function(e,r,o){o.d(r,{m6:function(){return ev}});let t=(e,r)=>{let o=Array(e.length+r.length);for(let r=0;r({classGroupId:e,validator:r}),a=(e=new Map,r=null,o)=>({nextPart:e,validators:r,classGroupId:o}),n=[],s=e=>{let r=c(e),{conflictingClassGroups:o,conflictingClassGroupModifiers:l}=e;return{getClassGroupId:e=>{if(e.startsWith("[")&&e.endsWith("]"))return d(e);let o=e.split("-"),t=""===o[0]&&o.length>1?1:0;return i(o,t,r)},getConflictingClassGroupIds:(e,r)=>{if(r){let r=l[e],a=o[e];return r?a?t(a,r):r:a||n}return o[e]||n}}},i=(e,r,o)=>{if(0==e.length-r)return o.classGroupId;let t=e[r],l=o.nextPart.get(t);if(l){let o=i(e,r+1,l);if(o)return o}let a=o.validators;if(null===a)return;let n=0===r?e.join("-"):e.slice(r).join("-"),s=a.length;for(let e=0;e-1===e.slice(1,-1).indexOf(":")?void 0:(()=>{let r=e.slice(1,-1),o=r.indexOf(":"),t=r.slice(0,o);return t?"arbitrary.."+t:void 0})(),c=e=>{let{theme:r,classGroups:o}=e;return m(o,r)},m=(e,r)=>{let o=a();for(let t in e)p(e[t],o,t,r);return o},p=(e,r,o,t)=>{let l=e.length;for(let a=0;a{if("string"==typeof e){b(e,r,o);return}if("function"==typeof e){f(e,r,o,t);return}g(e,r,o,t)},b=(e,r,o)=>{(""===e?r:h(r,e)).classGroupId=o},f=(e,r,o,t)=>{if(k(e)){p(e(t),r,o,t);return}null===r.validators&&(r.validators=[]),r.validators.push(l(o,e))},g=(e,r,o,t)=>{let l=Object.entries(e),a=l.length;for(let e=0;e{let o=e,t=r.split("-"),l=t.length;for(let e=0;e"isThemeGetter"in e&&!0===e.isThemeGetter,v=e=>{if(e<1)return{get:()=>void 0,set:()=>{}};let r=0,o=Object.create(null),t=Object.create(null),l=(l,a)=>{o[l]=a,++r>e&&(r=0,t=o,o=Object.create(null))};return{get(e){let r=o[e];return void 0!==r?r:void 0!==(r=t[e])?(l(e,r),r):void 0},set(e,r){e in o?o[e]=r:l(e,r)}}},x=[],w=(e,r,o,t,l)=>({modifiers:e,hasImportantModifier:r,baseClassName:o,maybePostfixModifierPosition:t,isExternal:l}),y=e=>{let{prefix:r,experimentalParseClassName:o}=e,t=e=>{let r;let o=[],t=0,l=0,a=0,n=e.length;for(let s=0;sa?r-a:void 0)};if(r){let e=r+":",o=t;t=r=>r.startsWith(e)?o(r.slice(e.length)):w(x,!1,r,void 0,!0)}if(o){let e=t;t=r=>o({className:r,parseClassName:e})}return t},z=e=>{let r=new Map;return e.orderSensitiveModifiers.forEach((e,o)=>{r.set(e,1e6+o)}),e=>{let o=[],t=[];for(let l=0;l0&&(t.sort(),o.push(...t),t=[]),o.push(a)):t.push(a)}return t.length>0&&(t.sort(),o.push(...t)),o}},j=e=>({cache:v(e.cacheSize),parseClassName:y(e),sortModifiers:z(e),...s(e)}),N=/\s+/,C=(e,r)=>{let{parseClassName:o,getClassGroupId:t,getConflictingClassGroupIds:l,sortModifiers:a}=r,n=[],s=e.trim().split(N),i="";for(let e=s.length-1;e>=0;e-=1){let r=s[e],{isExternal:d,modifiers:c,hasImportantModifier:m,baseClassName:p,maybePostfixModifierPosition:u}=o(r);if(d){i=r+(i.length>0?" "+i:i);continue}let b=!!u,f=t(b?p.substring(0,u):p);if(!f){if(!b||!(f=t(p))){i=r+(i.length>0?" "+i:i);continue}b=!1}let g=0===c.length?"":1===c.length?c[0]:a(c).join(":"),h=m?g+"!":g,k=h+f;if(n.indexOf(k)>-1)continue;n.push(k);let v=l(f,b);for(let e=0;e0?" "+i:i)}return i},O=(...e)=>{let r,o,t=0,l="";for(;t{let r;if("string"==typeof e)return e;let o="";for(let t=0;t{let r=r=>r[e]||E;return r.isThemeGetter=!0,r},q=/^\[(?:(\w[\w-]*):)?(.+)\]$/i,W=/^\((?:(\w[\w-]*):)?(.+)\)$/i,$=/^\d+\/\d+$/,_=/^(\d+(\.\d+)?)?(xs|sm|md|lg|xl)$/,T=/\d+(%|px|r?em|[sdl]?v([hwib]|min|max)|pt|pc|in|cm|mm|cap|ch|ex|r?lh|cq(w|h|i|b|min|max))|\b(calc|min|max|clamp)\(.+\)|^0$/,I=/^(rgba?|hsla?|hwb|(ok)?(lab|lch)|color-mix)\(.+\)$/,Z=/^(inset_)?-?((\d+)?\.?(\d+)[a-z]+|0)_-?((\d+)?\.?(\d+)[a-z]+|0)/,A=/^(url|image|image-set|cross-fade|element|(repeating-)?(linear|radial|conic)-gradient)\(.+\)$/,H=e=>$.test(e),K=e=>!!e&&!Number.isNaN(Number(e)),S=e=>!!e&&Number.isInteger(Number(e)),V=e=>e.endsWith("%")&&K(e.slice(0,-1)),P=e=>_.test(e),R=()=>!0,B=e=>T.test(e)&&!I.test(e),D=()=>!1,F=e=>Z.test(e),J=e=>A.test(e),L=e=>!U(e)&&!et(e),Q=e=>ec(e,eb,D),U=e=>q.test(e),X=e=>ec(e,ef,B),Y=e=>ec(e,eg,K),ee=e=>ec(e,ep,D),er=e=>ec(e,eu,J),eo=e=>ec(e,ek,F),et=e=>W.test(e),el=e=>em(e,ef),ea=e=>em(e,eh),en=e=>em(e,ep),es=e=>em(e,eb),ei=e=>em(e,eu),ed=e=>em(e,ek,!0),ec=(e,r,o)=>{let t=q.exec(e);return!!t&&(t[1]?r(t[1]):o(t[2]))},em=(e,r,o=!1)=>{let t=W.exec(e);return!!t&&(t[1]?r(t[1]):o)},ep=e=>"position"===e||"percentage"===e,eu=e=>"image"===e||"url"===e,eb=e=>"length"===e||"size"===e||"bg-size"===e,ef=e=>"length"===e,eg=e=>"number"===e,eh=e=>"family-name"===e,ek=e=>"shadow"===e,ev=((e,...r)=>{let o,t,l,a;let n=e=>{let r=t(e);if(r)return r;let a=C(e,o);return l(e,a),a};return a=s=>(t=(o=j(r.reduce((e,r)=>r(e),e()))).cache.get,l=o.cache.set,a=n,n(s)),(...e)=>a(O(...e))})(()=>{let e=G("color"),r=G("font"),o=G("text"),t=G("font-weight"),l=G("tracking"),a=G("leading"),n=G("breakpoint"),s=G("container"),i=G("spacing"),d=G("radius"),c=G("shadow"),m=G("inset-shadow"),p=G("text-shadow"),u=G("drop-shadow"),b=G("blur"),f=G("perspective"),g=G("aspect"),h=G("ease"),k=G("animate"),v=()=>["auto","avoid","all","avoid-page","page","left","right","column"],x=()=>["center","top","bottom","left","right","top-left","left-top","top-right","right-top","bottom-right","right-bottom","bottom-left","left-bottom"],w=()=>[...x(),et,U],y=()=>["auto","hidden","clip","visible","scroll"],z=()=>["auto","contain","none"],j=()=>[et,U,i],N=()=>[H,"full","auto",...j()],C=()=>[S,"none","subgrid",et,U],O=()=>["auto",{span:["full",S,et,U]},S,et,U],M=()=>[S,"auto",et,U],E=()=>["auto","min","max","fr",et,U],q=()=>["start","end","center","between","around","evenly","stretch","baseline","center-safe","end-safe"],W=()=>["start","end","center","stretch","center-safe","end-safe"],$=()=>["auto",...j()],_=()=>[H,"auto","full","dvw","dvh","lvw","lvh","svw","svh","min","max","fit",...j()],T=()=>[e,et,U],I=()=>[...x(),en,ee,{position:[et,U]}],Z=()=>["no-repeat",{repeat:["","x","y","space","round"]}],A=()=>["auto","cover","contain",es,Q,{size:[et,U]}],B=()=>[V,el,X],D=()=>["","none","full",d,et,U],F=()=>["",K,el,X],J=()=>["solid","dashed","dotted","double"],ec=()=>["normal","multiply","screen","overlay","darken","lighten","color-dodge","color-burn","hard-light","soft-light","difference","exclusion","hue","saturation","color","luminosity"],em=()=>[K,V,en,ee],ep=()=>["","none",b,et,U],eu=()=>["none",K,et,U],eb=()=>["none",K,et,U],ef=()=>[K,et,U],eg=()=>[H,"full",...j()];return{cacheSize:500,theme:{animate:["spin","ping","pulse","bounce"],aspect:["video"],blur:[P],breakpoint:[P],color:[R],container:[P],"drop-shadow":[P],ease:["in","out","in-out"],font:[L],"font-weight":["thin","extralight","light","normal","medium","semibold","bold","extrabold","black"],"inset-shadow":[P],leading:["none","tight","snug","normal","relaxed","loose"],perspective:["dramatic","near","normal","midrange","distant","none"],radius:[P],shadow:[P],spacing:["px",K],text:[P],"text-shadow":[P],tracking:["tighter","tight","normal","wide","wider","widest"]},classGroups:{aspect:[{aspect:["auto","square",H,U,et,g]}],container:["container"],columns:[{columns:[K,U,et,s]}],"break-after":[{"break-after":v()}],"break-before":[{"break-before":v()}],"break-inside":[{"break-inside":["auto","avoid","avoid-page","avoid-column"]}],"box-decoration":[{"box-decoration":["slice","clone"]}],box:[{box:["border","content"]}],display:["block","inline-block","inline","flex","inline-flex","table","inline-table","table-caption","table-cell","table-column","table-column-group","table-footer-group","table-header-group","table-row-group","table-row","flow-root","grid","inline-grid","contents","list-item","hidden"],sr:["sr-only","not-sr-only"],float:[{float:["right","left","none","start","end"]}],clear:[{clear:["left","right","both","none","start","end"]}],isolation:["isolate","isolation-auto"],"object-fit":[{object:["contain","cover","fill","none","scale-down"]}],"object-position":[{object:w()}],overflow:[{overflow:y()}],"overflow-x":[{"overflow-x":y()}],"overflow-y":[{"overflow-y":y()}],overscroll:[{overscroll:z()}],"overscroll-x":[{"overscroll-x":z()}],"overscroll-y":[{"overscroll-y":z()}],position:["static","fixed","absolute","relative","sticky"],inset:[{inset:N()}],"inset-x":[{"inset-x":N()}],"inset-y":[{"inset-y":N()}],start:[{start:N()}],end:[{end:N()}],top:[{top:N()}],right:[{right:N()}],bottom:[{bottom:N()}],left:[{left:N()}],visibility:["visible","invisible","collapse"],z:[{z:[S,"auto",et,U]}],basis:[{basis:[H,"full","auto",s,...j()]}],"flex-direction":[{flex:["row","row-reverse","col","col-reverse"]}],"flex-wrap":[{flex:["nowrap","wrap","wrap-reverse"]}],flex:[{flex:[K,H,"auto","initial","none",U]}],grow:[{grow:["",K,et,U]}],shrink:[{shrink:["",K,et,U]}],order:[{order:[S,"first","last","none",et,U]}],"grid-cols":[{"grid-cols":C()}],"col-start-end":[{col:O()}],"col-start":[{"col-start":M()}],"col-end":[{"col-end":M()}],"grid-rows":[{"grid-rows":C()}],"row-start-end":[{row:O()}],"row-start":[{"row-start":M()}],"row-end":[{"row-end":M()}],"grid-flow":[{"grid-flow":["row","col","dense","row-dense","col-dense"]}],"auto-cols":[{"auto-cols":E()}],"auto-rows":[{"auto-rows":E()}],gap:[{gap:j()}],"gap-x":[{"gap-x":j()}],"gap-y":[{"gap-y":j()}],"justify-content":[{justify:[...q(),"normal"]}],"justify-items":[{"justify-items":[...W(),"normal"]}],"justify-self":[{"justify-self":["auto",...W()]}],"align-content":[{content:["normal",...q()]}],"align-items":[{items:[...W(),{baseline:["","last"]}]}],"align-self":[{self:["auto",...W(),{baseline:["","last"]}]}],"place-content":[{"place-content":q()}],"place-items":[{"place-items":[...W(),"baseline"]}],"place-self":[{"place-self":["auto",...W()]}],p:[{p:j()}],px:[{px:j()}],py:[{py:j()}],ps:[{ps:j()}],pe:[{pe:j()}],pt:[{pt:j()}],pr:[{pr:j()}],pb:[{pb:j()}],pl:[{pl:j()}],m:[{m:$()}],mx:[{mx:$()}],my:[{my:$()}],ms:[{ms:$()}],me:[{me:$()}],mt:[{mt:$()}],mr:[{mr:$()}],mb:[{mb:$()}],ml:[{ml:$()}],"space-x":[{"space-x":j()}],"space-x-reverse":["space-x-reverse"],"space-y":[{"space-y":j()}],"space-y-reverse":["space-y-reverse"],size:[{size:_()}],w:[{w:[s,"screen",..._()]}],"min-w":[{"min-w":[s,"screen","none",..._()]}],"max-w":[{"max-w":[s,"screen","none","prose",{screen:[n]},..._()]}],h:[{h:["screen","lh",..._()]}],"min-h":[{"min-h":["screen","lh","none",..._()]}],"max-h":[{"max-h":["screen","lh",..._()]}],"font-size":[{text:["base",o,el,X]}],"font-smoothing":["antialiased","subpixel-antialiased"],"font-style":["italic","not-italic"],"font-weight":[{font:[t,et,Y]}],"font-stretch":[{"font-stretch":["ultra-condensed","extra-condensed","condensed","semi-condensed","normal","semi-expanded","expanded","extra-expanded","ultra-expanded",V,U]}],"font-family":[{font:[ea,U,r]}],"fvn-normal":["normal-nums"],"fvn-ordinal":["ordinal"],"fvn-slashed-zero":["slashed-zero"],"fvn-figure":["lining-nums","oldstyle-nums"],"fvn-spacing":["proportional-nums","tabular-nums"],"fvn-fraction":["diagonal-fractions","stacked-fractions"],tracking:[{tracking:[l,et,U]}],"line-clamp":[{"line-clamp":[K,"none",et,Y]}],leading:[{leading:[a,...j()]}],"list-image":[{"list-image":["none",et,U]}],"list-style-position":[{list:["inside","outside"]}],"list-style-type":[{list:["disc","decimal","none",et,U]}],"text-alignment":[{text:["left","center","right","justify","start","end"]}],"placeholder-color":[{placeholder:T()}],"text-color":[{text:T()}],"text-decoration":["underline","overline","line-through","no-underline"],"text-decoration-style":[{decoration:[...J(),"wavy"]}],"text-decoration-thickness":[{decoration:[K,"from-font","auto",et,X]}],"text-decoration-color":[{decoration:T()}],"underline-offset":[{"underline-offset":[K,"auto",et,U]}],"text-transform":["uppercase","lowercase","capitalize","normal-case"],"text-overflow":["truncate","text-ellipsis","text-clip"],"text-wrap":[{text:["wrap","nowrap","balance","pretty"]}],indent:[{indent:j()}],"vertical-align":[{align:["baseline","top","middle","bottom","text-top","text-bottom","sub","super",et,U]}],whitespace:[{whitespace:["normal","nowrap","pre","pre-line","pre-wrap","break-spaces"]}],break:[{break:["normal","words","all","keep"]}],wrap:[{wrap:["break-word","anywhere","normal"]}],hyphens:[{hyphens:["none","manual","auto"]}],content:[{content:["none",et,U]}],"bg-attachment":[{bg:["fixed","local","scroll"]}],"bg-clip":[{"bg-clip":["border","padding","content","text"]}],"bg-origin":[{"bg-origin":["border","padding","content"]}],"bg-position":[{bg:I()}],"bg-repeat":[{bg:Z()}],"bg-size":[{bg:A()}],"bg-image":[{bg:["none",{linear:[{to:["t","tr","r","br","b","bl","l","tl"]},S,et,U],radial:["",et,U],conic:[S,et,U]},ei,er]}],"bg-color":[{bg:T()}],"gradient-from-pos":[{from:B()}],"gradient-via-pos":[{via:B()}],"gradient-to-pos":[{to:B()}],"gradient-from":[{from:T()}],"gradient-via":[{via:T()}],"gradient-to":[{to:T()}],rounded:[{rounded:D()}],"rounded-s":[{"rounded-s":D()}],"rounded-e":[{"rounded-e":D()}],"rounded-t":[{"rounded-t":D()}],"rounded-r":[{"rounded-r":D()}],"rounded-b":[{"rounded-b":D()}],"rounded-l":[{"rounded-l":D()}],"rounded-ss":[{"rounded-ss":D()}],"rounded-se":[{"rounded-se":D()}],"rounded-ee":[{"rounded-ee":D()}],"rounded-es":[{"rounded-es":D()}],"rounded-tl":[{"rounded-tl":D()}],"rounded-tr":[{"rounded-tr":D()}],"rounded-br":[{"rounded-br":D()}],"rounded-bl":[{"rounded-bl":D()}],"border-w":[{border:F()}],"border-w-x":[{"border-x":F()}],"border-w-y":[{"border-y":F()}],"border-w-s":[{"border-s":F()}],"border-w-e":[{"border-e":F()}],"border-w-t":[{"border-t":F()}],"border-w-r":[{"border-r":F()}],"border-w-b":[{"border-b":F()}],"border-w-l":[{"border-l":F()}],"divide-x":[{"divide-x":F()}],"divide-x-reverse":["divide-x-reverse"],"divide-y":[{"divide-y":F()}],"divide-y-reverse":["divide-y-reverse"],"border-style":[{border:[...J(),"hidden","none"]}],"divide-style":[{divide:[...J(),"hidden","none"]}],"border-color":[{border:T()}],"border-color-x":[{"border-x":T()}],"border-color-y":[{"border-y":T()}],"border-color-s":[{"border-s":T()}],"border-color-e":[{"border-e":T()}],"border-color-t":[{"border-t":T()}],"border-color-r":[{"border-r":T()}],"border-color-b":[{"border-b":T()}],"border-color-l":[{"border-l":T()}],"divide-color":[{divide:T()}],"outline-style":[{outline:[...J(),"none","hidden"]}],"outline-offset":[{"outline-offset":[K,et,U]}],"outline-w":[{outline:["",K,el,X]}],"outline-color":[{outline:T()}],shadow:[{shadow:["","none",c,ed,eo]}],"shadow-color":[{shadow:T()}],"inset-shadow":[{"inset-shadow":["none",m,ed,eo]}],"inset-shadow-color":[{"inset-shadow":T()}],"ring-w":[{ring:F()}],"ring-w-inset":["ring-inset"],"ring-color":[{ring:T()}],"ring-offset-w":[{"ring-offset":[K,X]}],"ring-offset-color":[{"ring-offset":T()}],"inset-ring-w":[{"inset-ring":F()}],"inset-ring-color":[{"inset-ring":T()}],"text-shadow":[{"text-shadow":["none",p,ed,eo]}],"text-shadow-color":[{"text-shadow":T()}],opacity:[{opacity:[K,et,U]}],"mix-blend":[{"mix-blend":[...ec(),"plus-darker","plus-lighter"]}],"bg-blend":[{"bg-blend":ec()}],"mask-clip":[{"mask-clip":["border","padding","content","fill","stroke","view"]},"mask-no-clip"],"mask-composite":[{mask:["add","subtract","intersect","exclude"]}],"mask-image-linear-pos":[{"mask-linear":[K]}],"mask-image-linear-from-pos":[{"mask-linear-from":em()}],"mask-image-linear-to-pos":[{"mask-linear-to":em()}],"mask-image-linear-from-color":[{"mask-linear-from":T()}],"mask-image-linear-to-color":[{"mask-linear-to":T()}],"mask-image-t-from-pos":[{"mask-t-from":em()}],"mask-image-t-to-pos":[{"mask-t-to":em()}],"mask-image-t-from-color":[{"mask-t-from":T()}],"mask-image-t-to-color":[{"mask-t-to":T()}],"mask-image-r-from-pos":[{"mask-r-from":em()}],"mask-image-r-to-pos":[{"mask-r-to":em()}],"mask-image-r-from-color":[{"mask-r-from":T()}],"mask-image-r-to-color":[{"mask-r-to":T()}],"mask-image-b-from-pos":[{"mask-b-from":em()}],"mask-image-b-to-pos":[{"mask-b-to":em()}],"mask-image-b-from-color":[{"mask-b-from":T()}],"mask-image-b-to-color":[{"mask-b-to":T()}],"mask-image-l-from-pos":[{"mask-l-from":em()}],"mask-image-l-to-pos":[{"mask-l-to":em()}],"mask-image-l-from-color":[{"mask-l-from":T()}],"mask-image-l-to-color":[{"mask-l-to":T()}],"mask-image-x-from-pos":[{"mask-x-from":em()}],"mask-image-x-to-pos":[{"mask-x-to":em()}],"mask-image-x-from-color":[{"mask-x-from":T()}],"mask-image-x-to-color":[{"mask-x-to":T()}],"mask-image-y-from-pos":[{"mask-y-from":em()}],"mask-image-y-to-pos":[{"mask-y-to":em()}],"mask-image-y-from-color":[{"mask-y-from":T()}],"mask-image-y-to-color":[{"mask-y-to":T()}],"mask-image-radial":[{"mask-radial":[et,U]}],"mask-image-radial-from-pos":[{"mask-radial-from":em()}],"mask-image-radial-to-pos":[{"mask-radial-to":em()}],"mask-image-radial-from-color":[{"mask-radial-from":T()}],"mask-image-radial-to-color":[{"mask-radial-to":T()}],"mask-image-radial-shape":[{"mask-radial":["circle","ellipse"]}],"mask-image-radial-size":[{"mask-radial":[{closest:["side","corner"],farthest:["side","corner"]}]}],"mask-image-radial-pos":[{"mask-radial-at":x()}],"mask-image-conic-pos":[{"mask-conic":[K]}],"mask-image-conic-from-pos":[{"mask-conic-from":em()}],"mask-image-conic-to-pos":[{"mask-conic-to":em()}],"mask-image-conic-from-color":[{"mask-conic-from":T()}],"mask-image-conic-to-color":[{"mask-conic-to":T()}],"mask-mode":[{mask:["alpha","luminance","match"]}],"mask-origin":[{"mask-origin":["border","padding","content","fill","stroke","view"]}],"mask-position":[{mask:I()}],"mask-repeat":[{mask:Z()}],"mask-size":[{mask:A()}],"mask-type":[{"mask-type":["alpha","luminance"]}],"mask-image":[{mask:["none",et,U]}],filter:[{filter:["","none",et,U]}],blur:[{blur:ep()}],brightness:[{brightness:[K,et,U]}],contrast:[{contrast:[K,et,U]}],"drop-shadow":[{"drop-shadow":["","none",u,ed,eo]}],"drop-shadow-color":[{"drop-shadow":T()}],grayscale:[{grayscale:["",K,et,U]}],"hue-rotate":[{"hue-rotate":[K,et,U]}],invert:[{invert:["",K,et,U]}],saturate:[{saturate:[K,et,U]}],sepia:[{sepia:["",K,et,U]}],"backdrop-filter":[{"backdrop-filter":["","none",et,U]}],"backdrop-blur":[{"backdrop-blur":ep()}],"backdrop-brightness":[{"backdrop-brightness":[K,et,U]}],"backdrop-contrast":[{"backdrop-contrast":[K,et,U]}],"backdrop-grayscale":[{"backdrop-grayscale":["",K,et,U]}],"backdrop-hue-rotate":[{"backdrop-hue-rotate":[K,et,U]}],"backdrop-invert":[{"backdrop-invert":["",K,et,U]}],"backdrop-opacity":[{"backdrop-opacity":[K,et,U]}],"backdrop-saturate":[{"backdrop-saturate":[K,et,U]}],"backdrop-sepia":[{"backdrop-sepia":["",K,et,U]}],"border-collapse":[{border:["collapse","separate"]}],"border-spacing":[{"border-spacing":j()}],"border-spacing-x":[{"border-spacing-x":j()}],"border-spacing-y":[{"border-spacing-y":j()}],"table-layout":[{table:["auto","fixed"]}],caption:[{caption:["top","bottom"]}],transition:[{transition:["","all","colors","opacity","shadow","transform","none",et,U]}],"transition-behavior":[{transition:["normal","discrete"]}],duration:[{duration:[K,"initial",et,U]}],ease:[{ease:["linear","initial",h,et,U]}],delay:[{delay:[K,et,U]}],animate:[{animate:["none",k,et,U]}],backface:[{backface:["hidden","visible"]}],perspective:[{perspective:[f,et,U]}],"perspective-origin":[{"perspective-origin":w()}],rotate:[{rotate:eu()}],"rotate-x":[{"rotate-x":eu()}],"rotate-y":[{"rotate-y":eu()}],"rotate-z":[{"rotate-z":eu()}],scale:[{scale:eb()}],"scale-x":[{"scale-x":eb()}],"scale-y":[{"scale-y":eb()}],"scale-z":[{"scale-z":eb()}],"scale-3d":["scale-3d"],skew:[{skew:ef()}],"skew-x":[{"skew-x":ef()}],"skew-y":[{"skew-y":ef()}],transform:[{transform:[et,U,"","none","gpu","cpu"]}],"transform-origin":[{origin:w()}],"transform-style":[{transform:["3d","flat"]}],translate:[{translate:eg()}],"translate-x":[{"translate-x":eg()}],"translate-y":[{"translate-y":eg()}],"translate-z":[{"translate-z":eg()}],"translate-none":["translate-none"],accent:[{accent:T()}],appearance:[{appearance:["none","auto"]}],"caret-color":[{caret:T()}],"color-scheme":[{scheme:["normal","dark","light","light-dark","only-dark","only-light"]}],cursor:[{cursor:["auto","default","pointer","wait","text","move","help","not-allowed","none","context-menu","progress","cell","crosshair","vertical-text","alias","copy","no-drop","grab","grabbing","all-scroll","col-resize","row-resize","n-resize","e-resize","s-resize","w-resize","ne-resize","nw-resize","se-resize","sw-resize","ew-resize","ns-resize","nesw-resize","nwse-resize","zoom-in","zoom-out",et,U]}],"field-sizing":[{"field-sizing":["fixed","content"]}],"pointer-events":[{"pointer-events":["auto","none"]}],resize:[{resize:["none","","y","x"]}],"scroll-behavior":[{scroll:["auto","smooth"]}],"scroll-m":[{"scroll-m":j()}],"scroll-mx":[{"scroll-mx":j()}],"scroll-my":[{"scroll-my":j()}],"scroll-ms":[{"scroll-ms":j()}],"scroll-me":[{"scroll-me":j()}],"scroll-mt":[{"scroll-mt":j()}],"scroll-mr":[{"scroll-mr":j()}],"scroll-mb":[{"scroll-mb":j()}],"scroll-ml":[{"scroll-ml":j()}],"scroll-p":[{"scroll-p":j()}],"scroll-px":[{"scroll-px":j()}],"scroll-py":[{"scroll-py":j()}],"scroll-ps":[{"scroll-ps":j()}],"scroll-pe":[{"scroll-pe":j()}],"scroll-pt":[{"scroll-pt":j()}],"scroll-pr":[{"scroll-pr":j()}],"scroll-pb":[{"scroll-pb":j()}],"scroll-pl":[{"scroll-pl":j()}],"snap-align":[{snap:["start","end","center","align-none"]}],"snap-stop":[{snap:["normal","always"]}],"snap-type":[{snap:["none","x","y","both"]}],"snap-strictness":[{snap:["mandatory","proximity"]}],touch:[{touch:["auto","none","manipulation"]}],"touch-x":[{"touch-pan":["x","left","right"]}],"touch-y":[{"touch-pan":["y","up","down"]}],"touch-pz":["touch-pinch-zoom"],select:[{select:["none","text","all","auto"]}],"will-change":[{"will-change":["auto","scroll","contents","transform",et,U]}],fill:[{fill:["none",...T()]}],"stroke-w":[{stroke:[K,el,X,Y]}],stroke:[{stroke:["none",...T()]}],"forced-color-adjust":[{"forced-color-adjust":["auto","none"]}]},conflictingClassGroups:{overflow:["overflow-x","overflow-y"],overscroll:["overscroll-x","overscroll-y"],inset:["inset-x","inset-y","start","end","top","right","bottom","left"],"inset-x":["right","left"],"inset-y":["top","bottom"],flex:["basis","grow","shrink"],gap:["gap-x","gap-y"],p:["px","py","ps","pe","pt","pr","pb","pl"],px:["pr","pl"],py:["pt","pb"],m:["mx","my","ms","me","mt","mr","mb","ml"],mx:["mr","ml"],my:["mt","mb"],size:["w","h"],"font-size":["leading"],"fvn-normal":["fvn-ordinal","fvn-slashed-zero","fvn-figure","fvn-spacing","fvn-fraction"],"fvn-ordinal":["fvn-normal"],"fvn-slashed-zero":["fvn-normal"],"fvn-figure":["fvn-normal"],"fvn-spacing":["fvn-normal"],"fvn-fraction":["fvn-normal"],"line-clamp":["display","overflow"],rounded:["rounded-s","rounded-e","rounded-t","rounded-r","rounded-b","rounded-l","rounded-ss","rounded-se","rounded-ee","rounded-es","rounded-tl","rounded-tr","rounded-br","rounded-bl"],"rounded-s":["rounded-ss","rounded-es"],"rounded-e":["rounded-se","rounded-ee"],"rounded-t":["rounded-tl","rounded-tr"],"rounded-r":["rounded-tr","rounded-br"],"rounded-b":["rounded-br","rounded-bl"],"rounded-l":["rounded-tl","rounded-bl"],"border-spacing":["border-spacing-x","border-spacing-y"],"border-w":["border-w-x","border-w-y","border-w-s","border-w-e","border-w-t","border-w-r","border-w-b","border-w-l"],"border-w-x":["border-w-r","border-w-l"],"border-w-y":["border-w-t","border-w-b"],"border-color":["border-color-x","border-color-y","border-color-s","border-color-e","border-color-t","border-color-r","border-color-b","border-color-l"],"border-color-x":["border-color-r","border-color-l"],"border-color-y":["border-color-t","border-color-b"],translate:["translate-x","translate-y","translate-none"],"translate-none":["translate","translate-x","translate-y","translate-z"],"scroll-m":["scroll-mx","scroll-my","scroll-ms","scroll-me","scroll-mt","scroll-mr","scroll-mb","scroll-ml"],"scroll-mx":["scroll-mr","scroll-ml"],"scroll-my":["scroll-mt","scroll-mb"],"scroll-p":["scroll-px","scroll-py","scroll-ps","scroll-pe","scroll-pt","scroll-pr","scroll-pb","scroll-pl"],"scroll-px":["scroll-pr","scroll-pl"],"scroll-py":["scroll-pt","scroll-pb"],touch:["touch-x","touch-y","touch-pz"],"touch-x":["touch"],"touch-y":["touch"],"touch-pz":["touch"]},conflictingClassGroupModifiers:{"font-size":["leading"]},orderSensitiveModifiers:["*","**","after","backdrop","before","details-content","file","first-letter","first-line","marker","placeholder","selection"]}})}}]); \ No newline at end of file +"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[3250],{49634:function(e,r,o){o.d(r,{Z:function(){return s}});var t=o(1119),l=o(2265),a={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M880 184H712v-64c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v64H384v-64c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v64H144c-17.7 0-32 14.3-32 32v664c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V216c0-17.7-14.3-32-32-32zm-40 656H184V460h656v380zM184 392V256h128v48c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-48h256v48c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-48h128v136H184z"}}]},name:"calendar",theme:"outlined"},n=o(55015),s=l.forwardRef(function(e,r){return l.createElement(n.Z,(0,t.Z)({},e,{ref:r,icon:a}))})},94789:function(e,r,o){o.d(r,{Z:function(){return d}});var t=o(5853),l=o(2265),a=o(26898),n=o(13241),s=o(1153);let i=(0,s.fn)("Callout"),d=l.forwardRef((e,r)=>{let{title:o,icon:d,color:c,className:m,children:p}=e,u=(0,t._T)(e,["title","icon","color","className","children"]);return l.createElement("div",Object.assign({ref:r,className:(0,n.q)(i("root"),"flex flex-col overflow-hidden rounded-tremor-default text-tremor-default border-l-4 py-3 pr-3 pl-4",c?(0,n.q)((0,s.bM)(c,a.K.background).bgColor,(0,s.bM)(c,a.K.darkBorder).borderColor,(0,s.bM)(c,a.K.darkText).textColor,"dark:bg-opacity-10 bg-opacity-10"):(0,n.q)("bg-tremor-brand-faint border-tremor-brand-emphasis text-tremor-brand-emphasis","dark:bg-dark-tremor-brand-muted/70 dark:border-dark-tremor-brand-emphasis dark:text-dark-tremor-brand-emphasis"),m)},u),l.createElement("div",{className:(0,n.q)(i("header"),"flex items-start")},d?l.createElement(d,{className:(0,n.q)(i("icon"),"flex-none h-5 w-5 mr-1.5")}):null,l.createElement("h4",{className:(0,n.q)(i("title"),"font-semibold")},o)),l.createElement("p",{className:(0,n.q)(i("body"),"overflow-y-auto",p?"mt-2":"")},p))});d.displayName="Callout"},35829:function(e,r,o){o.d(r,{Z:function(){return i}});var t=o(5853),l=o(26898),a=o(13241),n=o(1153),s=o(2265);let i=s.forwardRef((e,r)=>{let{color:o,children:i,className:d}=e,c=(0,t._T)(e,["color","children","className"]);return s.createElement("p",Object.assign({ref:r,className:(0,a.q)("font-semibold text-tremor-metric",o?(0,n.bM)(o,l.K.darkText).textColor:"text-tremor-content-strong dark:text-dark-tremor-content-strong",d)},c),i)});i.displayName="Metric"},49096:function(e,r,o){o.d(r,{ZD:function(){return a}});var t=o(87602);let l=e=>"boolean"==typeof e?`${e}`:0===e?"0":e,a=e=>{let r=function(){for(var r,o,l=arguments.length,a=Array(l),n=0;n{let t=Object.fromEntries(Object.entries(e||{}).filter(e=>{let[r]=e;return!["class","className"].includes(r)}));return r(o.map(e=>e(t)),null==e?void 0:e.class,null==e?void 0:e.className)}},cva:e=>o=>{var t;if((null==e?void 0:e.variants)==null)return r(null==e?void 0:e.base,null==o?void 0:o.class,null==o?void 0:o.className);let{variants:a,defaultVariants:n}=e,s=Object.keys(a).map(e=>{let r=null==o?void 0:o[e],t=null==n?void 0:n[e],s=l(r)||l(t);return a[e][s]}),i={...n,...o&&Object.entries(o).reduce((e,r)=>{let[o,t]=r;return void 0===t?e:{...e,[o]:t}},{})},d=null==e?void 0:null===(t=e.compoundVariants)||void 0===t?void 0:t.reduce((e,r)=>{let{class:o,className:t,...l}=r;return Object.entries(l).every(e=>{let[r,o]=e,t=i[r];return Array.isArray(o)?o.includes(t):t===o})?[...e,o,t]:e},[]);return r(null==e?void 0:e.base,s,d,null==o?void 0:o.class,null==o?void 0:o.className)},cx:r}},{compose:n,cva:s,cx:i}=a()},53335:function(e,r,o){o.d(r,{m6:function(){return ev}});let t=(e,r)=>{let o=Array(e.length+r.length);for(let r=0;r({classGroupId:e,validator:r}),a=(e=new Map,r=null,o)=>({nextPart:e,validators:r,classGroupId:o}),n=[],s=e=>{let r=c(e),{conflictingClassGroups:o,conflictingClassGroupModifiers:l}=e;return{getClassGroupId:e=>{if(e.startsWith("[")&&e.endsWith("]"))return d(e);let o=e.split("-"),t=""===o[0]&&o.length>1?1:0;return i(o,t,r)},getConflictingClassGroupIds:(e,r)=>{if(r){let r=l[e],a=o[e];return r?a?t(a,r):r:a||n}return o[e]||n}}},i=(e,r,o)=>{if(0==e.length-r)return o.classGroupId;let t=e[r],l=o.nextPart.get(t);if(l){let o=i(e,r+1,l);if(o)return o}let a=o.validators;if(null===a)return;let n=0===r?e.join("-"):e.slice(r).join("-"),s=a.length;for(let e=0;e-1===e.slice(1,-1).indexOf(":")?void 0:(()=>{let r=e.slice(1,-1),o=r.indexOf(":"),t=r.slice(0,o);return t?"arbitrary.."+t:void 0})(),c=e=>{let{theme:r,classGroups:o}=e;return m(o,r)},m=(e,r)=>{let o=a();for(let t in e)p(e[t],o,t,r);return o},p=(e,r,o,t)=>{let l=e.length;for(let a=0;a{if("string"==typeof e){b(e,r,o);return}if("function"==typeof e){f(e,r,o,t);return}g(e,r,o,t)},b=(e,r,o)=>{(""===e?r:h(r,e)).classGroupId=o},f=(e,r,o,t)=>{if(k(e)){p(e(t),r,o,t);return}null===r.validators&&(r.validators=[]),r.validators.push(l(o,e))},g=(e,r,o,t)=>{let l=Object.entries(e),a=l.length;for(let e=0;e{let o=e,t=r.split("-"),l=t.length;for(let e=0;e"isThemeGetter"in e&&!0===e.isThemeGetter,v=e=>{if(e<1)return{get:()=>void 0,set:()=>{}};let r=0,o=Object.create(null),t=Object.create(null),l=(l,a)=>{o[l]=a,++r>e&&(r=0,t=o,o=Object.create(null))};return{get(e){let r=o[e];return void 0!==r?r:void 0!==(r=t[e])?(l(e,r),r):void 0},set(e,r){e in o?o[e]=r:l(e,r)}}},x=[],w=(e,r,o,t,l)=>({modifiers:e,hasImportantModifier:r,baseClassName:o,maybePostfixModifierPosition:t,isExternal:l}),y=e=>{let{prefix:r,experimentalParseClassName:o}=e,t=e=>{let r;let o=[],t=0,l=0,a=0,n=e.length;for(let s=0;sa?r-a:void 0)};if(r){let e=r+":",o=t;t=r=>r.startsWith(e)?o(r.slice(e.length)):w(x,!1,r,void 0,!0)}if(o){let e=t;t=r=>o({className:r,parseClassName:e})}return t},z=e=>{let r=new Map;return e.orderSensitiveModifiers.forEach((e,o)=>{r.set(e,1e6+o)}),e=>{let o=[],t=[];for(let l=0;l0&&(t.sort(),o.push(...t),t=[]),o.push(a)):t.push(a)}return t.length>0&&(t.sort(),o.push(...t)),o}},j=e=>({cache:v(e.cacheSize),parseClassName:y(e),sortModifiers:z(e),...s(e)}),N=/\s+/,C=(e,r)=>{let{parseClassName:o,getClassGroupId:t,getConflictingClassGroupIds:l,sortModifiers:a}=r,n=[],s=e.trim().split(N),i="";for(let e=s.length-1;e>=0;e-=1){let r=s[e],{isExternal:d,modifiers:c,hasImportantModifier:m,baseClassName:p,maybePostfixModifierPosition:u}=o(r);if(d){i=r+(i.length>0?" "+i:i);continue}let b=!!u,f=t(b?p.substring(0,u):p);if(!f){if(!b||!(f=t(p))){i=r+(i.length>0?" "+i:i);continue}b=!1}let g=0===c.length?"":1===c.length?c[0]:a(c).join(":"),h=m?g+"!":g,k=h+f;if(n.indexOf(k)>-1)continue;n.push(k);let v=l(f,b);for(let e=0;e0?" "+i:i)}return i},O=(...e)=>{let r,o,t=0,l="";for(;t{let r;if("string"==typeof e)return e;let o="";for(let t=0;t{let r=r=>r[e]||E;return r.isThemeGetter=!0,r},q=/^\[(?:(\w[\w-]*):)?(.+)\]$/i,W=/^\((?:(\w[\w-]*):)?(.+)\)$/i,$=/^\d+\/\d+$/,_=/^(\d+(\.\d+)?)?(xs|sm|md|lg|xl)$/,T=/\d+(%|px|r?em|[sdl]?v([hwib]|min|max)|pt|pc|in|cm|mm|cap|ch|ex|r?lh|cq(w|h|i|b|min|max))|\b(calc|min|max|clamp)\(.+\)|^0$/,I=/^(rgba?|hsla?|hwb|(ok)?(lab|lch)|color-mix)\(.+\)$/,Z=/^(inset_)?-?((\d+)?\.?(\d+)[a-z]+|0)_-?((\d+)?\.?(\d+)[a-z]+|0)/,A=/^(url|image|image-set|cross-fade|element|(repeating-)?(linear|radial|conic)-gradient)\(.+\)$/,H=e=>$.test(e),K=e=>!!e&&!Number.isNaN(Number(e)),S=e=>!!e&&Number.isInteger(Number(e)),V=e=>e.endsWith("%")&&K(e.slice(0,-1)),P=e=>_.test(e),R=()=>!0,B=e=>T.test(e)&&!I.test(e),D=()=>!1,F=e=>Z.test(e),J=e=>A.test(e),L=e=>!U(e)&&!et(e),Q=e=>ec(e,eb,D),U=e=>q.test(e),X=e=>ec(e,ef,B),Y=e=>ec(e,eg,K),ee=e=>ec(e,ep,D),er=e=>ec(e,eu,J),eo=e=>ec(e,ek,F),et=e=>W.test(e),el=e=>em(e,ef),ea=e=>em(e,eh),en=e=>em(e,ep),es=e=>em(e,eb),ei=e=>em(e,eu),ed=e=>em(e,ek,!0),ec=(e,r,o)=>{let t=q.exec(e);return!!t&&(t[1]?r(t[1]):o(t[2]))},em=(e,r,o=!1)=>{let t=W.exec(e);return!!t&&(t[1]?r(t[1]):o)},ep=e=>"position"===e||"percentage"===e,eu=e=>"image"===e||"url"===e,eb=e=>"length"===e||"size"===e||"bg-size"===e,ef=e=>"length"===e,eg=e=>"number"===e,eh=e=>"family-name"===e,ek=e=>"shadow"===e,ev=((e,...r)=>{let o,t,l,a;let n=e=>{let r=t(e);if(r)return r;let a=C(e,o);return l(e,a),a};return a=s=>(t=(o=j(r.reduce((e,r)=>r(e),e()))).cache.get,l=o.cache.set,a=n,n(s)),(...e)=>a(O(...e))})(()=>{let e=G("color"),r=G("font"),o=G("text"),t=G("font-weight"),l=G("tracking"),a=G("leading"),n=G("breakpoint"),s=G("container"),i=G("spacing"),d=G("radius"),c=G("shadow"),m=G("inset-shadow"),p=G("text-shadow"),u=G("drop-shadow"),b=G("blur"),f=G("perspective"),g=G("aspect"),h=G("ease"),k=G("animate"),v=()=>["auto","avoid","all","avoid-page","page","left","right","column"],x=()=>["center","top","bottom","left","right","top-left","left-top","top-right","right-top","bottom-right","right-bottom","bottom-left","left-bottom"],w=()=>[...x(),et,U],y=()=>["auto","hidden","clip","visible","scroll"],z=()=>["auto","contain","none"],j=()=>[et,U,i],N=()=>[H,"full","auto",...j()],C=()=>[S,"none","subgrid",et,U],O=()=>["auto",{span:["full",S,et,U]},S,et,U],M=()=>[S,"auto",et,U],E=()=>["auto","min","max","fr",et,U],q=()=>["start","end","center","between","around","evenly","stretch","baseline","center-safe","end-safe"],W=()=>["start","end","center","stretch","center-safe","end-safe"],$=()=>["auto",...j()],_=()=>[H,"auto","full","dvw","dvh","lvw","lvh","svw","svh","min","max","fit",...j()],T=()=>[e,et,U],I=()=>[...x(),en,ee,{position:[et,U]}],Z=()=>["no-repeat",{repeat:["","x","y","space","round"]}],A=()=>["auto","cover","contain",es,Q,{size:[et,U]}],B=()=>[V,el,X],D=()=>["","none","full",d,et,U],F=()=>["",K,el,X],J=()=>["solid","dashed","dotted","double"],ec=()=>["normal","multiply","screen","overlay","darken","lighten","color-dodge","color-burn","hard-light","soft-light","difference","exclusion","hue","saturation","color","luminosity"],em=()=>[K,V,en,ee],ep=()=>["","none",b,et,U],eu=()=>["none",K,et,U],eb=()=>["none",K,et,U],ef=()=>[K,et,U],eg=()=>[H,"full",...j()];return{cacheSize:500,theme:{animate:["spin","ping","pulse","bounce"],aspect:["video"],blur:[P],breakpoint:[P],color:[R],container:[P],"drop-shadow":[P],ease:["in","out","in-out"],font:[L],"font-weight":["thin","extralight","light","normal","medium","semibold","bold","extrabold","black"],"inset-shadow":[P],leading:["none","tight","snug","normal","relaxed","loose"],perspective:["dramatic","near","normal","midrange","distant","none"],radius:[P],shadow:[P],spacing:["px",K],text:[P],"text-shadow":[P],tracking:["tighter","tight","normal","wide","wider","widest"]},classGroups:{aspect:[{aspect:["auto","square",H,U,et,g]}],container:["container"],columns:[{columns:[K,U,et,s]}],"break-after":[{"break-after":v()}],"break-before":[{"break-before":v()}],"break-inside":[{"break-inside":["auto","avoid","avoid-page","avoid-column"]}],"box-decoration":[{"box-decoration":["slice","clone"]}],box:[{box:["border","content"]}],display:["block","inline-block","inline","flex","inline-flex","table","inline-table","table-caption","table-cell","table-column","table-column-group","table-footer-group","table-header-group","table-row-group","table-row","flow-root","grid","inline-grid","contents","list-item","hidden"],sr:["sr-only","not-sr-only"],float:[{float:["right","left","none","start","end"]}],clear:[{clear:["left","right","both","none","start","end"]}],isolation:["isolate","isolation-auto"],"object-fit":[{object:["contain","cover","fill","none","scale-down"]}],"object-position":[{object:w()}],overflow:[{overflow:y()}],"overflow-x":[{"overflow-x":y()}],"overflow-y":[{"overflow-y":y()}],overscroll:[{overscroll:z()}],"overscroll-x":[{"overscroll-x":z()}],"overscroll-y":[{"overscroll-y":z()}],position:["static","fixed","absolute","relative","sticky"],inset:[{inset:N()}],"inset-x":[{"inset-x":N()}],"inset-y":[{"inset-y":N()}],start:[{start:N()}],end:[{end:N()}],top:[{top:N()}],right:[{right:N()}],bottom:[{bottom:N()}],left:[{left:N()}],visibility:["visible","invisible","collapse"],z:[{z:[S,"auto",et,U]}],basis:[{basis:[H,"full","auto",s,...j()]}],"flex-direction":[{flex:["row","row-reverse","col","col-reverse"]}],"flex-wrap":[{flex:["nowrap","wrap","wrap-reverse"]}],flex:[{flex:[K,H,"auto","initial","none",U]}],grow:[{grow:["",K,et,U]}],shrink:[{shrink:["",K,et,U]}],order:[{order:[S,"first","last","none",et,U]}],"grid-cols":[{"grid-cols":C()}],"col-start-end":[{col:O()}],"col-start":[{"col-start":M()}],"col-end":[{"col-end":M()}],"grid-rows":[{"grid-rows":C()}],"row-start-end":[{row:O()}],"row-start":[{"row-start":M()}],"row-end":[{"row-end":M()}],"grid-flow":[{"grid-flow":["row","col","dense","row-dense","col-dense"]}],"auto-cols":[{"auto-cols":E()}],"auto-rows":[{"auto-rows":E()}],gap:[{gap:j()}],"gap-x":[{"gap-x":j()}],"gap-y":[{"gap-y":j()}],"justify-content":[{justify:[...q(),"normal"]}],"justify-items":[{"justify-items":[...W(),"normal"]}],"justify-self":[{"justify-self":["auto",...W()]}],"align-content":[{content:["normal",...q()]}],"align-items":[{items:[...W(),{baseline:["","last"]}]}],"align-self":[{self:["auto",...W(),{baseline:["","last"]}]}],"place-content":[{"place-content":q()}],"place-items":[{"place-items":[...W(),"baseline"]}],"place-self":[{"place-self":["auto",...W()]}],p:[{p:j()}],px:[{px:j()}],py:[{py:j()}],ps:[{ps:j()}],pe:[{pe:j()}],pt:[{pt:j()}],pr:[{pr:j()}],pb:[{pb:j()}],pl:[{pl:j()}],m:[{m:$()}],mx:[{mx:$()}],my:[{my:$()}],ms:[{ms:$()}],me:[{me:$()}],mt:[{mt:$()}],mr:[{mr:$()}],mb:[{mb:$()}],ml:[{ml:$()}],"space-x":[{"space-x":j()}],"space-x-reverse":["space-x-reverse"],"space-y":[{"space-y":j()}],"space-y-reverse":["space-y-reverse"],size:[{size:_()}],w:[{w:[s,"screen",..._()]}],"min-w":[{"min-w":[s,"screen","none",..._()]}],"max-w":[{"max-w":[s,"screen","none","prose",{screen:[n]},..._()]}],h:[{h:["screen","lh",..._()]}],"min-h":[{"min-h":["screen","lh","none",..._()]}],"max-h":[{"max-h":["screen","lh",..._()]}],"font-size":[{text:["base",o,el,X]}],"font-smoothing":["antialiased","subpixel-antialiased"],"font-style":["italic","not-italic"],"font-weight":[{font:[t,et,Y]}],"font-stretch":[{"font-stretch":["ultra-condensed","extra-condensed","condensed","semi-condensed","normal","semi-expanded","expanded","extra-expanded","ultra-expanded",V,U]}],"font-family":[{font:[ea,U,r]}],"fvn-normal":["normal-nums"],"fvn-ordinal":["ordinal"],"fvn-slashed-zero":["slashed-zero"],"fvn-figure":["lining-nums","oldstyle-nums"],"fvn-spacing":["proportional-nums","tabular-nums"],"fvn-fraction":["diagonal-fractions","stacked-fractions"],tracking:[{tracking:[l,et,U]}],"line-clamp":[{"line-clamp":[K,"none",et,Y]}],leading:[{leading:[a,...j()]}],"list-image":[{"list-image":["none",et,U]}],"list-style-position":[{list:["inside","outside"]}],"list-style-type":[{list:["disc","decimal","none",et,U]}],"text-alignment":[{text:["left","center","right","justify","start","end"]}],"placeholder-color":[{placeholder:T()}],"text-color":[{text:T()}],"text-decoration":["underline","overline","line-through","no-underline"],"text-decoration-style":[{decoration:[...J(),"wavy"]}],"text-decoration-thickness":[{decoration:[K,"from-font","auto",et,X]}],"text-decoration-color":[{decoration:T()}],"underline-offset":[{"underline-offset":[K,"auto",et,U]}],"text-transform":["uppercase","lowercase","capitalize","normal-case"],"text-overflow":["truncate","text-ellipsis","text-clip"],"text-wrap":[{text:["wrap","nowrap","balance","pretty"]}],indent:[{indent:j()}],"vertical-align":[{align:["baseline","top","middle","bottom","text-top","text-bottom","sub","super",et,U]}],whitespace:[{whitespace:["normal","nowrap","pre","pre-line","pre-wrap","break-spaces"]}],break:[{break:["normal","words","all","keep"]}],wrap:[{wrap:["break-word","anywhere","normal"]}],hyphens:[{hyphens:["none","manual","auto"]}],content:[{content:["none",et,U]}],"bg-attachment":[{bg:["fixed","local","scroll"]}],"bg-clip":[{"bg-clip":["border","padding","content","text"]}],"bg-origin":[{"bg-origin":["border","padding","content"]}],"bg-position":[{bg:I()}],"bg-repeat":[{bg:Z()}],"bg-size":[{bg:A()}],"bg-image":[{bg:["none",{linear:[{to:["t","tr","r","br","b","bl","l","tl"]},S,et,U],radial:["",et,U],conic:[S,et,U]},ei,er]}],"bg-color":[{bg:T()}],"gradient-from-pos":[{from:B()}],"gradient-via-pos":[{via:B()}],"gradient-to-pos":[{to:B()}],"gradient-from":[{from:T()}],"gradient-via":[{via:T()}],"gradient-to":[{to:T()}],rounded:[{rounded:D()}],"rounded-s":[{"rounded-s":D()}],"rounded-e":[{"rounded-e":D()}],"rounded-t":[{"rounded-t":D()}],"rounded-r":[{"rounded-r":D()}],"rounded-b":[{"rounded-b":D()}],"rounded-l":[{"rounded-l":D()}],"rounded-ss":[{"rounded-ss":D()}],"rounded-se":[{"rounded-se":D()}],"rounded-ee":[{"rounded-ee":D()}],"rounded-es":[{"rounded-es":D()}],"rounded-tl":[{"rounded-tl":D()}],"rounded-tr":[{"rounded-tr":D()}],"rounded-br":[{"rounded-br":D()}],"rounded-bl":[{"rounded-bl":D()}],"border-w":[{border:F()}],"border-w-x":[{"border-x":F()}],"border-w-y":[{"border-y":F()}],"border-w-s":[{"border-s":F()}],"border-w-e":[{"border-e":F()}],"border-w-t":[{"border-t":F()}],"border-w-r":[{"border-r":F()}],"border-w-b":[{"border-b":F()}],"border-w-l":[{"border-l":F()}],"divide-x":[{"divide-x":F()}],"divide-x-reverse":["divide-x-reverse"],"divide-y":[{"divide-y":F()}],"divide-y-reverse":["divide-y-reverse"],"border-style":[{border:[...J(),"hidden","none"]}],"divide-style":[{divide:[...J(),"hidden","none"]}],"border-color":[{border:T()}],"border-color-x":[{"border-x":T()}],"border-color-y":[{"border-y":T()}],"border-color-s":[{"border-s":T()}],"border-color-e":[{"border-e":T()}],"border-color-t":[{"border-t":T()}],"border-color-r":[{"border-r":T()}],"border-color-b":[{"border-b":T()}],"border-color-l":[{"border-l":T()}],"divide-color":[{divide:T()}],"outline-style":[{outline:[...J(),"none","hidden"]}],"outline-offset":[{"outline-offset":[K,et,U]}],"outline-w":[{outline:["",K,el,X]}],"outline-color":[{outline:T()}],shadow:[{shadow:["","none",c,ed,eo]}],"shadow-color":[{shadow:T()}],"inset-shadow":[{"inset-shadow":["none",m,ed,eo]}],"inset-shadow-color":[{"inset-shadow":T()}],"ring-w":[{ring:F()}],"ring-w-inset":["ring-inset"],"ring-color":[{ring:T()}],"ring-offset-w":[{"ring-offset":[K,X]}],"ring-offset-color":[{"ring-offset":T()}],"inset-ring-w":[{"inset-ring":F()}],"inset-ring-color":[{"inset-ring":T()}],"text-shadow":[{"text-shadow":["none",p,ed,eo]}],"text-shadow-color":[{"text-shadow":T()}],opacity:[{opacity:[K,et,U]}],"mix-blend":[{"mix-blend":[...ec(),"plus-darker","plus-lighter"]}],"bg-blend":[{"bg-blend":ec()}],"mask-clip":[{"mask-clip":["border","padding","content","fill","stroke","view"]},"mask-no-clip"],"mask-composite":[{mask:["add","subtract","intersect","exclude"]}],"mask-image-linear-pos":[{"mask-linear":[K]}],"mask-image-linear-from-pos":[{"mask-linear-from":em()}],"mask-image-linear-to-pos":[{"mask-linear-to":em()}],"mask-image-linear-from-color":[{"mask-linear-from":T()}],"mask-image-linear-to-color":[{"mask-linear-to":T()}],"mask-image-t-from-pos":[{"mask-t-from":em()}],"mask-image-t-to-pos":[{"mask-t-to":em()}],"mask-image-t-from-color":[{"mask-t-from":T()}],"mask-image-t-to-color":[{"mask-t-to":T()}],"mask-image-r-from-pos":[{"mask-r-from":em()}],"mask-image-r-to-pos":[{"mask-r-to":em()}],"mask-image-r-from-color":[{"mask-r-from":T()}],"mask-image-r-to-color":[{"mask-r-to":T()}],"mask-image-b-from-pos":[{"mask-b-from":em()}],"mask-image-b-to-pos":[{"mask-b-to":em()}],"mask-image-b-from-color":[{"mask-b-from":T()}],"mask-image-b-to-color":[{"mask-b-to":T()}],"mask-image-l-from-pos":[{"mask-l-from":em()}],"mask-image-l-to-pos":[{"mask-l-to":em()}],"mask-image-l-from-color":[{"mask-l-from":T()}],"mask-image-l-to-color":[{"mask-l-to":T()}],"mask-image-x-from-pos":[{"mask-x-from":em()}],"mask-image-x-to-pos":[{"mask-x-to":em()}],"mask-image-x-from-color":[{"mask-x-from":T()}],"mask-image-x-to-color":[{"mask-x-to":T()}],"mask-image-y-from-pos":[{"mask-y-from":em()}],"mask-image-y-to-pos":[{"mask-y-to":em()}],"mask-image-y-from-color":[{"mask-y-from":T()}],"mask-image-y-to-color":[{"mask-y-to":T()}],"mask-image-radial":[{"mask-radial":[et,U]}],"mask-image-radial-from-pos":[{"mask-radial-from":em()}],"mask-image-radial-to-pos":[{"mask-radial-to":em()}],"mask-image-radial-from-color":[{"mask-radial-from":T()}],"mask-image-radial-to-color":[{"mask-radial-to":T()}],"mask-image-radial-shape":[{"mask-radial":["circle","ellipse"]}],"mask-image-radial-size":[{"mask-radial":[{closest:["side","corner"],farthest:["side","corner"]}]}],"mask-image-radial-pos":[{"mask-radial-at":x()}],"mask-image-conic-pos":[{"mask-conic":[K]}],"mask-image-conic-from-pos":[{"mask-conic-from":em()}],"mask-image-conic-to-pos":[{"mask-conic-to":em()}],"mask-image-conic-from-color":[{"mask-conic-from":T()}],"mask-image-conic-to-color":[{"mask-conic-to":T()}],"mask-mode":[{mask:["alpha","luminance","match"]}],"mask-origin":[{"mask-origin":["border","padding","content","fill","stroke","view"]}],"mask-position":[{mask:I()}],"mask-repeat":[{mask:Z()}],"mask-size":[{mask:A()}],"mask-type":[{"mask-type":["alpha","luminance"]}],"mask-image":[{mask:["none",et,U]}],filter:[{filter:["","none",et,U]}],blur:[{blur:ep()}],brightness:[{brightness:[K,et,U]}],contrast:[{contrast:[K,et,U]}],"drop-shadow":[{"drop-shadow":["","none",u,ed,eo]}],"drop-shadow-color":[{"drop-shadow":T()}],grayscale:[{grayscale:["",K,et,U]}],"hue-rotate":[{"hue-rotate":[K,et,U]}],invert:[{invert:["",K,et,U]}],saturate:[{saturate:[K,et,U]}],sepia:[{sepia:["",K,et,U]}],"backdrop-filter":[{"backdrop-filter":["","none",et,U]}],"backdrop-blur":[{"backdrop-blur":ep()}],"backdrop-brightness":[{"backdrop-brightness":[K,et,U]}],"backdrop-contrast":[{"backdrop-contrast":[K,et,U]}],"backdrop-grayscale":[{"backdrop-grayscale":["",K,et,U]}],"backdrop-hue-rotate":[{"backdrop-hue-rotate":[K,et,U]}],"backdrop-invert":[{"backdrop-invert":["",K,et,U]}],"backdrop-opacity":[{"backdrop-opacity":[K,et,U]}],"backdrop-saturate":[{"backdrop-saturate":[K,et,U]}],"backdrop-sepia":[{"backdrop-sepia":["",K,et,U]}],"border-collapse":[{border:["collapse","separate"]}],"border-spacing":[{"border-spacing":j()}],"border-spacing-x":[{"border-spacing-x":j()}],"border-spacing-y":[{"border-spacing-y":j()}],"table-layout":[{table:["auto","fixed"]}],caption:[{caption:["top","bottom"]}],transition:[{transition:["","all","colors","opacity","shadow","transform","none",et,U]}],"transition-behavior":[{transition:["normal","discrete"]}],duration:[{duration:[K,"initial",et,U]}],ease:[{ease:["linear","initial",h,et,U]}],delay:[{delay:[K,et,U]}],animate:[{animate:["none",k,et,U]}],backface:[{backface:["hidden","visible"]}],perspective:[{perspective:[f,et,U]}],"perspective-origin":[{"perspective-origin":w()}],rotate:[{rotate:eu()}],"rotate-x":[{"rotate-x":eu()}],"rotate-y":[{"rotate-y":eu()}],"rotate-z":[{"rotate-z":eu()}],scale:[{scale:eb()}],"scale-x":[{"scale-x":eb()}],"scale-y":[{"scale-y":eb()}],"scale-z":[{"scale-z":eb()}],"scale-3d":["scale-3d"],skew:[{skew:ef()}],"skew-x":[{"skew-x":ef()}],"skew-y":[{"skew-y":ef()}],transform:[{transform:[et,U,"","none","gpu","cpu"]}],"transform-origin":[{origin:w()}],"transform-style":[{transform:["3d","flat"]}],translate:[{translate:eg()}],"translate-x":[{"translate-x":eg()}],"translate-y":[{"translate-y":eg()}],"translate-z":[{"translate-z":eg()}],"translate-none":["translate-none"],accent:[{accent:T()}],appearance:[{appearance:["none","auto"]}],"caret-color":[{caret:T()}],"color-scheme":[{scheme:["normal","dark","light","light-dark","only-dark","only-light"]}],cursor:[{cursor:["auto","default","pointer","wait","text","move","help","not-allowed","none","context-menu","progress","cell","crosshair","vertical-text","alias","copy","no-drop","grab","grabbing","all-scroll","col-resize","row-resize","n-resize","e-resize","s-resize","w-resize","ne-resize","nw-resize","se-resize","sw-resize","ew-resize","ns-resize","nesw-resize","nwse-resize","zoom-in","zoom-out",et,U]}],"field-sizing":[{"field-sizing":["fixed","content"]}],"pointer-events":[{"pointer-events":["auto","none"]}],resize:[{resize:["none","","y","x"]}],"scroll-behavior":[{scroll:["auto","smooth"]}],"scroll-m":[{"scroll-m":j()}],"scroll-mx":[{"scroll-mx":j()}],"scroll-my":[{"scroll-my":j()}],"scroll-ms":[{"scroll-ms":j()}],"scroll-me":[{"scroll-me":j()}],"scroll-mt":[{"scroll-mt":j()}],"scroll-mr":[{"scroll-mr":j()}],"scroll-mb":[{"scroll-mb":j()}],"scroll-ml":[{"scroll-ml":j()}],"scroll-p":[{"scroll-p":j()}],"scroll-px":[{"scroll-px":j()}],"scroll-py":[{"scroll-py":j()}],"scroll-ps":[{"scroll-ps":j()}],"scroll-pe":[{"scroll-pe":j()}],"scroll-pt":[{"scroll-pt":j()}],"scroll-pr":[{"scroll-pr":j()}],"scroll-pb":[{"scroll-pb":j()}],"scroll-pl":[{"scroll-pl":j()}],"snap-align":[{snap:["start","end","center","align-none"]}],"snap-stop":[{snap:["normal","always"]}],"snap-type":[{snap:["none","x","y","both"]}],"snap-strictness":[{snap:["mandatory","proximity"]}],touch:[{touch:["auto","none","manipulation"]}],"touch-x":[{"touch-pan":["x","left","right"]}],"touch-y":[{"touch-pan":["y","up","down"]}],"touch-pz":["touch-pinch-zoom"],select:[{select:["none","text","all","auto"]}],"will-change":[{"will-change":["auto","scroll","contents","transform",et,U]}],fill:[{fill:["none",...T()]}],"stroke-w":[{stroke:[K,el,X,Y]}],stroke:[{stroke:["none",...T()]}],"forced-color-adjust":[{"forced-color-adjust":["auto","none"]}]},conflictingClassGroups:{overflow:["overflow-x","overflow-y"],overscroll:["overscroll-x","overscroll-y"],inset:["inset-x","inset-y","start","end","top","right","bottom","left"],"inset-x":["right","left"],"inset-y":["top","bottom"],flex:["basis","grow","shrink"],gap:["gap-x","gap-y"],p:["px","py","ps","pe","pt","pr","pb","pl"],px:["pr","pl"],py:["pt","pb"],m:["mx","my","ms","me","mt","mr","mb","ml"],mx:["mr","ml"],my:["mt","mb"],size:["w","h"],"font-size":["leading"],"fvn-normal":["fvn-ordinal","fvn-slashed-zero","fvn-figure","fvn-spacing","fvn-fraction"],"fvn-ordinal":["fvn-normal"],"fvn-slashed-zero":["fvn-normal"],"fvn-figure":["fvn-normal"],"fvn-spacing":["fvn-normal"],"fvn-fraction":["fvn-normal"],"line-clamp":["display","overflow"],rounded:["rounded-s","rounded-e","rounded-t","rounded-r","rounded-b","rounded-l","rounded-ss","rounded-se","rounded-ee","rounded-es","rounded-tl","rounded-tr","rounded-br","rounded-bl"],"rounded-s":["rounded-ss","rounded-es"],"rounded-e":["rounded-se","rounded-ee"],"rounded-t":["rounded-tl","rounded-tr"],"rounded-r":["rounded-tr","rounded-br"],"rounded-b":["rounded-br","rounded-bl"],"rounded-l":["rounded-tl","rounded-bl"],"border-spacing":["border-spacing-x","border-spacing-y"],"border-w":["border-w-x","border-w-y","border-w-s","border-w-e","border-w-t","border-w-r","border-w-b","border-w-l"],"border-w-x":["border-w-r","border-w-l"],"border-w-y":["border-w-t","border-w-b"],"border-color":["border-color-x","border-color-y","border-color-s","border-color-e","border-color-t","border-color-r","border-color-b","border-color-l"],"border-color-x":["border-color-r","border-color-l"],"border-color-y":["border-color-t","border-color-b"],translate:["translate-x","translate-y","translate-none"],"translate-none":["translate","translate-x","translate-y","translate-z"],"scroll-m":["scroll-mx","scroll-my","scroll-ms","scroll-me","scroll-mt","scroll-mr","scroll-mb","scroll-ml"],"scroll-mx":["scroll-mr","scroll-ml"],"scroll-my":["scroll-mt","scroll-mb"],"scroll-p":["scroll-px","scroll-py","scroll-ps","scroll-pe","scroll-pt","scroll-pr","scroll-pb","scroll-pl"],"scroll-px":["scroll-pr","scroll-pl"],"scroll-py":["scroll-pt","scroll-pb"],touch:["touch-x","touch-y","touch-pz"],"touch-x":["touch"],"touch-y":["touch"],"touch-pz":["touch"]},conflictingClassGroupModifiers:{"font-size":["leading"]},orderSensitiveModifiers:["*","**","after","backdrop","before","details-content","file","first-letter","first-line","marker","placeholder","selection"]}})}}]); \ No newline at end of file diff --git a/litellm/proxy/_experimental/out/_next/static/chunks/3325-4a3c766c7d12465e.js b/litellm/proxy/_experimental/out/_next/static/chunks/3325-4a3c766c7d12465e.js deleted file mode 100644 index 3b9478ef07f..00000000000 --- a/litellm/proxy/_experimental/out/_next/static/chunks/3325-4a3c766c7d12465e.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[3325],{41649:function(e,r,t){t.d(r,{Z:function(){return f}});var n=t(5853),o=t(2265),a=t(47187),l=t(7084),i=t(26898),d=t(13241),s=t(1153);let c={xs:{paddingX:"px-2",paddingY:"py-0.5",fontSize:"text-xs"},sm:{paddingX:"px-2.5",paddingY:"py-0.5",fontSize:"text-sm"},md:{paddingX:"px-3",paddingY:"py-0.5",fontSize:"text-md"},lg:{paddingX:"px-3.5",paddingY:"py-0.5",fontSize:"text-lg"},xl:{paddingX:"px-4",paddingY:"py-1",fontSize:"text-xl"}},u={xs:{height:"h-4",width:"w-4"},sm:{height:"h-4",width:"w-4"},md:{height:"h-4",width:"w-4"},lg:{height:"h-5",width:"w-5"},xl:{height:"h-6",width:"w-6"}},m=(0,s.fn)("Badge"),f=o.forwardRef((e,r)=>{let{color:t,icon:f,size:p=l.u8.SM,tooltip:g,className:b,children:h}=e,k=(0,n._T)(e,["color","icon","size","tooltip","className","children"]),v=f||null,{tooltipProps:x,getReferenceProps:w}=(0,a.l)();return o.createElement("span",Object.assign({ref:(0,s.lq)([r,x.refs.setReference]),className:(0,d.q)(m("root"),"w-max shrink-0 inline-flex justify-center items-center cursor-default rounded-tremor-small ring-1 ring-inset",t?(0,d.q)((0,s.bM)(t,i.K.background).bgColor,(0,s.bM)(t,i.K.iconText).textColor,(0,s.bM)(t,i.K.iconRing).ringColor,"bg-opacity-10 ring-opacity-20","dark:bg-opacity-5 dark:ring-opacity-60"):(0,d.q)("bg-tremor-brand-faint text-tremor-brand-emphasis ring-tremor-brand/20","dark:bg-dark-tremor-brand-muted/50 dark:text-dark-tremor-brand dark:ring-dark-tremor-subtle/20"),c[p].paddingX,c[p].paddingY,c[p].fontSize,b)},w,k),o.createElement(a.Z,Object.assign({text:g},x)),v?o.createElement(v,{className:(0,d.q)(m("icon"),"shrink-0 -ml-1 mr-1.5",u[p].height,u[p].width)}):null,o.createElement("span",{className:(0,d.q)(m("text"),"whitespace-nowrap")},h))});f.displayName="Badge"},47323:function(e,r,t){t.d(r,{Z:function(){return g}});var n=t(5853),o=t(2265),a=t(47187),l=t(7084),i=t(13241),d=t(1153),s=t(26898);let c={xs:{paddingX:"px-1.5",paddingY:"py-1.5"},sm:{paddingX:"px-1.5",paddingY:"py-1.5"},md:{paddingX:"px-2",paddingY:"py-2"},lg:{paddingX:"px-2",paddingY:"py-2"},xl:{paddingX:"px-2.5",paddingY:"py-2.5"}},u={xs:{height:"h-3",width:"w-3"},sm:{height:"h-5",width:"w-5"},md:{height:"h-5",width:"w-5"},lg:{height:"h-7",width:"w-7"},xl:{height:"h-9",width:"w-9"}},m={simple:{rounded:"",border:"",ring:"",shadow:""},light:{rounded:"rounded-tremor-default",border:"",ring:"",shadow:""},shadow:{rounded:"rounded-tremor-default",border:"border",ring:"",shadow:"shadow-tremor-card dark:shadow-dark-tremor-card"},solid:{rounded:"rounded-tremor-default",border:"border-2",ring:"ring-1",shadow:""},outlined:{rounded:"rounded-tremor-default",border:"border",ring:"ring-2",shadow:""}},f=(e,r)=>{switch(e){case"simple":return{textColor:r?(0,d.bM)(r,s.K.text).textColor:"text-tremor-brand dark:text-dark-tremor-brand",bgColor:"",borderColor:"",ringColor:""};case"light":return{textColor:r?(0,d.bM)(r,s.K.text).textColor:"text-tremor-brand dark:text-dark-tremor-brand",bgColor:r?(0,i.q)((0,d.bM)(r,s.K.background).bgColor,"bg-opacity-20"):"bg-tremor-brand-muted dark:bg-dark-tremor-brand-muted",borderColor:"",ringColor:""};case"shadow":return{textColor:r?(0,d.bM)(r,s.K.text).textColor:"text-tremor-brand dark:text-dark-tremor-brand",bgColor:r?(0,i.q)((0,d.bM)(r,s.K.background).bgColor,"bg-opacity-20"):"bg-tremor-background dark:bg-dark-tremor-background",borderColor:"border-tremor-border dark:border-dark-tremor-border",ringColor:""};case"solid":return{textColor:r?(0,d.bM)(r,s.K.text).textColor:"text-tremor-brand-inverted dark:text-dark-tremor-brand-inverted",bgColor:r?(0,i.q)((0,d.bM)(r,s.K.background).bgColor,"bg-opacity-20"):"bg-tremor-brand dark:bg-dark-tremor-brand",borderColor:"border-tremor-brand-inverted dark:border-dark-tremor-brand-inverted",ringColor:"ring-tremor-ring dark:ring-dark-tremor-ring"};case"outlined":return{textColor:r?(0,d.bM)(r,s.K.text).textColor:"text-tremor-brand dark:text-dark-tremor-brand",bgColor:r?(0,i.q)((0,d.bM)(r,s.K.background).bgColor,"bg-opacity-20"):"bg-tremor-background dark:bg-dark-tremor-background",borderColor:r?(0,d.bM)(r,s.K.ring).borderColor:"border-tremor-brand-subtle dark:border-dark-tremor-brand-subtle",ringColor:r?(0,i.q)((0,d.bM)(r,s.K.ring).ringColor,"ring-opacity-40"):"ring-tremor-brand-muted dark:ring-dark-tremor-brand-muted"}}},p=(0,d.fn)("Icon"),g=o.forwardRef((e,r)=>{let{icon:t,variant:s="simple",tooltip:g,size:b=l.u8.SM,color:h,className:k}=e,v=(0,n._T)(e,["icon","variant","tooltip","size","color","className"]),x=f(s,h),{tooltipProps:w,getReferenceProps:C}=(0,a.l)();return o.createElement("span",Object.assign({ref:(0,d.lq)([r,w.refs.setReference]),className:(0,i.q)(p("root"),"inline-flex shrink-0 items-center justify-center",x.bgColor,x.textColor,x.borderColor,x.ringColor,m[s].rounded,m[s].border,m[s].shadow,m[s].ring,c[b].paddingX,c[b].paddingY,k)},C,v),o.createElement(a.Z,Object.assign({text:g},w)),o.createElement(t,{className:(0,i.q)(p("icon"),"shrink-0",u[b].height,u[b].width)}))});g.displayName="Icon"},59341:function(e,r,t){t.d(r,{Z:function(){return R}});var n=t(5853),o=t(71049),a=t(11323),l=t(2265),i=t(66797),d=t(40099),s=t(74275),c=t(59456),u=t(93980),m=t(65573),f=t(67561),p=t(87550),g=t(628),b=t(80281),h=t(31370),k=t(20131),v=t(38929),x=t(52307),w=t(52724),C=t(7935);let y=(0,l.createContext)(null);y.displayName="GroupContext";let E=l.Fragment,N=Object.assign((0,v.yV)(function(e,r){var t;let n=(0,l.useId)(),E=(0,b.Q)(),N=(0,p.B)(),{id:T=E||"headlessui-switch-".concat(n),disabled:M=N||!1,checked:S,defaultChecked:q,onChange:L,name:j,value:R,form:O,autoFocus:P=!1,...F}=e,z=(0,l.useContext)(y),[I,_]=(0,l.useState)(null),K=(0,l.useRef)(null),B=(0,f.T)(K,r,null===z?null:z.setSwitch,_),H=(0,s.L)(q),[Z,D]=(0,d.q)(S,L,null!=H&&H),Y=(0,c.G)(),[X,A]=(0,l.useState)(!1),G=(0,u.z)(()=>{A(!0),null==D||D(!Z),Y.nextFrame(()=>{A(!1)})}),U=(0,u.z)(e=>{if((0,h.P)(e.currentTarget))return e.preventDefault();e.preventDefault(),G()}),V=(0,u.z)(e=>{e.key===w.R.Space?(e.preventDefault(),G()):e.key===w.R.Enter&&(0,k.g)(e.currentTarget)}),$=(0,u.z)(e=>e.preventDefault()),Q=(0,C.wp)(),W=(0,x.zH)(),{isFocusVisible:J,focusProps:ee}=(0,o.F)({autoFocus:P}),{isHovered:er,hoverProps:et}=(0,a.X)({isDisabled:M}),{pressed:en,pressProps:eo}=(0,i.x)({disabled:M}),ea=(0,l.useMemo)(()=>({checked:Z,disabled:M,hover:er,focus:J,active:en,autofocus:P,changing:X}),[Z,er,J,en,M,X,P]),el=(0,v.dG)({id:T,ref:B,role:"switch",type:(0,m.f)(e,I),tabIndex:-1===e.tabIndex?0:null!=(t=e.tabIndex)?t:0,"aria-checked":Z,"aria-labelledby":Q,"aria-describedby":W,disabled:M||void 0,autoFocus:P,onClick:U,onKeyUp:V,onKeyPress:$},ee,et,eo),ei=(0,l.useCallback)(()=>{if(void 0!==H)return null==D?void 0:D(H)},[D,H]),ed=(0,v.L6)();return l.createElement(l.Fragment,null,null!=j&&l.createElement(g.Mt,{disabled:M,data:{[j]:R||"on"},overrides:{type:"checkbox",checked:Z},form:O,onReset:ei}),ed({ourProps:el,theirProps:F,slot:ea,defaultTag:"button",name:"Switch"}))}),{Group:function(e){var r;let[t,n]=(0,l.useState)(null),[o,a]=(0,C.bE)(),[i,d]=(0,x.fw)(),s=(0,l.useMemo)(()=>({switch:t,setSwitch:n}),[t,n]),c=(0,v.L6)();return l.createElement(d,{name:"Switch.Description",value:i},l.createElement(a,{name:"Switch.Label",value:o,props:{htmlFor:null==(r=s.switch)?void 0:r.id,onClick(e){t&&(e.currentTarget instanceof HTMLLabelElement&&e.preventDefault(),t.click(),t.focus({preventScroll:!0}))}}},l.createElement(y.Provider,{value:s},c({ourProps:{},theirProps:e,slot:{},defaultTag:E,name:"Switch.Group"}))))},Label:C.__,Description:x.dk});var T=t(44140),M=t(26898),S=t(13241),q=t(1153),L=t(47187);let j=(0,q.fn)("Switch"),R=l.forwardRef((e,r)=>{let{checked:t,defaultChecked:o=!1,onChange:a,color:i,name:d,error:s,errorMessage:c,disabled:u,required:m,tooltip:f,id:p}=e,g=(0,n._T)(e,["checked","defaultChecked","onChange","color","name","error","errorMessage","disabled","required","tooltip","id"]),b={bgColor:i?(0,q.bM)(i,M.K.background).bgColor:"bg-tremor-brand dark:bg-dark-tremor-brand",ringColor:i?(0,q.bM)(i,M.K.ring).ringColor:"ring-tremor-brand-muted dark:ring-dark-tremor-brand-muted"},[h,k]=(0,T.Z)(o,t),[v,x]=(0,l.useState)(!1),{tooltipProps:w,getReferenceProps:C}=(0,L.l)(300);return l.createElement("div",{className:"flex flex-row items-center justify-start"},l.createElement(L.Z,Object.assign({text:f},w)),l.createElement("div",Object.assign({ref:(0,q.lq)([r,w.refs.setReference]),className:(0,S.q)(j("root"),"flex flex-row relative h-5")},g,C),l.createElement("input",{type:"checkbox",className:(0,S.q)(j("input"),"absolute w-5 h-5 cursor-pointer left-0 top-0 opacity-0"),name:d,required:m,checked:h,onChange:e=>{e.preventDefault()}}),l.createElement(N,{checked:h,onChange:e=>{k(e),null==a||a(e)},disabled:u,className:(0,S.q)(j("switch"),"w-10 h-5 group relative inline-flex shrink-0 cursor-pointer items-center justify-center rounded-tremor-full","focus:outline-none",u?"cursor-not-allowed":""),onFocus:()=>x(!0),onBlur:()=>x(!1),id:p},l.createElement("span",{className:(0,S.q)(j("sr-only"),"sr-only")},"Switch ",h?"on":"off"),l.createElement("span",{"aria-hidden":"true",className:(0,S.q)(j("background"),h?b.bgColor:"bg-tremor-border dark:bg-dark-tremor-border","pointer-events-none absolute mx-auto h-3 w-9 rounded-tremor-full transition-colors duration-100 ease-in-out")}),l.createElement("span",{"aria-hidden":"true",className:(0,S.q)(j("round"),h?(0,S.q)(b.bgColor,"translate-x-5 border-tremor-background dark:border-dark-tremor-background"):"translate-x-0 bg-tremor-border dark:bg-dark-tremor-border border-tremor-background dark:border-dark-tremor-background","pointer-events-none absolute left-0 inline-block h-5 w-5 transform rounded-tremor-full border-2 shadow-tremor-input duration-100 ease-in-out transition",v?(0,S.q)("ring-2",b.ringColor):"")}))),s&&c?l.createElement("p",{className:(0,S.q)(j("errorMessage"),"text-sm text-red-500 mt-1 ")},c):null)});R.displayName="Switch"},21626:function(e,r,t){t.d(r,{Z:function(){return i}});var n=t(5853),o=t(2265),a=t(13241);let l=(0,t(1153).fn)("Table"),i=o.forwardRef((e,r)=>{let{children:t,className:i}=e,d=(0,n._T)(e,["children","className"]);return o.createElement("div",{className:(0,a.q)(l("root"),"overflow-auto",i)},o.createElement("table",Object.assign({ref:r,className:(0,a.q)(l("table"),"w-full text-tremor-default","text-tremor-content","dark:text-dark-tremor-content")},d),t))});i.displayName="Table"},97214:function(e,r,t){t.d(r,{Z:function(){return i}});var n=t(5853),o=t(2265),a=t(13241);let l=(0,t(1153).fn)("TableBody"),i=o.forwardRef((e,r)=>{let{children:t,className:i}=e,d=(0,n._T)(e,["children","className"]);return o.createElement(o.Fragment,null,o.createElement("tbody",Object.assign({ref:r,className:(0,a.q)(l("root"),"align-top divide-y","divide-tremor-border","dark:divide-dark-tremor-border",i)},d),t))});i.displayName="TableBody"},28241:function(e,r,t){t.d(r,{Z:function(){return i}});var n=t(5853),o=t(2265),a=t(13241);let l=(0,t(1153).fn)("TableCell"),i=o.forwardRef((e,r)=>{let{children:t,className:i}=e,d=(0,n._T)(e,["children","className"]);return o.createElement(o.Fragment,null,o.createElement("td",Object.assign({ref:r,className:(0,a.q)(l("root"),"align-middle whitespace-nowrap text-left p-4",i)},d),t))});i.displayName="TableCell"},58834:function(e,r,t){t.d(r,{Z:function(){return i}});var n=t(5853),o=t(2265),a=t(13241);let l=(0,t(1153).fn)("TableHead"),i=o.forwardRef((e,r)=>{let{children:t,className:i}=e,d=(0,n._T)(e,["children","className"]);return o.createElement(o.Fragment,null,o.createElement("thead",Object.assign({ref:r,className:(0,a.q)(l("root"),"text-left","text-tremor-content","dark:text-dark-tremor-content",i)},d),t))});i.displayName="TableHead"},69552:function(e,r,t){t.d(r,{Z:function(){return i}});var n=t(5853),o=t(2265),a=t(13241);let l=(0,t(1153).fn)("TableHeaderCell"),i=o.forwardRef((e,r)=>{let{children:t,className:i}=e,d=(0,n._T)(e,["children","className"]);return o.createElement(o.Fragment,null,o.createElement("th",Object.assign({ref:r,className:(0,a.q)(l("root"),"whitespace-nowrap text-left font-semibold top-0 px-4 py-3.5","text-tremor-content-strong","dark:text-dark-tremor-content-strong",i)},d),t))});i.displayName="TableHeaderCell"},71876:function(e,r,t){t.d(r,{Z:function(){return i}});var n=t(5853),o=t(2265),a=t(13241);let l=(0,t(1153).fn)("TableRow"),i=o.forwardRef((e,r)=>{let{children:t,className:i}=e,d=(0,n._T)(e,["children","className"]);return o.createElement(o.Fragment,null,o.createElement("tr",Object.assign({ref:r,className:(0,a.q)(l("row"),i)},d),t))});i.displayName="TableRow"},44140:function(e,r,t){t.d(r,{Z:function(){return o}});var n=t(2265);let o=(e,r)=>{let t=void 0!==r,[o,a]=(0,n.useState)(e);return[t?r:o,e=>{t||a(e)}]}},44643:function(e,r,t){var n=t(2265);let o=n.forwardRef(function(e,r){return n.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:2,stroke:"currentColor","aria-hidden":"true",ref:r},e),n.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"}))});r.Z=o},91126:function(e,r,t){var n=t(2265);let o=n.forwardRef(function(e,r){return n.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:2,stroke:"currentColor","aria-hidden":"true",ref:r},e),n.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"}),n.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M21 12a9 9 0 11-18 0 9 9 0 0118 0z"}))});r.Z=o},74998:function(e,r,t){var n=t(2265);let o=n.forwardRef(function(e,r){return n.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:2,stroke:"currentColor","aria-hidden":"true",ref:r},e),n.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"}))});r.Z=o},52307:function(e,r,t){t.d(r,{dk:function(){return m},fw:function(){return u},zH:function(){return c}});var n=t(2265),o=t(93980),a=t(73389),l=t(67561),i=t(87550),d=t(38929);let s=(0,n.createContext)(null);function c(){var e,r;return null!=(r=null==(e=(0,n.useContext)(s))?void 0:e.value)?r:void 0}function u(){let[e,r]=(0,n.useState)([]);return[e.length>0?e.join(" "):void 0,(0,n.useMemo)(()=>function(e){let t=(0,o.z)(e=>(r(r=>[...r,e]),()=>r(r=>{let t=r.slice(),n=t.indexOf(e);return -1!==n&&t.splice(n,1),t}))),a=(0,n.useMemo)(()=>({register:t,slot:e.slot,name:e.name,props:e.props,value:e.value}),[t,e.slot,e.name,e.props,e.value]);return n.createElement(s.Provider,{value:a},e.children)},[r])]}s.displayName="DescriptionContext";let m=Object.assign((0,d.yV)(function(e,r){let t=(0,n.useId)(),o=(0,i.B)(),{id:c="headlessui-description-".concat(t),...u}=e,m=function e(){let r=(0,n.useContext)(s);if(null===r){let r=Error("You used a component, but it is not inside a relevant parent.");throw Error.captureStackTrace&&Error.captureStackTrace(r,e),r}return r}(),f=(0,l.T)(r);(0,a.e)(()=>m.register(c),[c,m.register]);let p=o||!1,g=(0,n.useMemo)(()=>({...m.slot,disabled:p}),[m.slot,p]),b={ref:f,...m.props,id:c};return(0,d.L6)()({ourProps:b,theirProps:u,slot:g,defaultTag:"p",name:m.name||"Description"})}),{})},7935:function(e,r,t){t.d(r,{__:function(){return f},bE:function(){return m},wp:function(){return u}});var n=t(2265),o=t(93980),a=t(73389),l=t(67561),i=t(87550),d=t(80281),s=t(38929);let c=(0,n.createContext)(null);function u(e){var r,t,o;let a=null!=(t=null==(r=(0,n.useContext)(c))?void 0:r.value)?t:void 0;return(null!=(o=null==e?void 0:e.length)?o:0)>0?[a,...e].filter(Boolean).join(" "):a}function m(){let{inherit:e=!1}=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},r=u(),[t,a]=(0,n.useState)([]),l=e?[r,...t].filter(Boolean):t;return[l.length>0?l.join(" "):void 0,(0,n.useMemo)(()=>function(e){let r=(0,o.z)(e=>(a(r=>[...r,e]),()=>a(r=>{let t=r.slice(),n=t.indexOf(e);return -1!==n&&t.splice(n,1),t}))),t=(0,n.useMemo)(()=>({register:r,slot:e.slot,name:e.name,props:e.props,value:e.value}),[r,e.slot,e.name,e.props,e.value]);return n.createElement(c.Provider,{value:t},e.children)},[a])]}c.displayName="LabelContext";let f=Object.assign((0,s.yV)(function(e,r){var t;let u=(0,n.useId)(),m=function e(){let r=(0,n.useContext)(c);if(null===r){let r=Error("You used a