From 4b80813fc20dbab597a312c8f6e5e3b29a1ecaaa Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Wed, 19 Nov 2025 13:37:51 -0800 Subject: [PATCH 001/107] 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 9edc50efbd117b38e54c038cbc1af1dd32572206 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Mon, 1 Dec 2025 10:21:44 +0530 Subject: [PATCH 002/107] 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 8f6822a64264474d2643c12a2f5b1d8857d169d9 Mon Sep 17 00:00:00 2001 From: Chesars Date: Mon, 1 Dec 2025 12:55:01 -0300 Subject: [PATCH 003/107] 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 625b2fd54930a46d4647b906157ea94566fe5d0e Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Mon, 1 Dec 2025 10:18:38 -0800 Subject: [PATCH 004/107] 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 69a6c25a5ff0c04799b597c602e2853f011d7819 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Mon, 1 Dec 2025 12:28:53 -0800 Subject: [PATCH 005/107] 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 21baa354cc17b6f706f3bf93c9c8ba024a72bdb0 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Mon, 1 Dec 2025 13:13:39 -0800 Subject: [PATCH 006/107] 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 082c8af37f604b945893056b891dbc7a252474da Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Tue, 2 Dec 2025 11:25:32 +0530 Subject: [PATCH 007/107] Fix: litellm user auth not passing issue --- ...odel_prices_and_context_window_backup.json | 360 ++++++++++++++++-- .../mcp_server/mcp_server_manager.py | 12 +- litellm/proxy/litellm_pre_call_utils.py | 2 + litellm/responses/main.py | 9 +- .../mcp_server/test_mcp_server_manager.py | 102 ++++- 5 files changed, 452 insertions(+), 33 deletions(-) diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 9fdc1704f41..f28e9b1290f 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.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, @@ -7824,26 +7851,298 @@ "source": "https://www.databricks.com/product/pricing/foundation-model-serving" }, "databricks/databricks-claude-3-7-sonnet": { - "input_cost_per_token": 2.5e-06, - "input_dbu_cost_per_token": 3.571e-05, + "input_cost_per_token": 2.9999900000000002e-06, + "input_dbu_cost_per_token": 4.2857e-05, "litellm_provider": "databricks", "max_input_tokens": 200000, "max_output_tokens": 128000, "max_tokens": 200000, "metadata": { - "notes": "Input/output cost per token is dbu cost * $0.070, based on databricks Claude 3.7 conversion. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." }, "mode": "chat", - "output_cost_per_token": 1.7857e-05, - "output_db_cost_per_token": 0.000214286, - "source": "https://www.databricks.com/product/pricing/foundation-model-serving", + "output_cost_per_token": 1.5000020000000002e-05, + "output_dbu_cost_per_token": 0.000214286, + "source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving", "supports_assistant_prefill": true, "supports_function_calling": true, "supports_reasoning": true, "supports_tool_choice": true }, + "databricks/databricks-claude-haiku-4-5": { + "input_cost_per_token": 1.00002e-06, + "input_dbu_cost_per_token": 1.4286e-05, + "litellm_provider": "databricks", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 200000, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 5.00003e-06, + "output_dbu_cost_per_token": 7.1429e-05, + "source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving", + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "databricks/databricks-claude-opus-4": { + "input_cost_per_token": 1.5000020000000002e-05, + "input_dbu_cost_per_token": 0.000214286, + "litellm_provider": "databricks", + "max_input_tokens": 200000, + "max_output_tokens": 32000, + "max_tokens": 200000, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 7.500003000000001e-05, + "output_dbu_cost_per_token": 0.001071429, + "source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving", + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "databricks/databricks-claude-opus-4-1": { + "input_cost_per_token": 1.5000020000000002e-05, + "input_dbu_cost_per_token": 0.000214286, + "litellm_provider": "databricks", + "max_input_tokens": 200000, + "max_output_tokens": 32000, + "max_tokens": 200000, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 7.500003000000001e-05, + "output_dbu_cost_per_token": 0.001071429, + "source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving", + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "databricks/databricks-claude-opus-4-5": { + "input_cost_per_token": 5.00003e-06, + "input_dbu_cost_per_token": 7.1429e-05, + "litellm_provider": "databricks", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 200000, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 2.5000010000000002e-05, + "output_dbu_cost_per_token": 0.000357143, + "source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving", + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "databricks/databricks-claude-sonnet-4": { + "input_cost_per_token": 2.9999900000000002e-06, + "input_dbu_cost_per_token": 4.2857e-05, + "litellm_provider": "databricks", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 200000, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 1.5000020000000002e-05, + "output_dbu_cost_per_token": 0.000214286, + "source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving", + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "databricks/databricks-claude-sonnet-4-1": { + "input_cost_per_token": 2.9999900000000002e-06, + "input_dbu_cost_per_token": 4.2857e-05, + "litellm_provider": "databricks", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 200000, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 1.5000020000000002e-05, + "output_dbu_cost_per_token": 0.000214286, + "source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving", + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "databricks/databricks-claude-sonnet-4-5": { + "input_cost_per_token": 2.9999900000000002e-06, + "input_dbu_cost_per_token": 4.2857e-05, + "litellm_provider": "databricks", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 200000, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 1.5000020000000002e-05, + "output_dbu_cost_per_token": 0.000214286, + "source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving", + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "databricks/databricks-gemini-2-5-flash": { + "input_cost_per_token": 3.0001999999999996e-07, + "input_dbu_cost_per_token": 4.285999999999999e-06, + "litellm_provider": "databricks", + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_tokens": 1048576, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 2.49998e-06, + "output_dbu_cost_per_token": 3.5714e-05, + "source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving", + "supports_function_calling": true, + "supports_tool_choice": true + }, + "databricks/databricks-gemini-2-5-pro": { + "input_cost_per_token": 1.24999e-06, + "input_dbu_cost_per_token": 1.7857e-05, + "litellm_provider": "databricks", + "max_input_tokens": 1048576, + "max_output_tokens": 65536, + "max_tokens": 1048576, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 9.999990000000002e-06, + "output_dbu_cost_per_token": 0.000142857, + "source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving", + "supports_function_calling": true, + "supports_tool_choice": true + }, + "databricks/databricks-gemma-3-12b": { + "input_cost_per_token": 1.5000999999999998e-07, + "input_dbu_cost_per_token": 2.1429999999999996e-06, + "litellm_provider": "databricks", + "max_input_tokens": 128000, + "max_output_tokens": 32000, + "max_tokens": 128000, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 5.0001e-07, + "output_dbu_cost_per_token": 7.143e-06, + "source": "https://www.databricks.com/product/pricing/foundation-model-serving" + }, + "databricks/databricks-gpt-5": { + "input_cost_per_token": 1.24999e-06, + "input_dbu_cost_per_token": 1.7857e-05, + "litellm_provider": "databricks", + "max_input_tokens": 400000, + "max_output_tokens": 128000, + "max_tokens": 400000, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 9.999990000000002e-06, + "output_dbu_cost_per_token": 0.000142857, + "source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving" + }, + "databricks/databricks-gpt-5-1": { + "input_cost_per_token": 1.24999e-06, + "input_dbu_cost_per_token": 1.7857e-05, + "litellm_provider": "databricks", + "max_input_tokens": 400000, + "max_output_tokens": 128000, + "max_tokens": 400000, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 9.999990000000002e-06, + "output_dbu_cost_per_token": 0.000142857, + "source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving" + }, + "databricks/databricks-gpt-5-mini": { + "input_cost_per_token": 2.4997000000000006e-07, + "input_dbu_cost_per_token": 3.571e-06, + "litellm_provider": "databricks", + "max_input_tokens": 400000, + "max_output_tokens": 128000, + "max_tokens": 400000, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 1.9999700000000004e-06, + "output_dbu_cost_per_token": 2.8571e-05, + "source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving" + }, + "databricks/databricks-gpt-5-nano": { + "input_cost_per_token": 4.998e-08, + "input_dbu_cost_per_token": 7.14e-07, + "litellm_provider": "databricks", + "max_input_tokens": 400000, + "max_output_tokens": 128000, + "max_tokens": 400000, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 3.9998000000000007e-07, + "output_dbu_cost_per_token": 5.714000000000001e-06, + "source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving" + }, + "databricks/databricks-gpt-oss-120b": { + "input_cost_per_token": 1.5000999999999998e-07, + "input_dbu_cost_per_token": 2.1429999999999996e-06, + "litellm_provider": "databricks", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 5.9997e-07, + "output_dbu_cost_per_token": 8.571e-06, + "source": "https://www.databricks.com/product/pricing/foundation-model-serving" + }, + "databricks/databricks-gpt-oss-20b": { + "input_cost_per_token": 7e-08, + "input_dbu_cost_per_token": 1e-06, + "litellm_provider": "databricks", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 3.0001999999999996e-07, + "output_dbu_cost_per_token": 4.285999999999999e-06, + "source": "https://www.databricks.com/product/pricing/foundation-model-serving" + }, "databricks/databricks-gte-large-en": { - "input_cost_per_token": 1.2999e-07, + "input_cost_per_token": 1.2999000000000001e-07, "input_dbu_cost_per_token": 1.857e-06, "litellm_provider": "databricks", "max_input_tokens": 8192, @@ -7868,14 +8167,14 @@ "notes": "Input/output cost per token is dbu cost * $0.070, based on databricks Llama 3.1 70B conversion. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." }, "mode": "chat", - "output_cost_per_token": 1.5e-06, + "output_cost_per_token": 1.5000300000000002e-06, "output_dbu_cost_per_token": 2.1429e-05, "source": "https://www.databricks.com/product/pricing/foundation-model-serving", "supports_tool_choice": true }, "databricks/databricks-llama-4-maverick": { - "input_cost_per_token": 5e-06, - "input_dbu_cost_per_token": 7.143e-05, + "input_cost_per_token": 5.0001e-07, + "input_dbu_cost_per_token": 7.143e-06, "litellm_provider": "databricks", "max_input_tokens": 128000, "max_output_tokens": 128000, @@ -7884,13 +8183,13 @@ "notes": "Databricks documentation now provides both DBU costs (_dbu_cost_per_token) and dollar costs(_cost_per_token)." }, "mode": "chat", - "output_cost_per_token": 1.5e-05, - "output_dbu_cost_per_token": 0.00021429, + "output_cost_per_token": 1.5000300000000002e-06, + "output_dbu_cost_per_token": 2.1429e-05, "source": "https://www.databricks.com/product/pricing/foundation-model-serving", "supports_tool_choice": true }, "databricks/databricks-meta-llama-3-1-405b-instruct": { - "input_cost_per_token": 5e-06, + "input_cost_per_token": 5.00003e-06, "input_dbu_cost_per_token": 7.1429e-05, "litellm_provider": "databricks", "max_input_tokens": 128000, @@ -7900,14 +8199,29 @@ "notes": "Input/output cost per token is dbu cost * $0.070, based on databricks Llama 3.1 70B conversion. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." }, "mode": "chat", - "output_cost_per_token": 1.500002e-05, - "output_db_cost_per_token": 0.000214286, + "output_cost_per_token": 1.5000020000000002e-05, + "output_dbu_cost_per_token": 0.000214286, "source": "https://www.databricks.com/product/pricing/foundation-model-serving", "supports_tool_choice": true }, + "databricks/databricks-meta-llama-3-1-8b-instruct": { + "input_cost_per_token": 1.5000999999999998e-07, + "input_dbu_cost_per_token": 2.1429999999999996e-06, + "litellm_provider": "databricks", + "max_input_tokens": 200000, + "max_output_tokens": 128000, + "max_tokens": 200000, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 4.5003000000000007e-07, + "output_dbu_cost_per_token": 6.429000000000001e-06, + "source": "https://www.databricks.com/product/pricing/foundation-model-serving" + }, "databricks/databricks-meta-llama-3-3-70b-instruct": { - "input_cost_per_token": 1.00002e-06, - "input_dbu_cost_per_token": 1.4286e-05, + "input_cost_per_token": 5.0001e-07, + "input_dbu_cost_per_token": 7.143e-06, "litellm_provider": "databricks", "max_input_tokens": 128000, "max_output_tokens": 128000, @@ -7916,8 +8230,8 @@ "notes": "Input/output cost per token is dbu cost * $0.070, based on databricks Llama 3.1 70B conversion. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." }, "mode": "chat", - "output_cost_per_token": 2.99999e-06, - "output_dbu_cost_per_token": 4.2857e-05, + "output_cost_per_token": 1.5000300000000002e-06, + "output_dbu_cost_per_token": 2.1429e-05, "source": "https://www.databricks.com/product/pricing/foundation-model-serving", "supports_tool_choice": true }, @@ -7932,7 +8246,7 @@ "notes": "Input/output cost per token is dbu cost * $0.070, based on databricks Llama 3.1 70B conversion. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." }, "mode": "chat", - "output_cost_per_token": 2.99999e-06, + "output_cost_per_token": 2.9999900000000002e-06, "output_dbu_cost_per_token": 4.2857e-05, "source": "https://www.databricks.com/product/pricing/foundation-model-serving", "supports_tool_choice": true @@ -7948,13 +8262,13 @@ "notes": "Input/output cost per token is dbu cost * $0.070, based on databricks Llama 3.1 70B conversion. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." }, "mode": "chat", - "output_cost_per_token": 9.9902e-07, + "output_cost_per_token": 1.00002e-06, "output_dbu_cost_per_token": 1.4286e-05, "source": "https://www.databricks.com/product/pricing/foundation-model-serving", "supports_tool_choice": true }, "databricks/databricks-mpt-30b-instruct": { - "input_cost_per_token": 9.9902e-07, + "input_cost_per_token": 1.00002e-06, "input_dbu_cost_per_token": 1.4286e-05, "litellm_provider": "databricks", "max_input_tokens": 8192, @@ -7964,7 +8278,7 @@ "notes": "Input/output cost per token is dbu cost * $0.070, based on databricks Llama 3.1 70B conversion. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." }, "mode": "chat", - "output_cost_per_token": 9.9902e-07, + "output_cost_per_token": 1.00002e-06, "output_dbu_cost_per_token": 1.4286e-05, "source": "https://www.databricks.com/product/pricing/foundation-model-serving", "supports_tool_choice": true diff --git a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py index 2c03cbdae31..d42fd80cbd7 100644 --- a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py +++ b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py @@ -17,16 +17,15 @@ from urllib.parse import urlparse from fastapi import HTTPException from httpx import HTTPStatusError from mcp import ReadResourceResult, Resource +from mcp.types import CallToolRequestParams as MCPCallToolRequestParams from mcp.types import ( - CallToolRequestParams as MCPCallToolRequestParams, + CallToolResult, GetPromptRequestParams, GetPromptResult, Prompt, ResourceTemplate, ) -from mcp.types import CallToolResult from mcp.types import Tool as MCPTool - from pydantic import AnyUrl import litellm @@ -1949,7 +1948,12 @@ class MCPServerManager: ) = split_server_prefix_from_name(tool_name) if original_tool_name in self.tool_name_to_mcp_server_name_mapping: for server in self.get_registry().values(): - if normalize_server_name(server.name) == normalize_server_name( + if server.server_name is None: + if normalize_server_name(server.name) == normalize_server_name( + server_name_from_prefix + ): + return server + elif normalize_server_name(server.server_name) == normalize_server_name( server_name_from_prefix ): return server diff --git a/litellm/proxy/litellm_pre_call_utils.py b/litellm/proxy/litellm_pre_call_utils.py index 0a7fc62a42a..9dc255bd79a 100644 --- a/litellm/proxy/litellm_pre_call_utils.py +++ b/litellm/proxy/litellm_pre_call_utils.py @@ -611,6 +611,8 @@ class LiteLLMProxyRequestSetup: data[_metadata_variable_name]["user_api_end_user_max_budget"] = getattr( user_api_key_dict, "end_user_max_budget", None ) + # Add the full UserAPIKeyAuth object for MCP server access control + data[_metadata_variable_name]["user_api_key_auth"] = user_api_key_dict return data @staticmethod diff --git a/litellm/responses/main.py b/litellm/responses/main.py index 950ea7063f5..e837346df23 100644 --- a/litellm/responses/main.py +++ b/litellm/responses/main.py @@ -22,6 +22,9 @@ import litellm from litellm._logging import verbose_logger from litellm.constants import request_timeout from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj +from litellm.litellm_core_utils.prompt_templates.common_utils import ( + update_responses_input_with_model_file_ids, +) from litellm.llms.base_llm.responses.transformation import BaseResponsesAPIConfig from litellm.llms.custom_httpx.llm_http_handler import BaseLLMHTTPHandler from litellm.responses.litellm_completion_transformation.handler import ( @@ -38,9 +41,6 @@ from litellm.types.llms.openai import ( ToolChoice, ToolParam, ) -from litellm.litellm_core_utils.prompt_templates.common_utils import ( - update_responses_input_with_model_file_ids, -) # Handle ResponseText import with fallback if TYPE_CHECKING: @@ -168,7 +168,8 @@ async def aresponses_api_with_mcp( ) = LiteLLM_Proxy_MCP_Handler._parse_mcp_tools(tools) # Process MCP tools through the complete pipeline (fetch + filter + deduplicate + transform) - user_api_key_auth = kwargs.get("user_api_key_auth") + # Extract user_api_key_auth from litellm_metadata (where it's added by add_user_api_key_auth_to_request_metadata) + user_api_key_auth = kwargs.get("user_api_key_auth") or kwargs.get("litellm_metadata", {}).get("user_api_key_auth") # Get original MCP tools (for events) and OpenAI tools (for LLM) by reusing existing methods ( diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server_manager.py b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server_manager.py index 940dcf1a7a1..86e25c46c8e 100644 --- a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server_manager.py +++ b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server_manager.py @@ -9,6 +9,8 @@ from fastapi import HTTPException sys.path.insert(0, "../../../../../") import httpx +from mcp import ReadResourceResult, Resource +from mcp.types import GetPromptResult, Prompt, ResourceTemplate, TextResourceContents from litellm.proxy._experimental.mcp_server.mcp_server_manager import ( MCPServerManager, @@ -17,8 +19,6 @@ from litellm.proxy._experimental.mcp_server.mcp_server_manager import ( from litellm.proxy._types import LiteLLM_MCPServerTable, MCPTransport from litellm.types.mcp import MCPAuth from litellm.types.mcp_server.mcp_server_manager import MCPOAuthMetadata, MCPServer -from mcp import ReadResourceResult, Resource -from mcp.types import GetPromptResult, Prompt, ResourceTemplate, TextResourceContents class TestMCPServerManager: @@ -1606,6 +1606,104 @@ class TestMCPServerManager: # Verify the MCP client call was awaited exactly once assert mock_client.call_tool.await_count == 1 + @pytest.mark.asyncio + async def test_get_allowed_mcp_servers_with_user_api_key_auth(self): + """ + Test that get_allowed_mcp_servers properly receives and uses user_api_key_auth + when called. This verifies the fix where user_api_key_auth is passed through + litellm_metadata from responses API. + """ + from litellm.proxy._experimental.mcp_server.auth.user_api_key_auth_mcp import ( + MCPRequestHandler, + ) + from litellm.proxy._types import LiteLLM_ObjectPermissionTable, UserAPIKeyAuth + + manager = MCPServerManager() + + # Create a mock user_api_key_auth with object_permission + object_permission = LiteLLM_ObjectPermissionTable( + object_permission_id="perm_123", + mcp_servers=["test_server_1", "test_server_2"], + mcp_access_groups=[], + ) + + user_api_key_auth = UserAPIKeyAuth( + api_key="sk-test", + user_id="user-123", + object_permission=object_permission, + object_permission_id="perm_123", + ) + + # Mock MCPRequestHandler.get_allowed_mcp_servers to verify it receives user_api_key_auth + with patch.object( + MCPRequestHandler, + "get_allowed_mcp_servers", + new_callable=AsyncMock, + ) as mock_get_allowed: + # Configure mock to return servers from object_permission + mock_get_allowed.return_value = ["test_server_1", "test_server_2"] + + # Call get_allowed_mcp_servers with user_api_key_auth + result = await manager.get_allowed_mcp_servers(user_api_key_auth) + + # Verify MCPRequestHandler.get_allowed_mcp_servers was called with user_api_key_auth + mock_get_allowed.assert_called_once() + call_args = mock_get_allowed.call_args + assert call_args[0][0] is user_api_key_auth # First positional arg should be user_api_key_auth + assert call_args[0][0].user_id == "user-123" + assert call_args[0][0].object_permission_id == "perm_123" + assert call_args[0][0].object_permission is not None + assert call_args[0][0].object_permission.mcp_servers == ["test_server_1", "test_server_2"] + + # Verify result contains the expected servers + assert "test_server_1" in result + assert "test_server_2" in result + + def test_get_mcp_server_from_tool_name_uses_server_name_not_name(self): + """ + Test that _get_mcp_server_from_tool_name uses server.server_name instead of server.name + when extracting server name from prefixed tool name (second case). + This ensures the fix for using server_name instead of name works correctly. + """ + from litellm.proxy._experimental.mcp_server.utils import ( + add_server_prefix_to_name, + ) + + manager = MCPServerManager() + + # Create a server where server_name differs from name + # This tests the scenario where server.name != server.server_name + server = MCPServer( + server_id="test-server-id", + name="Test Server Name", # Different from server_name + server_name="test_server", # This is what should be used + alias="test_server", + transport=MCPTransport.http, + ) + + # Register the server + manager.registry = {server.server_id: server} + + # Create a tool with prefixed name + tool_name = "test_tool" + prefixed_tool_name = add_server_prefix_to_name(tool_name, "test_server") + + # Populate the mapping with the original tool name + manager.tool_name_to_mcp_server_name_mapping[tool_name] = "test_server" + manager.tool_name_to_mcp_server_name_mapping[prefixed_tool_name] = "test_server" + + # Test: _get_mcp_server_from_tool_name should find the server using server.server_name + # even when server.name is different + resolved_server = manager._get_mcp_server_from_tool_name(prefixed_tool_name) + + # Verify the server was found correctly + assert resolved_server is not None + assert resolved_server.server_id == server.server_id + assert resolved_server.server_name == "test_server" + # Verify it matched using server_name, not name + assert resolved_server.name == "Test Server Name" # name is different + assert resolved_server.server_name == "test_server" # server_name matches + if __name__ == "__main__": pytest.main([__file__]) From 6d296b1d257c00446ff2ef2e221f0096aa6b874a Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Tue, 2 Dec 2025 14:00:34 +0530 Subject: [PATCH 008/107] Add other routes in jwt auth --- docs/my-website/docs/proxy/pass_through.md | 14 ++++++ docs/my-website/docs/proxy/token_auth.md | 52 ++++++++++++++++++++++ litellm/proxy/_types.py | 4 +- 3 files changed, 67 insertions(+), 3 deletions(-) diff --git a/docs/my-website/docs/proxy/pass_through.md b/docs/my-website/docs/proxy/pass_through.md index 7309cdeda26..03454004b8c 100644 --- a/docs/my-website/docs/proxy/pass_through.md +++ b/docs/my-website/docs/proxy/pass_through.md @@ -275,6 +275,20 @@ In this video, we'll add the Azure OpenAI Assistants API as a pass through endpo - Check LiteLLM proxy logs for error details - Verify the target API's expected request format +### Allowing Team JWTs to use pass-through routes + +If you are using pass-through provider routes (e.g., `/anthropic/*`) and want your JWT team tokens to access these routes, add `mapped_pass_through_routes` to the `team_allowed_routes` in `litellm_jwtauth` or explicitly add the relevant route(s). + +Example (`proxy_server_config.yaml`): + +```yaml +general_settings: + enable_jwt_auth: True + litellm_jwtauth: + team_ids_jwt_field: "team_ids" + team_allowed_routes: ["openai_routes","info_routes","mapped_pass_through_routes"] +``` + ### Getting Help [Schedule Demo 👋](https://calendly.com/d/4mp-gd3-k5k/berriai-1-1-onboarding-litellm-hosted-version) diff --git a/docs/my-website/docs/proxy/token_auth.md b/docs/my-website/docs/proxy/token_auth.md index c465c1022e4..1db1b2a8965 100644 --- a/docs/my-website/docs/proxy/token_auth.md +++ b/docs/my-website/docs/proxy/token_auth.md @@ -338,6 +338,58 @@ general_settings: team_allowed_routes: ["/v1/chat/completions"] # 👈 Set accepted routes ``` +### Allowing other provider routes for Teams + +To enable team JWT tokens to access Anthropic-style endpoints such as `/v1/messages`, update `team_allowed_routes` in your `litellm_jwtauth` configuration. `team_allowed_routes` supports the following values: + +- Named route groups from `LiteLLMRoutes` (e.g., `openai_routes`, `anthropic_routes`, `info_routes`, `mapped_pass_through_routes`). + +Below is a quick reference for the route groups you can use and example representative routes from each group. If you need the exhaustive list, see the `LiteLLMRoutes` enum in `litellm/proxy/_types.py` for the authoritative list. + +| Route Group | What it contains | Representative routes | +|-------------|------------------|-----------------------| +| `openai_routes` | OpenAI-compatible REST endpoints (chat, completion, embeddings, images, responses, models, etc.) | `/v1/chat/completions`, `/v1/completions`, `/v1/embeddings`, `/v1/images/generations`, `/v1/models` | +| `anthropic_routes` | Anthropic-style endpoints (`/v1/messages` and related) | `/v1/messages`, `/v1/messages/count_tokens`, `/v1/skills` | +| `mapped_pass_through_routes` | Provider-specific pass-through route prefixes (e.g., Anthropic when proxied via `/anthropic`). Use with `mapped_pass_through_routes` for provider wildcard mapping | `/anthropic/*`, `/vertex-ai/*`, `/bedrock/*` | +| `passthrough_routes_wildcard` | Wildcard mapping for providers (e.g., `/anthropic/*`) - precomputed wildcard list used by the proxy | `/anthropic/*`, `/vllm/*` | +| `google_routes` | Google-specific (e.g., Vertex / Batching endpoints) | `/v1beta/models/{model_name}:generateContent` | +| `mcp_routes` | Internal MCP management endpoints | `/mcp/tools`, `/mcp/tools/call` | +| `info_routes` | Read-only & info endpoints used by the UI | `/key/info`, `/team/info`, `/v1/models` | +| `management_routes` | Admin-only management endpoints (create/update/delete user/team/model) | `/team/new`, `/key/generate`, `/model/new` | +| `spend_tracking_routes` | Budget/spend related endpoints | `/spend/logs`, `/spend/keys` | +| `public_routes` | Public and unauthenticated endpoints | `/`, `/routes`, `/.well-known/litellm-ui-config` | + +Note: `llm_api_routes` is the union of OpenAI, Anthropic, Google, pass-through and other LLM routes (`openai_routes + anthropic_routes + google_routes + mapped_pass_through_routes + passthrough_routes_wildcard + apply_guardrail_routes + mcp_routes + litellm_native_routes`). + +Defaults (what the proxy uses if you don't override them in `litellm_jwtauth`): + +- `admin_jwt_scope`: `litellm_proxy_admin` +- `admin_allowed_routes` (default): `management_routes`, `spend_tracking_routes`, `global_spend_tracking_routes`, `info_routes` +- `team_allowed_routes` (default): `openai_routes`, `info_routes` +- `public_allowed_routes` (default): `public_routes` + + +Example: Allow team JWTs to call Anthropic `/v1/messages` (either by route group or by explicit route string): + +```yaml +general_settings: + enable_jwt_auth: True + litellm_jwtauth: + team_ids_jwt_field: "team_ids" + team_allowed_routes: ["openai_routes", "info_routes", "anthropic_routes"] +``` + +Or selectively allow the exact Anthropic message endpoint only: + +```yaml +general_settings: + enable_jwt_auth: True + litellm_jwtauth: + team_ids_jwt_field: "team_ids" + team_allowed_routes: ["/v1/messages", "info_routes"] +``` + + ### Caching Public Keys Control how long public keys are cached for (in seconds). diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index b5b0bd80602..7e7d4049818 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -3440,9 +3440,7 @@ class LiteLLM_JWTAuth(LiteLLMPydanticObjectBase): team_id_upsert: bool = False team_ids_jwt_field: Optional[str] = None upsert_sso_user_to_team: bool = False - team_allowed_routes: List[ - Literal["openai_routes", "info_routes", "management_routes"] - ] = ["openai_routes", "info_routes"] + team_allowed_routes: List[str] = ["openai_routes", "info_routes"] team_id_default: Optional[str] = Field( default=None, description="If no team_id given, default permissions/spend-tracking to this team.s", From 831ad45c4df36eeb401d718157ae391151e693ab Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Tue, 2 Dec 2025 18:18:08 +0530 Subject: [PATCH 009/107] Add ragflow support --- docs/my-website/docs/providers/ragflow.md | 244 ++++++++++++ docs/my-website/sidebars.js | 1 + litellm/__init__.py | 1 + litellm/constants.py | 1 + .../get_llm_provider_logic.py | 10 + litellm/llms/ragflow/__init__.py | 8 + litellm/llms/ragflow/chat/__init__.py | 4 + litellm/llms/ragflow/chat/transformation.py | 264 ++++++++++++ litellm/main.py | 30 ++ ...odel_prices_and_context_window_backup.json | 360 +++++++++++++++-- litellm/types/utils.py | 1 + litellm/utils.py | 2 + .../llms/ragflow/chat/__init__.py | 4 + .../chat/test_ragflow_chat_transformation.py | 376 ++++++++++++++++++ 14 files changed, 1283 insertions(+), 23 deletions(-) create mode 100644 docs/my-website/docs/providers/ragflow.md create mode 100644 litellm/llms/ragflow/__init__.py create mode 100644 litellm/llms/ragflow/chat/__init__.py create mode 100644 litellm/llms/ragflow/chat/transformation.py create mode 100644 tests/test_litellm/llms/ragflow/chat/__init__.py create mode 100644 tests/test_litellm/llms/ragflow/chat/test_ragflow_chat_transformation.py diff --git a/docs/my-website/docs/providers/ragflow.md b/docs/my-website/docs/providers/ragflow.md new file mode 100644 index 00000000000..73223bd07b5 --- /dev/null +++ b/docs/my-website/docs/providers/ragflow.md @@ -0,0 +1,244 @@ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# RAGFlow + +Litellm supports Ragflow's chat completions APIs + +## Supported Features + +- ✅ Chat completions +- ✅ Streaming responses +- ✅ Both chat and agent endpoints +- ✅ Multiple credential sources (params, env vars, litellm_params) +- ✅ OpenAI-compatible API format + + +## API Key + +```python +# env variable +os.environ['RAGFLOW_API_KEY'] +``` + +## API Base + +```python +# env variable +os.environ['RAGFLOW_API_BASE'] +``` + +## Overview + +RAGFlow provides OpenAI-compatible APIs with unique path structures that include chat and agent IDs: + +- **Chat endpoint**: `/api/v1/chats_openai/{chat_id}/chat/completions` +- **Agent endpoint**: `/api/v1/agents_openai/{agent_id}/chat/completions` + +The model name format embeds the endpoint type and ID: +- Chat: `ragflow/chat/{chat_id}/{model_name}` +- Agent: `ragflow/agent/{agent_id}/{model_name}` + + +## Sample Usage - Chat Endpoint + +```python +from litellm import completion +import os + +os.environ['RAGFLOW_API_KEY'] = "your-ragflow-api-key" +os.environ['RAGFLOW_API_BASE'] = "http://localhost:9380" # or your hosted URL + +response = completion( + model="ragflow/chat/my-chat-id/gpt-4o-mini", + messages=[{"role": "user", "content": "How does the deep doc understanding work?"}] +) +print(response) +``` + +## Sample Usage - Agent Endpoint + +```python +from litellm import completion +import os + +os.environ['RAGFLOW_API_KEY'] = "your-ragflow-api-key" +os.environ['RAGFLOW_API_BASE'] = "http://localhost:9380" # or your hosted URL + +response = completion( + model="ragflow/agent/my-agent-id/gpt-4o-mini", + messages=[{"role": "user", "content": "What are the key features?"}] +) +print(response) +``` + +## Sample Usage - With Parameters + +You can also pass `api_key` and `api_base` directly as parameters: + +```python +from litellm import completion + +response = completion( + model="ragflow/chat/my-chat-id/gpt-4o-mini", + messages=[{"role": "user", "content": "Hello!"}], + api_key="your-ragflow-api-key", + api_base="http://localhost:9380" +) +print(response) +``` + +## Sample Usage - Streaming + +```python +from litellm import completion +import os + +os.environ['RAGFLOW_API_KEY'] = "your-ragflow-api-key" +os.environ['RAGFLOW_API_BASE'] = "http://localhost:9380" + +response = completion( + model="ragflow/agent/my-agent-id/gpt-4o-mini", + messages=[{"role": "user", "content": "Explain RAGFlow"}], + stream=True +) + +for chunk in response: + print(chunk) +``` + +## Model Name Format + +The model name must follow one of these formats: + +### Chat Endpoint +``` +ragflow/chat/{chat_id}/{model_name} +``` + +Example: `ragflow/chat/my-chat-id/gpt-4o-mini` + +### Agent Endpoint +``` +ragflow/agent/{agent_id}/{model_name} +``` + +Example: `ragflow/agent/my-agent-id/gpt-4o-mini` + +Where: +- `{chat_id}` or `{agent_id}` is the ID of your chat or agent in RAGFlow +- `{model_name}` is the actual model name (e.g., `gpt-4o-mini`, `gpt-4o`, etc.) + +## Configuration Sources + +LiteLLM supports multiple ways to provide credentials, checked in this order: + +1. **Function parameters**: `api_key="..."`, `api_base="..."` +2. **litellm_params**: `litellm_params={"api_key": "...", "api_base": "..."}` +3. **Environment variables**: `RAGFLOW_API_KEY`, `RAGFLOW_API_BASE` +4. **Global litellm settings**: `litellm.api_key`, `litellm.api_base` + +## Usage - LiteLLM Proxy Server + +### 1. Save key in your environment + +```bash +export RAGFLOW_API_KEY="your-ragflow-api-key" +export RAGFLOW_API_BASE="http://localhost:9380" +``` + +### 2. Start the proxy + + + + +```yaml +model_list: + - model_name: ragflow-chat-gpt4 + litellm_params: + model: ragflow/chat/my-chat-id/gpt-4o-mini + api_key: os.environ/RAGFLOW_API_KEY + api_base: os.environ/RAGFLOW_API_BASE + - model_name: ragflow-agent-gpt4 + litellm_params: + model: ragflow/agent/my-agent-id/gpt-4o-mini + api_key: os.environ/RAGFLOW_API_KEY + api_base: os.environ/RAGFLOW_API_BASE +``` + + + + +```bash +$ litellm --config /path/to/config.yaml + +# Server running on http://0.0.0.0:4000 +``` + + + + +### 3. Test it + + + + +```bash +curl http://0.0.0.0:4000/v1/chat/completions \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer sk-1234" \ + -d '{ + "model": "ragflow-chat-gpt4", + "messages": [ + {"role": "user", "content": "How does RAGFlow work?"} + ] + }' +``` + + + + +```python +from openai import OpenAI + +client = OpenAI( + api_key="sk-1234", # Your LiteLLM proxy key + base_url="http://0.0.0.0:4000" +) + +response = client.chat.completions.create( + model="ragflow-chat-gpt4", + messages=[ + {"role": "user", "content": "How does RAGFlow work?"} + ] +) +print(response) +``` + + + + +## API Base URL Handling + +The `api_base` parameter can be provided with or without `/v1` suffix. LiteLLM will automatically handle it: + +- `http://localhost:9380` → `http://localhost:9380/api/v1/chats_openai/{chat_id}/chat/completions` +- `http://localhost:9380/v1` → `http://localhost:9380/api/v1/chats_openai/{chat_id}/chat/completions` +- `http://localhost:9380/api/v1` → `http://localhost:9380/api/v1/chats_openai/{chat_id}/chat/completions` + +All three formats will work correctly. + +## Error Handling + +If you encounter errors: + +1. **Invalid model format**: Ensure your model name follows `ragflow/{chat|agent}/{id}/{model_name}` format +2. **Missing api_base**: Provide `api_base` via parameter, environment variable, or litellm_params +3. **Connection errors**: Verify your RAGFlow server is running and accessible at the provided `api_base` + +:::info + +For more information about passing provider-specific parameters, [go here](../completion/provider_specific_params.md) + +::: + diff --git a/docs/my-website/sidebars.js b/docs/my-website/sidebars.js index e467711b59d..55638512ef2 100644 --- a/docs/my-website/sidebars.js +++ b/docs/my-website/sidebars.js @@ -625,6 +625,7 @@ const sidebars = { "providers/petals", "providers/publicai", "providers/predibase", + "providers/ragflow", "providers/recraft", "providers/replicate", { diff --git a/litellm/__init__.py b/litellm/__init__.py index 007eff892c8..595a0132099 100644 --- a/litellm/__init__.py +++ b/litellm/__init__.py @@ -1387,6 +1387,7 @@ from .llms.docker_model_runner.chat.transformation import DockerModelRunnerChatC from .llms.v0.chat.transformation import V0ChatConfig from .llms.oci.chat.transformation import OCIChatConfig from .llms.morph.chat.transformation import MorphChatConfig +from .llms.ragflow.chat.transformation import RAGFlowConfig from .llms.lambda_ai.chat.transformation import LambdaAIChatConfig from .llms.hyperbolic.chat.transformation import HyperbolicChatConfig from .llms.vercel_ai_gateway.chat.transformation import VercelAIGatewayConfig diff --git a/litellm/constants.py b/litellm/constants.py index e3de7368c8a..e252c86777c 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -586,6 +586,7 @@ openai_compatible_providers: List = [ "cometapi", "clarifai", "docker_model_runner", + "ragflow", ] openai_text_completion_compatible_providers: List = ( [ # providers that support `/v1/completions` diff --git a/litellm/litellm_core_utils/get_llm_provider_logic.py b/litellm/litellm_core_utils/get_llm_provider_logic.py index b10011befcd..a90d16dba49 100644 --- a/litellm/litellm_core_utils/get_llm_provider_logic.py +++ b/litellm/litellm_core_utils/get_llm_provider_logic.py @@ -840,6 +840,16 @@ def _get_openai_compatible_provider_info( # noqa: PLR0915 ) = litellm.ClarifaiConfig()._get_openai_compatible_provider_info( api_base, api_key ) + elif custom_llm_provider == "ragflow": + full_model = f"ragflow/{model}" + ( + api_base, + dynamic_api_key, + _, + ) = litellm.RAGFlowConfig()._get_openai_compatible_provider_info( + full_model, api_base, api_key, "ragflow" + ) + model = full_model if api_base is not None and not isinstance(api_base, str): raise Exception("api base needs to be a string. api_base={}".format(api_base)) diff --git a/litellm/llms/ragflow/__init__.py b/litellm/llms/ragflow/__init__.py new file mode 100644 index 00000000000..17d12bed31c --- /dev/null +++ b/litellm/llms/ragflow/__init__.py @@ -0,0 +1,8 @@ +""" +RAGFlow provider for LiteLLM. + +RAGFlow provides OpenAI-compatible APIs with unique path structures: +- Chat endpoint: /api/v1/chats_openai/{chat_id}/chat/completions +- Agent endpoint: /api/v1/agents_openai/{agent_id}/chat/completions +""" + diff --git a/litellm/llms/ragflow/chat/__init__.py b/litellm/llms/ragflow/chat/__init__.py new file mode 100644 index 00000000000..0e0f47d07b6 --- /dev/null +++ b/litellm/llms/ragflow/chat/__init__.py @@ -0,0 +1,4 @@ +""" +RAGFlow chat completion configuration. +""" + diff --git a/litellm/llms/ragflow/chat/transformation.py b/litellm/llms/ragflow/chat/transformation.py new file mode 100644 index 00000000000..d33a1593be8 --- /dev/null +++ b/litellm/llms/ragflow/chat/transformation.py @@ -0,0 +1,264 @@ +""" +RAGFlow provider configuration for OpenAI-compatible API. + +RAGFlow provides OpenAI-compatible APIs with unique path structures: +- Chat endpoint: /api/v1/chats_openai/{chat_id}/chat/completions +- Agent endpoint: /api/v1/agents_openai/{agent_id}/chat/completions + +Model name format: +- Chat: ragflow/chat/{chat_id}/{model_name} +- Agent: ragflow/agent/{agent_id}/{model_name} +""" + +from typing import Any, List, Optional, Tuple + +import litellm +from litellm.llms.openai.openai import OpenAIConfig +from litellm.secret_managers.main import get_secret, get_secret_str +from litellm.types.llms.openai import AllMessageValues + + +class RAGFlowConfig(OpenAIConfig): + """ + Configuration for RAGFlow OpenAI-compatible API. + + Handles both chat and agent endpoints by parsing the model name format: + - ragflow/chat/{chat_id}/{model_name} for chat endpoints + - ragflow/agent/{agent_id}/{model_name} for agent endpoints + """ + + def _parse_ragflow_model(self, model: str) -> Tuple[str, str, str]: + """ + Parse RAGFlow model name format: ragflow/{endpoint_type}/{id}/{model_name} + + Args: + model: Model name in format ragflow/chat/{chat_id}/{model} or ragflow/agent/{agent_id}/{model} + + Returns: + Tuple of (endpoint_type, id, model_name) + + Raises: + ValueError: If model format is invalid + """ + parts = model.split("/") + if len(parts) < 4: + raise ValueError( + f"Invalid RAGFlow model format: {model}. " + f"Expected format: ragflow/chat/{{chat_id}}/{{model}} or ragflow/agent/{{agent_id}}/{{model}}" + ) + + if parts[0] != "ragflow": + raise ValueError( + f"Invalid RAGFlow model format: {model}. Must start with 'ragflow/'" + ) + + endpoint_type = parts[1] + if endpoint_type not in ["chat", "agent"]: + raise ValueError( + f"Invalid RAGFlow endpoint type: {endpoint_type}. Must be 'chat' or 'agent'" + ) + + entity_id = parts[2] + model_name = "/".join(parts[3:]) # Handle model names that might contain slashes + + return endpoint_type, entity_id, model_name + + 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: + """ + Get the complete URL for the RAGFlow API call. + + Constructs URL based on endpoint type: + - Chat: /api/v1/chats_openai/{chat_id}/chat/completions + - Agent: /api/v1/agents_openai/{agent_id}/chat/completions + + Args: + api_base: Base API URL (e.g., http://ragflow-server:port or http://ragflow-server:port/v1) + api_key: API key (not used in URL construction) + model: Model name in format ragflow/{endpoint_type}/{id}/{model} + optional_params: Optional parameters + litellm_params: LiteLLM parameters (may contain api_base) + stream: Whether streaming is enabled + + Returns: + Complete URL for the API call + """ + # Get api_base from multiple sources: input param, litellm_params, environment, or global litellm setting + if litellm_params and hasattr(litellm_params, 'api_base') and litellm_params.api_base: + api_base = api_base or litellm_params.api_base + + api_base = ( + api_base + or litellm.api_base + or get_secret("RAGFLOW_API_BASE") + or get_secret_str("RAGFLOW_API_BASE") + ) + + if api_base is None: + raise ValueError("api_base is required for RAGFlow provider. Set it via api_base parameter, RAGFLOW_API_BASE environment variable, or litellm.api_base") + + # Parse model name to extract endpoint type and ID + endpoint_type, entity_id, _ = self._parse_ragflow_model(model) + + # Remove trailing slash from api_base if present + api_base = api_base.rstrip("/") + + # Strip /v1 or /api/v1 from api_base if present, since we'll add the full path + # Check /api/v1 first because /api/v1 ends with /v1 + if api_base.endswith("/api/v1"): + api_base = api_base[:-7] # Remove /api/v1 + elif api_base.endswith("/v1"): + api_base = api_base[:-3] # Remove /v1 + + # Construct the RAGFlow-specific path + if endpoint_type == "chat": + path = f"/api/v1/chats_openai/{entity_id}/chat/completions" + else: # agent + path = f"/api/v1/agents_openai/{entity_id}/chat/completions" + + # Ensure path starts with / + if not path.startswith("/"): + path = "/" + path + + return f"{api_base}{path}" + + def _get_openai_compatible_provider_info( + self, + model: str, + api_base: Optional[str], + api_key: Optional[str], + custom_llm_provider: str, + ) -> Tuple[Optional[str], Optional[str], str]: + """ + Get OpenAI-compatible provider information for RAGFlow. + + Args: + model: Model name (will be parsed to extract actual model name) + api_base: Base API URL (from input params) + api_key: API key (from input params) + custom_llm_provider: Custom LLM provider name + + Returns: + Tuple of (api_base, api_key, custom_llm_provider) + """ + # Parse model to extract the actual model name + # The model name will be stored in litellm_params for use in requests + _, _, actual_model = self._parse_ragflow_model(model) + + # Get api_base from multiple sources: input param, environment, or global litellm setting + dynamic_api_base = ( + api_base + or litellm.api_base + or get_secret("RAGFLOW_API_BASE") + or get_secret_str("RAGFLOW_API_BASE") + ) + + # Get api_key from multiple sources: input param, environment, or global litellm setting + dynamic_api_key = ( + api_key + or litellm.api_key + or get_secret_str("RAGFLOW_API_KEY") + ) + + return dynamic_api_base, dynamic_api_key, custom_llm_provider + + 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: + """ + Validate environment and set up headers for RAGFlow API. + + Args: + headers: Request headers + model: Model name + messages: Chat messages + optional_params: Optional parameters + litellm_params: LiteLLM parameters (may contain api_key) + api_key: API key (from input params) + api_base: Base API URL + + Returns: + Updated headers dictionary + """ + # Use api_key from litellm_params if available, otherwise fall back to other sources + if litellm_params and hasattr(litellm_params, 'api_key') and litellm_params.api_key: + api_key = api_key or litellm_params.api_key + + # Get api_key from multiple sources: input param, litellm_params, environment, or global litellm setting + api_key = ( + api_key + or litellm.api_key + or get_secret_str("RAGFLOW_API_KEY") + ) + + if api_key is not None: + headers["Authorization"] = f"Bearer {api_key}" + + # Ensure Content-Type is set to application/json + if "content-type" not in headers and "Content-Type" not in headers: + headers["Content-Type"] = "application/json" + + # Parse model to extract actual model name and store it + # The actual model name should be used in the request body + try: + _, _, actual_model = self._parse_ragflow_model(model) + # Store the actual model name in litellm_params for use in transform_request + litellm_params["_ragflow_actual_model"] = actual_model + except ValueError: + # If parsing fails, use the original model name + pass + + return headers + + def transform_request( + self, + model: str, + messages: List[AllMessageValues], + optional_params: dict, + litellm_params: dict, + headers: dict, + ) -> dict: + """ + Transform request for RAGFlow API. + + Uses the actual model name extracted from the RAGFlow model format. + + Args: + model: Model name in RAGFlow format + messages: Chat messages + optional_params: Optional parameters + litellm_params: LiteLLM parameters (may contain _ragflow_actual_model) + headers: Request headers + + Returns: + Transformed request dictionary + """ + # Get the actual model name from litellm_params if available + actual_model = litellm_params.get("_ragflow_actual_model") + if actual_model is None: + # Fallback: try to parse the model name + try: + _, _, actual_model = self._parse_ragflow_model(model) + except ValueError: + # If parsing fails, use the original model name + actual_model = model + + # Use parent's transform_request with the actual model name + return super().transform_request( + actual_model, messages, optional_params, litellm_params, headers + ) + diff --git a/litellm/main.py b/litellm/main.py index a09a9453017..fba2fc9b9a6 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -1989,6 +1989,36 @@ def completion( # type: ignore # noqa: PLR0915 ) raise e + elif custom_llm_provider == "ragflow": + ## COMPLETION CALL - RAGFlow uses HTTP handler to support custom URL paths + try: + response = base_llm_http_handler.completion( + model=model, + messages=messages, + headers=headers, + model_response=model_response, + api_key=api_key, + api_base=api_base, + acompletion=acompletion, + logging_obj=logging, + optional_params=optional_params, + litellm_params=litellm_params, + shared_session=shared_session, + timeout=timeout, + client=client, + custom_llm_provider=custom_llm_provider, + encoding=encoding, + stream=stream, + provider_config=provider_config, + ) + except Exception as e: + logging.post_call( + input=messages, + api_key=api_key, + original_response=str(e), + additional_args={"headers": headers}, + ) + raise e elif custom_llm_provider == "xai": ## COMPLETION CALL try: diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 9fdc1704f41..f28e9b1290f 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.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, @@ -7824,26 +7851,298 @@ "source": "https://www.databricks.com/product/pricing/foundation-model-serving" }, "databricks/databricks-claude-3-7-sonnet": { - "input_cost_per_token": 2.5e-06, - "input_dbu_cost_per_token": 3.571e-05, + "input_cost_per_token": 2.9999900000000002e-06, + "input_dbu_cost_per_token": 4.2857e-05, "litellm_provider": "databricks", "max_input_tokens": 200000, "max_output_tokens": 128000, "max_tokens": 200000, "metadata": { - "notes": "Input/output cost per token is dbu cost * $0.070, based on databricks Claude 3.7 conversion. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." }, "mode": "chat", - "output_cost_per_token": 1.7857e-05, - "output_db_cost_per_token": 0.000214286, - "source": "https://www.databricks.com/product/pricing/foundation-model-serving", + "output_cost_per_token": 1.5000020000000002e-05, + "output_dbu_cost_per_token": 0.000214286, + "source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving", "supports_assistant_prefill": true, "supports_function_calling": true, "supports_reasoning": true, "supports_tool_choice": true }, + "databricks/databricks-claude-haiku-4-5": { + "input_cost_per_token": 1.00002e-06, + "input_dbu_cost_per_token": 1.4286e-05, + "litellm_provider": "databricks", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 200000, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 5.00003e-06, + "output_dbu_cost_per_token": 7.1429e-05, + "source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving", + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "databricks/databricks-claude-opus-4": { + "input_cost_per_token": 1.5000020000000002e-05, + "input_dbu_cost_per_token": 0.000214286, + "litellm_provider": "databricks", + "max_input_tokens": 200000, + "max_output_tokens": 32000, + "max_tokens": 200000, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 7.500003000000001e-05, + "output_dbu_cost_per_token": 0.001071429, + "source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving", + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "databricks/databricks-claude-opus-4-1": { + "input_cost_per_token": 1.5000020000000002e-05, + "input_dbu_cost_per_token": 0.000214286, + "litellm_provider": "databricks", + "max_input_tokens": 200000, + "max_output_tokens": 32000, + "max_tokens": 200000, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 7.500003000000001e-05, + "output_dbu_cost_per_token": 0.001071429, + "source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving", + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "databricks/databricks-claude-opus-4-5": { + "input_cost_per_token": 5.00003e-06, + "input_dbu_cost_per_token": 7.1429e-05, + "litellm_provider": "databricks", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 200000, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 2.5000010000000002e-05, + "output_dbu_cost_per_token": 0.000357143, + "source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving", + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "databricks/databricks-claude-sonnet-4": { + "input_cost_per_token": 2.9999900000000002e-06, + "input_dbu_cost_per_token": 4.2857e-05, + "litellm_provider": "databricks", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 200000, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 1.5000020000000002e-05, + "output_dbu_cost_per_token": 0.000214286, + "source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving", + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "databricks/databricks-claude-sonnet-4-1": { + "input_cost_per_token": 2.9999900000000002e-06, + "input_dbu_cost_per_token": 4.2857e-05, + "litellm_provider": "databricks", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 200000, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 1.5000020000000002e-05, + "output_dbu_cost_per_token": 0.000214286, + "source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving", + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "databricks/databricks-claude-sonnet-4-5": { + "input_cost_per_token": 2.9999900000000002e-06, + "input_dbu_cost_per_token": 4.2857e-05, + "litellm_provider": "databricks", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 200000, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 1.5000020000000002e-05, + "output_dbu_cost_per_token": 0.000214286, + "source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving", + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "databricks/databricks-gemini-2-5-flash": { + "input_cost_per_token": 3.0001999999999996e-07, + "input_dbu_cost_per_token": 4.285999999999999e-06, + "litellm_provider": "databricks", + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_tokens": 1048576, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 2.49998e-06, + "output_dbu_cost_per_token": 3.5714e-05, + "source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving", + "supports_function_calling": true, + "supports_tool_choice": true + }, + "databricks/databricks-gemini-2-5-pro": { + "input_cost_per_token": 1.24999e-06, + "input_dbu_cost_per_token": 1.7857e-05, + "litellm_provider": "databricks", + "max_input_tokens": 1048576, + "max_output_tokens": 65536, + "max_tokens": 1048576, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 9.999990000000002e-06, + "output_dbu_cost_per_token": 0.000142857, + "source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving", + "supports_function_calling": true, + "supports_tool_choice": true + }, + "databricks/databricks-gemma-3-12b": { + "input_cost_per_token": 1.5000999999999998e-07, + "input_dbu_cost_per_token": 2.1429999999999996e-06, + "litellm_provider": "databricks", + "max_input_tokens": 128000, + "max_output_tokens": 32000, + "max_tokens": 128000, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 5.0001e-07, + "output_dbu_cost_per_token": 7.143e-06, + "source": "https://www.databricks.com/product/pricing/foundation-model-serving" + }, + "databricks/databricks-gpt-5": { + "input_cost_per_token": 1.24999e-06, + "input_dbu_cost_per_token": 1.7857e-05, + "litellm_provider": "databricks", + "max_input_tokens": 400000, + "max_output_tokens": 128000, + "max_tokens": 400000, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 9.999990000000002e-06, + "output_dbu_cost_per_token": 0.000142857, + "source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving" + }, + "databricks/databricks-gpt-5-1": { + "input_cost_per_token": 1.24999e-06, + "input_dbu_cost_per_token": 1.7857e-05, + "litellm_provider": "databricks", + "max_input_tokens": 400000, + "max_output_tokens": 128000, + "max_tokens": 400000, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 9.999990000000002e-06, + "output_dbu_cost_per_token": 0.000142857, + "source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving" + }, + "databricks/databricks-gpt-5-mini": { + "input_cost_per_token": 2.4997000000000006e-07, + "input_dbu_cost_per_token": 3.571e-06, + "litellm_provider": "databricks", + "max_input_tokens": 400000, + "max_output_tokens": 128000, + "max_tokens": 400000, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 1.9999700000000004e-06, + "output_dbu_cost_per_token": 2.8571e-05, + "source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving" + }, + "databricks/databricks-gpt-5-nano": { + "input_cost_per_token": 4.998e-08, + "input_dbu_cost_per_token": 7.14e-07, + "litellm_provider": "databricks", + "max_input_tokens": 400000, + "max_output_tokens": 128000, + "max_tokens": 400000, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 3.9998000000000007e-07, + "output_dbu_cost_per_token": 5.714000000000001e-06, + "source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving" + }, + "databricks/databricks-gpt-oss-120b": { + "input_cost_per_token": 1.5000999999999998e-07, + "input_dbu_cost_per_token": 2.1429999999999996e-06, + "litellm_provider": "databricks", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 5.9997e-07, + "output_dbu_cost_per_token": 8.571e-06, + "source": "https://www.databricks.com/product/pricing/foundation-model-serving" + }, + "databricks/databricks-gpt-oss-20b": { + "input_cost_per_token": 7e-08, + "input_dbu_cost_per_token": 1e-06, + "litellm_provider": "databricks", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 3.0001999999999996e-07, + "output_dbu_cost_per_token": 4.285999999999999e-06, + "source": "https://www.databricks.com/product/pricing/foundation-model-serving" + }, "databricks/databricks-gte-large-en": { - "input_cost_per_token": 1.2999e-07, + "input_cost_per_token": 1.2999000000000001e-07, "input_dbu_cost_per_token": 1.857e-06, "litellm_provider": "databricks", "max_input_tokens": 8192, @@ -7868,14 +8167,14 @@ "notes": "Input/output cost per token is dbu cost * $0.070, based on databricks Llama 3.1 70B conversion. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." }, "mode": "chat", - "output_cost_per_token": 1.5e-06, + "output_cost_per_token": 1.5000300000000002e-06, "output_dbu_cost_per_token": 2.1429e-05, "source": "https://www.databricks.com/product/pricing/foundation-model-serving", "supports_tool_choice": true }, "databricks/databricks-llama-4-maverick": { - "input_cost_per_token": 5e-06, - "input_dbu_cost_per_token": 7.143e-05, + "input_cost_per_token": 5.0001e-07, + "input_dbu_cost_per_token": 7.143e-06, "litellm_provider": "databricks", "max_input_tokens": 128000, "max_output_tokens": 128000, @@ -7884,13 +8183,13 @@ "notes": "Databricks documentation now provides both DBU costs (_dbu_cost_per_token) and dollar costs(_cost_per_token)." }, "mode": "chat", - "output_cost_per_token": 1.5e-05, - "output_dbu_cost_per_token": 0.00021429, + "output_cost_per_token": 1.5000300000000002e-06, + "output_dbu_cost_per_token": 2.1429e-05, "source": "https://www.databricks.com/product/pricing/foundation-model-serving", "supports_tool_choice": true }, "databricks/databricks-meta-llama-3-1-405b-instruct": { - "input_cost_per_token": 5e-06, + "input_cost_per_token": 5.00003e-06, "input_dbu_cost_per_token": 7.1429e-05, "litellm_provider": "databricks", "max_input_tokens": 128000, @@ -7900,14 +8199,29 @@ "notes": "Input/output cost per token is dbu cost * $0.070, based on databricks Llama 3.1 70B conversion. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." }, "mode": "chat", - "output_cost_per_token": 1.500002e-05, - "output_db_cost_per_token": 0.000214286, + "output_cost_per_token": 1.5000020000000002e-05, + "output_dbu_cost_per_token": 0.000214286, "source": "https://www.databricks.com/product/pricing/foundation-model-serving", "supports_tool_choice": true }, + "databricks/databricks-meta-llama-3-1-8b-instruct": { + "input_cost_per_token": 1.5000999999999998e-07, + "input_dbu_cost_per_token": 2.1429999999999996e-06, + "litellm_provider": "databricks", + "max_input_tokens": 200000, + "max_output_tokens": 128000, + "max_tokens": 200000, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 4.5003000000000007e-07, + "output_dbu_cost_per_token": 6.429000000000001e-06, + "source": "https://www.databricks.com/product/pricing/foundation-model-serving" + }, "databricks/databricks-meta-llama-3-3-70b-instruct": { - "input_cost_per_token": 1.00002e-06, - "input_dbu_cost_per_token": 1.4286e-05, + "input_cost_per_token": 5.0001e-07, + "input_dbu_cost_per_token": 7.143e-06, "litellm_provider": "databricks", "max_input_tokens": 128000, "max_output_tokens": 128000, @@ -7916,8 +8230,8 @@ "notes": "Input/output cost per token is dbu cost * $0.070, based on databricks Llama 3.1 70B conversion. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." }, "mode": "chat", - "output_cost_per_token": 2.99999e-06, - "output_dbu_cost_per_token": 4.2857e-05, + "output_cost_per_token": 1.5000300000000002e-06, + "output_dbu_cost_per_token": 2.1429e-05, "source": "https://www.databricks.com/product/pricing/foundation-model-serving", "supports_tool_choice": true }, @@ -7932,7 +8246,7 @@ "notes": "Input/output cost per token is dbu cost * $0.070, based on databricks Llama 3.1 70B conversion. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." }, "mode": "chat", - "output_cost_per_token": 2.99999e-06, + "output_cost_per_token": 2.9999900000000002e-06, "output_dbu_cost_per_token": 4.2857e-05, "source": "https://www.databricks.com/product/pricing/foundation-model-serving", "supports_tool_choice": true @@ -7948,13 +8262,13 @@ "notes": "Input/output cost per token is dbu cost * $0.070, based on databricks Llama 3.1 70B conversion. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." }, "mode": "chat", - "output_cost_per_token": 9.9902e-07, + "output_cost_per_token": 1.00002e-06, "output_dbu_cost_per_token": 1.4286e-05, "source": "https://www.databricks.com/product/pricing/foundation-model-serving", "supports_tool_choice": true }, "databricks/databricks-mpt-30b-instruct": { - "input_cost_per_token": 9.9902e-07, + "input_cost_per_token": 1.00002e-06, "input_dbu_cost_per_token": 1.4286e-05, "litellm_provider": "databricks", "max_input_tokens": 8192, @@ -7964,7 +8278,7 @@ "notes": "Input/output cost per token is dbu cost * $0.070, based on databricks Llama 3.1 70B conversion. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." }, "mode": "chat", - "output_cost_per_token": 9.9902e-07, + "output_cost_per_token": 1.00002e-06, "output_dbu_cost_per_token": 1.4286e-05, "source": "https://www.databricks.com/product/pricing/foundation-model-serving", "supports_tool_choice": true diff --git a/litellm/types/utils.py b/litellm/types/utils.py index 58267fdfea9..942192e2edf 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -2618,6 +2618,7 @@ class LlmProviders(str, Enum): DATABRICKS = "databricks" EMPOWER = "empower" GITHUB = "github" + RAGFLOW = "ragflow" COMPACTIFAI = "compactifai" DOCKER_MODEL_RUNNER = "docker_model_runner" CUSTOM = "custom" diff --git a/litellm/utils.py b/litellm/utils.py index 6c50afc5f49..eb671ce2b99 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -7110,6 +7110,8 @@ class ProviderConfigManager: return litellm.CompactifAIChatConfig() elif litellm.LlmProviders.GITHUB_COPILOT == provider: return litellm.GithubCopilotConfig() + elif litellm.LlmProviders.RAGFLOW == provider: + return litellm.RAGFlowConfig() elif ( litellm.LlmProviders.CUSTOM == provider or litellm.LlmProviders.CUSTOM_OPENAI == provider diff --git a/tests/test_litellm/llms/ragflow/chat/__init__.py b/tests/test_litellm/llms/ragflow/chat/__init__.py new file mode 100644 index 00000000000..4e074b84150 --- /dev/null +++ b/tests/test_litellm/llms/ragflow/chat/__init__.py @@ -0,0 +1,4 @@ +""" +RAGFlow chat transformation tests. +""" + diff --git a/tests/test_litellm/llms/ragflow/chat/test_ragflow_chat_transformation.py b/tests/test_litellm/llms/ragflow/chat/test_ragflow_chat_transformation.py new file mode 100644 index 00000000000..90f2504f94c --- /dev/null +++ b/tests/test_litellm/llms/ragflow/chat/test_ragflow_chat_transformation.py @@ -0,0 +1,376 @@ +""" +Test file for RAGFlow chat transformation functionality. + +Tests the model name parsing, URL construction, and request transformation +for RAGFlow's OpenAI-compatible API with custom path structures. +""" + +import os +import sys +from unittest.mock import Mock, patch + +import pytest + +# Add the project root to Python path +sys.path.insert(0, os.path.abspath("../../../../..")) + +import litellm +from litellm.llms.ragflow.chat.transformation import RAGFlowConfig +from litellm.types.llms.openai import AllMessageValues + + +class TestRAGFlowChatTransformation: + """Test suite for RAGFlow chat transformation functionality.""" + + def test_parse_ragflow_model_chat(self): + """Test parsing of chat model format.""" + config = RAGFlowConfig() + + model = "ragflow/chat/my-chat-id/gpt-4o-mini" + endpoint_type, entity_id, model_name = config._parse_ragflow_model(model) + + assert endpoint_type == "chat" + assert entity_id == "my-chat-id" + assert model_name == "gpt-4o-mini" + + def test_parse_ragflow_model_agent(self): + """Test parsing of agent model format.""" + config = RAGFlowConfig() + + model = "ragflow/agent/my-agent-id/gpt-4o-mini" + endpoint_type, entity_id, model_name = config._parse_ragflow_model(model) + + assert endpoint_type == "agent" + assert entity_id == "my-agent-id" + assert model_name == "gpt-4o-mini" + + def test_parse_ragflow_model_with_slashes_in_model_name(self): + """Test parsing when model name contains slashes.""" + config = RAGFlowConfig() + + model = "ragflow/chat/my-chat-id/openai/gpt-4o-mini" + endpoint_type, entity_id, model_name = config._parse_ragflow_model(model) + + assert endpoint_type == "chat" + assert entity_id == "my-chat-id" + assert model_name == "openai/gpt-4o-mini" + + def test_parse_ragflow_model_invalid_format(self): + """Test parsing with invalid model format.""" + config = RAGFlowConfig() + + with pytest.raises(ValueError, match="Invalid RAGFlow model format"): + config._parse_ragflow_model("ragflow/chat/model-name") + + with pytest.raises(ValueError, match="Invalid RAGFlow model format"): + config._parse_ragflow_model("invalid/chat/id/model") + + with pytest.raises(ValueError, match="Must start with 'ragflow/'"): + config._parse_ragflow_model("not-ragflow/chat/id/model") + + def test_parse_ragflow_model_invalid_endpoint_type(self): + """Test parsing with invalid endpoint type.""" + config = RAGFlowConfig() + + with pytest.raises(ValueError, match="Invalid RAGFlow endpoint type"): + config._parse_ragflow_model("ragflow/invalid/my-id/model") + + def test_get_complete_url_chat(self): + """Test URL construction for chat endpoint.""" + config = RAGFlowConfig() + + model = "ragflow/chat/my-chat-id/gpt-4o-mini" + api_base = "http://localhost:9380" + + url = config.get_complete_url( + api_base=api_base, + api_key=None, + model=model, + optional_params={}, + litellm_params={}, + stream=False, + ) + + assert url == "http://localhost:9380/api/v1/chats_openai/my-chat-id/chat/completions" + + def test_get_complete_url_agent(self): + """Test URL construction for agent endpoint.""" + config = RAGFlowConfig() + + model = "ragflow/agent/my-agent-id/gpt-4o-mini" + api_base = "http://localhost:9380" + + url = config.get_complete_url( + api_base=api_base, + api_key=None, + model=model, + optional_params={}, + litellm_params={}, + stream=False, + ) + + assert url == "http://localhost:9380/api/v1/agents_openai/my-agent-id/chat/completions" + + def test_get_complete_url_strips_v1(self): + """Test URL construction when api_base ends with /v1.""" + config = RAGFlowConfig() + + model = "ragflow/chat/my-chat-id/gpt-4o-mini" + api_base = "http://localhost:9380/v1" + + url = config.get_complete_url( + api_base=api_base, + api_key=None, + model=model, + optional_params={}, + litellm_params={}, + stream=False, + ) + + assert url == "http://localhost:9380/api/v1/chats_openai/my-chat-id/chat/completions" + + def test_get_complete_url_strips_api_v1(self): + """Test URL construction when api_base ends with /api/v1.""" + config = RAGFlowConfig() + + model = "ragflow/agent/my-agent-id/gpt-4o-mini" + api_base = "http://localhost:9380/api/v1" + + url = config.get_complete_url( + api_base=api_base, + api_key=None, + model=model, + optional_params={}, + litellm_params={}, + stream=False, + ) + + assert url == "http://localhost:9380/api/v1/agents_openai/my-agent-id/chat/completions" + + def test_get_complete_url_from_litellm_params(self): + """Test URL construction with api_base from litellm_params.""" + config = RAGFlowConfig() + + model = "ragflow/chat/my-chat-id/gpt-4o-mini" + # Create a simple dict-like object for litellm_params + class LiteLLMParams: + def __init__(self): + self.api_base = "http://ragflow-server:9380" + + litellm_params = LiteLLMParams() + + url = config.get_complete_url( + api_base=None, + api_key=None, + model=model, + optional_params={}, + litellm_params=litellm_params, + stream=False, + ) + + assert url == "http://ragflow-server:9380/api/v1/chats_openai/my-chat-id/chat/completions" + + def test_get_complete_url_missing_api_base(self): + """Test URL construction when api_base is missing.""" + config = RAGFlowConfig() + + model = "ragflow/chat/my-chat-id/gpt-4o-mini" + + with pytest.raises(ValueError, match="api_base is required"): + config.get_complete_url( + api_base=None, + api_key=None, + model=model, + optional_params={}, + litellm_params={}, + stream=False, + ) + + @patch.dict(os.environ, {"RAGFLOW_API_BASE": "http://env-ragflow:9380"}) + def test_get_complete_url_from_environment(self): + """Test URL construction with api_base from environment variable.""" + config = RAGFlowConfig() + + model = "ragflow/agent/my-agent-id/gpt-4o-mini" + + url = config.get_complete_url( + api_base=None, + api_key=None, + model=model, + optional_params={}, + litellm_params={}, + stream=False, + ) + + assert url == "http://env-ragflow:9380/api/v1/agents_openai/my-agent-id/chat/completions" + + def test_validate_environment_sets_headers(self): + """Test that validate_environment sets proper headers.""" + config = RAGFlowConfig() + + headers = {} + model = "ragflow/chat/my-chat-id/gpt-4o-mini" + messages = [{"role": "user", "content": "Hello"}] + api_key = "test-api-key" + + result_headers = config.validate_environment( + headers=headers, + model=model, + messages=messages, + optional_params={}, + litellm_params={}, + api_key=api_key, + api_base="http://localhost:9380", + ) + + assert result_headers["Authorization"] == "Bearer test-api-key" + assert result_headers["Content-Type"] == "application/json" + + def test_validate_environment_stores_actual_model(self): + """Test that validate_environment stores actual model name.""" + config = RAGFlowConfig() + + headers = {} + model = "ragflow/chat/my-chat-id/gpt-4o-mini" + messages = [{"role": "user", "content": "Hello"}] + litellm_params = {} + + config.validate_environment( + headers=headers, + model=model, + messages=messages, + optional_params={}, + litellm_params=litellm_params, + api_key="test-key", + api_base="http://localhost:9380", + ) + + assert litellm_params["_ragflow_actual_model"] == "gpt-4o-mini" + + @patch.dict(os.environ, {"RAGFLOW_API_KEY": "env-api-key"}) + def test_validate_environment_from_environment(self): + """Test that validate_environment gets api_key from environment.""" + config = RAGFlowConfig() + + headers = {} + model = "ragflow/agent/my-agent-id/gpt-4o-mini" + messages = [{"role": "user", "content": "Hello"}] + + result_headers = config.validate_environment( + headers=headers, + model=model, + messages=messages, + optional_params={}, + litellm_params={}, + api_key=None, + api_base="http://localhost:9380", + ) + + assert result_headers["Authorization"] == "Bearer env-api-key" + + def test_validate_environment_from_litellm_params(self): + """Test that validate_environment gets api_key from litellm_params.""" + config = RAGFlowConfig() + + headers = {} + model = "ragflow/chat/my-chat-id/gpt-4o-mini" + messages = [{"role": "user", "content": "Hello"}] + # Create a simple object for litellm_params with api_key attribute + class LiteLLMParams: + def __init__(self): + self.api_key = "litellm-params-key" + def __setitem__(self, key, value): + setattr(self, key, value) + + litellm_params = LiteLLMParams() + + result_headers = config.validate_environment( + headers=headers, + model=model, + messages=messages, + optional_params={}, + litellm_params=litellm_params, + api_key=None, + api_base="http://localhost:9380", + ) + + assert result_headers["Authorization"] == "Bearer litellm-params-key" + + def test_transform_request_uses_actual_model(self): + """Test that transform_request uses the actual model name.""" + config = RAGFlowConfig() + + model = "ragflow/chat/my-chat-id/gpt-4o-mini" + messages = [{"role": "user", "content": "Hello"}] + litellm_params = {"_ragflow_actual_model": "gpt-4o-mini"} + + # Test the actual behavior by checking the model in the result + result = config.transform_request( + model=model, + messages=messages, + optional_params={}, + litellm_params=litellm_params, + headers={}, + ) + + # The result should contain the actual model name, not the full ragflow path + assert result["model"] == "gpt-4o-mini" + assert result["messages"] == messages + + def test_transform_request_fallback_parsing(self): + """Test that transform_request falls back to parsing if _ragflow_actual_model is missing.""" + config = RAGFlowConfig() + + model = "ragflow/agent/my-agent-id/gpt-4o-mini" + messages = [{"role": "user", "content": "Hello"}] + litellm_params = {} # Missing _ragflow_actual_model + + result = config.transform_request( + model=model, + messages=messages, + optional_params={}, + litellm_params=litellm_params, + headers={}, + ) + + # Should parse and use the actual model name + assert result["model"] == "gpt-4o-mini" + assert result["messages"] == messages + + def test_get_openai_compatible_provider_info(self): + """Test _get_openai_compatible_provider_info returns correct values.""" + config = RAGFlowConfig() + + model = "ragflow/chat/my-chat-id/gpt-4o-mini" + api_base = "http://localhost:9380" + api_key = "test-key" + + result_api_base, result_api_key, result_provider = config._get_openai_compatible_provider_info( + model=model, + api_base=api_base, + api_key=api_key, + custom_llm_provider="ragflow", + ) + + assert result_api_base == api_base + assert result_api_key == api_key + assert result_provider == "ragflow" + + @patch.dict(os.environ, {"RAGFLOW_API_BASE": "http://env-base:9380", "RAGFLOW_API_KEY": "env-key"}) + def test_get_openai_compatible_provider_info_from_env(self): + """Test _get_openai_compatible_provider_info gets values from environment.""" + config = RAGFlowConfig() + + model = "ragflow/agent/my-agent-id/gpt-4o-mini" + + result_api_base, result_api_key, result_provider = config._get_openai_compatible_provider_info( + model=model, + api_base=None, + api_key=None, + custom_llm_provider="ragflow", + ) + + assert result_api_base == "http://env-base:9380" + assert result_api_key == "env-key" + assert result_provider == "ragflow" + From 7324905c957fa2e1f1fee9d24b246a44ced170fe Mon Sep 17 00:00:00 2001 From: AlexsanderHamir Date: Tue, 2 Dec 2025 05:29:03 -0800 Subject: [PATCH 010/107] fix: update default database connection number --- docs/my-website/docs/proxy/config_settings.md | 2 +- docs/my-website/docs/proxy/configs.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/my-website/docs/proxy/config_settings.md b/docs/my-website/docs/proxy/config_settings.md index 7140c99e6fb..1f8ca59622a 100644 --- a/docs/my-website/docs/proxy/config_settings.md +++ b/docs/my-website/docs/proxy/config_settings.md @@ -113,7 +113,7 @@ general_settings: # Database Settings database_url: string - database_connection_pool_limit: 0 # default 100 + database_connection_pool_limit: 0 # default 10 database_connection_timeout: 0 # default 60s allow_requests_on_db_unavailable: boolean # if true, will allow requests that can not connect to the DB to verify Virtual Key to still work diff --git a/docs/my-website/docs/proxy/configs.md b/docs/my-website/docs/proxy/configs.md index 18177b7c4d2..77ab3158f74 100644 --- a/docs/my-website/docs/proxy/configs.md +++ b/docs/my-website/docs/proxy/configs.md @@ -576,7 +576,7 @@ custom_tokenizer: ```yaml general_settings: - database_connection_pool_limit: 100 # sets connection pool for prisma client to postgres db at 100 + database_connection_pool_limit: 10 # sets connection pool for prisma client to postgres db (default: 10, recommended: 10-20) database_connection_timeout: 60 # sets a 60s timeout for any connection call to the db ``` From 9ff2ecc16d13c79ada59711b958c8730940eeae5 Mon Sep 17 00:00:00 2001 From: Alexsander Hamir Date: Tue, 2 Dec 2025 05:52:00 -0800 Subject: [PATCH 011/107] Fix: update default proxy_batch_write_at number (#17355) The default is 10 seconds, not 30. --- docs/my-website/docs/proxy/config_settings.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/my-website/docs/proxy/config_settings.md b/docs/my-website/docs/proxy/config_settings.md index 1f8ca59622a..82955dadb53 100644 --- a/docs/my-website/docs/proxy/config_settings.md +++ b/docs/my-website/docs/proxy/config_settings.md @@ -234,7 +234,7 @@ router_settings: | max_response_size_mb | int | The maximum size for responses in MB. LLM Responses above this size will not be sent. | | proxy_budget_rescheduler_min_time | int | The minimum time (in seconds) to wait before checking db for budget resets. **Default is 597 seconds** | | proxy_budget_rescheduler_max_time | int | The maximum time (in seconds) to wait before checking db for budget resets. **Default is 605 seconds** | -| proxy_batch_write_at | int | Time (in seconds) to wait before batch writing spend logs to the db. **Default is 30 seconds** | +| proxy_batch_write_at | int | Time (in seconds) to wait before batch writing spend logs to the db. **Default is 10 seconds** | | proxy_batch_polling_interval | int | Time (in seconds) to wait before polling a batch, to check if it's completed. **Default is 6000 seconds (1 hour)** | | alerting_args | dict | Args for Slack Alerting [Doc on Slack Alerting](./alerting.md) | | custom_key_generate | str | Custom function for key generation [Doc on custom key generation](./virtual_keys.md#custom--key-generate) | @@ -763,7 +763,7 @@ router_settings: | PROMPTLAYER_API_KEY | API key for PromptLayer integration | PROXY_ADMIN_ID | Admin identifier for proxy server | PROXY_BASE_URL | Base URL for proxy service -| PROXY_BATCH_WRITE_AT | Time in seconds to wait before batch writing spend logs to the database. Default is 30 +| PROXY_BATCH_WRITE_AT | Time in seconds to wait before batch writing spend logs to the database. Default is 10 | PROXY_BATCH_POLLING_INTERVAL | Time in seconds to wait before polling a batch, to check if it's completed. Default is 6000s (1 hour) | PROXY_BUDGET_RESCHEDULER_MAX_TIME | Maximum time in seconds to wait before checking database for budget resets. Default is 605 | PROXY_BUDGET_RESCHEDULER_MIN_TIME | Minimum time in seconds to wait before checking database for budget resets. Default is 597 From 1bb9e1bde8c2573fb3ca57489facc595188a830d Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Tue, 2 Dec 2025 08:41:50 -0800 Subject: [PATCH 012/107] [Feat] Add `vllm` batch+files API support (#15823) * add OPENAI_COMPATIBLE_BATCH_AND_FILES_PROVIDERS * fix use OPENAI_COMPATIBLE_BATCH_AND_FILES_PROVIDERS * add _get_batch_job_total_usage_from_file_content * fixes for vLLM + 12 labs async invoke * fix: vLLM Batch APIs * afile_retrieve * test_hosted_vllm_full_workflow * fix SERVER_URL for test --- litellm/batches/batch_utils.py | 16 +- litellm/batches/main.py | 42 +- litellm/files/main.py | 29 +- litellm/llms/bedrock/batches/handler.py | 96 +++++ ...odel_prices_and_context_window_backup.json | 360 ++++++++++++++++-- litellm/types/utils.py | 6 + .../test_hosted_vllm_batches_and_files.py | 105 +++++ 7 files changed, 591 insertions(+), 63 deletions(-) create mode 100644 litellm/llms/bedrock/batches/handler.py create mode 100644 tests/batches_tests/test_hosted_vllm_batches_and_files.py diff --git a/litellm/batches/batch_utils.py b/litellm/batches/batch_utils.py index 8289801ee30..50b48321db5 100644 --- a/litellm/batches/batch_utils.py +++ b/litellm/batches/batch_utils.py @@ -14,8 +14,7 @@ from litellm.utils import token_counter async def calculate_batch_cost_and_usage( file_content_dictionary: List[dict], - custom_llm_provider: Literal["openai", "azure", "vertex_ai"], - model_name: Optional[str] = None, + custom_llm_provider: Literal["openai", "azure", "vertex_ai", "hosted_vllm"], ) -> Tuple[float, Usage, List[str]]: """ Calculate the cost and usage of a batch @@ -37,8 +36,7 @@ async def calculate_batch_cost_and_usage( async def _handle_completed_batch( batch: Batch, - custom_llm_provider: Literal["openai", "azure", "vertex_ai"], - model_name: Optional[str] = None, + custom_llm_provider: Literal["openai", "azure", "vertex_ai", "hosted_vllm"], ) -> Tuple[float, Usage, List[str]]: """Helper function to process a completed batch and handle logging""" # Get batch results @@ -84,8 +82,7 @@ def _get_batch_models_from_file_content( def _batch_cost_calculator( file_content_dictionary: List[dict], - custom_llm_provider: Literal["openai", "azure", "vertex_ai"] = "openai", - model_name: Optional[str] = None, + custom_llm_provider: Literal["openai", "azure", "vertex_ai", "hosted_vllm"] = "openai", ) -> float: """ Calculate the cost of a batch based on the output file id @@ -186,7 +183,7 @@ def calculate_vertex_ai_batch_cost_and_usage( async def _get_batch_output_file_content_as_dictionary( batch: Batch, - custom_llm_provider: Literal["openai", "azure", "vertex_ai"] = "openai", + custom_llm_provider: Literal["openai", "azure", "vertex_ai", "hosted_vllm"] = "openai", ) -> List[dict]: """ Get the batch output file content as a list of dictionaries @@ -225,7 +222,7 @@ def _get_file_content_as_dictionary(file_content: bytes) -> List[dict]: def _get_batch_job_cost_from_file_content( file_content_dictionary: List[dict], - custom_llm_provider: Literal["openai", "azure", "vertex_ai"] = "openai", + custom_llm_provider: Literal["openai", "azure", "vertex_ai", "hosted_vllm"] = "openai", ) -> float: """ Get the cost of a batch job from the file content @@ -253,8 +250,7 @@ def _get_batch_job_cost_from_file_content( def _get_batch_job_total_usage_from_file_content( file_content_dictionary: List[dict], - custom_llm_provider: Literal["openai", "azure", "vertex_ai"] = "openai", - model_name: Optional[str] = None, + custom_llm_provider: Literal["openai", "azure", "vertex_ai", "hosted_vllm"] = "openai", ) -> Usage: """ Get the tokens of a batch job from the file content diff --git a/litellm/batches/main.py b/litellm/batches/main.py index 57a9857dd6a..353b1e25698 100644 --- a/litellm/batches/main.py +++ b/litellm/batches/main.py @@ -18,11 +18,14 @@ from typing import Any, Coroutine, Dict, Literal, Optional, Union, cast import httpx from openai.types.batch import BatchRequestCounts +from openai.types.batch import Metadata +from openai.types.batch import Metadata as OpenAIBatchMetadata import litellm from litellm._logging import verbose_logger from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj from litellm.llms.azure.batches.handler import AzureBatchesAPI +from litellm.llms.bedrock.batches.handler import BedrockBatchesHandler from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler from litellm.llms.custom_httpx.llm_http_handler import BaseLLMHTTPHandler from litellm.llms.openai.openai import OpenAIBatchesAPI @@ -35,7 +38,11 @@ from litellm.types.llms.openai import ( RetrieveBatchRequest, ) from litellm.types.router import GenericLiteLLMParams -from litellm.types.utils import LiteLLMBatch, LlmProviders +from litellm.types.utils import ( + OPENAI_COMPATIBLE_BATCH_AND_FILES_PROVIDERS, + LiteLLMBatch, + LlmProviders, +) from litellm.utils import ( ProviderConfigManager, client, @@ -100,7 +107,7 @@ async def acreate_batch( completion_window: Literal["24h"], endpoint: Literal["/v1/chat/completions", "/v1/embeddings", "/v1/completions"], input_file_id: str, - custom_llm_provider: Literal["openai", "azure", "vertex_ai", "bedrock"] = "openai", + custom_llm_provider: Literal["openai", "azure", "vertex_ai", "bedrock", "hosted_vllm"] = "openai", metadata: Optional[Dict[str, str]] = None, extra_headers: Optional[Dict[str, str]] = None, extra_body: Optional[Dict[str, str]] = None, @@ -148,7 +155,7 @@ def create_batch( completion_window: Literal["24h"], endpoint: Literal["/v1/chat/completions", "/v1/embeddings", "/v1/completions"], input_file_id: str, - custom_llm_provider: Literal["openai", "azure", "vertex_ai", "bedrock"] = "openai", + custom_llm_provider: Literal["openai", "azure", "vertex_ai", "bedrock", "hosted_vllm"] = "openai", metadata: Optional[Dict[str, str]] = None, extra_headers: Optional[Dict[str, str]] = None, extra_body: Optional[Dict[str, str]] = None, @@ -235,7 +242,7 @@ def create_batch( ) return response api_base: Optional[str] = None - if custom_llm_provider == "openai": + if custom_llm_provider in OPENAI_COMPATIBLE_BATCH_AND_FILES_PROVIDERS: # for deepinfra/perplexity/anyscale/groq we check in get_llm_provider and pass in the api base from there api_base = ( optional_params.api_base @@ -350,7 +357,7 @@ def create_batch( @client async def aretrieve_batch( batch_id: str, - custom_llm_provider: Literal["openai", "azure", "vertex_ai", "bedrock"] = "openai", + custom_llm_provider: Literal["openai", "azure", "vertex_ai", "bedrock", "hosted_vllm"] = "openai", metadata: Optional[Dict[str, str]] = None, extra_headers: Optional[Dict[str, str]] = None, extra_body: Optional[Dict[str, str]] = None, @@ -396,10 +403,10 @@ def _handle_retrieve_batch_providers_without_provider_config( litellm_params: dict, _retrieve_batch_request: RetrieveBatchRequest, _is_async: bool, - custom_llm_provider: Literal["openai", "azure", "vertex_ai", "bedrock"] = "openai", + custom_llm_provider: Literal["openai", "azure", "vertex_ai", "bedrock", "hosted_vllm"] = "openai", ): api_base: Optional[str] = None - if custom_llm_provider == "openai": + if custom_llm_provider in OPENAI_COMPATIBLE_BATCH_AND_FILES_PROVIDERS: # for deepinfra/perplexity/anyscale/groq we check in get_llm_provider and pass in the api base from there api_base = ( optional_params.api_base @@ -512,7 +519,7 @@ def _handle_retrieve_batch_providers_without_provider_config( @client def retrieve_batch( batch_id: str, - custom_llm_provider: Literal["openai", "azure", "vertex_ai", "bedrock"] = "openai", + custom_llm_provider: Literal["openai", "azure", "vertex_ai", "bedrock", "hosted_vllm"] = "openai", metadata: Optional[Dict[str, str]] = None, extra_headers: Optional[Dict[str, str]] = None, extra_body: Optional[Dict[str, str]] = None, @@ -576,7 +583,7 @@ def retrieve_batch( async_kwargs = kwargs.copy() async_kwargs.pop("aws_region_name", None) - return _handle_async_invoke_status( + return BedrockBatchesHandler._handle_async_invoke_status( batch_id=batch_id, aws_region_name=kwargs.get("aws_region_name", "us-east-1"), logging_obj=litellm_logging_obj, @@ -644,7 +651,7 @@ def retrieve_batch( async def alist_batches( after: Optional[str] = None, limit: Optional[int] = None, - custom_llm_provider: Literal["openai", "azure", "vertex_ai"] = "openai", + custom_llm_provider: Literal["openai", "azure", "hosted_vllm", "vertex_ai"] = "openai", metadata: Optional[Dict[str, str]] = None, extra_headers: Optional[Dict[str, str]] = None, extra_body: Optional[Dict[str, str]] = None, @@ -687,7 +694,7 @@ async def alist_batches( def list_batches( after: Optional[str] = None, limit: Optional[int] = None, - custom_llm_provider: Literal["openai", "azure", "vertex_ai"] = "openai", + custom_llm_provider: Literal["openai", "azure", "hosted_vllm", "vertex_ai"] = "openai", extra_headers: Optional[Dict[str, str]] = None, extra_body: Optional[Dict[str, str]] = None, **kwargs, @@ -727,7 +734,7 @@ def list_batches( timeout = 600.0 _is_async = kwargs.pop("alist_batches", False) is True - if custom_llm_provider == "openai": + if custom_llm_provider in OPENAI_COMPATIBLE_BATCH_AND_FILES_PROVIDERS: # for deepinfra/perplexity/anyscale/groq we check in get_llm_provider and pass in the api base from there api_base = ( optional_params.api_base @@ -928,7 +935,7 @@ def cancel_batch( _is_async = kwargs.pop("acancel_batch", False) is True api_base: Optional[str] = None - if custom_llm_provider == "openai": + if custom_llm_provider in OPENAI_COMPATIBLE_BATCH_AND_FILES_PROVIDERS: api_base = ( optional_params.api_base or litellm.api_base @@ -1043,19 +1050,20 @@ def _handle_async_invoke_status( ) # Transform response to a LiteLLMBatch object + from litellm.types.llms.openai import BatchJobStatus 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 = { + status_mapping: dict[str, BatchJobStatus] = { "completed": "completed", "failed": "failed", "inprogress": "in_progress", "in_progress": "in_progress", } - normalized_status = status_mapping.get(aws_status_lower, aws_status_lower) + normalized_status: BatchJobStatus = status_mapping.get(aws_status_lower, "failed") # Default to "failed" if unknown status # Get output S3 URI safely output_s3_uri = "" @@ -1065,13 +1073,15 @@ def _handle_async_invoke_status( pass # Use BedrockBatchesConfig's timestamp parsing method (expects raw AWS status string) + import time + 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=normalized_status, - created_at=created_at, + created_at=created_at or int(time.time()), # Provide default timestamp if None in_progress_at=in_progress_at, completed_at=completed_at, failed_at=failed_at, diff --git a/litellm/files/main.py b/litellm/files/main.py index 535772fa42c..71139001e52 100644 --- a/litellm/files/main.py +++ b/litellm/files/main.py @@ -30,7 +30,10 @@ from litellm.types.llms.openai import ( OpenAIFileObject, ) from litellm.types.router import * -from litellm.types.utils import LlmProviders +from litellm.types.utils import ( + OPENAI_COMPATIBLE_BATCH_AND_FILES_PROVIDERS, + LlmProviders, +) from litellm.utils import ( ProviderConfigManager, client, @@ -51,7 +54,7 @@ vertex_ai_files_instance = VertexAIFilesHandler() async def acreate_file( file: FileTypes, purpose: Literal["assistants", "batch", "fine-tune"], - custom_llm_provider: Literal["openai", "azure", "vertex_ai", "bedrock"] = "openai", + custom_llm_provider: Literal["openai", "azure", "vertex_ai", "bedrock", "hosted_vllm"] = "openai", extra_headers: Optional[Dict[str, str]] = None, extra_body: Optional[Dict[str, str]] = None, **kwargs, @@ -95,9 +98,7 @@ async def acreate_file( def create_file( file: FileTypes, purpose: Literal["assistants", "batch", "fine-tune"], - custom_llm_provider: Optional[ - Literal["openai", "azure", "vertex_ai", "bedrock"] - ] = None, + custom_llm_provider: Optional[Literal["openai", "azure", "vertex_ai", "bedrock", "hosted_vllm"]] = None, extra_headers: Optional[Dict[str, str]] = None, extra_body: Optional[Dict[str, str]] = None, **kwargs, @@ -165,7 +166,7 @@ def create_file( ), timeout=timeout, ) - elif custom_llm_provider == "openai": + elif custom_llm_provider in OPENAI_COMPATIBLE_BATCH_AND_FILES_PROVIDERS: # for deepinfra/perplexity/anyscale/groq we check in get_llm_provider and pass in the api base from there api_base = ( optional_params.api_base @@ -276,7 +277,7 @@ def create_file( @client async def afile_retrieve( file_id: str, - custom_llm_provider: Literal["openai", "azure"] = "openai", + custom_llm_provider: Literal["openai", "azure", "hosted_vllm"] = "openai", extra_headers: Optional[Dict[str, str]] = None, extra_body: Optional[Dict[str, str]] = None, **kwargs, @@ -317,7 +318,7 @@ async def afile_retrieve( @client def file_retrieve( file_id: str, - custom_llm_provider: Literal["openai", "azure"] = "openai", + custom_llm_provider: Literal["openai", "azure", "hosted_vllm"] = "openai", extra_headers: Optional[Dict[str, str]] = None, extra_body: Optional[Dict[str, str]] = None, **kwargs, @@ -347,7 +348,7 @@ def file_retrieve( _is_async = kwargs.pop("is_async", False) is True - if custom_llm_provider == "openai": + if custom_llm_provider in OPENAI_COMPATIBLE_BATCH_AND_FILES_PROVIDERS: # for deepinfra/perplexity/anyscale/groq we check in get_llm_provider and pass in the api base from there api_base = ( optional_params.api_base @@ -514,7 +515,7 @@ def file_delete( elif timeout is None: timeout = 600.0 _is_async = kwargs.pop("is_async", False) is True - if custom_llm_provider == "openai": + if custom_llm_provider in OPENAI_COMPATIBLE_BATCH_AND_FILES_PROVIDERS: # for deepinfra/perplexity/anyscale/groq we check in get_llm_provider and pass in the api base from there api_base = ( optional_params.api_base @@ -670,7 +671,7 @@ def file_list( timeout = 600.0 _is_async = kwargs.pop("is_async", False) is True - if custom_llm_provider == "openai": + if custom_llm_provider in OPENAI_COMPATIBLE_BATCH_AND_FILES_PROVIDERS: # for deepinfra/perplexity/anyscale/groq we check in get_llm_provider and pass in the api base from there api_base = ( optional_params.api_base @@ -754,7 +755,7 @@ def file_list( @client async def afile_content( file_id: str, - custom_llm_provider: Literal["openai", "azure", "vertex_ai"] = "openai", + custom_llm_provider: Literal["openai", "azure", "vertex_ai", "hosted_vllm"] = "openai", extra_headers: Optional[Dict[str, str]] = None, extra_body: Optional[Dict[str, str]] = None, **kwargs, @@ -799,7 +800,7 @@ def file_content( file_id: str, model: Optional[str] = None, custom_llm_provider: Optional[ - Union[Literal["openai", "azure", "vertex_ai"], str] + Union[Literal["openai", "azure", "vertex_ai", "hosted_vllm"], str] ] = None, extra_headers: Optional[Dict[str, str]] = None, extra_body: Optional[Dict[str, str]] = None, @@ -846,7 +847,7 @@ def file_content( _is_async = kwargs.pop("afile_content", False) is True - if custom_llm_provider == "openai": + if custom_llm_provider in OPENAI_COMPATIBLE_BATCH_AND_FILES_PROVIDERS: # for deepinfra/perplexity/anyscale/groq we check in get_llm_provider and pass in the api base from there api_base = ( optional_params.api_base diff --git a/litellm/llms/bedrock/batches/handler.py b/litellm/llms/bedrock/batches/handler.py new file mode 100644 index 00000000000..4a26bd43348 --- /dev/null +++ b/litellm/llms/bedrock/batches/handler.py @@ -0,0 +1,96 @@ +from openai.types.batch import BatchRequestCounts +from openai.types.batch import Metadata as OpenAIBatchMetadata + +from litellm.types.utils import LiteLLMBatch + + +class BedrockBatchesHandler: + """ + Handler for Bedrock Batches. + + Specific providers/models needed some special handling. + + E.g. Twelve Labs Embedding Async Invoke + """ + @staticmethod + def _handle_async_invoke_status( + batch_id: str, aws_region_name: str, logging_obj=None, **kwargs + ) -> "LiteLLMBatch": + """ + Handle async invoke status check for AWS Bedrock. + + This is for Twelve Labs Embedding Async Invoke. + + Args: + batch_id: The async invoke ARN + aws_region_name: AWS region name + **kwargs: Additional parameters + + Returns: + dict: Status information including status, output_file_id (S3 URL), etc. + """ + import asyncio + + from litellm.llms.bedrock.embed.embedding import BedrockEmbedding + + async def _async_get_status(): + # Create embedding handler instance + embedding_handler = BedrockEmbedding() + + # Get the status of the async invoke job + status_response = await embedding_handler._get_async_invoke_status( + invocation_arn=batch_id, + aws_region_name=aws_region_name, + logging_obj=logging_obj, + **kwargs, + ) + + # Transform response to a LiteLLMBatch object + from litellm.types.utils import LiteLLMBatch + + openai_batch_metadata: OpenAIBatchMetadata = { + "output_file_id": status_response["outputDataConfig"][ + "s3OutputDataConfig" + ]["s3Uri"], + "failure_message": status_response.get("failureMessage") or "", + "model_arn": status_response["modelArn"], + } + + 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, + request_counts=BatchRequestCounts( + total=1, + completed=1 if status_response["status"] == "completed" else 0, + failed=1 if status_response["status"] == "failed" else 0, + ), + metadata=openai_batch_metadata, + completion_window="24h", + endpoint="/v1/embeddings", + input_file_id="", + ) + + return result + + # Since this function is called from within an async context via run_in_executor, + # we need to create a new event loop in a thread to avoid conflicts + import concurrent.futures + + def run_in_thread(): + new_loop = asyncio.new_event_loop() + asyncio.set_event_loop(new_loop) + try: + return new_loop.run_until_complete(_async_get_status()) + finally: + new_loop.close() + + with concurrent.futures.ThreadPoolExecutor() as executor: + future = executor.submit(run_in_thread) + return future.result() diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 9fdc1704f41..f28e9b1290f 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.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, @@ -7824,26 +7851,298 @@ "source": "https://www.databricks.com/product/pricing/foundation-model-serving" }, "databricks/databricks-claude-3-7-sonnet": { - "input_cost_per_token": 2.5e-06, - "input_dbu_cost_per_token": 3.571e-05, + "input_cost_per_token": 2.9999900000000002e-06, + "input_dbu_cost_per_token": 4.2857e-05, "litellm_provider": "databricks", "max_input_tokens": 200000, "max_output_tokens": 128000, "max_tokens": 200000, "metadata": { - "notes": "Input/output cost per token is dbu cost * $0.070, based on databricks Claude 3.7 conversion. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." }, "mode": "chat", - "output_cost_per_token": 1.7857e-05, - "output_db_cost_per_token": 0.000214286, - "source": "https://www.databricks.com/product/pricing/foundation-model-serving", + "output_cost_per_token": 1.5000020000000002e-05, + "output_dbu_cost_per_token": 0.000214286, + "source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving", "supports_assistant_prefill": true, "supports_function_calling": true, "supports_reasoning": true, "supports_tool_choice": true }, + "databricks/databricks-claude-haiku-4-5": { + "input_cost_per_token": 1.00002e-06, + "input_dbu_cost_per_token": 1.4286e-05, + "litellm_provider": "databricks", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 200000, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 5.00003e-06, + "output_dbu_cost_per_token": 7.1429e-05, + "source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving", + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "databricks/databricks-claude-opus-4": { + "input_cost_per_token": 1.5000020000000002e-05, + "input_dbu_cost_per_token": 0.000214286, + "litellm_provider": "databricks", + "max_input_tokens": 200000, + "max_output_tokens": 32000, + "max_tokens": 200000, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 7.500003000000001e-05, + "output_dbu_cost_per_token": 0.001071429, + "source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving", + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "databricks/databricks-claude-opus-4-1": { + "input_cost_per_token": 1.5000020000000002e-05, + "input_dbu_cost_per_token": 0.000214286, + "litellm_provider": "databricks", + "max_input_tokens": 200000, + "max_output_tokens": 32000, + "max_tokens": 200000, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 7.500003000000001e-05, + "output_dbu_cost_per_token": 0.001071429, + "source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving", + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "databricks/databricks-claude-opus-4-5": { + "input_cost_per_token": 5.00003e-06, + "input_dbu_cost_per_token": 7.1429e-05, + "litellm_provider": "databricks", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 200000, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 2.5000010000000002e-05, + "output_dbu_cost_per_token": 0.000357143, + "source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving", + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "databricks/databricks-claude-sonnet-4": { + "input_cost_per_token": 2.9999900000000002e-06, + "input_dbu_cost_per_token": 4.2857e-05, + "litellm_provider": "databricks", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 200000, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 1.5000020000000002e-05, + "output_dbu_cost_per_token": 0.000214286, + "source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving", + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "databricks/databricks-claude-sonnet-4-1": { + "input_cost_per_token": 2.9999900000000002e-06, + "input_dbu_cost_per_token": 4.2857e-05, + "litellm_provider": "databricks", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 200000, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 1.5000020000000002e-05, + "output_dbu_cost_per_token": 0.000214286, + "source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving", + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "databricks/databricks-claude-sonnet-4-5": { + "input_cost_per_token": 2.9999900000000002e-06, + "input_dbu_cost_per_token": 4.2857e-05, + "litellm_provider": "databricks", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 200000, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 1.5000020000000002e-05, + "output_dbu_cost_per_token": 0.000214286, + "source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving", + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "databricks/databricks-gemini-2-5-flash": { + "input_cost_per_token": 3.0001999999999996e-07, + "input_dbu_cost_per_token": 4.285999999999999e-06, + "litellm_provider": "databricks", + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_tokens": 1048576, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 2.49998e-06, + "output_dbu_cost_per_token": 3.5714e-05, + "source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving", + "supports_function_calling": true, + "supports_tool_choice": true + }, + "databricks/databricks-gemini-2-5-pro": { + "input_cost_per_token": 1.24999e-06, + "input_dbu_cost_per_token": 1.7857e-05, + "litellm_provider": "databricks", + "max_input_tokens": 1048576, + "max_output_tokens": 65536, + "max_tokens": 1048576, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 9.999990000000002e-06, + "output_dbu_cost_per_token": 0.000142857, + "source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving", + "supports_function_calling": true, + "supports_tool_choice": true + }, + "databricks/databricks-gemma-3-12b": { + "input_cost_per_token": 1.5000999999999998e-07, + "input_dbu_cost_per_token": 2.1429999999999996e-06, + "litellm_provider": "databricks", + "max_input_tokens": 128000, + "max_output_tokens": 32000, + "max_tokens": 128000, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 5.0001e-07, + "output_dbu_cost_per_token": 7.143e-06, + "source": "https://www.databricks.com/product/pricing/foundation-model-serving" + }, + "databricks/databricks-gpt-5": { + "input_cost_per_token": 1.24999e-06, + "input_dbu_cost_per_token": 1.7857e-05, + "litellm_provider": "databricks", + "max_input_tokens": 400000, + "max_output_tokens": 128000, + "max_tokens": 400000, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 9.999990000000002e-06, + "output_dbu_cost_per_token": 0.000142857, + "source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving" + }, + "databricks/databricks-gpt-5-1": { + "input_cost_per_token": 1.24999e-06, + "input_dbu_cost_per_token": 1.7857e-05, + "litellm_provider": "databricks", + "max_input_tokens": 400000, + "max_output_tokens": 128000, + "max_tokens": 400000, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 9.999990000000002e-06, + "output_dbu_cost_per_token": 0.000142857, + "source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving" + }, + "databricks/databricks-gpt-5-mini": { + "input_cost_per_token": 2.4997000000000006e-07, + "input_dbu_cost_per_token": 3.571e-06, + "litellm_provider": "databricks", + "max_input_tokens": 400000, + "max_output_tokens": 128000, + "max_tokens": 400000, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 1.9999700000000004e-06, + "output_dbu_cost_per_token": 2.8571e-05, + "source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving" + }, + "databricks/databricks-gpt-5-nano": { + "input_cost_per_token": 4.998e-08, + "input_dbu_cost_per_token": 7.14e-07, + "litellm_provider": "databricks", + "max_input_tokens": 400000, + "max_output_tokens": 128000, + "max_tokens": 400000, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 3.9998000000000007e-07, + "output_dbu_cost_per_token": 5.714000000000001e-06, + "source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving" + }, + "databricks/databricks-gpt-oss-120b": { + "input_cost_per_token": 1.5000999999999998e-07, + "input_dbu_cost_per_token": 2.1429999999999996e-06, + "litellm_provider": "databricks", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 5.9997e-07, + "output_dbu_cost_per_token": 8.571e-06, + "source": "https://www.databricks.com/product/pricing/foundation-model-serving" + }, + "databricks/databricks-gpt-oss-20b": { + "input_cost_per_token": 7e-08, + "input_dbu_cost_per_token": 1e-06, + "litellm_provider": "databricks", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 3.0001999999999996e-07, + "output_dbu_cost_per_token": 4.285999999999999e-06, + "source": "https://www.databricks.com/product/pricing/foundation-model-serving" + }, "databricks/databricks-gte-large-en": { - "input_cost_per_token": 1.2999e-07, + "input_cost_per_token": 1.2999000000000001e-07, "input_dbu_cost_per_token": 1.857e-06, "litellm_provider": "databricks", "max_input_tokens": 8192, @@ -7868,14 +8167,14 @@ "notes": "Input/output cost per token is dbu cost * $0.070, based on databricks Llama 3.1 70B conversion. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." }, "mode": "chat", - "output_cost_per_token": 1.5e-06, + "output_cost_per_token": 1.5000300000000002e-06, "output_dbu_cost_per_token": 2.1429e-05, "source": "https://www.databricks.com/product/pricing/foundation-model-serving", "supports_tool_choice": true }, "databricks/databricks-llama-4-maverick": { - "input_cost_per_token": 5e-06, - "input_dbu_cost_per_token": 7.143e-05, + "input_cost_per_token": 5.0001e-07, + "input_dbu_cost_per_token": 7.143e-06, "litellm_provider": "databricks", "max_input_tokens": 128000, "max_output_tokens": 128000, @@ -7884,13 +8183,13 @@ "notes": "Databricks documentation now provides both DBU costs (_dbu_cost_per_token) and dollar costs(_cost_per_token)." }, "mode": "chat", - "output_cost_per_token": 1.5e-05, - "output_dbu_cost_per_token": 0.00021429, + "output_cost_per_token": 1.5000300000000002e-06, + "output_dbu_cost_per_token": 2.1429e-05, "source": "https://www.databricks.com/product/pricing/foundation-model-serving", "supports_tool_choice": true }, "databricks/databricks-meta-llama-3-1-405b-instruct": { - "input_cost_per_token": 5e-06, + "input_cost_per_token": 5.00003e-06, "input_dbu_cost_per_token": 7.1429e-05, "litellm_provider": "databricks", "max_input_tokens": 128000, @@ -7900,14 +8199,29 @@ "notes": "Input/output cost per token is dbu cost * $0.070, based on databricks Llama 3.1 70B conversion. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." }, "mode": "chat", - "output_cost_per_token": 1.500002e-05, - "output_db_cost_per_token": 0.000214286, + "output_cost_per_token": 1.5000020000000002e-05, + "output_dbu_cost_per_token": 0.000214286, "source": "https://www.databricks.com/product/pricing/foundation-model-serving", "supports_tool_choice": true }, + "databricks/databricks-meta-llama-3-1-8b-instruct": { + "input_cost_per_token": 1.5000999999999998e-07, + "input_dbu_cost_per_token": 2.1429999999999996e-06, + "litellm_provider": "databricks", + "max_input_tokens": 200000, + "max_output_tokens": 128000, + "max_tokens": 200000, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 4.5003000000000007e-07, + "output_dbu_cost_per_token": 6.429000000000001e-06, + "source": "https://www.databricks.com/product/pricing/foundation-model-serving" + }, "databricks/databricks-meta-llama-3-3-70b-instruct": { - "input_cost_per_token": 1.00002e-06, - "input_dbu_cost_per_token": 1.4286e-05, + "input_cost_per_token": 5.0001e-07, + "input_dbu_cost_per_token": 7.143e-06, "litellm_provider": "databricks", "max_input_tokens": 128000, "max_output_tokens": 128000, @@ -7916,8 +8230,8 @@ "notes": "Input/output cost per token is dbu cost * $0.070, based on databricks Llama 3.1 70B conversion. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." }, "mode": "chat", - "output_cost_per_token": 2.99999e-06, - "output_dbu_cost_per_token": 4.2857e-05, + "output_cost_per_token": 1.5000300000000002e-06, + "output_dbu_cost_per_token": 2.1429e-05, "source": "https://www.databricks.com/product/pricing/foundation-model-serving", "supports_tool_choice": true }, @@ -7932,7 +8246,7 @@ "notes": "Input/output cost per token is dbu cost * $0.070, based on databricks Llama 3.1 70B conversion. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." }, "mode": "chat", - "output_cost_per_token": 2.99999e-06, + "output_cost_per_token": 2.9999900000000002e-06, "output_dbu_cost_per_token": 4.2857e-05, "source": "https://www.databricks.com/product/pricing/foundation-model-serving", "supports_tool_choice": true @@ -7948,13 +8262,13 @@ "notes": "Input/output cost per token is dbu cost * $0.070, based on databricks Llama 3.1 70B conversion. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." }, "mode": "chat", - "output_cost_per_token": 9.9902e-07, + "output_cost_per_token": 1.00002e-06, "output_dbu_cost_per_token": 1.4286e-05, "source": "https://www.databricks.com/product/pricing/foundation-model-serving", "supports_tool_choice": true }, "databricks/databricks-mpt-30b-instruct": { - "input_cost_per_token": 9.9902e-07, + "input_cost_per_token": 1.00002e-06, "input_dbu_cost_per_token": 1.4286e-05, "litellm_provider": "databricks", "max_input_tokens": 8192, @@ -7964,7 +8278,7 @@ "notes": "Input/output cost per token is dbu cost * $0.070, based on databricks Llama 3.1 70B conversion. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." }, "mode": "chat", - "output_cost_per_token": 9.9902e-07, + "output_cost_per_token": 1.00002e-06, "output_dbu_cost_per_token": 1.4286e-05, "source": "https://www.databricks.com/product/pricing/foundation-model-serving", "supports_tool_choice": true diff --git a/litellm/types/utils.py b/litellm/types/utils.py index 58267fdfea9..5a582194147 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -2660,6 +2660,12 @@ class LlmProviders(str, Enum): # Create a set of all provider values for quick lookup LlmProvidersSet = {provider.value for provider in LlmProviders} +# File and Batch API providers that are OpenAI-compatible +OPENAI_COMPATIBLE_BATCH_AND_FILES_PROVIDERS: set[str] = { + LlmProviders.OPENAI.value, + LlmProviders.HOSTED_VLLM.value, +} + class SearchProviders(str, Enum): """ diff --git a/tests/batches_tests/test_hosted_vllm_batches_and_files.py b/tests/batches_tests/test_hosted_vllm_batches_and_files.py new file mode 100644 index 00000000000..432dc81bad2 --- /dev/null +++ b/tests/batches_tests/test_hosted_vllm_batches_and_files.py @@ -0,0 +1,105 @@ +""" +Unit Tests for hosted_vllm Batches and Files API + +Tests the integration of hosted_vllm provider with LiteLLM's batch and file operations. +Tests against a real OpenAI-compatible endpoint. +""" +import json +import os +import sys +import time +import uuid + +import httpx +import pytest +from dotenv import load_dotenv + +load_dotenv() +sys.path.insert( + 0, os.path.abspath("../..") +) + +import litellm + + +SERVER_URL = "https://exampleopenaiendpoint-production-0ee2.up.railway.app/v1" + + +@pytest.mark.asyncio() +async def test_hosted_vllm_full_workflow(): + """ + Test the complete workflow: create file -> create batch -> retrieve batch -> retrieve file. + Tests against real OpenAI-compatible endpoint. + """ + litellm._turn_on_debug() + file_name = "openai_batch_completions.jsonl" + _current_dir = os.path.dirname(os.path.abspath(__file__)) + file_path = os.path.join(_current_dir, file_name) + + # Step 1: Create file + print("\n=== Step 1: Creating file ===") + file_obj = await litellm.acreate_file( + file=open(file_path, "rb"), + purpose="batch", + custom_llm_provider="hosted_vllm", + api_base=SERVER_URL, + api_key="test-api-key", + ) + + print(f"✓ Created file: {file_obj.id}") + assert file_obj.id is not None + assert file_obj.object == "file" + assert file_obj.purpose == "batch" + + # Step 2: Create batch + print("\n=== Step 2: Creating batch ===") + batch_obj = await litellm.acreate_batch( + completion_window="24h", + endpoint="/v1/chat/completions", + input_file_id=file_obj.id, + custom_llm_provider="hosted_vllm", + metadata={"test": "hosted_vllm_integration"}, + api_base=SERVER_URL, + api_key="test-api-key", + ) + + print(f"✓ Created batch: {batch_obj.id}") + print(f" Status: {batch_obj.status}") + print(f" Input file: {batch_obj.input_file_id}") + assert batch_obj.id is not None + assert batch_obj.object == "batch" + assert batch_obj.input_file_id == file_obj.id + assert batch_obj.endpoint == "/v1/chat/completions" + + # Step 3: Retrieve batch + print("\n=== Step 3: Retrieving batch ===") + retrieved_batch = await litellm.aretrieve_batch( + batch_id=batch_obj.id, + custom_llm_provider="hosted_vllm", + api_base=SERVER_URL, + api_key="test-api-key", + ) + + print(f"✓ Retrieved batch: {retrieved_batch.id}") + print(f" Status: {retrieved_batch.status}") + print(f" Output file: {retrieved_batch.output_file_id}") + assert retrieved_batch.id == batch_obj.id + assert retrieved_batch.object == "batch" + assert retrieved_batch.input_file_id == file_obj.id + + # Step 4: Retrieve file (verify file still accessible) + print("\n=== Step 4: Retrieving original file ===") + retrieved_file = await litellm.afile_retrieve( + file_id=file_obj.id, + custom_llm_provider="hosted_vllm", + api_base=SERVER_URL, + api_key="test-api-key", + ) + + print(f"✓ Retrieved file: {retrieved_file.id}") + print(f" Filename: {retrieved_file.filename}") + print(f" Bytes: {retrieved_file.bytes}") + assert retrieved_file.id == file_obj.id + assert retrieved_file.object == "file" + + print("\n✅ Full workflow test completed successfully!") From 81f4d863caa81bb615849ca5180a0b664d300972 Mon Sep 17 00:00:00 2001 From: Cesar Garcia <128240629+Chesars@users.noreply.github.com> Date: Tue, 2 Dec 2025 14:08:10 -0300 Subject: [PATCH 013/107] docs: add Azure AI Foundry documentation for Claude models (#17104) * docs: add Azure AI Foundry documentation for Claude models Add documentation explaining how to use Claude models (Sonnet 4.5, Haiku 4.5, Opus 4.1) deployed on Azure AI Foundry with LiteLLM. Azure exposes Claude using Anthropic's native API, so users can use the existing anthropic/ provider with their Azure endpoint. Closes #17066 * docs: Add alternative method for Azure AI Foundry using anthropic/ provider Document that users can use anthropic/ provider with Azure endpoint as an alternative to the dedicated azure_ai/ provider. --- docs/my-website/docs/providers/anthropic.md | 24 +++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/my-website/docs/providers/anthropic.md b/docs/my-website/docs/providers/anthropic.md index d84c1c23048..f78af51bd90 100644 --- a/docs/my-website/docs/providers/anthropic.md +++ b/docs/my-website/docs/providers/anthropic.md @@ -201,6 +201,30 @@ Without `LITELLM_ANTHROPIC_DISABLE_URL_SUFFIX`: With `LITELLM_ANTHROPIC_DISABLE_URL_SUFFIX=true`: - Base URL `https://my-proxy.com/custom/path` → `https://my-proxy.com/custom/path` (unchanged) +### Azure AI Foundry (Alternative Method) + +:::tip Recommended Method +For full Azure support including Azure AD authentication, use the dedicated [Azure Anthropic provider](./azure/azure_anthropic) with `azure_ai/` prefix. +::: + +As an alternative, you can use the `anthropic/` provider directly with your Azure endpoint since Azure exposes Claude using Anthropic's native API. + +```python +from litellm import completion + +response = completion( + model="anthropic/claude-sonnet-4-5", + api_base="https://.services.ai.azure.com/anthropic", + api_key="", + messages=[{"role": "user", "content": "Hello!"}], +) +print(response) +``` + +:::info +**Finding your Azure endpoint:** Go to Azure AI Foundry → Your deployment → Overview. Your base URL will be `https://.services.ai.azure.com/anthropic` +::: + ## Usage ```python From 12530b375fe969ae95972c1b6ef4cf5d6802d000 Mon Sep 17 00:00:00 2001 From: kothamah <104782493+kothamah@users.noreply.github.com> Date: Tue, 2 Dec 2025 12:19:53 -0500 Subject: [PATCH 014/107] Litellm bedrock OpenAI model support (#17368) * Update constants.py added constants * Update base_aws_llm.py added steps * Update invoke_handler.py added openai support * Update base_invoke_transformation.py added * Update test_bedrock_completion.py added --- litellm/constants.py | 1 + litellm/llms/bedrock/base_aws_llm.py | 4 + litellm/llms/bedrock/chat/invoke_handler.py | 45 +++ .../base_invoke_transformation.py | 9 + .../test_bedrock_completion.py | 333 ++++++++++++++++++ 5 files changed, 392 insertions(+) diff --git a/litellm/constants.py b/litellm/constants.py index e3de7368c8a..1d42ef9a910 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -859,6 +859,7 @@ BEDROCK_INVOKE_PROVIDERS_LITERAL = Literal[ "deepseek_r1", "qwen3", "twelvelabs", + "openai" ] BEDROCK_EMBEDDING_PROVIDERS_LITERAL = Literal[ diff --git a/litellm/llms/bedrock/base_aws_llm.py b/litellm/llms/bedrock/base_aws_llm.py index ed658c793af..816b93edd20 100644 --- a/litellm/llms/bedrock/base_aws_llm.py +++ b/litellm/llms/bedrock/base_aws_llm.py @@ -353,6 +353,10 @@ class BaseAWSLLM: model_id = BaseAWSLLM._get_model_id_from_model_with_spec( model_id, spec="deepseek_r1" ) + elif provider == "openai" and "openai/" in model_id: + model_id = BaseAWSLLM._get_model_id_from_model_with_spec( + model_id, spec="openai" + ) return model_id @staticmethod diff --git a/litellm/llms/bedrock/chat/invoke_handler.py b/litellm/llms/bedrock/chat/invoke_handler.py index b35e86cabd2..7a960fd45d2 100644 --- a/litellm/llms/bedrock/chat/invoke_handler.py +++ b/litellm/llms/bedrock/chat/invoke_handler.py @@ -73,6 +73,9 @@ bedrock_tool_name_mappings: InMemoryCache = InMemoryCache( max_size_in_memory=50, default_ttl=600 ) from litellm.llms.bedrock.chat.converse_transformation import AmazonConverseConfig +from litellm.llms.bedrock.chat.invoke_transformations.amazon_openai_transformation import ( + AmazonBedrockOpenAIConfig, +) converse_config = AmazonConverseConfig() @@ -401,6 +404,10 @@ class BedrockLLM(BaseAWSLLM): prompt = prompt_factory( model=model, messages=messages, custom_llm_provider="bedrock" ) + elif provider == "openai": + # OpenAI uses messages directly, no prompt conversion needed + # Return empty prompt as it won't be used + prompt = "" elif provider == "cohere": prompt, chat_history = cohere_message_pt(messages=messages) else: @@ -578,6 +585,30 @@ class BedrockLLM(BaseAWSLLM): ) elif provider == "meta" or provider == "llama": outputText = completion_response["generation"] + elif provider == "openai": + # OpenAI imported models use OpenAI Chat Completions format + if "choices" in completion_response and len(completion_response["choices"]) > 0: + choice = completion_response["choices"][0] + if "message" in choice: + outputText = choice["message"].get("content") + elif "text" in choice: # fallback for completion format + outputText = choice["text"] + + # Set finish reason + if "finish_reason" in choice: + model_response.choices[0].finish_reason = map_finish_reason( + choice["finish_reason"] + ) + + # Set usage if available + if "usage" in completion_response: + usage = completion_response["usage"] + _usage = litellm.Usage( + prompt_tokens=usage.get("prompt_tokens", 0), + completion_tokens=usage.get("completion_tokens", 0), + total_tokens=usage.get("total_tokens", 0), + ) + setattr(model_response, "usage", _usage) elif provider == "mistral": outputText = completion_response["outputs"][0]["text"] model_response.choices[0].finish_reason = completion_response[ @@ -895,6 +926,20 @@ class BedrockLLM(BaseAWSLLM): ): # completion(top_k=3) > anthropic_config(top_k=3) <- allows for dynamic variables to be passed in inference_params[k] = v data = json.dumps({"prompt": prompt, **inference_params}) + elif provider == "openai": + ## OpenAI imported models use OpenAI Chat Completions format (messages-based) + # Use AmazonBedrockOpenAIConfig for proper OpenAI transformation + openai_config = AmazonBedrockOpenAIConfig() + supported_params = openai_config.get_supported_openai_params(model=model) + + # Filter to only supported OpenAI params + filtered_params = { + k: v for k, v in inference_params.items() + if k in supported_params + } + + # OpenAI uses messages format, not prompt + data = json.dumps({"messages": messages, **filtered_params}) else: ## LOGGING logging_obj.pre_call( 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 6c389ff3b7d..bcb4cae1c8b 100644 --- a/litellm/llms/bedrock/chat/invoke_transformations/base_invoke_transformation.py +++ b/litellm/llms/bedrock/chat/invoke_transformations/base_invoke_transformation.py @@ -258,6 +258,15 @@ class AmazonInvokeConfig(BaseConfig, BaseAWSLLM): litellm_params=litellm_params, headers=headers, ) + elif provider == "openai": + # OpenAI imported models use OpenAI Chat Completions format + return litellm.AmazonBedrockOpenAIConfig().transform_request( + model=model, + messages=messages, + optional_params=optional_params, + litellm_params=litellm_params, + headers=headers, + ) else: raise BedrockError( status_code=404, diff --git a/tests/llm_translation/test_bedrock_completion.py b/tests/llm_translation/test_bedrock_completion.py index f43e939c681..bd08d4444f6 100644 --- a/tests/llm_translation/test_bedrock_completion.py +++ b/tests/llm_translation/test_bedrock_completion.py @@ -3531,3 +3531,336 @@ def test_bedrock_openai_imported_model(): # Check max_tokens and temperature assert request_body["max_tokens"] == 300 assert request_body["temperature"] == 0.5 + +def test_bedrock_openai_provider_detection(): + """ + Test that the OpenAI provider is correctly detected from model strings. + """ + from litellm.llms.bedrock.base_aws_llm import BaseAWSLLM + + # Test various OpenAI model formats + test_cases = [ + "openai/arn:aws:bedrock:us-east-1:123456789012:imported-model/abc123", + "bedrock/openai/arn:aws:bedrock:us-east-1:123456789012:imported-model/xyz789", + ] + + for model in test_cases: + provider = BaseAWSLLM.get_bedrock_invoke_provider(model) + assert provider == "openai", f"Failed for model: {model}, got provider: {provider}" + print(f"✓ Provider detection works for: {model}") + + +def test_bedrock_openai_model_id_extraction(): + """ + Test that the model ID (ARN) is correctly extracted and encoded for OpenAI models. + """ + from litellm.llms.bedrock.base_aws_llm import BaseAWSLLM + + model = "openai/arn:aws:bedrock:us-east-1:123456789012:imported-model/test-model-123" + provider = BaseAWSLLM.get_bedrock_invoke_provider(model) + + model_id = BaseAWSLLM.get_bedrock_model_id( + model=model, + provider=provider, + optional_params={} + ) + + # The ARN should be double URL encoded + assert "arn" in model_id + assert "imported-model" in model_id + print(f"✓ Model ID extracted and encoded: {model_id}") + + +def test_bedrock_openai_convert_messages_to_prompt(): + """ + Test that convert_messages_to_prompt returns empty string for OpenAI models. + """ + from litellm.llms.bedrock.chat.invoke_handler import BedrockLLM + + bedrock_llm = BedrockLLM() + messages = [ + {"role": "system", "content": "You are helpful"}, + {"role": "user", "content": "Hello"} + ] + + prompt, chat_history = bedrock_llm.convert_messages_to_prompt( + model="test-model", + messages=messages, + provider="openai", + custom_prompt_dict={} + ) + + # OpenAI models use messages directly, no prompt conversion + assert prompt == "" + assert chat_history is None + print("✓ convert_messages_to_prompt returns empty for OpenAI") + + +def test_bedrock_openai_response_parsing(): + """ + Test that OpenAI responses are correctly parsed. + """ + from litellm.llms.bedrock.chat.invoke_handler import BedrockLLM + from litellm import ModelResponse + from unittest.mock import Mock + import json + + bedrock_llm = BedrockLLM() + + # Mock OpenAI-style response + openai_response = { + "choices": [ + { + "message": { + "content": "The capital of France is Paris.", + "role": "assistant" + }, + "finish_reason": "stop", + "index": 0 + } + ], + "usage": { + "prompt_tokens": 10, + "completion_tokens": 8, + "total_tokens": 18 + } + } + + mock_response = Mock() + mock_response.json.return_value = openai_response + mock_response.text = json.dumps(openai_response) + mock_response.status_code = 200 + mock_response.headers = {} + + model_response = ModelResponse() + mock_logging = Mock() + + result = bedrock_llm.process_response( + model="openai/arn:aws:bedrock:us-east-1:123:imported-model/test", + response=mock_response, + model_response=model_response, + stream=False, + logging_obj=mock_logging, + optional_params={}, + api_key="", + data={}, + messages=[{"role": "user", "content": "What is the capital of France?"}], + print_verbose=lambda x: None, + encoding=None + ) + + # Verify response content + assert result.choices[0].message.content == "The capital of France is Paris." + assert result.choices[0].finish_reason == "stop" + + # Verify usage + assert result.usage.prompt_tokens == 10 + assert result.usage.completion_tokens == 8 + assert result.usage.total_tokens == 18 + + print("✓ OpenAI response parsing works correctly") + + +def test_bedrock_openai_request_transformation(): + """ + Test that the request is correctly transformed for OpenAI models. + """ + from litellm.llms.bedrock.chat.invoke_transformations.base_invoke_transformation import AmazonInvokeConfig + + config = AmazonInvokeConfig() + + model = "openai/arn:aws:bedrock:us-east-1:123:imported-model/test" + messages = [ + {"role": "system", "content": "You are helpful"}, + {"role": "user", "content": "Hello"} + ] + + optional_params = { + "max_tokens": 100, + "temperature": 0.7, + "top_p": 0.9, + "stream": False + } + + litellm_params = {} + headers = {} + + with patch.object(config, 'get_bedrock_invoke_provider', return_value="openai"): + result = config.transform_request( + model=model, + messages=messages, + optional_params=optional_params.copy(), + litellm_params=litellm_params, + headers=headers + ) + + # Verify the request uses messages format (not prompt) + assert "messages" in result + assert len(result["messages"]) == 2 + assert result["messages"][0]["role"] == "system" + assert result["messages"][1]["role"] == "user" + + # Verify parameters are included + assert "max_tokens" in result + assert "temperature" in result + + print("✓ Request transformation works correctly") + + +def test_bedrock_openai_parameter_filtering(): + """ + Test that only supported OpenAI parameters are included in the request. + """ + from litellm.llms.bedrock.chat.invoke_transformations.amazon_openai_transformation import AmazonBedrockOpenAIConfig + + config = AmazonBedrockOpenAIConfig() + model = "test-model" + + supported_params = config.get_supported_openai_params(model=model) + + # Verify common OpenAI parameters are supported + assert "max_tokens" in supported_params + assert "temperature" in supported_params + assert "top_p" in supported_params + assert "stream" in supported_params + assert "stop" in supported_params + + print(f"✓ Parameter filtering supports: {len(supported_params)} parameters") + print(f" Supported params: {supported_params}") + + +def test_bedrock_openai_route_detection(): + """ + Test that the OpenAI route is correctly detected. + """ + from litellm.llms.bedrock.common_utils import BedrockModelInfo + + test_cases = [ + ("openai/arn:aws:bedrock:us-east-1:123:imported-model/test", "openai"), + ("bedrock/openai/arn:aws:bedrock:us-east-1:123:imported-model/test", "openai"), + ] + + for model, expected_route in test_cases: + route = BedrockModelInfo.get_bedrock_route(model) + assert route == expected_route, f"Failed for model: {model}, got route: {route}" + print(f"✓ Route detection works for: {model} -> {route}") + + +def test_bedrock_openai_explicit_route_check(): + """ + Test the explicit OpenAI route checker helper method. + """ + from litellm.llms.bedrock.common_utils import BedrockModelInfo + + # Test with openai/ prefix + assert BedrockModelInfo._explicit_openai_route("openai/arn:aws:bedrock:us-east-1:123:imported-model/test") is True + assert BedrockModelInfo._explicit_openai_route("bedrock/openai/arn:aws:bedrock:us-east-1:123:imported-model/test") is True + + # Test without openai/ prefix + assert BedrockModelInfo._explicit_openai_route("anthropic.claude-3-sonnet") is False + assert BedrockModelInfo._explicit_openai_route("arn:aws:bedrock:us-east-1:123:imported-model/test") is False + + print("✓ Explicit route check works correctly") + + +def test_bedrock_openai_config_initialization(): + """ + Test that AmazonBedrockOpenAIConfig can be properly initialized. + """ + from litellm.llms.bedrock.chat.invoke_transformations.amazon_openai_transformation import AmazonBedrockOpenAIConfig + + config = AmazonBedrockOpenAIConfig() + + # Verify it has the necessary methods + assert hasattr(config, 'get_supported_openai_params') + assert hasattr(config, 'transform_request') + assert hasattr(config, 'transform_response') + assert hasattr(config, 'map_openai_params') + + print("✓ AmazonBedrockOpenAIConfig initializes correctly") + + +def test_bedrock_openai_multiple_message_types(): + """ + Test that various message content types are handled correctly. + """ + from litellm.llms.custom_httpx.http_handler import HTTPHandler + + client = HTTPHandler() + + # Test with mixed content types + messages = [ + {"role": "system", "content": "You are helpful"}, + {"role": "user", "content": "Simple text message"}, + { + "role": "user", + "content": [ + {"type": "text", "text": "Complex message with text"}, + {"type": "image_url", "image_url": {"url": "data:image/jpeg;base64,iVBORw0KGg"}} + ] + } + ] + + with patch.object(client, "post") as mock_post: + try: + response = completion( + model="bedrock/openai/arn:aws:bedrock:us-east-1:123:imported-model/test", + messages=messages, + max_tokens=50, + client=client, + ) + except Exception as e: + pass + + # Verify the request was made + if mock_post.called: + request_body = json.loads(mock_post.call_args.kwargs["data"]) + + # Verify messages are preserved + assert "messages" in request_body + assert len(request_body["messages"]) == 3 + + # Verify mixed content is handled + assert isinstance(request_body["messages"][2]["content"], list) + + print("✓ Multiple message types handled correctly") + + +def test_bedrock_openai_error_handling(): + """ + Test that errors from OpenAI models are properly handled. + """ + from litellm.llms.bedrock.chat.invoke_handler import BedrockLLM + from litellm import ModelResponse + from litellm.llms.bedrock.common_utils import BedrockError + from unittest.mock import Mock + import json + + bedrock_llm = BedrockLLM() + + # Mock error response + mock_response = Mock() + mock_response.json.side_effect = Exception("Invalid JSON") + mock_response.text = "Invalid response" + mock_response.status_code = 422 + + model_response = ModelResponse() + mock_logging = Mock() + + with pytest.raises(BedrockError) as exc_info: + bedrock_llm.process_response( + model="openai/arn:aws:bedrock:us-east-1:123:imported-model/test", + response=mock_response, + model_response=model_response, + stream=False, + logging_obj=mock_logging, + optional_params={}, + api_key="", + data={}, + messages=[], + print_verbose=lambda x: None, + encoding=None + ) + + assert exc_info.value.status_code == 422 + print("✓ Error handling works correctly") From 6ee9d9c344ce4f383e2f72ff73ba7e17c1398536 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Tue, 2 Dec 2025 11:19:27 -0800 Subject: [PATCH 015/107] /login route refactor --- litellm/proxy/auth/login_utils.py | 324 ++++++++++++++++++ litellm/proxy/proxy_server.py | 271 ++------------- .../proxy/auth/test_login_utils.py | 248 ++++++++++++++ 3 files changed, 604 insertions(+), 239 deletions(-) create mode 100644 litellm/proxy/auth/login_utils.py create mode 100644 tests/test_litellm/proxy/auth/test_login_utils.py diff --git a/litellm/proxy/auth/login_utils.py b/litellm/proxy/auth/login_utils.py new file mode 100644 index 00000000000..94f2f698e04 --- /dev/null +++ b/litellm/proxy/auth/login_utils.py @@ -0,0 +1,324 @@ +""" +Login utilities for handling user authentication in the proxy server. + +This module contains the core login logic that can be reused across different +login endpoints (e.g., /login and /v2/login). +""" + +import os +import secrets +from typing import Literal, Optional, cast + +import litellm +from fastapi import HTTPException + +from litellm.constants import LITELLM_PROXY_ADMIN_NAME +from litellm.proxy._types import ( + LiteLLM_UserTable, + LitellmUserRoles, + ProxyErrorTypes, + ProxyException, + UpdateUserRequest, + UserAPIKeyAuth, + hash_token, +) +from litellm.proxy.management_endpoints.internal_user_endpoints import user_update +from litellm.proxy.management_endpoints.key_management_endpoints import ( + generate_key_helper_fn, +) +from litellm.proxy.management_endpoints.ui_sso import ( + get_disabled_non_admin_personal_key_creation, +) +from litellm.proxy.utils import PrismaClient, get_server_root_path +from litellm.secret_managers.main import get_secret_bool +from litellm.types.proxy.ui_sso import ReturnedUITokenObject + + +class LoginResult: + """Result object containing authentication data from login.""" + + def __init__( + self, + user_id: str, + key: str, + user_email: Optional[str], + user_role: str, + login_method: str = "username_password", + ): + self.user_id = user_id + self.key = key + self.user_email = user_email + self.user_role = user_role + self.login_method = login_method + + +async def authenticate_user( + username: str, + password: str, + master_key: Optional[str], + prisma_client: Optional[PrismaClient], +) -> LoginResult: + """ + Authenticate a user and generate an API key for UI access. + + This function handles two login scenarios: + 1. Admin login using UI_USERNAME and UI_PASSWORD + 2. User login using email and password from database + + Args: + username: Username or email from the login form + password: Password from the login form + master_key: Master key for the proxy (required) + prisma_client: Prisma database client (optional) + + Returns: + LoginResult: Object containing authentication data + + Raises: + ProxyException: If authentication fails or required configuration is missing + """ + if master_key is None: + raise ProxyException( + message="Master Key not set for Proxy. Please set Master Key to use Admin UI. Set `LITELLM_MASTER_KEY` in .env or set general_settings:master_key in config.yaml. https://docs.litellm.ai/docs/proxy/virtual_keys. If set, use `--detailed_debug` to debug issue.", + type=ProxyErrorTypes.auth_error, + param="master_key", + code=500, + ) + + ui_username = os.getenv("UI_USERNAME", "admin") + ui_password = os.getenv("UI_PASSWORD", None) + if ui_password is None: + ui_password = str(master_key) if master_key is not None else None + if ui_password is None: + raise ProxyException( + message="set Proxy master key to use UI. https://docs.litellm.ai/docs/proxy/virtual_keys. If set, use `--detailed_debug` to debug issue.", + type=ProxyErrorTypes.auth_error, + param="UI_PASSWORD", + code=500, + ) + + # Check if we can find the `username` in the db. On the UI, users can enter username=their email + _user_row: Optional[LiteLLM_UserTable] = None + user_role: Optional[ + Literal[ + LitellmUserRoles.PROXY_ADMIN, + LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY, + LitellmUserRoles.INTERNAL_USER, + LitellmUserRoles.INTERNAL_USER_VIEW_ONLY, + ] + ] = None + + if prisma_client is not None: + _user_row = cast( + Optional[LiteLLM_UserTable], + await prisma_client.db.litellm_usertable.find_first( + where={"user_email": {"equals": username}} + ), + ) + + disabled_non_admin_personal_key_creation = ( + get_disabled_non_admin_personal_key_creation() + ) + + """ + To login to Admin UI, we support the following + - Login with UI_USERNAME and UI_PASSWORD + - Login with Invite Link `user_email` and `password` combination + """ + if secrets.compare_digest(username, ui_username) and secrets.compare_digest( + password, ui_password + ): + # Non SSO -> If user is using UI_USERNAME and UI_PASSWORD they are Proxy admin + user_role = LitellmUserRoles.PROXY_ADMIN + user_id = LITELLM_PROXY_ADMIN_NAME + + # we want the key created to have PROXY_ADMIN_PERMISSIONS + key_user_id = LITELLM_PROXY_ADMIN_NAME + if ( + os.getenv("PROXY_ADMIN_ID", None) is not None + and os.environ["PROXY_ADMIN_ID"] == user_id + ) or user_id == LITELLM_PROXY_ADMIN_NAME: + # checks if user is admin + key_user_id = os.getenv("PROXY_ADMIN_ID", LITELLM_PROXY_ADMIN_NAME) + + # Admin is Authe'd in - generate key for the UI to access Proxy + + # ensure this user is set as the proxy admin, in this route there is no sso, we can assume this user is only the admin + await user_update( + data=UpdateUserRequest( + user_id=key_user_id, + user_role=user_role, + ), + user_api_key_dict=UserAPIKeyAuth( + user_role=LitellmUserRoles.PROXY_ADMIN, + ), + ) + + if os.getenv("DATABASE_URL") is not None: + response = await generate_key_helper_fn( + request_type="key", + **{ + "user_role": LitellmUserRoles.PROXY_ADMIN, + "duration": "24hr", + "key_max_budget": litellm.max_ui_session_budget, + "models": [], + "aliases": {}, + "config": {}, + "spend": 0, + "user_id": key_user_id, + "team_id": "litellm-dashboard", + }, # type: ignore + ) + else: + raise ProxyException( + message="No Database connected. Set DATABASE_URL in .env. If set, use `--detailed_debug` to debug issue.", + type=ProxyErrorTypes.auth_error, + param="DATABASE_URL", + code=500, + ) + + key = response["token"] # type: ignore + + if get_secret_bool("EXPERIMENTAL_UI_LOGIN"): + user_info: Optional[LiteLLM_UserTable] = None + if _user_row is not None: + user_info = _user_row + elif ( + user_id is not None + ): # if user_id is not None, we are using the UI_USERNAME and UI_PASSWORD + from litellm.proxy.auth.auth_checks import ExperimentalUIJWTToken + + user_info = LiteLLM_UserTable( + user_id=user_id, + user_role=user_role, + models=[], + max_budget=litellm.max_ui_session_budget, + ) + if user_info is None: + raise HTTPException( + status_code=401, + detail={ + "error": "User Information is required for experimental UI login" + }, + ) + + key = ExperimentalUIJWTToken.get_experimental_ui_login_jwt_auth_token( + user_info + ) + + return LoginResult( + user_id=user_id, + key=key, + user_email=None, + user_role=user_role, + login_method="username_password", + ) + + elif _user_row is not None: + """ + When sharing invite links + + -> if the user has no role in the DB assume they are only a viewer + """ + user_id = getattr(_user_row, "user_id", "unknown") + user_role = getattr( + _user_row, "user_role", LitellmUserRoles.INTERNAL_USER_VIEW_ONLY + ) + user_email = getattr(_user_row, "user_email", "unknown") + _password = getattr(_user_row, "password", "unknown") + + if _password is None: + raise ProxyException( + message="User has no password set. Please set a password for the user via `/user/update`.", + type=ProxyErrorTypes.auth_error, + param="password", + code=401, + ) + + # check if password == _user_row.password + hash_password = hash_token(token=password) + if secrets.compare_digest(password, _password) or secrets.compare_digest( + hash_password, _password + ): + if os.getenv("DATABASE_URL") is not None: + response = await generate_key_helper_fn( + request_type="key", + **{ # type: ignore + "user_role": user_role, + "duration": "24hr", + "key_max_budget": litellm.max_ui_session_budget, + "models": [], + "aliases": {}, + "config": {}, + "spend": 0, + "user_id": user_id, + "team_id": "litellm-dashboard", + }, + ) + else: + raise ProxyException( + message="No Database connected. Set DATABASE_URL in .env. If set, use `--detailed_debug` to debug issue.", + type=ProxyErrorTypes.auth_error, + param="DATABASE_URL", + code=500, + ) + + key = response["token"] # type: ignore + + return LoginResult( + user_id=user_id, + key=key, + user_email=user_email, + user_role=cast(str, user_role), + login_method="username_password", + ) + else: + raise ProxyException( + message=f"Invalid credentials used to access UI.\nNot valid credentials for {username}", + type=ProxyErrorTypes.auth_error, + param="invalid_credentials", + code=401, + ) + else: + raise ProxyException( + message="Invalid credentials used to access UI.\nCheck 'UI_USERNAME', 'UI_PASSWORD' in .env file", + type=ProxyErrorTypes.auth_error, + param="invalid_credentials", + code=401, + ) + + +def create_ui_token_object( + login_result: LoginResult, + general_settings: dict, + premium_user: bool, +) -> ReturnedUITokenObject: + """ + Create a ReturnedUITokenObject from a LoginResult. + + Args: + login_result: The result from authenticate_user + general_settings: General proxy settings dictionary + premium_user: Whether premium features are enabled + + Returns: + ReturnedUITokenObject: Token object ready for JWT encoding + """ + disabled_non_admin_personal_key_creation = ( + get_disabled_non_admin_personal_key_creation() + ) + + return ReturnedUITokenObject( + user_id=login_result.user_id, + key=login_result.key, + user_email=login_result.user_email, + user_role=login_result.user_role, + login_method=login_result.login_method, + premium_user=premium_user, + auth_header_name=general_settings.get( + "litellm_key_header_name", "Authorization" + ), + disabled_non_admin_personal_key_creation=disabled_non_admin_personal_key_creation, + server_root_path=get_server_root_path(), + ) + diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 99789162318..790c80b97da 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -8266,256 +8266,49 @@ async def fallback_login(request: Request): ) # hidden since this is a helper for UI sso login async def login(request: Request): # noqa: PLR0915 global premium_user, general_settings, master_key - from litellm.types.proxy.ui_sso import ReturnedUITokenObject + from litellm.proxy.auth.login_utils import authenticate_user, create_ui_token_object + from litellm.proxy.utils import get_custom_url - if master_key is None: - raise ProxyException( - message="Master Key not set for Proxy. Please set Master Key to use Admin UI. Set `LITELLM_MASTER_KEY` in .env or set general_settings:master_key in config.yaml. https://docs.litellm.ai/docs/proxy/virtual_keys. If set, use `--detailed_debug` to debug issue.", - type=ProxyErrorTypes.auth_error, - param="master_key", - code=status.HTTP_500_INTERNAL_SERVER_ERROR, - ) form = await request.form() username = str(form.get("username")) password = str(form.get("password")) - ui_username = os.getenv("UI_USERNAME", "admin") - ui_password = os.getenv("UI_PASSWORD", None) - if ui_password is None: - ui_password = str(master_key) if master_key is not None else None - if ui_password is None: - raise ProxyException( - message="set Proxy master key to use UI. https://docs.litellm.ai/docs/proxy/virtual_keys. If set, use `--detailed_debug` to debug issue.", - type=ProxyErrorTypes.auth_error, - param="UI_PASSWORD", - code=status.HTTP_500_INTERNAL_SERVER_ERROR, - ) - # check if we can find the `username` in the db. on the ui, users can enter username=their email - _user_row: Optional[LiteLLM_UserTable] = None - user_role: Optional[ - Literal[ - LitellmUserRoles.PROXY_ADMIN, - LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY, - LitellmUserRoles.INTERNAL_USER, - LitellmUserRoles.INTERNAL_USER_VIEW_ONLY, - ] - ] = None - if prisma_client is not None: - _user_row = cast( - Optional[LiteLLM_UserTable], - await prisma_client.db.litellm_usertable.find_first( - where={"user_email": {"equals": username}} - ), - ) - disabled_non_admin_personal_key_creation = ( - get_disabled_non_admin_personal_key_creation() + # Authenticate user and get login result + login_result = await authenticate_user( + username=username, + password=password, + master_key=master_key, + prisma_client=prisma_client, ) - """ - To login to Admin UI, we support the following - - Login with UI_USERNAME and UI_PASSWORD - - Login with Invite Link `user_email` and `password` combination - """ - if secrets.compare_digest(username, ui_username) and secrets.compare_digest( - password, ui_password - ): - # Non SSO -> If user is using UI_USERNAME and UI_PASSWORD they are Proxy admin - user_role = LitellmUserRoles.PROXY_ADMIN - user_id = litellm_proxy_admin_name - # we want the key created to have PROXY_ADMIN_PERMISSIONS - key_user_id = litellm_proxy_admin_name - if ( - os.getenv("PROXY_ADMIN_ID", None) is not None - and os.environ["PROXY_ADMIN_ID"] == user_id - ) or user_id == litellm_proxy_admin_name: - # checks if user is admin - key_user_id = os.getenv("PROXY_ADMIN_ID", litellm_proxy_admin_name) + # Create UI token object + returned_ui_token_object = create_ui_token_object( + login_result=login_result, + general_settings=general_settings, + premium_user=premium_user, + ) - # Admin is Authe'd in - generate key for the UI to access Proxy + # Generate JWT token + import jwt - # ensure this user is set as the proxy admin, in this route there is no sso, we can assume this user is only the admin - await user_update( - data=UpdateUserRequest( - user_id=key_user_id, - user_role=user_role, - ), - user_api_key_dict=UserAPIKeyAuth( - user_role=LitellmUserRoles.PROXY_ADMIN, - ), - ) - if os.getenv("DATABASE_URL") is not None: - response = await generate_key_helper_fn( - request_type="key", - **{ - "user_role": LitellmUserRoles.PROXY_ADMIN, - "duration": "24hr", - "key_max_budget": litellm.max_ui_session_budget, - "models": [], - "aliases": {}, - "config": {}, - "spend": 0, - "user_id": key_user_id, - "team_id": "litellm-dashboard", - }, # type: ignore - ) - else: - raise ProxyException( - message="No Database connected. Set DATABASE_URL in .env. If set, use `--detailed_debug` to debug issue.", - type=ProxyErrorTypes.auth_error, - param="DATABASE_URL", - code=status.HTTP_500_INTERNAL_SERVER_ERROR, - ) - key = response["token"] # type: ignore - litellm_dashboard_ui = get_custom_url(str(request.base_url)) - if litellm_dashboard_ui.endswith("/"): - litellm_dashboard_ui += "ui/" - else: - litellm_dashboard_ui += "/ui/" - import jwt + jwt_token = jwt.encode( # type: ignore + cast(dict, returned_ui_token_object), + master_key, + algorithm="HS256", + ) - if get_secret_bool("EXPERIMENTAL_UI_LOGIN"): - user_info: Optional[LiteLLM_UserTable] = None - if _user_row is not None: - user_info = _user_row - elif ( - user_id is not None - ): # if user_id is not None, we are using the UI_USERNAME and UI_PASSWORD - user_info = LiteLLM_UserTable( - user_id=user_id, - user_role=user_role, - models=[], - max_budget=litellm.max_ui_session_budget, - ) - if user_info is None: - raise HTTPException( - status_code=401, - detail={ - "error": "User Information is required for experimental UI login" - }, - ) - - key = ExperimentalUIJWTToken.get_experimental_ui_login_jwt_auth_token( - user_info - ) - - returned_ui_token_object = ReturnedUITokenObject( - user_id=user_id, - key=key, - user_email=None, - user_role=user_role, - login_method="username_password", - premium_user=premium_user, - auth_header_name=general_settings.get( - "litellm_key_header_name", "Authorization" - ), - disabled_non_admin_personal_key_creation=disabled_non_admin_personal_key_creation, - server_root_path=get_server_root_path(), - ) - - jwt_token = jwt.encode( # type: ignore - cast(dict, returned_ui_token_object), - master_key, - algorithm="HS256", - ) - litellm_dashboard_ui += "?login=success" - redirect_response = RedirectResponse(url=litellm_dashboard_ui, status_code=303) - redirect_response.set_cookie(key="token", value=jwt_token) - return redirect_response - elif _user_row is not None: - """ - When sharing invite links - - -> if the user has no role in the DB assume they are only a viewer - """ - user_id = getattr(_user_row, "user_id", "unknown") - user_role = getattr( - _user_row, "user_role", LitellmUserRoles.INTERNAL_USER_VIEW_ONLY - ) - user_email = getattr(_user_row, "user_email", "unknown") - _password = getattr(_user_row, "password", "unknown") - - if _password is None: - raise ProxyException( - message="User has no password set. Please set a password for the user via `/user/update`.", - type=ProxyErrorTypes.auth_error, - param="password", - code=status.HTTP_401_UNAUTHORIZED, - ) - - # check if password == _user_row.password - hash_password = hash_token(token=password) - if secrets.compare_digest(password, _password) or secrets.compare_digest( - hash_password, _password - ): - if os.getenv("DATABASE_URL") is not None: - response = await generate_key_helper_fn( - request_type="key", - **{ # type: ignore - "user_role": user_role, - "duration": "24hr", - "key_max_budget": litellm.max_ui_session_budget, - "models": [], - "aliases": {}, - "config": {}, - "spend": 0, - "user_id": user_id, - "team_id": "litellm-dashboard", - }, - ) - else: - raise ProxyException( - message="No Database connected. Set DATABASE_URL in .env. If set, use `--detailed_debug` to debug issue.", - type=ProxyErrorTypes.auth_error, - param="DATABASE_URL", - code=status.HTTP_500_INTERNAL_SERVER_ERROR, - ) - key = response["token"] # type: ignore - litellm_dashboard_ui = get_custom_url(str(request.base_url)) - if litellm_dashboard_ui.endswith("/"): - litellm_dashboard_ui += "ui/" - else: - litellm_dashboard_ui += "/ui/" - import jwt - - returned_ui_token_object = ReturnedUITokenObject( - user_id=user_id, - key=key, - user_email=user_email, - user_role=cast(str, user_role), - login_method="username_password", - premium_user=premium_user, - auth_header_name=general_settings.get( - "litellm_key_header_name", "Authorization" - ), - disabled_non_admin_personal_key_creation=disabled_non_admin_personal_key_creation, - server_root_path=get_server_root_path(), - ) - - jwt_token = jwt.encode( # type: ignore - cast(dict, returned_ui_token_object), - master_key, - algorithm="HS256", - ) - litellm_dashboard_ui += "?login=success" - redirect_response = RedirectResponse( - url=litellm_dashboard_ui, status_code=303 - ) - redirect_response.set_cookie(key="token", value=jwt_token) - return redirect_response - else: - raise ProxyException( - message=f"Invalid credentials used to access UI.\nNot valid credentials for {username}", - type=ProxyErrorTypes.auth_error, - param="invalid_credentials", - code=status.HTTP_401_UNAUTHORIZED, - ) + # Build redirect URL + litellm_dashboard_ui = get_custom_url(str(request.base_url)) + if litellm_dashboard_ui.endswith("/"): + litellm_dashboard_ui += "ui/" else: - raise ProxyException( - message="Invalid credentials used to access UI.\nCheck 'UI_USERNAME', 'UI_PASSWORD' in .env file", - type=ProxyErrorTypes.auth_error, - param="invalid_credentials", - code=status.HTTP_401_UNAUTHORIZED, - ) + litellm_dashboard_ui += "/ui/" + litellm_dashboard_ui += "?login=success" + + # Create redirect response with cookie + redirect_response = RedirectResponse(url=litellm_dashboard_ui, status_code=303) + redirect_response.set_cookie(key="token", value=jwt_token) + return redirect_response @app.get("/onboarding/get_token", include_in_schema=False) diff --git a/tests/test_litellm/proxy/auth/test_login_utils.py b/tests/test_litellm/proxy/auth/test_login_utils.py new file mode 100644 index 00000000000..abb7b58c292 --- /dev/null +++ b/tests/test_litellm/proxy/auth/test_login_utils.py @@ -0,0 +1,248 @@ +""" +Tests for login_utils module. + +This module tests the refactored login logic that was moved from proxy_server.py +to login_utils.py for better reusability. +""" + +import os +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from litellm.constants import LITELLM_PROXY_ADMIN_NAME +from litellm.proxy._types import ( + LiteLLM_UserTable, + LitellmUserRoles, + ProxyErrorTypes, + ProxyException, + hash_token, +) +from litellm.proxy.auth.login_utils import LoginResult, authenticate_user + + +@pytest.mark.asyncio +async def test_authenticate_user_admin_login_with_ui_credentials(): + """Test admin login using UI_USERNAME and UI_PASSWORD""" + master_key = "sk-1234" + ui_username = "admin" + ui_password = "sk-1234" + + mock_prisma_client = MagicMock() + mock_prisma_client.db.litellm_usertable.find_first = AsyncMock(return_value=None) + + with patch.dict( + os.environ, + { + "UI_USERNAME": ui_username, + "UI_PASSWORD": ui_password, + "DATABASE_URL": "postgresql://test:test@localhost/test", + }, + ): + with patch( + "litellm.proxy.auth.login_utils.generate_key_helper_fn", + new_callable=AsyncMock, + ) as mock_generate_key: + mock_generate_key.return_value = { + "token": "test-token-123", + "user_id": LITELLM_PROXY_ADMIN_NAME, + } + + with patch( + "litellm.proxy.auth.login_utils.user_update", + new_callable=AsyncMock, + return_value=None, + ) as mock_user_update: + with patch( + "litellm.proxy.auth.login_utils.get_secret_bool", + return_value=False, + ): + result = await authenticate_user( + username=ui_username, + password=ui_password, + master_key=master_key, + prisma_client=mock_prisma_client, + ) + + assert isinstance(result, LoginResult) + assert result.user_id == LITELLM_PROXY_ADMIN_NAME + assert result.key == "test-token-123" + assert result.user_email is None + assert result.user_role == LitellmUserRoles.PROXY_ADMIN + assert result.login_method == "username_password" + + +@pytest.mark.asyncio +async def test_authenticate_user_admin_login_with_master_key_as_password(): + """Test admin login when UI_PASSWORD is not set, should use master_key""" + master_key = "sk-1234" + ui_username = "admin" + + mock_prisma_client = MagicMock() + mock_prisma_client.db.litellm_usertable.find_first = AsyncMock(return_value=None) + + env_vars = {"UI_USERNAME": ui_username, "DATABASE_URL": "postgresql://test:test@localhost/test"} + # Remove UI_PASSWORD to test fallback to master_key + if "UI_PASSWORD" in os.environ: + # Keep other env vars but don't set UI_PASSWORD + pass + else: + # Ensure UI_PASSWORD is not in the patched env + pass + + with patch.dict(os.environ, env_vars, clear=False): + # Explicitly remove UI_PASSWORD if it exists + original_ui_password = os.environ.pop("UI_PASSWORD", None) + try: + with patch( + "litellm.proxy.auth.login_utils.generate_key_helper_fn", + new_callable=AsyncMock, + ) as mock_generate_key: + mock_generate_key.return_value = { + "token": "test-token-123", + "user_id": LITELLM_PROXY_ADMIN_NAME, + } + + with patch( + "litellm.proxy.auth.login_utils.user_update", + new_callable=AsyncMock, + return_value=None, + ) as mock_user_update: + with patch( + "litellm.proxy.auth.login_utils.get_secret_bool", + return_value=False, + ): + result = await authenticate_user( + username=ui_username, + password=master_key, + master_key=master_key, + prisma_client=mock_prisma_client, + ) + + assert isinstance(result, LoginResult) + assert result.user_id == LITELLM_PROXY_ADMIN_NAME + assert result.user_role == LitellmUserRoles.PROXY_ADMIN + finally: + if original_ui_password: + os.environ["UI_PASSWORD"] = original_ui_password + +@pytest.mark.asyncio +async def test_authenticate_user_invalid_credentials(): + """Test authentication failure with invalid credentials""" + master_key = "sk-1234" + ui_username = "admin" + wrong_password = "wrong-password" + + mock_prisma_client = MagicMock() + mock_prisma_client.db.litellm_usertable.find_first = AsyncMock(return_value=None) + + with patch.dict(os.environ, {"UI_USERNAME": ui_username, "UI_PASSWORD": "correct-password"}): + with pytest.raises(ProxyException) as exc_info: + await authenticate_user( + username=ui_username, + password=wrong_password, + master_key=master_key, + prisma_client=mock_prisma_client, + ) + + assert exc_info.value.type == ProxyErrorTypes.auth_error + assert exc_info.value.code == "401" + assert "Invalid credentials" in exc_info.value.message + + +@pytest.mark.asyncio +async def test_authenticate_user_missing_master_key(): + """Test authentication failure when master_key is None""" + mock_prisma_client = MagicMock() + + with pytest.raises(ProxyException) as exc_info: + await authenticate_user( + username="admin", + password="password", + master_key=None, + prisma_client=mock_prisma_client, + ) + + assert exc_info.value.type == ProxyErrorTypes.auth_error + assert exc_info.value.code == "500" + assert "Master Key not set" in exc_info.value.message + + +@pytest.mark.asyncio +async def test_authenticate_user_wrong_password(): + """Test authentication failure with wrong password for database user""" + master_key = "sk-1234" + user_email = "test@example.com" + correct_password = "correct-password" + wrong_password = "wrong-password" + hashed_password = hash_token(token=correct_password) + + mock_user = LiteLLM_UserTable( + user_id="test-user-123", + user_email=user_email, + password=hashed_password, + user_role=LitellmUserRoles.INTERNAL_USER, + ) + + mock_prisma_client = MagicMock() + mock_prisma_client.db.litellm_usertable.find_first = AsyncMock( + return_value=mock_user + ) + + with patch.dict( + os.environ, + { + "DATABASE_URL": "postgresql://test:test@localhost/test", + "UI_USERNAME": "admin", + "UI_PASSWORD": "admin-password", + }, + ): + with pytest.raises(ProxyException) as exc_info: + await authenticate_user( + username=user_email, + password=wrong_password, + master_key=master_key, + prisma_client=mock_prisma_client, + ) + + assert exc_info.value.type == ProxyErrorTypes.auth_error + assert exc_info.value.code == "401" + assert "Invalid credentials" in exc_info.value.message + + +@pytest.mark.asyncio +async def test_authenticate_user_database_required_for_admin(): + """Test that database is required for admin login""" + master_key = "sk-1234" + ui_username = "admin" + ui_password = "sk-1234" + + mock_prisma_client = MagicMock() + mock_prisma_client.db.litellm_usertable.find_first = AsyncMock(return_value=None) + + with patch.dict(os.environ, {"UI_USERNAME": ui_username, "UI_PASSWORD": ui_password}): + with patch( + "litellm.proxy.auth.login_utils.user_update", + new_callable=AsyncMock, + return_value=None, + ): + # Remove DATABASE_URL to simulate no database + original_db_url = os.environ.get("DATABASE_URL") + if "DATABASE_URL" in os.environ: + del os.environ["DATABASE_URL"] + + try: + with pytest.raises(ProxyException) as exc_info: + await authenticate_user( + username=ui_username, + password=ui_password, + master_key=master_key, + prisma_client=mock_prisma_client, + ) + + assert exc_info.value.type == ProxyErrorTypes.auth_error + assert exc_info.value.code == "500" + assert "No Database connected" in exc_info.value.message + finally: + if original_db_url: + os.environ["DATABASE_URL"] = original_db_url From 8c1290dcd2bcb12cba8fab8dfd4c8a94e0c85382 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Tue, 2 Dec 2025 11:26:29 -0800 Subject: [PATCH 016/107] Indent and import fix --- .../guardrail_hooks/bedrock_guardrails.py | 51 ++++++++++--------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/litellm/proxy/guardrails/guardrail_hooks/bedrock_guardrails.py b/litellm/proxy/guardrails/guardrail_hooks/bedrock_guardrails.py index f9c3caf4944..bd1e805361c 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/bedrock_guardrails.py +++ b/litellm/proxy/guardrails/guardrail_hooks/bedrock_guardrails.py @@ -20,6 +20,7 @@ from typing import ( AsyncGenerator, List, Literal, + NamedTuple, Optional, Tuple, Union, @@ -1291,38 +1292,38 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM): request_data=request_data, ) - bedrock_response = await self.make_bedrock_api_request( - source="INPUT", - messages=mock_messages, - request_data=request_data, + bedrock_response = await self.make_bedrock_api_request( + source="INPUT", + messages=mock_messages, + request_data=request_data, + ) + + if bedrock_response.get("action") == "BLOCKED": + raise Exception( + f"Content blocked by Bedrock guardrail: {bedrock_response.get('reason', 'Unknown reason')}" ) - if bedrock_response.get("action") == "BLOCKED": - raise Exception( - f"Content blocked by Bedrock guardrail: {bedrock_response.get('reason', 'Unknown reason')}" - ) - - # Apply any masking that was applied by the guardrail - masked_text = text - output_list = bedrock_response.get("output") - if output_list: - # If the guardrail returned modified content, use that - for output_item in output_list: + # Apply any masking that was applied by the guardrail + masked_text = text + output_list = bedrock_response.get("output") + if output_list: + # If the guardrail returned modified content, use that + for output_item in output_list: + text_content = output_item.get("text") + if text_content: + masked_text = str(text_content) + break + else: + outputs_list = bedrock_response.get("outputs") + if outputs_list: + # Fallback to outputs field if output is not available + for output_item in outputs_list: text_content = output_item.get("text") if text_content: masked_text = str(text_content) break - else: - outputs_list = bedrock_response.get("outputs") - if outputs_list: - # Fallback to outputs field if output is not available - for output_item in outputs_list: - text_content = output_item.get("text") - if text_content: - masked_text = str(text_content) - break - masked_texts.append(masked_text) + masked_texts.append(masked_text) verbose_proxy_logger.debug( "Bedrock Guardrail: Successfully applied guardrail" From 8b8f93d508219079d1327124de00df864d52c571 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Tue, 2 Dec 2025 12:24:00 -0800 Subject: [PATCH 017/107] Add Google Private API Endpoint to Vertex AI fields --- .../proxy/public_endpoints/provider_create_fields.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/litellm/proxy/public_endpoints/provider_create_fields.json b/litellm/proxy/public_endpoints/provider_create_fields.json index 8e08bd8d991..67f2afa1e71 100644 --- a/litellm/proxy/public_endpoints/provider_create_fields.json +++ b/litellm/proxy/public_endpoints/provider_create_fields.json @@ -2607,6 +2607,16 @@ "options": null, "default_value": null }, + { + "key": "api_base", + "label": "Google Private API Endpoint", + "placeholder": null, + "tooltip": null, + "required": false, + "field_type": "text", + "options": null, + "default_value": null + }, { "key": "vertex_credentials", "label": "Vertex Credentials", From 102b8401b3b3bd2ea00d1df18311a37c9d380346 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Tue, 2 Dec 2025 12:32:51 -0800 Subject: [PATCH 018/107] Change labeling around Vertex Fields --- litellm/proxy/public_endpoints/provider_create_fields.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/litellm/proxy/public_endpoints/provider_create_fields.json b/litellm/proxy/public_endpoints/provider_create_fields.json index 67f2afa1e71..c7f88c9e826 100644 --- a/litellm/proxy/public_endpoints/provider_create_fields.json +++ b/litellm/proxy/public_endpoints/provider_create_fields.json @@ -2609,9 +2609,9 @@ }, { "key": "api_base", - "label": "Google Private API Endpoint", + "label": "API Base", "placeholder": null, - "tooltip": null, + "tooltip": "Supports custom API base for Google Private API endpoints.", "required": false, "field_type": "text", "options": null, From db6c6eea89ebf4ae93f7d9030c1e3d098f09038a Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Tue, 2 Dec 2025 14:10:00 -0800 Subject: [PATCH 019/107] [Docs] Add guide on how to debug gateway error vs provider error (#17387) * add error diagnosis * docs error diagnosis --- docs/my-website/docs/proxy/error_diagnosis.md | 90 +++++++++++++++++++ docs/my-website/sidebars.js | 1 + 2 files changed, 91 insertions(+) create mode 100644 docs/my-website/docs/proxy/error_diagnosis.md diff --git a/docs/my-website/docs/proxy/error_diagnosis.md b/docs/my-website/docs/proxy/error_diagnosis.md new file mode 100644 index 00000000000..9629fc52b0c --- /dev/null +++ b/docs/my-website/docs/proxy/error_diagnosis.md @@ -0,0 +1,90 @@ +# Diagnosing Errors - Provider vs Gateway + +Having trouble diagnosing if an error is from the **LLM Provider** (OpenAI, Anthropic, etc.) or from the **LiteLLM AI Gateway** itself? Here's how to tell. + +## Quick Rule + +**If the error contains `Exception`, it's from the provider.** + +| Error Contains | Error Source | +|----------------|--------------| +| `AnthropicException` | Anthropic | +| `OpenAIException` | OpenAI | +| `AzureException` | Azure | +| `BedrockException` | AWS Bedrock | +| `VertexAIException` | Google Vertex AI | +| No provider name | LiteLLM AI Gateway | + +## Examples + +### Provider Error (from AWS Bedrock) + +``` +{ + "error": { + "message": "litellm.BadRequestError: BedrockException - {\"message\":\"The model returned the following errors: messages.1.content.0.type: Expected `thinking` or `redacted_thinking`, but found `text`.\"}", + "type": "invalid_request_error", + "param": null, + "code": "400" + } +} +``` + +This error is from **AWS Bedrock** (notice `BedrockException`). The Bedrock API is rejecting the request due to invalid message format - this is not a LiteLLM issue. + +### Provider Error (from OpenAI) + +``` +{ + "error": { + "message": "litellm.AuthenticationError: OpenAIException - Incorrect API key provided: . You can find your API key at https://platform.openai.com/account/api-keys.", + "type": "invalid_request_error", + "param": null, + "code": "invalid_api_key" + } +} +``` + +This error is from **OpenAI** (notice `OpenAIException`). The OpenAI API key configured in LiteLLM is invalid. + +### Provider Error (from Anthropic) + +``` +{ + "error": { + "message": "litellm.InternalServerError: AnthropicException - Overloaded. Handle with `litellm.InternalServerError`.", + "type": "internal_server_error", + "param": null, + "code": "500" + } +} +``` + +This error is from **Anthropic** (notice `AnthropicException`). The Anthropic API is overloaded - this is not a LiteLLM issue. + +### Gateway Error (from LiteLLM) + +``` +{ + "error": { + "message": "Invalid API Key. Please check your LiteLLM API key.", + "type": "auth_error", + "param": null, + "code": "401" + } +} +``` + +This error is from the **LiteLLM AI Gateway** (no provider name). Your LiteLLM virtual key is invalid. + +## What to do? + +| Error Source | Action | +|--------------|--------| +| Provider Error | Check the provider's status page, adjust rate limits, or retry later | +| Gateway Error | Check your LiteLLM configuration, API keys, or [open an issue](https://github.com/BerriAI/litellm/issues) | + +## See Also + +- [Debugging](/docs/proxy/debugging) - Enable debug logs to see detailed request/response info +- [Exception Mapping](/docs/exception_mapping) - Full list of LiteLLM exception types diff --git a/docs/my-website/sidebars.js b/docs/my-website/sidebars.js index e467711b59d..a4a2956ff6a 100644 --- a/docs/my-website/sidebars.js +++ b/docs/my-website/sidebars.js @@ -141,6 +141,7 @@ const sidebars = { "proxy/quick_start", "proxy/cli", "proxy/debugging", + "proxy/error_diagnosis", "proxy/deploy", "proxy/health", "proxy/master_key_rotations", From de4ff120ebe049cf86b07483d32c5c7b22c5190f Mon Sep 17 00:00:00 2001 From: Leslie Cheng Date: Tue, 2 Dec 2025 14:37:45 -0800 Subject: [PATCH 020/107] =?UTF-8?q?=F0=9F=90=9B=20Fix=20proxy=20caching=20?= =?UTF-8?q?between=20requests=20in=20aiohttp=20transport=20(#17122)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * write a regression test * impl fix * add test for host case * use the host as cache key --- .../llms/custom_httpx/aiohttp_transport.py | 67 ++++---- .../custom_httpx/test_aiohttp_transport.py | 143 ++++++++++++++---- 2 files changed, 137 insertions(+), 73 deletions(-) diff --git a/litellm/llms/custom_httpx/aiohttp_transport.py b/litellm/llms/custom_httpx/aiohttp_transport.py index 6997afafd8d..f845bf7cb90 100644 --- a/litellm/llms/custom_httpx/aiohttp_transport.py +++ b/litellm/llms/custom_httpx/aiohttp_transport.py @@ -82,9 +82,7 @@ class AiohttpResponseStream(httpx.AsyncByteStream): async def __aiter__(self) -> typing.AsyncIterator[bytes]: try: - async for chunk in self._aiohttp_response.content.iter_chunked( - self.CHUNK_SIZE - ): + async for chunk in self._aiohttp_response.content.iter_chunked(self.CHUNK_SIZE): yield chunk except ( aiohttp.ClientPayloadError, @@ -120,16 +118,13 @@ class AiohttpResponseStream(httpx.AsyncByteStream): class AiohttpTransport(httpx.AsyncBaseTransport): - def __init__( - self, client: Union[ClientSession, Callable[[], ClientSession]] - ) -> None: + def __init__(self, client: Union[ClientSession, Callable[[], ClientSession]]) -> None: self.client = client ######################################################### # Class variables for proxy settings ######################################################### - self.proxy: Optional[str] = None - self.checked_proxy_env_settings: bool = False + self.proxy_cache: Dict[str, Optional[str]] = {} async def aclose(self) -> None: if isinstance(self.client, ClientSession): @@ -184,11 +179,7 @@ class LiteLLMAiohttpTransport(AiohttpTransport): current_loop = asyncio.get_running_loop() # If session is from a different or closed loop, recreate it - if ( - session_loop is None - or session_loop != current_loop - or session_loop.is_closed() - ): + if session_loop is None or session_loop != current_loop or session_loop.is_closed(): # Close old session to prevent leaks old_session = self.client try: @@ -215,7 +206,7 @@ class LiteLLMAiohttpTransport(AiohttpTransport): self.client = ClientSession() return self.client - + async def _make_aiohttp_request( self, client_session: ClientSession, @@ -226,20 +217,20 @@ class LiteLLMAiohttpTransport(AiohttpTransport): ) -> ClientResponse: """ Helper function to make an aiohttp request with the given parameters. - + Args: client_session: The aiohttp ClientSession to use request: The httpx Request to send timeout: Timeout settings dict with 'connect', 'read', 'pool' keys proxy: Optional proxy URL sni_hostname: Optional SNI hostname for SSL - + Returns: ClientResponse from aiohttp """ from aiohttp import ClientTimeout from yarl import URL as YarlURL - + try: data = request.content except httpx.RequestNotRead: @@ -262,9 +253,9 @@ class LiteLLMAiohttpTransport(AiohttpTransport): proxy=proxy, server_hostname=sni_hostname, ).__aenter__() - + return response - + async def handle_async_request( self, request: httpx.Request, @@ -297,7 +288,7 @@ class LiteLLMAiohttpTransport(AiohttpTransport): else: self.client = ClientSession() client_session = self.client - + # Retry the request with the new session with map_aiohttp_exceptions(): response = await self._make_aiohttp_request( @@ -317,45 +308,41 @@ class LiteLLMAiohttpTransport(AiohttpTransport): content=AiohttpResponseStream(response), request=request, ) - async def _get_proxy_settings(self, request: httpx.Request): proxy = None - if not ( - litellm.disable_aiohttp_trust_env - or str_to_bool(os.getenv("DISABLE_AIOHTTP_TRUST_ENV", "False")) - ): + if not (litellm.disable_aiohttp_trust_env or str_to_bool(os.getenv("DISABLE_AIOHTTP_TRUST_ENV", "False"))): try: proxy = self._proxy_from_env(request.url) except Exception as e: # pragma: no cover - best effort verbose_logger.debug(f"Error reading proxy env: {e}") return proxy - def _proxy_from_env(self, url: httpx.URL) -> typing.Optional[str]: """ Return proxy URL from env for the given request URL Only check the proxy env settings once, this is a costly operation for CPU % usage - + .""" ######################################################### # Check if we've already checked the proxy env settings ######################################################### - if self.checked_proxy_env_settings is True: - return self.proxy - - ######################################################### - # set self.checked_proxy_env_settings to True - ######################################################### - self.checked_proxy_env_settings = True + proxy_cache_key = url.host + + if proxy_cache_key in self.proxy_cache: + return self.proxy_cache[proxy_cache_key] + proxies = urllib.request.getproxies() if urllib.request.proxy_bypass(url.host): - return None + proxy_url = None + else: + proxy = proxies.get(url.scheme) or proxies.get("all") + if proxy and "://" not in proxy: + proxy = f"http://{proxy}" + proxy_url = proxy - proxy = proxies.get(url.scheme) or proxies.get("all") - if proxy and "://" not in proxy: - proxy = f"http://{proxy}" - self.proxy = proxy - return self.proxy + self.proxy_cache[proxy_cache_key] = proxy_url + + return proxy_url diff --git a/tests/test_litellm/llms/custom_httpx/test_aiohttp_transport.py b/tests/test_litellm/llms/custom_httpx/test_aiohttp_transport.py index 1f1a36fd7ab..f0dac113645 100644 --- a/tests/test_litellm/llms/custom_httpx/test_aiohttp_transport.py +++ b/tests/test_litellm/llms/custom_httpx/test_aiohttp_transport.py @@ -1,6 +1,6 @@ +import asyncio import os import sys -from unittest.mock import AsyncMock, MagicMock, patch import aiohttp import aiohttp.client_exceptions @@ -8,14 +8,11 @@ import aiohttp.http_exceptions import httpx import pytest -sys.path.insert( - 0, os.path.abspath("../../../..") -) # Adds the parent directory to the system path +sys.path.insert(0, os.path.abspath("../../../..")) # Adds the parent directory to the system path from litellm.llms.custom_httpx.aiohttp_transport import ( AiohttpResponseStream, LiteLLMAiohttpTransport, - map_aiohttp_exceptions, ) @@ -32,9 +29,7 @@ class MockAiohttpResponse: ): self.status = status self.headers = headers or {} - self.content = MockContent( - content_chunks, exception_to_raise, exception_at_chunk - ) + self.content = MockContent(content_chunks, exception_to_raise, exception_at_chunk) async def __aexit__(self, exc_type, exc_val, exc_tb): pass @@ -74,7 +69,6 @@ async def test_aiohttp_response_stream_normal_flow(): @pytest.mark.asyncio async def test_transfer_encoding_error_no_httpx_read_error(): """Test that TransferEncodingError doesn't get converted to httpx.ReadError""" - import logging # Create a TransferEncodingError wrapped in ClientPayloadError (like in real scenarios) transfer_error = aiohttp.http_exceptions.TransferEncodingError( @@ -82,9 +76,7 @@ async def test_transfer_encoding_error_no_httpx_read_error(): ) # Wrap it in ClientPayloadError as aiohttp does - client_payload_error = aiohttp.ClientPayloadError( - "Response payload is not completed" - ) + client_payload_error = aiohttp.ClientPayloadError("Response payload is not completed") client_payload_error.__cause__ = transfer_error mock_response = MockAiohttpResponse( @@ -111,9 +103,7 @@ async def test_transfer_encoding_error_no_httpx_read_error(): async def test_client_payload_error_graceful_handling(): """Test that ClientPayloadError is handled gracefully without stacktrace""" # Create a ClientPayloadError directly - client_error = aiohttp.client_exceptions.ClientPayloadError( - "Response payload is not completed" - ) + client_error = aiohttp.client_exceptions.ClientPayloadError("Response payload is not completed") mock_response = MockAiohttpResponse( content_chunks=[b"data1", b"data2", b"data3"], @@ -181,7 +171,6 @@ async def test_timeout_exception_gets_mapped(): @pytest.mark.asyncio async def test_handle_async_request_uses_env_proxy(monkeypatch): """Aiohttp transport should honor HTTP(S)_PROXY env vars""" - import asyncio proxy_url = "http://proxy.local:3128" monkeypatch.setenv("HTTP_PROXY", proxy_url) monkeypatch.setenv("http_proxy", proxy_url) @@ -200,7 +189,7 @@ async def test_handle_async_request_uses_env_proxy(monkeypatch): self._loop = asyncio.get_running_loop() except RuntimeError: self._loop = None - + def request(self, *args, **kwargs): captured["proxy"] = kwargs.get("proxy") @@ -231,30 +220,118 @@ async def test_handle_async_request_uses_env_proxy(monkeypatch): assert captured["proxy"] == proxy_url +@pytest.mark.asyncio +async def test_handle_async_request_uses_env_proxy_per_url(monkeypatch): + """Aiohttp transport should honor HTTP(S)_PROXY env vars unless NO_PROXY matches""" + proxy_url = "http://proxy.local:3128" + monkeypatch.setenv("NO_PROXY", "example.com") + monkeypatch.setenv("HTTP_PROXY", proxy_url) + monkeypatch.setenv("http_proxy", proxy_url) + monkeypatch.setenv("HTTPS_PROXY", proxy_url) + monkeypatch.setenv("https_proxy", proxy_url) + monkeypatch.delenv("DISABLE_AIOHTTP_TRUST_ENV", raising=False) + + request_count = 0 + proxied_count = 0 + + class FakeSession: + def __init__(self): + self.closed = False + try: + self._loop = asyncio.get_running_loop() + except RuntimeError: + self._loop = None + + def request(self, *args, **kwargs): + nonlocal request_count + nonlocal proxied_count + request_count += 1 + + if kwargs.get("proxy") is not None: + proxied_count += 1 + + class Resp: + status = 200 + headers = {} + + async def __aenter__(self): + return self + + async def __aexit__(self, exc_type, exc, tb): + pass + + @property + def content(self): + class C: + async def iter_chunked(self, size): + yield b"" + + return C() + + return Resp() + + transport = LiteLLMAiohttpTransport(client=lambda: FakeSession()) # type: ignore + request = httpx.Request("GET", "http://example.com") + await transport.handle_async_request(request) + + request = httpx.Request("GET", "http://foo.com") + await transport.handle_async_request(request) + + assert request_count == 2 + assert proxied_count == 1 + + +@pytest.mark.asyncio +async def test_handle_async_request_proxy_cache_per_host(monkeypatch): + """Aiohttp transport should only cache a proxy per host rather than full URL""" + proxy_url = "http://proxy.local:3128" + monkeypatch.setenv("NO_PROXY", "example.com") + monkeypatch.setenv("HTTP_PROXY", proxy_url) + monkeypatch.setenv("http_proxy", proxy_url) + monkeypatch.setenv("HTTPS_PROXY", proxy_url) + monkeypatch.setenv("https_proxy", proxy_url) + monkeypatch.delenv("DISABLE_AIOHTTP_TRUST_ENV", raising=False) + + def factory(): + return _make_mock_session() + + transport = LiteLLMAiohttpTransport(client=factory) # type: ignore + request = httpx.Request("GET", "http://foo.com/path1") + await transport.handle_async_request(request) + + request = httpx.Request("GET", "http://foo.com/path2") + await transport.handle_async_request(request) + + assert len(transport.proxy_cache) == 1 + + def _make_mock_response(should_fail=False, fail_count={"count": 0}): """Helper to create a mock aiohttp response""" + class MockResp: status = 200 headers = {} - + async def __aenter__(self): if should_fail and fail_count["count"] < 1: fail_count["count"] += 1 raise RuntimeError("Session is closed") return self - + async def __aexit__(self, *args): pass - + @property def content(self): class C: async def iter_chunked(self, size): yield b"test" + return C() - + return MockResp() + @pytest.mark.asyncio async def test_handle_async_request_total_timeout_triggers(): """ @@ -298,10 +375,10 @@ async def test_handle_async_request_total_timeout_triggers(): await transport.aclose() await runner.cleanup() + def _make_mock_session(closed=False): """Helper to create a mock aiohttp session""" - import asyncio - + class MockSession: def __init__(self): self.closed = closed @@ -309,10 +386,10 @@ def _make_mock_session(closed=False): self._loop = asyncio.get_running_loop() except RuntimeError: self._loop = None - + def request(self, *args, **kwargs): return _make_mock_response() - + return MockSession() @@ -320,14 +397,14 @@ def _make_mock_session(closed=False): async def test_handle_closed_session_before_request(): """Test that closed sessions are detected and recreated""" counts = {"sessions": 0} - + def factory(): counts["sessions"] += 1 return _make_mock_session(closed=counts["sessions"] == 1) - + transport = LiteLLMAiohttpTransport(client=factory) # type: ignore response = await transport.handle_async_request(httpx.Request("GET", "http://example.com")) - + assert counts["sessions"] == 2 # Created 2 sessions: closed one, then open one assert response.status_code == 200 @@ -337,7 +414,7 @@ async def test_handle_session_closed_during_request(): """Test that sessions closed during request are handled with retry""" counts = {"sessions": 0, "requests": 0} fail_count = {"count": 0} - + class MockSession: def __init__(self): self.closed = False @@ -345,18 +422,18 @@ async def test_handle_session_closed_during_request(): self._loop = __import__("asyncio").get_running_loop() except RuntimeError: self._loop = None - + def request(self, *args, **kwargs): counts["requests"] += 1 return _make_mock_response(should_fail=True, fail_count=fail_count) - + def factory(): counts["sessions"] += 1 return MockSession() - + transport = LiteLLMAiohttpTransport(client=factory) # type: ignore response = await transport.handle_async_request(httpx.Request("GET", "http://example.com")) - + assert counts["requests"] == 2 # First request failed, second succeeded assert counts["sessions"] == 2 # Created 2 sessions for retry assert response.status_code == 200 From 5a8154335fa5c12e1624c6ee874f41e6829c4264 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Tue, 2 Dec 2025 15:00:52 -0800 Subject: [PATCH 021/107] Changed default provider and made optional status of Vertex API Base more obvious --- litellm/proxy/public_endpoints/provider_create_fields.json | 2 +- .../(dashboard)/models-and-endpoints/ModelsAndEndpointsView.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/litellm/proxy/public_endpoints/provider_create_fields.json b/litellm/proxy/public_endpoints/provider_create_fields.json index c7f88c9e826..a23775f122d 100644 --- a/litellm/proxy/public_endpoints/provider_create_fields.json +++ b/litellm/proxy/public_endpoints/provider_create_fields.json @@ -2609,7 +2609,7 @@ }, { "key": "api_base", - "label": "API Base", + "label": "API Base (Optional)", "placeholder": null, "tooltip": "Supports custom API base for Google Private API endpoints.", "required": false, diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView.tsx index 4f33ba6585d..4d400ba680e 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView.tsx @@ -100,7 +100,7 @@ const ModelsAndEndpointsView: React.FC = ({ const [providerModels, setProviderModels] = useState>([]); // Explicitly typing providerModels as a string array const [providerSettings, setProviderSettings] = useState([]); - const [selectedProvider, setSelectedProvider] = useState(Providers.OpenAI); + const [selectedProvider, setSelectedProvider] = useState(Providers.Anthropic); const [editModalVisible, setEditModalVisible] = useState(false); const [selectedModel, setSelectedModel] = useState(null); From 6c188c5ae2fa36d8f7d479479e93037f5777c06c Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Tue, 2 Dec 2025 15:36:23 -0800 Subject: [PATCH 022/107] [Feat] New model/provider - Adds support for Google Cloud Chirp3 HD on /speech (#17391) * docs vertex tts * place vertex ai types in file * use VertexAITextToSpeechConfig * use vertex_voice_dict * refactor docs * docs vertex ai chirp * TestVertexAITextToSpeechConfig * new provider vertex ai chirp3 * test_litellm_speech_vertex_ai_chirp * add vertex_ai/chirp cost trackign --- docs/my-website/docs/providers/vertex.md | 349 ------------- .../docs/providers/vertex_speech.md | 423 ++++++++++++++++ docs/my-website/sidebars.js | 1 + .../text_to_speech/transformation.py | 472 ++++++++++++++++++ litellm/main.py | 64 +-- ...odel_prices_and_context_window_backup.json | 9 + .../types/llms/vertex_ai_text_to_speech.py | 54 ++ litellm/utils.py | 6 + model_prices_and_context_window.json | 9 + provider_endpoints_support.json | 16 + tests/audio_tests/speech_vertex.mp3 | Bin 0 -> 122924 bytes .../text_to_speech/test_transformation.py | 190 +++++++ 12 files changed, 1212 insertions(+), 381 deletions(-) create mode 100644 docs/my-website/docs/providers/vertex_speech.md create mode 100644 litellm/llms/vertex_ai/text_to_speech/transformation.py create mode 100644 litellm/types/llms/vertex_ai_text_to_speech.py create mode 100644 tests/audio_tests/speech_vertex.mp3 create mode 100644 tests/litellm/llms/vertex_ai/text_to_speech/test_transformation.py diff --git a/docs/my-website/docs/providers/vertex.md b/docs/my-website/docs/providers/vertex.md index 70babea3814..da2997f6202 100644 --- a/docs/my-website/docs/providers/vertex.md +++ b/docs/my-website/docs/providers/vertex.md @@ -2550,355 +2550,6 @@ print(response) - -## **Gemini TTS (Text-to-Speech) Audio Output** - -:::info - -LiteLLM supports Gemini TTS models on Vertex AI that can generate audio responses using the OpenAI-compatible `audio` parameter format. - -::: - -### Supported Models - -LiteLLM supports Gemini TTS models with audio capabilities on Vertex AI (e.g. `vertex_ai/gemini-2.5-flash-preview-tts` and `vertex_ai/gemini-2.5-pro-preview-tts`). For the complete list of available TTS models and voices, see the [official Gemini TTS documentation](https://ai.google.dev/gemini-api/docs/speech-generation). - -### Limitations - -:::warning - -**Important Limitations**: -- Gemini TTS models only support the `pcm16` audio format -- **Streaming support has not been added** to TTS models yet -- The `modalities` parameter must be set to `['audio']` for TTS requests - -::: - -### Quick Start - - - - -```python -from litellm import completion -import json - -## GET CREDENTIALS -file_path = 'path/to/vertex_ai_service_account.json' - -# Load the JSON file -with open(file_path, 'r') as file: - vertex_credentials = json.load(file) - -# Convert to JSON string -vertex_credentials_json = json.dumps(vertex_credentials) - -response = completion( - model="vertex_ai/gemini-2.5-flash-preview-tts", - messages=[{"role": "user", "content": "Say hello in a friendly voice"}], - modalities=["audio"], # Required for TTS models - audio={ - "voice": "Kore", - "format": "pcm16" # Required: must be "pcm16" - }, - vertex_credentials=vertex_credentials_json -) - -print(response) -``` - - - - -1. Setup config.yaml - -```yaml -model_list: - - model_name: gemini-tts-flash - litellm_params: - model: vertex_ai/gemini-2.5-flash-preview-tts - vertex_project: "your-project-id" - vertex_location: "us-central1" - vertex_credentials: "/path/to/service_account.json" - - model_name: gemini-tts-pro - litellm_params: - model: vertex_ai/gemini-2.5-pro-preview-tts - vertex_project: "your-project-id" - vertex_location: "us-central1" - vertex_credentials: "/path/to/service_account.json" -``` - -2. Start proxy - -```bash -litellm --config /path/to/config.yaml -``` - -3. Make TTS request - -```bash -curl http://0.0.0.0:4000/v1/chat/completions \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer " \ - -d '{ - "model": "gemini-tts-flash", - "messages": [{"role": "user", "content": "Say hello in a friendly voice"}], - "modalities": ["audio"], - "audio": { - "voice": "Kore", - "format": "pcm16" - } - }' -``` - - - - -### Advanced Usage - -You can combine TTS with other Gemini features: - -```python -response = completion( - model="vertex_ai/gemini-2.5-pro-preview-tts", - messages=[ - {"role": "system", "content": "You are a helpful assistant that speaks clearly."}, - {"role": "user", "content": "Explain quantum computing in simple terms"} - ], - modalities=["audio"], - audio={ - "voice": "Charon", - "format": "pcm16" - }, - temperature=0.7, - max_tokens=150, - vertex_credentials=vertex_credentials_json -) -``` - -For more information about Gemini's TTS capabilities and available voices, see the [official Gemini TTS documentation](https://ai.google.dev/gemini-api/docs/speech-generation). - -## **Text to Speech APIs** - -:::info - -LiteLLM supports calling [Vertex AI Text to Speech API](https://console.cloud.google.com/vertex-ai/generative/speech/text-to-speech) in the OpenAI text to speech API format - -::: - - - -### Usage - Basic - - - - -Vertex AI does not support passing a `model` param - so passing `model=vertex_ai/` is the only required param - -**Sync Usage** - -```python -speech_file_path = Path(__file__).parent / "speech_vertex.mp3" -response = litellm.speech( - model="vertex_ai/", - input="hello what llm guardrail do you have", -) -response.stream_to_file(speech_file_path) -``` - -**Async Usage** -```python -speech_file_path = Path(__file__).parent / "speech_vertex.mp3" -response = litellm.aspeech( - model="vertex_ai/", - input="hello what llm guardrail do you have", -) -response.stream_to_file(speech_file_path) -``` - - - - -1. Add model to config.yaml -```yaml -model_list: - - model_name: vertex-tts - litellm_params: - model: vertex_ai/ # Vertex AI does not support passing a `model` param - so passing `model=vertex_ai/` is the only required param - vertex_project: "adroit-crow-413218" - vertex_location: "us-central1" - vertex_credentials: adroit-crow-413218-a956eef1a2a8.json - -litellm_settings: - drop_params: True -``` - -2. Start Proxy - -``` -$ litellm --config /path/to/config.yaml -``` - -3. Make Request use OpenAI Python SDK - - -```python -import openai - -client = openai.OpenAI(api_key="sk-1234", base_url="http://0.0.0.0:4000") - -# see supported values for "voice" on vertex here: -# https://console.cloud.google.com/vertex-ai/generative/speech/text-to-speech -response = client.audio.speech.create( - model = "vertex-tts", - input="the quick brown fox jumped over the lazy dogs", - voice={'languageCode': 'en-US', 'name': 'en-US-Studio-O'} -) -print("response from proxy", response) -``` - - - - - -### Usage - `ssml` as input - -Pass your `ssml` as input to the `input` param, if it contains ``, it will be automatically detected and passed as `ssml` to the Vertex AI API - -If you need to force your `input` to be passed as `ssml`, set `use_ssml=True` - - - - -Vertex AI does not support passing a `model` param - so passing `model=vertex_ai/` is the only required param - - -```python -speech_file_path = Path(__file__).parent / "speech_vertex.mp3" - - -ssml = """ - -

    Hello, world!

    -

    This is a test of the text-to-speech API.

    -
    -""" - -response = litellm.speech( - input=ssml, - model="vertex_ai/test", - voice={ - "languageCode": "en-UK", - "name": "en-UK-Studio-O", - }, - audioConfig={ - "audioEncoding": "LINEAR22", - "speakingRate": "10", - }, -) -response.stream_to_file(speech_file_path) -``` - -
    - - - -```python -import openai - -client = openai.OpenAI(api_key="sk-1234", base_url="http://0.0.0.0:4000") - -ssml = """ - -

    Hello, world!

    -

    This is a test of the text-to-speech API.

    -
    -""" - -# see supported values for "voice" on vertex here: -# https://console.cloud.google.com/vertex-ai/generative/speech/text-to-speech -response = client.audio.speech.create( - model = "vertex-tts", - input=ssml, - voice={'languageCode': 'en-US', 'name': 'en-US-Studio-O'}, -) -print("response from proxy", response) -``` - -
    -
    - - -### Forcing SSML Usage - -You can force the use of SSML by setting the `use_ssml` parameter to `True`. This is useful when you want to ensure that your input is treated as SSML, even if it doesn't contain the `` tags. - -Here are examples of how to force SSML usage: - - - - - -Vertex AI does not support passing a `model` param - so passing `model=vertex_ai/` is the only required param - - -```python -speech_file_path = Path(__file__).parent / "speech_vertex.mp3" - - -ssml = """ - -

    Hello, world!

    -

    This is a test of the text-to-speech API.

    -
    -""" - -response = litellm.speech( - input=ssml, - use_ssml=True, - model="vertex_ai/test", - voice={ - "languageCode": "en-UK", - "name": "en-UK-Studio-O", - }, - audioConfig={ - "audioEncoding": "LINEAR22", - "speakingRate": "10", - }, -) -response.stream_to_file(speech_file_path) -``` - -
    - - - -```python -import openai - -client = openai.OpenAI(api_key="sk-1234", base_url="http://0.0.0.0:4000") - -ssml = """ - -

    Hello, world!

    -

    This is a test of the text-to-speech API.

    -
    -""" - -# see supported values for "voice" on vertex here: -# https://console.cloud.google.com/vertex-ai/generative/speech/text-to-speech -response = client.audio.speech.create( - model = "vertex-tts", - input=ssml, # pass as None since OpenAI SDK requires this param - voice={'languageCode': 'en-US', 'name': 'en-US-Studio-O'}, - extra_body={"use_ssml": True}, -) -print("response from proxy", response) -``` - -
    -
    - ## **Fine Tuning APIs** diff --git a/docs/my-website/docs/providers/vertex_speech.md b/docs/my-website/docs/providers/vertex_speech.md new file mode 100644 index 00000000000..d0acacb5aec --- /dev/null +++ b/docs/my-website/docs/providers/vertex_speech.md @@ -0,0 +1,423 @@ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Vertex AI Text to Speech + +| Property | Details | +|-------|-------| +| Description | Google Cloud Text-to-Speech with Chirp3 HD voices and Gemini TTS | +| Provider Route on LiteLLM | `vertex_ai/chirp` (Chirp), `vertex_ai/gemini-*-tts` (Gemini) | + +## Chirp3 HD Voices + +Google Cloud Text-to-Speech API with high-quality Chirp3 HD voices. + +### Quick Start + +#### LiteLLM Python SDK + +```python showLineNumbers title="Chirp3 Quick Start" +from litellm import speech +from pathlib import Path + +speech_file_path = Path(__file__).parent / "speech.mp3" +response = speech( + model="vertex_ai/chirp", + voice="alloy", # OpenAI voice name - automatically mapped + input="Hello, this is Vertex AI Text to Speech", + vertex_project="your-project-id", + vertex_location="us-central1", +) +response.stream_to_file(speech_file_path) +``` + +#### LiteLLM AI Gateway + +**1. Setup config.yaml** + +```yaml showLineNumbers title="config.yaml" +model_list: + - model_name: vertex-tts + litellm_params: + model: vertex_ai/chirp + vertex_project: "your-project-id" + vertex_location: "us-central1" + vertex_credentials: "/path/to/service_account.json" +``` + +**2. Start the proxy** + +```bash title="Start LiteLLM Proxy" +litellm --config /path/to/config.yaml +``` + +**3. Make requests** + + + + +```bash showLineNumbers title="Chirp3 Quick Start" +curl http://0.0.0.0:4000/v1/audio/speech \ + -H "Authorization: Bearer sk-1234" \ + -H "Content-Type: application/json" \ + -d '{ + "model": "vertex-tts", + "voice": "alloy", + "input": "Hello, this is Vertex AI Text to Speech" + }' \ + --output speech.mp3 +``` + + + + +```python showLineNumbers title="Chirp3 Quick Start" +import openai + +client = openai.OpenAI(api_key="sk-1234", base_url="http://0.0.0.0:4000") + +response = client.audio.speech.create( + model="vertex-tts", + voice="alloy", + input="Hello, this is Vertex AI Text to Speech", +) +response.stream_to_file("speech.mp3") +``` + + + + +### Voice Mapping + +LiteLLM maps OpenAI voice names to Google Cloud voices. You can use either OpenAI voices or Google Cloud voices directly. + +| OpenAI Voice | Google Cloud Voice | +|-------------|-------------------| +| `alloy` | en-US-Studio-O | +| `echo` | en-US-Studio-M | +| `fable` | en-GB-Studio-B | +| `onyx` | en-US-Wavenet-D | +| `nova` | en-US-Studio-O | +| `shimmer` | en-US-Wavenet-F | + +### Using Google Cloud Voices Directly + +#### LiteLLM Python SDK + +```python showLineNumbers title="Chirp3 HD Voice" +from litellm import speech + +# Pass Chirp3 HD voice name directly +response = speech( + model="vertex_ai/chirp", + voice="en-US-Chirp3-HD-Charon", + input="Hello with a Chirp3 HD voice", + vertex_project="your-project-id", +) +response.stream_to_file("speech.mp3") +``` + +```python showLineNumbers title="Voice as Dict (Multilingual)" +from litellm import speech + +# Pass as dict for full control over language and voice +response = speech( + model="vertex_ai/chirp", + voice={ + "languageCode": "de-DE", + "name": "de-DE-Chirp3-HD-Charon", + }, + input="Hallo, dies ist ein Test", + vertex_project="your-project-id", +) +response.stream_to_file("speech.mp3") +``` + +#### LiteLLM AI Gateway + + + + +```bash showLineNumbers title="Chirp3 HD Voice" +curl http://0.0.0.0:4000/v1/audio/speech \ + -H "Authorization: Bearer sk-1234" \ + -H "Content-Type: application/json" \ + -d '{ + "model": "vertex-tts", + "voice": "en-US-Chirp3-HD-Charon", + "input": "Hello with a Chirp3 HD voice" + }' \ + --output speech.mp3 +``` + +```bash showLineNumbers title="Voice as Dict (Multilingual)" +curl http://0.0.0.0:4000/v1/audio/speech \ + -H "Authorization: Bearer sk-1234" \ + -H "Content-Type: application/json" \ + -d '{ + "model": "vertex-tts", + "voice": {"languageCode": "de-DE", "name": "de-DE-Chirp3-HD-Charon"}, + "input": "Hallo, dies ist ein Test" + }' \ + --output speech.mp3 +``` + + + + +```python showLineNumbers title="Chirp3 HD Voice" +import openai + +client = openai.OpenAI(api_key="sk-1234", base_url="http://0.0.0.0:4000") + +response = client.audio.speech.create( + model="vertex-tts", + voice="en-US-Chirp3-HD-Charon", + input="Hello with a Chirp3 HD voice", +) +response.stream_to_file("speech.mp3") +``` + +```python showLineNumbers title="Voice as Dict (Multilingual)" +import openai + +client = openai.OpenAI(api_key="sk-1234", base_url="http://0.0.0.0:4000") + +response = client.audio.speech.create( + model="vertex-tts", + voice={"languageCode": "de-DE", "name": "de-DE-Chirp3-HD-Charon"}, + input="Hallo, dies ist ein Test", +) +response.stream_to_file("speech.mp3") +``` + + + + +Browse available voices: [Google Cloud Text-to-Speech Console](https://console.cloud.google.com/vertex-ai/generative/speech/text-to-speech) + +### Passing Raw SSML + +LiteLLM auto-detects SSML when your input contains `` tags and passes it through unchanged. + +#### LiteLLM Python SDK + +```python showLineNumbers title="SSML Input" +from litellm import speech + +ssml = """ + +

    Hello, world!

    +

    This is a test of the text-to-speech API.

    +
    +""" + +response = speech( + model="vertex_ai/chirp", + voice="en-US-Studio-O", + input=ssml, # Auto-detected as SSML + vertex_project="your-project-id", +) +response.stream_to_file("speech.mp3") +``` + +```python showLineNumbers title="Force SSML Mode" +from litellm import speech + +# Force SSML mode with use_ssml=True +response = speech( + model="vertex_ai/chirp", + voice="en-US-Studio-O", + input="Speaking slowly", + use_ssml=True, + vertex_project="your-project-id", +) +response.stream_to_file("speech.mp3") +``` + +#### LiteLLM AI Gateway + + + + +```bash showLineNumbers title="SSML Input" +curl http://0.0.0.0:4000/v1/audio/speech \ + -H "Authorization: Bearer sk-1234" \ + -H "Content-Type: application/json" \ + -d '{ + "model": "vertex-tts", + "voice": "en-US-Studio-O", + "input": "

    Hello!

    How are you?

    " + }' \ + --output speech.mp3 +``` + +
    + + +```python showLineNumbers title="SSML Input" +import openai + +client = openai.OpenAI(api_key="sk-1234", base_url="http://0.0.0.0:4000") + +ssml = """

    Hello!

    How are you?

    """ + +response = client.audio.speech.create( + model="vertex-tts", + voice="en-US-Studio-O", + input=ssml, +) +response.stream_to_file("speech.mp3") +``` + +
    +
    + +### Supported Parameters + +| Parameter | Description | Values | +|-----------|-------------|--------| +| `voice` | Voice selection | OpenAI voice, Google Cloud voice name, or dict | +| `input` | Text to convert | Plain text or SSML | +| `speed` | Speaking rate | 0.25 to 4.0 (default: 1.0) | +| `response_format` | Audio format | `mp3`, `opus`, `wav`, `pcm`, `flac` | +| `use_ssml` | Force SSML mode | `True` / `False` | + +### Async Usage + +```python showLineNumbers title="Async Speech Generation" +import asyncio +from litellm import aspeech + +async def main(): + response = await aspeech( + model="vertex_ai/chirp", + voice="alloy", + input="Hello from async", + vertex_project="your-project-id", + ) + response.stream_to_file("speech.mp3") + +asyncio.run(main()) +``` + +--- + +## Gemini TTS + +Gemini models with audio output capabilities using the chat completions API. + +:::warning +**Limitations:** +- Only supports `pcm16` audio format +- Streaming not yet supported +- Must set `modalities: ["audio"]` +::: + +### Quick Start + +#### LiteLLM Python SDK + +```python showLineNumbers title="Gemini TTS Quick Start" +from litellm import completion +import json + +# Load credentials +with open('path/to/service_account.json', 'r') as file: + vertex_credentials = json.dumps(json.load(file)) + +response = completion( + model="vertex_ai/gemini-2.5-flash-preview-tts", + messages=[{"role": "user", "content": "Say hello in a friendly voice"}], + modalities=["audio"], + audio={ + "voice": "Kore", + "format": "pcm16" + }, + vertex_credentials=vertex_credentials +) +print(response) +``` + +#### LiteLLM AI Gateway + +**1. Setup config.yaml** + +```yaml showLineNumbers title="config.yaml" +model_list: + - model_name: gemini-tts + litellm_params: + model: vertex_ai/gemini-2.5-flash-preview-tts + vertex_project: "your-project-id" + vertex_location: "us-central1" + vertex_credentials: "/path/to/service_account.json" +``` + +**2. Start the proxy** + +```bash title="Start LiteLLM Proxy" +litellm --config /path/to/config.yaml +``` + +**3. Make requests** + + + + +```bash showLineNumbers title="Gemini TTS Request" +curl http://0.0.0.0:4000/v1/chat/completions \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer sk-1234" \ + -d '{ + "model": "gemini-tts", + "messages": [{"role": "user", "content": "Say hello in a friendly voice"}], + "modalities": ["audio"], + "audio": {"voice": "Kore", "format": "pcm16"} + }' +``` + + + + +```python showLineNumbers title="Gemini TTS Request" +import openai + +client = openai.OpenAI(api_key="sk-1234", base_url="http://0.0.0.0:4000") + +response = client.chat.completions.create( + model="gemini-tts", + messages=[{"role": "user", "content": "Say hello in a friendly voice"}], + modalities=["audio"], + audio={"voice": "Kore", "format": "pcm16"}, +) +print(response) +``` + + + + +### Supported Models + +- `vertex_ai/gemini-2.5-flash-preview-tts` +- `vertex_ai/gemini-2.5-pro-preview-tts` + +See [Gemini TTS documentation](https://ai.google.dev/gemini-api/docs/speech-generation) for available voices. + +### Advanced Usage + +```python showLineNumbers title="Gemini TTS with System Prompt" +from litellm import completion + +response = completion( + model="vertex_ai/gemini-2.5-pro-preview-tts", + messages=[ + {"role": "system", "content": "You are a helpful assistant that speaks clearly."}, + {"role": "user", "content": "Explain quantum computing in simple terms"} + ], + modalities=["audio"], + audio={"voice": "Charon", "format": "pcm16"}, + temperature=0.7, + max_tokens=150, + vertex_credentials=vertex_credentials +) +``` diff --git a/docs/my-website/sidebars.js b/docs/my-website/sidebars.js index a4a2956ff6a..983816ed216 100644 --- a/docs/my-website/sidebars.js +++ b/docs/my-website/sidebars.js @@ -521,6 +521,7 @@ const sidebars = { "providers/vertex_partner", "providers/vertex_self_deployed", "providers/vertex_image", + "providers/vertex_speech", "providers/vertex_batch", "providers/vertex_ocr", ] diff --git a/litellm/llms/vertex_ai/text_to_speech/transformation.py b/litellm/llms/vertex_ai/text_to_speech/transformation.py new file mode 100644 index 00000000000..aff14b1004f --- /dev/null +++ b/litellm/llms/vertex_ai/text_to_speech/transformation.py @@ -0,0 +1,472 @@ +""" +Vertex AI Text-to-Speech transformation + +Maps OpenAI TTS spec to Google Cloud Text-to-Speech API +Reference: https://cloud.google.com/text-to-speech/docs/reference/rest/v1/text/synthesize +""" + +import base64 +from typing import TYPE_CHECKING, Any, Coroutine, Dict, Optional, Tuple, Union + +import httpx + +from litellm.llms.base_llm.text_to_speech.transformation import ( + BaseTextToSpeechConfig, + TextToSpeechRequestData, +) +from litellm.llms.vertex_ai.vertex_llm_base import VertexBase +from litellm.types.llms.vertex_ai import VERTEX_CREDENTIALS_TYPES +from litellm.types.llms.vertex_ai_text_to_speech import ( + VertexTextToSpeechAudioConfig, + VertexTextToSpeechInput, + VertexTextToSpeechVoice, +) + +if TYPE_CHECKING: + from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj + from litellm.types.llms.openai import HttpxBinaryResponseContent +else: + LiteLLMLoggingObj = Any + HttpxBinaryResponseContent = Any + + +class VertexAITextToSpeechConfig(BaseTextToSpeechConfig, VertexBase): + """ + Configuration for Google Cloud/Vertex AI Text-to-Speech + + Reference: https://cloud.google.com/text-to-speech/docs/reference/rest/v1/text/synthesize + """ + + # Default values + DEFAULT_LANGUAGE_CODE = "en-US" + DEFAULT_VOICE_NAME = "en-US-Studio-O" + DEFAULT_AUDIO_ENCODING = "LINEAR16" + DEFAULT_SPEAKING_RATE = "1" + + # API endpoint + TTS_API_URL = "https://texttospeech.googleapis.com/v1/text:synthesize" + + # Voice name mappings from OpenAI voices to Google Cloud voices + # Users can pass either: + # 1. OpenAI voice names (alloy, echo, fable, onyx, nova, shimmer) - will be mapped + # 2. Google Cloud/Vertex AI voice names (en-US-Studio-O, en-US-Wavenet-D, etc.) - used directly + VOICE_MAPPINGS = { + "alloy": "en-US-Studio-O", + "echo": "en-US-Studio-M", + "fable": "en-GB-Studio-B", + "onyx": "en-US-Wavenet-D", + "nova": "en-US-Studio-O", + "shimmer": "en-US-Wavenet-F", + } + + # Response format mappings from OpenAI to Google Cloud audio encoding + FORMAT_MAPPINGS = { + "mp3": "MP3", + "opus": "OGG_OPUS", + "aac": "MP3", # Google doesn't have AAC, use MP3 + "flac": "FLAC", + "wav": "LINEAR16", + "pcm": "LINEAR16", + } + + def __init__(self) -> None: + BaseTextToSpeechConfig.__init__(self) + VertexBase.__init__(self) + + def _map_voice_to_vertex_format( + self, + voice: Optional[Union[str, Dict]], + ) -> Tuple[Optional[str], Optional[Dict]]: + """ + Map voice to Vertex AI format. + + Supports both: + 1. OpenAI voice names (alloy, echo, fable, onyx, nova, shimmer) - will be mapped + 2. Vertex AI voice names (en-US-Studio-O, en-US-Wavenet-D, etc.) - used directly + 3. Dict with languageCode and name - used as-is + + Returns: + Tuple of (voice_str, voice_dict) where: + - voice_str: Original string voice (for interface compatibility) + - voice_dict: Vertex AI format dict with languageCode and name + """ + if voice is None: + return None, None + + if isinstance(voice, dict): + # Already in Vertex AI format + return None, voice + + # voice is a string + voice_str = voice + + # Map OpenAI voice if it's a known OpenAI voice, otherwise use directly + if voice in self.VOICE_MAPPINGS: + mapped_voice_name = self.VOICE_MAPPINGS[voice] + else: + # Assume it's already a Vertex AI voice name + mapped_voice_name = voice + + # Extract language code from voice name (e.g., "en-US-Studio-O" -> "en-US") + parts = mapped_voice_name.split("-") + if len(parts) >= 2: + language_code = f"{parts[0]}-{parts[1]}" + else: + language_code = self.DEFAULT_LANGUAGE_CODE + + voice_dict = { + "languageCode": language_code, + "name": mapped_voice_name, + } + + return voice_str, voice_dict + + def dispatch_text_to_speech( + self, + model: str, + input: str, + voice: Optional[Union[str, Dict]], + optional_params: Dict, + litellm_params_dict: Dict, + logging_obj: "LiteLLMLoggingObj", + timeout: Union[float, httpx.Timeout], + extra_headers: Optional[Dict[str, Any]], + base_llm_http_handler: Any, + aspeech: bool, + api_base: Optional[str], + api_key: Optional[str], + **kwargs: Any, + ) -> Union[ + "HttpxBinaryResponseContent", + Coroutine[Any, Any, "HttpxBinaryResponseContent"], + ]: + """ + Dispatch method to handle Vertex AI TTS requests + + This method encapsulates Vertex AI-specific credential resolution and parameter handling. + Voice mapping is handled in map_openai_params (similar to Azure AVA pattern). + + Args: + base_llm_http_handler: The BaseLLMHTTPHandler instance from main.py + """ + # Resolve Vertex AI credentials using VertexBase helpers + vertex_credentials = self.safe_get_vertex_ai_credentials(litellm_params_dict) + vertex_project = self.safe_get_vertex_ai_project(litellm_params_dict) + vertex_location = self.safe_get_vertex_ai_location(litellm_params_dict) + + # Convert voice to string if it's a dict (extract name) + # Actual voice mapping happens in map_openai_params + voice_str: Optional[str] = None + if isinstance(voice, str): + voice_str = voice + elif isinstance(voice, dict): + # Extract voice name from dict if needed + voice_str = voice.get("name") if voice else None + + # Store credentials in litellm_params for use in transform methods + litellm_params_dict.update({ + "vertex_credentials": vertex_credentials, + "vertex_project": vertex_project, + "vertex_location": vertex_location, + "api_base": api_base, + }) + + # Call the text_to_speech_handler + response = base_llm_http_handler.text_to_speech_handler( + model=model, + input=input, + voice=voice_str, + text_to_speech_provider_config=self, + text_to_speech_optional_params=optional_params, + custom_llm_provider="vertex_ai", + litellm_params=litellm_params_dict, + logging_obj=logging_obj, + timeout=timeout, + extra_headers=extra_headers, + client=None, + _is_async=aspeech, + ) + + return response + + def get_supported_openai_params(self, model: str) -> list: + """ + Vertex AI TTS supports these OpenAI parameters + + Note: Vertex AI also supports additional parameters like audioConfig + which can be passed but are not part of the OpenAI spec + """ + return ["voice", "response_format", "speed"] + + def map_openai_params( + self, + model: str, + optional_params: Dict, + voice: Optional[Union[str, Dict]] = None, + drop_params: bool = False, + kwargs: Dict = {}, + ) -> Tuple[Optional[str], Dict]: + """ + Map OpenAI parameters to Vertex AI TTS parameters + + Voice handling (similar to Azure AVA): + - If voice is an OpenAI voice name (alloy, echo, etc.), it maps to a Vertex AI voice + - If voice is already a Vertex AI voice name (en-US-Studio-O, etc.), it's used directly + - If voice is a dict with languageCode and name, it's used as-is + + Note: For Vertex AI, voice dict is stored in mapped_params["vertex_voice_dict"] + because the base class interface expects voice to be a string. + + Returns: + Tuple of (mapped_voice_str, mapped_params) + """ + mapped_params = {} + + ########################################################## + # Map voice using helper + ########################################################## + mapped_voice_str, voice_dict = self._map_voice_to_vertex_format(voice) + if voice_dict is not None: + mapped_params["vertex_voice_dict"] = voice_dict + + # Map response format + if "response_format" in optional_params: + format_name = optional_params["response_format"] + if format_name in self.FORMAT_MAPPINGS: + mapped_params["audioEncoding"] = self.FORMAT_MAPPINGS[format_name] + else: + # Try to use it directly as Google Cloud format + mapped_params["audioEncoding"] = format_name + else: + # Default to LINEAR16 + mapped_params["audioEncoding"] = self.DEFAULT_AUDIO_ENCODING + + # Map speed (OpenAI: 0.25-4.0, Vertex AI: speakingRate 0.25-4.0) + if "speed" in optional_params: + speed = optional_params["speed"] + if speed is not None: + mapped_params["speakingRate"] = str(speed) + + # Pass through Vertex AI-specific parameters from kwargs + if "audioConfig" in kwargs: + mapped_params["audioConfig"] = kwargs["audioConfig"] + + if "use_ssml" in kwargs: + mapped_params["use_ssml"] = kwargs["use_ssml"] + + return mapped_voice_str, mapped_params + + def validate_environment( + self, + headers: dict, + model: str, + api_key: Optional[str] = None, + api_base: Optional[str] = None, + ) -> dict: + """ + Validate Vertex AI environment and set up authentication headers + + Note: Actual authentication is handled in transform_text_to_speech_request + because Vertex AI requires OAuth2 token refresh + """ + validated_headers = headers.copy() + + # Content-Type for JSON + validated_headers["Content-Type"] = "application/json" + validated_headers["charset"] = "UTF-8" + + return validated_headers + + def get_complete_url( + self, + model: str, + api_base: Optional[str], + litellm_params: dict, + ) -> str: + """ + Get the complete URL for Vertex AI TTS request + + Google Cloud TTS endpoint: https://texttospeech.googleapis.com/v1/text:synthesize + """ + if api_base: + return api_base + + return self.TTS_API_URL + + def _validate_vertex_input( + self, + input_data: VertexTextToSpeechInput, + optional_params: Dict, + ) -> VertexTextToSpeechInput: + """ + Validate and transform input for Vertex AI TTS + + Handles text vs SSML input detection and validation + """ + # Remove None values + if input_data.get("text") is None: + input_data.pop("text", None) + if input_data.get("ssml") is None: + input_data.pop("ssml", None) + + # Check if use_ssml is set + use_ssml = optional_params.get("use_ssml", False) + + if use_ssml: + if "text" in input_data: + input_data["ssml"] = input_data.pop("text") + elif "ssml" not in input_data: + raise ValueError("SSML input is required when use_ssml is True.") + else: + # LiteLLM will auto-detect if text is in ssml format + # check if "text" is an ssml - in this case we should pass it as ssml instead of text + if input_data: + _text = input_data.get("text", None) or "" + if "" in _text: + input_data["ssml"] = input_data.pop("text") + + if not input_data: + raise ValueError("Either 'text' or 'ssml' must be provided.") + if "text" in input_data and "ssml" in input_data: + raise ValueError("Only one of 'text' or 'ssml' should be provided, not both.") + + return input_data + + def transform_text_to_speech_request( + self, + model: str, + input: str, + voice: Optional[str], + optional_params: Dict, + litellm_params: Dict, + headers: dict, + ) -> TextToSpeechRequestData: + """ + Transform OpenAI TTS request to Vertex AI TTS format + + This method handles: + 1. Authentication with Vertex AI + 2. Building the request body + 3. Setting up headers + + Returns: + TextToSpeechRequestData: Contains dict_body and headers + """ + # Get Vertex AI credentials from litellm_params + vertex_credentials: Optional[VERTEX_CREDENTIALS_TYPES] = litellm_params.get( + "vertex_credentials" + ) + vertex_project: Optional[str] = litellm_params.get("vertex_project") + + ####### Authenticate with Vertex AI ######## + _auth_header, vertex_project = self._ensure_access_token( + credentials=vertex_credentials, + project_id=vertex_project, + custom_llm_provider="vertex_ai_beta", + ) + + auth_header, _ = self._get_token_and_url( + model="", + auth_header=_auth_header, + gemini_api_key=None, + vertex_credentials=vertex_credentials, + vertex_project=vertex_project, + vertex_location=litellm_params.get("vertex_location"), + stream=False, + custom_llm_provider="vertex_ai_beta", + api_base=litellm_params.get("api_base"), + ) + + # Set authentication headers + headers["Authorization"] = f"Bearer {auth_header}" + headers["x-goog-user-project"] = vertex_project + + ####### Build the request ################ + vertex_input = VertexTextToSpeechInput(text=input) + vertex_input = self._validate_vertex_input(vertex_input, optional_params) + + # Build voice configuration + # Check for voice dict stored in: + # 1. litellm_params by dispatch method + # 2. optional_params by map_openai_params + voice_dict = ( + litellm_params.get("vertex_voice_dict") + or optional_params.get("vertex_voice_dict") + ) + if voice_dict is not None and isinstance(voice_dict, dict): + vertex_voice = VertexTextToSpeechVoice(**voice_dict) + elif voice is not None and isinstance(voice, str): + # Handle string voice (shouldn't normally happen if dispatch was called) + parts = voice.split("-") + if len(parts) >= 2: + language_code = f"{parts[0]}-{parts[1]}" + else: + language_code = self.DEFAULT_LANGUAGE_CODE + vertex_voice = VertexTextToSpeechVoice( + languageCode=language_code, + name=voice, + ) + else: + # Use defaults + vertex_voice = VertexTextToSpeechVoice( + languageCode=self.DEFAULT_LANGUAGE_CODE, + name=self.DEFAULT_VOICE_NAME, + ) + + # Build audio configuration + audio_encoding = optional_params.get("audioEncoding", self.DEFAULT_AUDIO_ENCODING) + speaking_rate = optional_params.get("speakingRate", self.DEFAULT_SPEAKING_RATE) + + # Check for full audioConfig in optional_params + if "audioConfig" in optional_params: + vertex_audio_config = VertexTextToSpeechAudioConfig(**optional_params["audioConfig"]) + else: + vertex_audio_config = VertexTextToSpeechAudioConfig( + audioEncoding=audio_encoding, + speakingRate=speaking_rate, + ) + + request_body: Dict[str, Any] = { + "input": dict(vertex_input), + "voice": dict(vertex_voice), + "audioConfig": dict(vertex_audio_config), + } + + return TextToSpeechRequestData( + dict_body=request_body, + headers=headers, + ) + + def transform_text_to_speech_response( + self, + model: str, + raw_response: httpx.Response, + logging_obj: "LiteLLMLoggingObj", + ) -> "HttpxBinaryResponseContent": + """ + Transform Vertex AI TTS response to standard format + + Vertex AI returns JSON with base64-encoded audio content. + We decode it and return as HttpxBinaryResponseContent. + """ + from litellm.types.llms.openai import HttpxBinaryResponseContent + + # Parse JSON response + _json_response = raw_response.json() + + # Get base64-encoded audio content + response_content = _json_response.get("audioContent") + if not response_content: + raise ValueError("No audioContent in Vertex AI TTS response") + + # Decode base64 to get binary content + binary_data = base64.b64decode(response_content) + + # Create an httpx.Response object with the binary data + response = httpx.Response( + status_code=200, + content=binary_data, + ) + + # Initialize the HttpxBinaryResponseContent instance + return HttpxBinaryResponseContent(response) diff --git a/litellm/main.py b/litellm/main.py index a09a9453017..fa9172632cf 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -206,7 +206,6 @@ from .llms.vertex_ai.image_generation.image_generation_handler import ( from .llms.vertex_ai.multimodal_embeddings.embedding_handler import ( VertexMultimodalEmbedding, ) -from .llms.vertex_ai.text_to_speech.text_to_speech_handler import VertexTextToSpeechAPI from .llms.vertex_ai.vertex_ai_partner_models.main import VertexAIPartnerModels from .llms.vertex_ai.vertex_embeddings.embedding_handler import VertexEmbedding from .llms.vertex_ai.vertex_gemma_models.main import VertexAIGemmaModels @@ -277,7 +276,7 @@ google_batch_embeddings = GoogleBatchEmbeddings() vertex_partner_models_chat_completion = VertexAIPartnerModels() vertex_gemma_chat_completion = VertexAIGemmaModels() vertex_model_garden_chat_completion = VertexAIModelGardenModels() -vertex_text_to_speech = VertexTextToSpeechAPI() +# vertex_text_to_speech is now replaced by VertexAITextToSpeechConfig sagemaker_llm = SagemakerLLM() watsonx_chat_completion = WatsonXChatHandler() openai_like_embedding = OpenAILikeEmbeddingHandler() @@ -6145,30 +6144,13 @@ def speech( # noqa: PLR0915 _is_async=aspeech or False, ) elif custom_llm_provider == "vertex_ai" or custom_llm_provider == "vertex_ai_beta": + from litellm.llms.vertex_ai.text_to_speech.transformation import ( + VertexAITextToSpeechConfig, + ) + generic_optional_params = GenericLiteLLMParams(**kwargs) - api_base = generic_optional_params.api_base or "" - vertex_ai_project = ( - generic_optional_params.vertex_project - or litellm.vertex_project - or get_secret_str("VERTEXAI_PROJECT") - ) - vertex_ai_location = ( - generic_optional_params.vertex_location - or litellm.vertex_location - or get_secret_str("VERTEXAI_LOCATION") - ) - vertex_credentials = ( - generic_optional_params.vertex_credentials - or get_secret_str("VERTEXAI_CREDENTIALS") - ) - - if voice is not None and not isinstance(voice, dict): - raise litellm.BadRequestError( - message=f"'voice' is required to be passed as a dict for Vertex AI TTS, passed in voice={voice}", - model=model, - llm_provider=custom_llm_provider, - ) + # Handle Gemini models separately (they use speech_to_completion_bridge) if "gemini" in model: from .endpoints.speech.speech_to_completion_bridge.handler import ( speech_to_completion_bridge_handler, @@ -6184,19 +6166,37 @@ def speech( # noqa: PLR0915 logging_obj=logging_obj, custom_llm_provider=custom_llm_provider, ) - response = vertex_text_to_speech.audio_speech( - _is_async=aspeech, - vertex_credentials=vertex_credentials, - vertex_project=vertex_ai_project, - vertex_location=vertex_ai_location, - timeout=timeout, - api_base=api_base, + + # Vertex AI Text-to-Speech (Google Cloud TTS) + if text_to_speech_provider_config is None: + text_to_speech_provider_config = VertexAITextToSpeechConfig() + + # Cast to specific Vertex AI config type to access dispatch method + vertex_config = cast( + VertexAITextToSpeechConfig, text_to_speech_provider_config + ) + + # Store Vertex AI specific params in litellm_params_dict + litellm_params_dict.update({ + "vertex_project": generic_optional_params.vertex_project, + "vertex_location": generic_optional_params.vertex_location, + "vertex_credentials": generic_optional_params.vertex_credentials, + }) + + response = vertex_config.dispatch_text_to_speech( model=model, input=input, voice=voice, optional_params=optional_params, - kwargs=kwargs, + litellm_params_dict=litellm_params_dict, logging_obj=logging_obj, + timeout=timeout, + extra_headers=headers, + base_llm_http_handler=base_llm_http_handler, + aspeech=aspeech or False, + api_base=generic_optional_params.api_base, + api_key=None, # Vertex AI uses OAuth, not API key + **kwargs, ) elif custom_llm_provider == "gemini": from .endpoints.speech.speech_to_completion_bridge.handler import ( diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index f28e9b1290f..f3398e470d8 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -24825,6 +24825,15 @@ "supports_parallel_function_calling": true, "supports_tool_choice": true }, + "vertex_ai/chirp": { + "input_cost_per_character": 30e-06, + "litellm_provider": "vertex_ai", + "mode": "audio_speech", + "source": "https://cloud.google.com/text-to-speech/pricing", + "supported_endpoints": [ + "/v1/audio/speech" + ] + }, "vertex_ai/claude-3-5-haiku": { "input_cost_per_token": 1e-06, "litellm_provider": "vertex_ai-anthropic_models", diff --git a/litellm/types/llms/vertex_ai_text_to_speech.py b/litellm/types/llms/vertex_ai_text_to_speech.py new file mode 100644 index 00000000000..e65b75356bf --- /dev/null +++ b/litellm/types/llms/vertex_ai_text_to_speech.py @@ -0,0 +1,54 @@ +""" +Type definitions for Vertex AI Text-to-Speech API + +Reference: https://cloud.google.com/text-to-speech/docs/reference/rest/v1/text/synthesize +""" + +from typing import Optional + +from typing_extensions import TypedDict + + +class VertexTextToSpeechInput(TypedDict, total=False): + """ + Input for Vertex AI Text-to-Speech synthesis. + + Exactly one of text or ssml must be provided. + """ + text: Optional[str] + ssml: Optional[str] + + +class VertexTextToSpeechVoice(TypedDict, total=False): + """ + Voice configuration for Vertex AI Text-to-Speech. + + Attributes: + languageCode: The language code (e.g., "en-US", "de-DE") + name: The voice name (e.g., "en-US-Studio-O", "en-US-Wavenet-D") + """ + languageCode: str + name: str + + +class VertexTextToSpeechAudioConfig(TypedDict, total=False): + """ + Audio configuration for Vertex AI Text-to-Speech. + + Attributes: + audioEncoding: The audio encoding format (e.g., "LINEAR16", "MP3", "OGG_OPUS") + speakingRate: The speaking rate (0.25 to 4.0, default "1") + """ + audioEncoding: str + speakingRate: str + + +class VertexTextToSpeechRequest(TypedDict, total=False): + """ + Request body for Vertex AI Text-to-Speech API. + + Reference: https://cloud.google.com/text-to-speech/docs/reference/rest/v1/text/synthesize + """ + input: VertexTextToSpeechInput + voice: VertexTextToSpeechVoice + audioConfig: Optional[VertexTextToSpeechAudioConfig] diff --git a/litellm/utils.py b/litellm/utils.py index 6c50afc5f49..3775506e248 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -7910,6 +7910,12 @@ class ProviderConfigManager: ) return RunwayMLTextToSpeechConfig() + elif litellm.LlmProviders.VERTEX_AI == provider: + from litellm.llms.vertex_ai.text_to_speech.transformation import ( + VertexAITextToSpeechConfig, + ) + + return VertexAITextToSpeechConfig() return None @staticmethod diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index f28e9b1290f..f3398e470d8 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -24825,6 +24825,15 @@ "supports_parallel_function_calling": true, "supports_tool_choice": true }, + "vertex_ai/chirp": { + "input_cost_per_character": 30e-06, + "litellm_provider": "vertex_ai", + "mode": "audio_speech", + "source": "https://cloud.google.com/text-to-speech/pricing", + "supported_endpoints": [ + "/v1/audio/speech" + ] + }, "vertex_ai/claude-3-5-haiku": { "input_cost_per_token": 1e-06, "litellm_provider": "vertex_ai-anthropic_models", diff --git a/provider_endpoints_support.json b/provider_endpoints_support.json index b5bde3e5ce4..441391dc742 100644 --- a/provider_endpoints_support.json +++ b/provider_endpoints_support.json @@ -734,6 +734,22 @@ "ocr": true } }, + "vertex_ai/chirp": { + "display_name": "Google - Vertex AI Chirp3 HD (`vertex_ai/chirp`)", + "url": "https://docs.litellm.ai/docs/providers/vertex_speech", + "endpoints": { + "chat_completions": false, + "messages": false, + "responses": false, + "embeddings": false, + "image_generations": false, + "audio_transcriptions": false, + "audio_speech": true, + "moderations": false, + "batches": false, + "rerank": false + } + }, "gemini": { "display_name": "Google AI Studio - Gemini (`gemini`)", "url": "https://docs.litellm.ai/docs/providers/gemini", diff --git a/tests/audio_tests/speech_vertex.mp3 b/tests/audio_tests/speech_vertex.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..91efaea150caa267f9f54ae56caed2c719dd1992 GIT binary patch literal 122924 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 l`z2)l`SgN#7i}u-K}Bg{{^?-@)rOA literal 0 HcmV?d00001 diff --git a/tests/litellm/llms/vertex_ai/text_to_speech/test_transformation.py b/tests/litellm/llms/vertex_ai/text_to_speech/test_transformation.py new file mode 100644 index 00000000000..9a5888e8506 --- /dev/null +++ b/tests/litellm/llms/vertex_ai/text_to_speech/test_transformation.py @@ -0,0 +1,190 @@ +import json +import os +import sys +from unittest.mock import MagicMock, Mock, patch + +import httpx +import pytest + +sys.path.insert( + 0, os.path.abspath("../../../../..") +) # Adds the parent directory to the system path + +import litellm +from litellm.llms.vertex_ai.text_to_speech.transformation import ( + VertexAITextToSpeechConfig, +) + + +class TestVertexAITextToSpeechConfig: + """Tests for VertexAITextToSpeechConfig transformation""" + + def test_get_complete_url(self): + """Test that get_complete_url returns the correct Google Cloud TTS API URL""" + config = VertexAITextToSpeechConfig() + + url = config.get_complete_url( + model="vertex_ai/chirp", + api_base=None, + litellm_params={}, + ) + + assert url == "https://texttospeech.googleapis.com/v1/text:synthesize" + + def test_get_complete_url_with_custom_api_base(self): + """Test that get_complete_url uses custom api_base when provided""" + config = VertexAITextToSpeechConfig() + + custom_url = "https://custom-tts-endpoint.example.com/v1/synthesize" + url = config.get_complete_url( + model="vertex_ai/chirp", + api_base=custom_url, + litellm_params={}, + ) + + assert url == custom_url + + @patch.object(VertexAITextToSpeechConfig, "_ensure_access_token") + @patch.object(VertexAITextToSpeechConfig, "_get_token_and_url") + def test_transform_text_to_speech_request_body( + self, mock_get_token, mock_ensure_token + ): + """Test that transform_text_to_speech_request generates correct request body""" + # Mock authentication + mock_ensure_token.return_value = ("mock-token", "test-project") + mock_get_token.return_value = ("mock-token", "mock-url") + + config = VertexAITextToSpeechConfig() + + # Test with voice dict in litellm_params (as set by dispatch) + result = config.transform_text_to_speech_request( + model="vertex_ai/chirp", + input="Hello, this is a test", + voice=None, + optional_params={ + "vertex_voice_dict": { + "languageCode": "en-US", + "name": "en-US-Chirp3-HD-Charon", + } + }, + litellm_params={ + "vertex_credentials": None, + "vertex_project": "test-project", + "vertex_location": "us-central1", + }, + headers={}, + ) + + # Verify request body structure + assert "dict_body" in result + request_body = result["dict_body"] + + assert "input" in request_body + assert request_body["input"] == {"text": "Hello, this is a test"} + + assert "voice" in request_body + assert request_body["voice"]["languageCode"] == "en-US" + assert request_body["voice"]["name"] == "en-US-Chirp3-HD-Charon" + + assert "audioConfig" in request_body + + # Verify headers contain auth + assert "headers" in result + assert "Authorization" in result["headers"] + + def test_voice_mapping_openai_to_vertex(self): + """Test that OpenAI voice names are correctly mapped to Vertex AI voices""" + config = VertexAITextToSpeechConfig() + + # Test the _map_voice_to_vertex_format helper + voice_str, voice_dict = config._map_voice_to_vertex_format("alloy") + + assert voice_str == "alloy" + assert voice_dict is not None + assert voice_dict["name"] == "en-US-Studio-O" + assert voice_dict["languageCode"] == "en-US" + + def test_voice_mapping_vertex_voice_passthrough(self): + """Test that Vertex AI voice names are passed through directly""" + config = VertexAITextToSpeechConfig() + + # Test with a Chirp3 HD voice + voice_str, voice_dict = config._map_voice_to_vertex_format( + "en-US-Chirp3-HD-Charon" + ) + + assert voice_str == "en-US-Chirp3-HD-Charon" + assert voice_dict is not None + assert voice_dict["name"] == "en-US-Chirp3-HD-Charon" + assert voice_dict["languageCode"] == "en-US" + + def test_voice_mapping_dict_passthrough(self): + """Test that voice dict is passed through unchanged""" + config = VertexAITextToSpeechConfig() + + voice_input = { + "languageCode": "de-DE", + "name": "de-DE-Chirp3-HD-Charon", + } + voice_str, voice_dict = config._map_voice_to_vertex_format(voice_input) + + assert voice_str is None + assert voice_dict == voice_input + + +@patch("litellm.llms.custom_httpx.llm_http_handler.HTTPHandler.post") +@patch.object(VertexAITextToSpeechConfig, "_ensure_access_token") +@patch.object(VertexAITextToSpeechConfig, "_get_token_and_url") +def test_litellm_speech_vertex_ai_chirp(mock_get_token, mock_ensure_token, mock_post): + """ + Test that litellm.speech(model="vertex_ai/chirp") sends the correct URL and request body + """ + # Mock authentication + mock_ensure_token.return_value = ("mock-token", "test-project") + mock_get_token.return_value = ("mock-token", "mock-url") + + # Mock HTTP response + mock_response = Mock(spec=httpx.Response) + mock_response.content = b'{"audioContent": "SGVsbG8gV29ybGQ="}' # base64 encoded "Hello World" + mock_response.status_code = 200 + mock_response.headers = {"content-type": "application/json"} + mock_response.json.return_value = {"audioContent": "SGVsbG8gV29ybGQ="} + mock_post.return_value = mock_response + + litellm.speech( + model="vertex_ai/chirp", + input="Hello, this is a test", + voice="en-US-Chirp3-HD-Charon", + vertex_project="test-project", + vertex_location="us-central1", + ) + + # Verify the HTTP call was made + mock_post.assert_called_once() + call_kwargs = mock_post.call_args.kwargs + + # Verify the URL is the Google Cloud TTS API + assert call_kwargs["url"] == "https://texttospeech.googleapis.com/v1/text:synthesize" + + # Verify request body structure + assert "data" in call_kwargs + request_body = json.loads(call_kwargs["data"]) + + # Verify input + assert "input" in request_body + assert request_body["input"] == {"text": "Hello, this is a test"} + + # Verify voice + assert "voice" in request_body + assert request_body["voice"]["name"] == "en-US-Chirp3-HD-Charon" + assert request_body["voice"]["languageCode"] == "en-US" + + # Verify audioConfig + assert "audioConfig" in request_body + + # Verify headers contain authorization + assert "headers" in call_kwargs + assert "Authorization" in call_kwargs["headers"] + assert call_kwargs["headers"]["Authorization"] == "Bearer mock-token" + + From 7fb2f4730b2fd0eef4269e1ce711b5dbfa2be88e Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Tue, 2 Dec 2025 15:53:10 -0800 Subject: [PATCH 023/107] build: remove duplicate packages --- docker/Dockerfile.non_root | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/docker/Dockerfile.non_root b/docker/Dockerfile.non_root index 2dcb7cb4787..4616ff9be60 100644 --- a/docker/Dockerfile.non_root +++ b/docker/Dockerfile.non_root @@ -10,7 +10,17 @@ WORKDIR /app # Install build dependencies including Node.js for UI build USER root -RUN apk add --no-cache build-base bash nodejs npm \ +RUN apk add --no-cache \ + clang \ + llvm \ + lld \ + gcc \ + python3-dev \ + linux-headers \ + build-base \ + bash \ + nodejs \ + npm \ && pip install --no-cache-dir --upgrade pip build # Copy project files From 9867cc6cf98422bb29aa326ec75a1c5068facce1 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Tue, 2 Dec 2025 15:58:27 -0800 Subject: [PATCH 024/107] Change edit team models to match create team models --- .../src/components/team/team_info.test.tsx | 115 +++++++++++++++++- .../src/components/team/team_info.tsx | 101 +++++++++++++-- 2 files changed, 202 insertions(+), 14 deletions(-) diff --git a/ui/litellm-dashboard/src/components/team/team_info.test.tsx b/ui/litellm-dashboard/src/components/team/team_info.test.tsx index 54362193350..3a87b42d253 100644 --- a/ui/litellm-dashboard/src/components/team/team_info.test.tsx +++ b/ui/litellm-dashboard/src/components/team/team_info.test.tsx @@ -13,6 +13,7 @@ vi.mock("@/components/networking", () => ({ getGuardrailsList: vi.fn(), fetchMCPAccessGroups: vi.fn(), getTeamPermissionsCall: vi.fn(), + organizationInfoCall: vi.fn(), })); describe("TeamInfoView", () => { @@ -161,6 +162,116 @@ describe("TeamInfoView", () => { const allProxyModelsOption = screen.queryByText("All Proxy Models"); expect(allProxyModelsOption).not.toBeInTheDocument(); - }, // This is a workaround to fix the flaky test issue. TODO: Remove this once we have a better solution. - 10000); + }, 10000); // This is a workaround to fix the flaky test issue. TODO: Remove this once we have a better solution. + + it("should only show organization models in dropdown when team is in organization with limited models", async () => { + const organizationId = "org-123"; + const organizationModels = ["gpt-4", "claude-3-opus"]; + const userModels = ["gpt-4", "gpt-3.5-turbo", "claude-3-opus", "claude-2"]; + + vi.mocked(networking.teamInfoCall).mockResolvedValue({ + team_id: "123", + team_info: { + team_alias: "Test Team", + team_id: "123", + organization_id: organizationId, + admins: ["admin@test.com"], + members: ["user1@test.com"], + members_with_roles: [ + { + user_id: "user1@test.com", + user_email: "user1@test.com", + role: "member", + spend: 0, + budget_id: "budget1", + }, + ], + metadata: {}, + tpm_limit: null, + rpm_limit: null, + max_budget: null, + budget_duration: null, + models: ["gpt-4"], + blocked: false, + spend: 0, + max_parallel_requests: null, + budget_reset_at: null, + model_id: null, + litellm_model_table: null, + created_at: "2024-01-01T00:00:00Z", + team_member_budget_table: null, + }, + keys: [], + team_memberships: [], + }); + + vi.mocked(networking.organizationInfoCall).mockResolvedValue({ + organization_id: organizationId, + organization_name: "Test Organization", + spend: 0, + max_budget: null, + models: organizationModels, + tpm_limit: null, + rpm_limit: null, + members: null, + }); + + vi.mocked(networking.getGuardrailsList).mockResolvedValue({ guardrails: [] }); + vi.mocked(networking.fetchMCPAccessGroups).mockResolvedValue([]); + + render( + {}} + onClose={() => {}} + accessToken="123" + is_team_admin={true} + is_proxy_admin={true} + userModels={userModels} + editTeam={false} + premiumUser={false} + />, + ); + + await waitFor(() => { + expect(screen.getAllByText("Test Team")).not.toBeNull(); + }); + + const settingsTab = screen.getByRole("tab", { name: "Settings" }); + act(() => { + fireEvent.click(settingsTab); + }); + + await waitFor(() => { + expect(screen.getByText("Team Settings")).toBeInTheDocument(); + }); + + const editButton = screen.getByRole("button", { name: "Edit Settings" }); + act(() => { + fireEvent.click(editButton); + }); + + await waitFor(() => { + expect(screen.getByLabelText("Models")).toBeInTheDocument(); + }); + + const modelsSelect = screen.getByLabelText("Models"); + act(() => { + fireEvent.mouseDown(modelsSelect); + }); + + await waitFor(() => { + const dropdownOptions = screen.getAllByRole("option"); + const optionTexts = dropdownOptions.map((option) => option.textContent); + + organizationModels.forEach((model) => { + expect(optionTexts).toContain(model); + }); + + const modelsNotInOrganization = userModels.filter((m) => !organizationModels.includes(m)); + modelsNotInOrganization.forEach((model) => { + expect(optionTexts).not.toContain(model); + }); + }); + }, 10000); }); diff --git a/ui/litellm-dashboard/src/components/team/team_info.tsx b/ui/litellm-dashboard/src/components/team/team_info.tsx index a53d35e57bb..581722e8fa7 100644 --- a/ui/litellm-dashboard/src/components/team/team_info.tsx +++ b/ui/litellm-dashboard/src/components/team/team_info.tsx @@ -2,6 +2,8 @@ import UserSearchModal from "@/components/common_components/user_search_modal"; import { getGuardrailsList, Member, + Organization, + organizationInfoCall, teamInfoCall, teamMemberAddCall, teamMemberDeleteCall, @@ -28,11 +30,11 @@ import { } from "@tremor/react"; import { Button, Form, Input, message, Select, Switch, Tooltip } from "antd"; import { CheckIcon, CopyIcon } from "lucide-react"; -import React, { useEffect, useState } from "react"; +import React, { useEffect, useMemo, useState } from "react"; import { copyToClipboard as utilCopyToClipboard } from "../../utils/dataUtils"; import DeleteResourceModal from "../common_components/DeleteResourceModal"; import PassThroughRoutesSelector from "../common_components/PassThroughRoutesSelector"; -import { getModelDisplayName } from "../key_team_helpers/fetch_available_models_team_key"; +import { getModelDisplayName, unfurlWildcardModelsInList } from "../key_team_helpers/fetch_available_models_team_key"; import LoggingSettingsView from "../logging_settings_view"; import MCPServerSelector from "../mcp_server_management/MCPServerSelector"; import MCPToolPermissions from "../mcp_server_management/MCPToolPermissions"; @@ -117,6 +119,29 @@ export interface TeamInfoProps { premiumUser?: boolean; } +const getOrganizationModels = (organization: Organization | null, userModels: string[]) => { + let tempModelsToPick = []; + + if (organization) { + // Check if organization has "all-proxy-models" in its models array + if (organization.models.includes("all-proxy-models")) { + // Treat as all-proxy-models (use userModels) + tempModelsToPick = userModels; + } else if (organization.models.length > 0) { + // Organization has specific models + tempModelsToPick = organization.models; + } else { + // Empty array [] is treated as all-proxy-models + tempModelsToPick = userModels; + } + } else { + // No organization, show all available models + tempModelsToPick = userModels; + } + + return unfurlWildcardModelsInList(tempModelsToPick, userModels); +}; + const TeamInfoView: React.FC = ({ teamId, onClose, @@ -143,6 +168,7 @@ const TeamInfoView: React.FC = ({ const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [isDeleting, setIsDeleting] = useState(false); const [isTeamSaving, setIsTeamSaving] = useState(false); + const [organization, setOrganization] = useState(null); console.log("userModels in team info", userModels); @@ -166,6 +192,31 @@ const TeamInfoView: React.FC = ({ fetchTeamInfo(); }, [teamId, accessToken]); + // Fetch organization data when team has organization_id + useEffect(() => { + const fetchOrganization = async () => { + if (!accessToken || !teamData?.team_info?.organization_id) { + setOrganization(null); + return; + } + + try { + const orgData = await organizationInfoCall(accessToken, teamData.team_info.organization_id); + setOrganization(orgData); + } catch (error) { + console.error("Error fetching organization info:", error); + setOrganization(null); + } + }; + + fetchOrganization(); + }, [accessToken, teamData?.team_info?.organization_id]); + + // Compute modelsToPick based on organization and userModels + const modelsToPick = useMemo(() => { + return getOrganizationModels(organization, userModels); + }, [organization, userModels]); + const fetchMcpAccessGroups = async () => { if (!accessToken) return; if (mcpAccessGroupsLoaded) return; @@ -596,15 +647,41 @@ const TeamInfoView: React.FC = ({ rules={[{ required: true, message: "Please select at least one model" }]} > + From 8ee298f9c9c178ccc7191ebfbf63736898557443 Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Tue, 2 Dec 2025 16:06:06 -0800 Subject: [PATCH 025/107] fix: remove python3 headers --- docker/Dockerfile.non_root | 1 - 1 file changed, 1 deletion(-) diff --git a/docker/Dockerfile.non_root b/docker/Dockerfile.non_root index 4616ff9be60..8b66a367eeb 100644 --- a/docker/Dockerfile.non_root +++ b/docker/Dockerfile.non_root @@ -15,7 +15,6 @@ RUN apk add --no-cache \ llvm \ lld \ gcc \ - python3-dev \ linux-headers \ build-base \ bash \ From 10d56e7c4682c868c9c11483d21f77abc3d042f5 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Tue, 2 Dec 2025 16:19:34 -0800 Subject: [PATCH 026/107] add new deepseek-v3p2 (#17395) --- 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 f3398e470d8..932508824af 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -10421,6 +10421,19 @@ "supports_response_schema": true, "supports_tool_choice": true }, + "fireworks_ai/accounts/fireworks/models/deepseek-v3p2": { + "input_cost_per_token": 1.2e-06, + "litellm_provider": "fireworks_ai", + "max_input_tokens": 163840, + "max_output_tokens": 163840, + "max_tokens": 163840, + "mode": "chat", + "output_cost_per_token": 1.2e-06, + "source": "https://fireworks.ai/models/fireworks/deepseek-v3p2", + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, "fireworks_ai/accounts/fireworks/models/firefunction-v2": { "input_cost_per_token": 9e-07, "litellm_provider": "fireworks_ai", diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index f3398e470d8..932508824af 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -10421,6 +10421,19 @@ "supports_response_schema": true, "supports_tool_choice": true }, + "fireworks_ai/accounts/fireworks/models/deepseek-v3p2": { + "input_cost_per_token": 1.2e-06, + "litellm_provider": "fireworks_ai", + "max_input_tokens": 163840, + "max_output_tokens": 163840, + "max_tokens": 163840, + "mode": "chat", + "output_cost_per_token": 1.2e-06, + "source": "https://fireworks.ai/models/fireworks/deepseek-v3p2", + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, "fireworks_ai/accounts/fireworks/models/firefunction-v2": { "input_cost_per_token": 9e-07, "litellm_provider": "fireworks_ai", From 31cad8e6e63a9de464c1dbc3d5635a7fcc4962f5 Mon Sep 17 00:00:00 2001 From: flozonn <74357383+flozonn@users.noreply.github.com> Date: Wed, 3 Dec 2025 01:33:07 +0100 Subject: [PATCH 027/107] feat: Add Nova lite 2 reasoning support with reasoningConfig (#17371) --- litellm/constants.py | 3 + .../bedrock/chat/converse_transformation.py | 102 ++- model_prices_and_context_window.json | 65 ++ .../chat/test_converse_transformation.py | 33 + .../test_converse_transformation_nova_2.py | 794 ++++++++++++++++++ 5 files changed, 995 insertions(+), 2 deletions(-) create mode 100644 tests/test_litellm/llms/bedrock/chat/test_converse_transformation_nova_2.py diff --git a/litellm/constants.py b/litellm/constants.py index 1d42ef9a910..6a67a9a0e18 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -909,6 +909,9 @@ BEDROCK_CONVERSE_MODELS = [ "meta.llama3-2-3b-instruct-v1:0", "meta.llama3-2-11b-instruct-v1:0", "meta.llama3-2-90b-instruct-v1:0", + "amazon.nova-lite-v1:0", + "amazon.nova-2-lite-v1:0", + "amazon.nova-pro-v1:0", ] diff --git a/litellm/llms/bedrock/chat/converse_transformation.py b/litellm/llms/bedrock/chat/converse_transformation.py index 3b3a138ec67..705f3c9e630 100644 --- a/litellm/llms/bedrock/chat/converse_transformation.py +++ b/litellm/llms/bedrock/chat/converse_transformation.py @@ -246,6 +246,93 @@ class AmazonConverseConfig(BaseConfig): llm_provider="bedrock", ) + def _is_nova_lite_2_model(self, model: str) -> bool: + """ + Check if the model is a Nova Lite 2 model that supports reasoningConfig. + + Nova Lite 2 models use a different reasoning configuration structure compared to + Anthropic's thinking parameter and GPT-OSS's reasoning_effort parameter. + + Supported models: + - amazon.nova-2-lite-v1:0 + - us.amazon.nova-2-lite-v1:0 + - eu.amazon.nova-2-lite-v1:0 + - apac.amazon.nova-2-lite-v1:0 + + Args: + model: The model identifier + + Returns: + True if the model is a Nova Lite 2 model, False otherwise + + Examples: + >>> config = AmazonConverseConfig() + >>> config._is_nova_lite_2_model("amazon.nova-2-lite-v1:0") + True + >>> config._is_nova_lite_2_model("us.amazon.nova-2-lite-v1:0") + True + >>> config._is_nova_lite_2_model("amazon.nova-pro-1-5-v1:0") + False + >>> config._is_nova_lite_2_model("amazon.nova-pro-v1:0") + False + """ + # Remove regional prefix if present (us., eu., apac.) + model_without_region = model + for prefix in ["us.", "eu.", "apac."]: + if model.startswith(prefix): + model_without_region = model[len(prefix) :] + break + + # Check if the model is specifically Nova Lite 2 + return "nova-2-lite" in model_without_region + + def _transform_reasoning_effort_to_reasoning_config( + self, reasoning_effort: str + ) -> dict: + """ + Transform reasoning_effort parameter to Nova 2 reasoningConfig structure. + + Nova 2 models use a reasoningConfig structure in additionalModelRequestFields + that differs from both Anthropic's thinking parameter and GPT-OSS's reasoning_effort. + + Args: + reasoning_effort: The reasoning effort level, must be "low" or "high" + + Returns: + dict: A dictionary containing the reasoningConfig structure: + { + "reasoningConfig": { + "type": "enabled", + "maxReasoningEffort": "low" | "medium" |"high" + } + } + + Raises: + BadRequestError: If reasoning_effort is not "low", "medium" or "high" + + Examples: + >>> config = AmazonConverseConfig() + >>> config._transform_reasoning_effort_to_reasoning_config("high") + {'reasoningConfig': {'type': 'enabled', 'maxReasoningEffort': 'high'}} + >>> config._transform_reasoning_effort_to_reasoning_config("low") + {'reasoningConfig': {'type': 'enabled', 'maxReasoningEffort': 'low'}} + """ + valid_values = ["low", "medium", "high"] + if reasoning_effort not in valid_values: + raise litellm.exceptions.BadRequestError( + message=f"Invalid reasoning_effort value '{reasoning_effort}' for Nova 2 models. " + f"Supported values: {valid_values}", + model="amazon.nova-2-lite-v1:0", + llm_provider="bedrock_converse", + ) + + return { + "reasoningConfig": { + "type": "enabled", + "maxReasoningEffort": reasoning_effort, + } + } + def get_supported_openai_params(self, model: str) -> List[str]: from litellm.utils import supports_function_calling @@ -299,6 +386,10 @@ class AmazonConverseConfig(BaseConfig): if "gpt-oss" in model: supported_params.append("reasoning_effort") + elif self._is_nova_lite_2_model(model): + # Nova Lite 2 models support reasoning_effort (transformed to reasoningConfig) + # These models use a different reasoning structure than Anthropic's thinking parameter + supported_params.append("reasoning_effort") elif ( "claude-3-7" in model or "claude-sonnet-4" in model @@ -564,6 +655,12 @@ class AmazonConverseConfig(BaseConfig): # GPT-OSS models: keep reasoning_effort as-is # It will be passed through to additionalModelRequestFields optional_params["reasoning_effort"] = value + elif self._is_nova_lite_2_model(model): + # Nova Lite 2 models: transform to reasoningConfig + reasoning_config = ( + self._transform_reasoning_effort_to_reasoning_config(value) + ) + optional_params.update(reasoning_config) else: # Anthropic and other models: convert to thinking parameter optional_params["thinking"] = AnthropicConfig._map_reasoning_effort( @@ -574,8 +671,9 @@ class AmazonConverseConfig(BaseConfig): self._validate_request_metadata(value) # type: ignore optional_params["requestMetadata"] = value - # Only update thinking tokens for non-GPT-OSS models - if "gpt-oss" not in model: + # Only update thinking tokens for non-GPT-OSS models and non-Nova-Lite-2 models + # Nova Lite 2 handles token budgeting differently through reasoningConfig + if "gpt-oss" not in model and not self._is_nova_lite_2_model(model): self.update_optional_params_with_thinking_tokens( non_default_params=non_default_params, optional_params=optional_params ) diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 932508824af..19ed734c5f8 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -269,6 +269,71 @@ "supports_response_schema": true, "supports_vision": true }, + "amazon.nova-2-lite-v1:0": { + "input_cost_per_token": 3e-07, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 1000000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 2.5e-06, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_video_input": true, + "supports_vision": true + }, + "apac.amazon.nova-2-lite-v1:0": { + "input_cost_per_token": 6e-08, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 1000000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 2.75e-06, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_video_input": true, + "supports_vision": true + }, + "eu.amazon.nova-2-lite-v1:0": { + "input_cost_per_token": 6e-08, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 1000000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 2.75e-06, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_video_input": true, + "supports_vision": true + }, + "us.amazon.nova-2-lite-v1:0": { + "input_cost_per_token": 6e-08, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 1000000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 2.75e-06, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_video_input": true, + "supports_vision": true + }, + "amazon.nova-micro-v1:0": { "input_cost_per_token": 3.5e-08, "litellm_provider": "bedrock_converse", diff --git a/tests/test_litellm/llms/bedrock/chat/test_converse_transformation.py b/tests/test_litellm/llms/bedrock/chat/test_converse_transformation.py index 37c95be72ce..e603f94ab87 100644 --- a/tests/test_litellm/llms/bedrock/chat/test_converse_transformation.py +++ b/tests/test_litellm/llms/bedrock/chat/test_converse_transformation.py @@ -2702,3 +2702,36 @@ def test_empty_assistant_message_handling(): finally: # Restore original modify_params setting litellm.modify_params = original_modify_params + + +def test_is_nova_lite_2_model(): + """Test the _is_nova_lite_2_model() method for detecting Nova 2 models.""" + config = AmazonConverseConfig() + + # Test with amazon.nova-2-lite-v1:0 + assert config._is_nova_lite_2_model("amazon.nova-2-lite-v1:0") is True + + # Test with regional variants + assert config._is_nova_lite_2_model("us.amazon.nova-2-lite-v1:0") is True + assert config._is_nova_lite_2_model("eu.amazon.nova-2-lite-v1:0") is True + assert config._is_nova_lite_2_model("apac.amazon.nova-2-lite-v1:0") is True + + # Test with other Nova 2 variants (pro, micro) + assert config._is_nova_lite_2_model("amazon.nova-pro-1-5-v1:0") is False + assert config._is_nova_lite_2_model("amazon.nova-micro-1-5-v1:0") is False + assert config._is_nova_lite_2_model("us.amazon.nova-pro-1-5-v1:0") is False + assert config._is_nova_lite_2_model("eu.amazon.nova-micro-1-5-v1:0") is False + + # Test with non-Nova-1.5 lite models (should return False) + assert config._is_nova_lite_2_model("amazon.nova-lite-v1:0") is False + assert config._is_nova_lite_2_model("amazon.nova-pro-v1:0") is False + assert config._is_nova_lite_2_model("amazon.nova-micro-v1:0") is False + + # Test with Nova v1:0 models (should return False) + assert config._is_nova_lite_2_model("us.amazon.nova-lite-v1:0") is False + assert config._is_nova_lite_2_model("eu.amazon.nova-pro-v1:0") is False + + # Test with completely different models (should return False) + assert config._is_nova_lite_2_model("anthropic.claude-3-5-sonnet-20240620-v1:0") is False + assert config._is_nova_lite_2_model("meta.llama3-70b-instruct-v1:0") is False + assert config._is_nova_lite_2_model("mistral.mistral-7b-instruct-v0:2") is False diff --git a/tests/test_litellm/llms/bedrock/chat/test_converse_transformation_nova_2.py b/tests/test_litellm/llms/bedrock/chat/test_converse_transformation_nova_2.py new file mode 100644 index 00000000000..23243dac201 --- /dev/null +++ b/tests/test_litellm/llms/bedrock/chat/test_converse_transformation_nova_2.py @@ -0,0 +1,794 @@ +""" +Unit tests for Amazon Nova 2 reasoning configuration transformation. + +Tests the _transform_reasoning_effort_to_reasoning_config method in AmazonConverseConfig. +""" + +import pytest +import sys +import os + +sys.path.insert( + 0, os.path.abspath("../..") +) # Adds the parent directory to the system path + +import litellm +from litellm.llms.bedrock.chat.converse_transformation import AmazonConverseConfig + + +class TestNova15ReasoningTransformation: + """Test suite for Nova 2 reasoning effort transformation.""" + + def test_reasoning_effort_low_transformation(self): + """Test that reasoning_effort='low' is transformed to correct reasoningConfig structure.""" + config = AmazonConverseConfig() + + result = config._transform_reasoning_effort_to_reasoning_config("low") + + # Verify the structure + assert "reasoningConfig" in result + assert result["reasoningConfig"]["type"] == "enabled" + assert result["reasoningConfig"]["maxReasoningEffort"] == "low" + + def test_reasoning_effort_high_transformation(self): + """Test that reasoning_effort='high' is transformed to correct reasoningConfig structure.""" + config = AmazonConverseConfig() + + result = config._transform_reasoning_effort_to_reasoning_config("high") + + # Verify the structure + assert "reasoningConfig" in result + assert result["reasoningConfig"]["type"] == "enabled" + assert result["reasoningConfig"]["maxReasoningEffort"] == "high" + + def test_invalid_reasoning_effort_value(self): + """Test that invalid reasoning_effort values raise BadRequestError.""" + config = AmazonConverseConfig() + + # Test with invalid value "invalid" + with pytest.raises(litellm.exceptions.BadRequestError) as exc_info: + config._transform_reasoning_effort_to_reasoning_config("invalid") + + # Verify error message contains the invalid value and valid values + error_message = str(exc_info.value) + assert "invalid" in error_message + assert "low" in error_message + assert "high" in error_message + assert "Nova 2" in error_message + + def test_invalid_reasoning_effort_empty_string(self): + """Test that empty string raises BadRequestError.""" + config = AmazonConverseConfig() + + with pytest.raises(litellm.exceptions.BadRequestError) as exc_info: + config._transform_reasoning_effort_to_reasoning_config("") + + # Verify error message + error_message = str(exc_info.value) + assert "low" in error_message + assert "high" in error_message + + def test_invalid_reasoning_effort_wrong_case(self): + """Test that case-sensitive values are rejected (e.g., 'Low' instead of 'low').""" + config = AmazonConverseConfig() + + with pytest.raises(litellm.exceptions.BadRequestError): + config._transform_reasoning_effort_to_reasoning_config("Low") + + with pytest.raises(litellm.exceptions.BadRequestError): + config._transform_reasoning_effort_to_reasoning_config("HIGH") + + +class TestNova2ParameterMapping: + """Test suite for Nova 2 parameter mapping integration.""" + + def test_nova_2_reasoning_effort_low_mapping(self): + """Test that reasoning_effort='low' is correctly mapped to reasoningConfig for Nova 2.""" + config = AmazonConverseConfig() + + model = "amazon.nova-2-lite-v1:0" + non_default_params = {"reasoning_effort": "low"} + optional_params = {} + + result = config.map_openai_params( + non_default_params=non_default_params, + optional_params=optional_params, + model=model, + drop_params=False, + ) + + # Verify reasoningConfig is in result + assert "reasoningConfig" in result + assert result["reasoningConfig"]["type"] == "enabled" + assert result["reasoningConfig"]["maxReasoningEffort"] == "low" + + # Verify thinking is NOT in result + assert "thinking" not in result + + # Verify reasoning_effort is NOT kept as-is (should be transformed) + assert "reasoning_effort" not in result + + def test_nova_2_reasoning_effort_high_mapping(self): + """Test that reasoning_effort='high' is correctly mapped to reasoningConfig for Nova 2.""" + config = AmazonConverseConfig() + + model = "amazon.nova-2-lite-v1:0" + non_default_params = {"reasoning_effort": "high"} + optional_params = {} + + result = config.map_openai_params( + non_default_params=non_default_params, + optional_params=optional_params, + model=model, + drop_params=False, + ) + + # Verify reasoningConfig is in result + assert "reasoningConfig" in result + assert result["reasoningConfig"]["type"] == "enabled" + assert result["reasoningConfig"]["maxReasoningEffort"] == "high" + + # Verify thinking is NOT in result + assert "thinking" not in result + + # Verify reasoning_effort is NOT kept as-is (should be transformed) + assert "reasoning_effort" not in result + + def test_nova_2_without_reasoning_effort(self): + """Test that Nova 2 without reasoning_effort has no reasoningConfig in result.""" + config = AmazonConverseConfig() + + model = "amazon.nova-2-lite-v1:0" + non_default_params = {"temperature": 0.7} + optional_params = {} + + result = config.map_openai_params( + non_default_params=non_default_params, + optional_params=optional_params, + model=model, + drop_params=False, + ) + + # Verify reasoningConfig is NOT in result + assert "reasoningConfig" not in result + + # Verify thinking is NOT in result + assert "thinking" not in result + + # Verify reasoning_effort is NOT in result + assert "reasoning_effort" not in result + + def test_nova_2_regional_variant_us(self): + """Test that US regional variant of Nova 2 works correctly.""" + config = AmazonConverseConfig() + + model = "us.amazon.nova-2-lite-v1:0" + non_default_params = {"reasoning_effort": "high"} + optional_params = {} + + result = config.map_openai_params( + non_default_params=non_default_params, + optional_params=optional_params, + model=model, + drop_params=False, + ) + + # Verify reasoningConfig is in result + assert "reasoningConfig" in result + assert result["reasoningConfig"]["type"] == "enabled" + assert result["reasoningConfig"]["maxReasoningEffort"] == "high" + + def test_nova_2_regional_variant_eu(self): + """Test that EU regional variant of Nova 2 works correctly.""" + config = AmazonConverseConfig() + + model = "eu.amazon.nova-2-lite-v1:0" + non_default_params = {"reasoning_effort": "low"} + optional_params = {} + + result = config.map_openai_params( + non_default_params=non_default_params, + optional_params=optional_params, + model=model, + drop_params=False, + ) + + # Verify reasoningConfig is in result + assert "reasoningConfig" in result + assert result["reasoningConfig"]["type"] == "enabled" + assert result["reasoningConfig"]["maxReasoningEffort"] == "low" + + def test_nova_2_regional_variant_apac(self): + """Test that APAC regional variant of Nova 2 works correctly.""" + config = AmazonConverseConfig() + + model = "apac.amazon.nova-2-lite-v1:0" + non_default_params = {"reasoning_effort": "high"} + optional_params = {} + + result = config.map_openai_params( + non_default_params=non_default_params, + optional_params=optional_params, + model=model, + drop_params=False, + ) + + # Verify reasoningConfig is in result + assert "reasoningConfig" in result + assert result["reasoningConfig"]["type"] == "enabled" + assert result["reasoningConfig"]["maxReasoningEffort"] == "high" + + def test_nova_2_with_other_params(self): + """Test that Nova 2 reasoning works alongside other parameters.""" + config = AmazonConverseConfig() + + model = "amazon.nova-2-lite-v1:0" + non_default_params = { + "reasoning_effort": "high", + "temperature": 0.8, + "max_tokens": 1000, + "top_p": 0.9, + } + optional_params = {} + + result = config.map_openai_params( + non_default_params=non_default_params, + optional_params=optional_params, + model=model, + drop_params=False, + ) + + # Verify reasoningConfig is in result + assert "reasoningConfig" in result + assert result["reasoningConfig"]["type"] == "enabled" + assert result["reasoningConfig"]["maxReasoningEffort"] == "high" + + # Verify other params are also present + assert result["temperature"] == 0.8 + assert result["maxTokens"] == 1000 + assert result["topP"] == 0.9 + + +class TestNova15SupportedParameters: + """Test suite for Nova 2 supported parameters.""" + + def test_nova_2_supports_reasoning_effort(self): + """Test that Nova 2 model reports reasoning_effort in supported params.""" + config = AmazonConverseConfig() + + model = "amazon.nova-2-lite-v1:0" + supported_params = config.get_supported_openai_params(model) + + # Verify reasoning_effort is in supported params + assert "reasoning_effort" in supported_params + + # Verify thinking is NOT in supported params (Nova 2 uses reasoningConfig, not thinking) + assert "thinking" not in supported_params + + def test_nova_2_regional_variant_us_supported_params(self): + """Test that US regional variant returns same supported params.""" + config = AmazonConverseConfig() + + model = "us.amazon.nova-2-lite-v1:0" + supported_params = config.get_supported_openai_params(model) + + # Verify reasoning_effort is in supported params + assert "reasoning_effort" in supported_params + + # Verify thinking is NOT in supported params + assert "thinking" not in supported_params + + def test_nova_2_regional_variant_eu_supported_params(self): + """Test that EU regional variant returns same supported params.""" + config = AmazonConverseConfig() + + model = "eu.amazon.nova-2-lite-v1:0" + supported_params = config.get_supported_openai_params(model) + + # Verify reasoning_effort is in supported params + assert "reasoning_effort" in supported_params + + # Verify thinking is NOT in supported params + assert "thinking" not in supported_params + + def test_nova_2_regional_variant_apac_supported_params(self): + """Test that APAC regional variant returns same supported params.""" + config = AmazonConverseConfig() + + model = "apac.amazon.nova-2-lite-v1:0" + supported_params = config.get_supported_openai_params(model) + + # Verify reasoning_effort is in supported params + assert "reasoning_effort" in supported_params + + # Verify thinking is NOT in supported params + assert "thinking" not in supported_params + + def test_nova_2_has_standard_params(self): + """Test that Nova 2 still has all standard supported params.""" + config = AmazonConverseConfig() + + model = "amazon.nova-2-lite-v1:0" + supported_params = config.get_supported_openai_params(model) + + # Verify standard params are present + assert "max_tokens" in supported_params + assert "max_completion_tokens" in supported_params + assert "stream" in supported_params + assert "stream_options" in supported_params + assert "stop" in supported_params + assert "temperature" in supported_params + assert "top_p" in supported_params + assert "tools" in supported_params + assert "response_format" in supported_params + + +class TestNova15ResponseParsing: + """Test suite for Nova 2 response parsing.""" + + def test_transform_reasoning_content_single_block(self): + """Test that reasoning content is extracted correctly from a single block.""" + config = AmazonConverseConfig() + + reasoning_blocks = [ + {"reasoningText": {"text": "Let me think through this step by step..."}} + ] + + result = config._transform_reasoning_content(reasoning_blocks) + + assert result == "Let me think through this step by step..." + + def test_transform_reasoning_content_multiple_blocks(self): + """Test that reasoning content is concatenated from multiple blocks.""" + config = AmazonConverseConfig() + + reasoning_blocks = [ + {"reasoningText": {"text": "First, I need to analyze the problem. "}}, + {"reasoningText": {"text": "Then, I'll consider the solution."}}, + ] + + result = config._transform_reasoning_content(reasoning_blocks) + + assert ( + result + == "First, I need to analyze the problem. Then, I'll consider the solution." + ) + + def test_transform_reasoning_content_empty_blocks(self): + """Test that empty reasoning blocks return empty string.""" + config = AmazonConverseConfig() + + reasoning_blocks = [] + + result = config._transform_reasoning_content(reasoning_blocks) + + assert result == "" + + def test_transform_thinking_blocks_with_text(self): + """Test that thinking blocks are populated correctly with text.""" + config = AmazonConverseConfig() + + reasoning_blocks = [{"reasoningText": {"text": "My reasoning process..."}}] + + result = config._transform_thinking_blocks(reasoning_blocks) + + assert len(result) == 1 + assert result[0]["type"] == "thinking" + assert result[0]["thinking"] == "My reasoning process..." + assert "signature" not in result[0] + + def test_transform_thinking_blocks_with_signature(self): + """Test that signature field is preserved when present.""" + config = AmazonConverseConfig() + + reasoning_blocks = [ + { + "reasoningText": { + "text": "My reasoning...", + "signature": "signature-hash-12345", + } + } + ] + + result = config._transform_thinking_blocks(reasoning_blocks) + + assert len(result) == 1 + assert result[0]["type"] == "thinking" + assert result[0]["thinking"] == "My reasoning..." + assert result[0]["signature"] == "signature-hash-12345" + + def test_transform_thinking_blocks_with_redacted_content(self): + """Test that redacted content blocks are handled correctly.""" + config = AmazonConverseConfig() + + reasoning_blocks = [ + {"reasoningText": {"text": "First part of reasoning..."}}, + {"redactedContent": {}}, + {"reasoningText": {"text": "Second part after redaction..."}}, + ] + + result = config._transform_thinking_blocks(reasoning_blocks) + + assert len(result) == 3 + assert result[0]["type"] == "thinking" + assert result[0]["thinking"] == "First part of reasoning..." + assert result[1]["type"] == "redacted_thinking" + assert result[2]["type"] == "thinking" + assert result[2]["thinking"] == "Second part after redaction..." + + def test_transform_thinking_blocks_multiple_blocks(self): + """Test that multiple thinking blocks are all transformed.""" + config = AmazonConverseConfig() + + reasoning_blocks = [ + {"reasoningText": {"text": "Step 1: Analyze the problem"}}, + { + "reasoningText": { + "text": "Step 2: Consider solutions", + "signature": "sig-abc", + } + }, + {"reasoningText": {"text": "Step 3: Choose best approach"}}, + ] + + result = config._transform_thinking_blocks(reasoning_blocks) + + assert len(result) == 3 + assert all(block["type"] == "thinking" for block in result) + assert result[0]["thinking"] == "Step 1: Analyze the problem" + assert result[1]["thinking"] == "Step 2: Consider solutions" + assert result[1]["signature"] == "sig-abc" + assert result[2]["thinking"] == "Step 3: Choose best approach" + + def test_transform_thinking_blocks_empty_list(self): + """Test that empty thinking blocks list returns empty list.""" + config = AmazonConverseConfig() + + reasoning_blocks = [] + + result = config._transform_thinking_blocks(reasoning_blocks) + + assert result == [] + + def test_response_parsing_integration(self): + """Test that response parsing works end-to-end with Nova 2 structure.""" + config = AmazonConverseConfig() + + # Simulate a Nova 2 response with reasoning content + reasoning_blocks = [ + { + "reasoningText": { + "text": "Let me analyze this carefully. ", + "signature": "test-signature", + } + }, + {"reasoningText": {"text": "Based on my analysis, the answer is clear."}}, + ] + + # Test reasoning content extraction + reasoning_content = config._transform_reasoning_content(reasoning_blocks) + assert ( + reasoning_content + == "Let me analyze this carefully. Based on my analysis, the answer is clear." + ) + + # Test thinking blocks transformation + thinking_blocks = config._transform_thinking_blocks(reasoning_blocks) + assert len(thinking_blocks) == 2 + assert thinking_blocks[0]["thinking"] == "Let me analyze this carefully. " + assert thinking_blocks[0]["signature"] == "test-signature" + assert ( + thinking_blocks[1]["thinking"] + == "Based on my analysis, the answer is clear." + ) + + +class TestNova15StreamingResponseParsing: + """Test suite for Nova 2 streaming response parsing.""" + + def test_streaming_reasoning_content_start_event(self): + """Test that streaming start event with reasoningContent is handled correctly.""" + from litellm.llms.bedrock.chat.invoke_handler import AWSEventStreamDecoder + + handler = AWSEventStreamDecoder(model="amazon.nova-2-lite-v1:0") + + # Simulate a start event with redacted reasoning content + chunk_data = { + "start": {"reasoningContent": {"redactedContent": {}}}, + "contentBlockIndex": 0, + } + + result = handler.converse_chunk_parser(chunk_data) + + # Verify thinking blocks are populated + assert result.choices[0].delta.thinking_blocks is not None + assert len(result.choices[0].delta.thinking_blocks) == 1 + assert result.choices[0].delta.thinking_blocks[0]["type"] == "redacted_thinking" + + def test_streaming_reasoning_content_delta_text(self): + """Test that streaming delta event with reasoning text is handled correctly.""" + from litellm.llms.bedrock.chat.invoke_handler import AWSEventStreamDecoder + + handler = AWSEventStreamDecoder(model="amazon.nova-2-lite-v1:0") + + # Simulate a delta event with reasoning text + chunk_data = { + "delta": {"reasoningContent": {"text": "Let me think about this..."}}, + "contentBlockIndex": 0, + } + + result = handler.converse_chunk_parser(chunk_data) + + # Verify reasoning content is extracted + assert result.choices[0].delta.reasoning_content == "Let me think about this..." + + # Verify thinking blocks are populated + assert result.choices[0].delta.thinking_blocks is not None + assert len(result.choices[0].delta.thinking_blocks) == 1 + assert result.choices[0].delta.thinking_blocks[0]["type"] == "thinking" + assert ( + result.choices[0].delta.thinking_blocks[0]["thinking"] + == "Let me think about this..." + ) + + def test_streaming_reasoning_content_delta_signature(self): + """Test that streaming delta event with signature is handled correctly.""" + from litellm.llms.bedrock.chat.invoke_handler import AWSEventStreamDecoder + + handler = AWSEventStreamDecoder(model="amazon.nova-2-lite-v1:0") + + # Simulate a delta event with signature + chunk_data = { + "delta": {"reasoningContent": {"signature": "signature-hash-xyz"}}, + "contentBlockIndex": 0, + } + + result = handler.converse_chunk_parser(chunk_data) + + # Verify reasoning content is set to empty string for consistency + assert result.choices[0].delta.reasoning_content == "" + + # Verify thinking blocks are populated with signature + assert result.choices[0].delta.thinking_blocks is not None + assert len(result.choices[0].delta.thinking_blocks) == 1 + assert result.choices[0].delta.thinking_blocks[0]["type"] == "thinking" + assert ( + result.choices[0].delta.thinking_blocks[0]["signature"] + == "signature-hash-xyz" + ) + assert result.choices[0].delta.thinking_blocks[0]["thinking"] == "" + + def test_streaming_reasoning_content_multiple_deltas(self): + """Test that multiple reasoning content deltas are accumulated correctly.""" + from litellm.llms.bedrock.chat.invoke_handler import AWSEventStreamDecoder + + handler = AWSEventStreamDecoder(model="amazon.nova-2-lite-v1:0") + + # Simulate multiple delta events + chunks = [ + { + "delta": {"reasoningContent": {"text": "First, "}}, + "contentBlockIndex": 0, + }, + { + "delta": {"reasoningContent": {"text": "I need to analyze "}}, + "contentBlockIndex": 0, + }, + { + "delta": {"reasoningContent": {"text": "the problem."}}, + "contentBlockIndex": 0, + }, + ] + + results = [] + for chunk_data in chunks: + result = handler.converse_chunk_parser(chunk_data) + results.append(result) + + # Verify each delta has the correct reasoning content + assert results[0].choices[0].delta.reasoning_content == "First, " + assert results[1].choices[0].delta.reasoning_content == "I need to analyze " + assert results[2].choices[0].delta.reasoning_content == "the problem." + + # Verify thinking blocks are populated for each delta + for result in results: + assert result.choices[0].delta.thinking_blocks is not None + assert len(result.choices[0].delta.thinking_blocks) == 1 + assert result.choices[0].delta.thinking_blocks[0]["type"] == "thinking" + + def test_streaming_reasoning_then_text_content(self): + """Test that reasoning content followed by text content is handled correctly.""" + from litellm.llms.bedrock.chat.invoke_handler import AWSEventStreamDecoder + + handler = AWSEventStreamDecoder(model="amazon.nova-2-lite-v1:0") + + # Simulate reasoning content followed by text content + chunks = [ + { + "delta": {"reasoningContent": {"text": "Let me think..."}}, + "contentBlockIndex": 0, + }, + {"delta": {"text": "Based on my reasoning, "}, "contentBlockIndex": 1}, + {"delta": {"text": "the answer is 42."}, "contentBlockIndex": 1}, + ] + + results = [] + for chunk_data in chunks: + result = handler.converse_chunk_parser(chunk_data) + results.append(result) + + # Verify first chunk has reasoning content + assert results[0].choices[0].delta.reasoning_content == "Let me think..." + assert results[0].choices[0].delta.thinking_blocks is not None + + # Verify subsequent chunks have text content + assert results[1].choices[0].delta.content == "Based on my reasoning, " + assert results[2].choices[0].delta.content == "the answer is 42." + + def test_streaming_redacted_content_delta(self): + """Test that streaming delta with redacted content is handled correctly.""" + from litellm.llms.bedrock.chat.invoke_handler import AWSEventStreamDecoder + + handler = AWSEventStreamDecoder(model="amazon.nova-2-lite-v1:0") + + # Simulate a delta event with redacted content + chunk_data = { + "delta": {"reasoningContent": {"redactedContent": {}}}, + "contentBlockIndex": 0, + } + + result = handler.converse_chunk_parser(chunk_data) + + # Verify reasoning content is set to empty string for consistency + assert result.choices[0].delta.reasoning_content == "" + + # Verify thinking blocks contain redacted block + assert result.choices[0].delta.thinking_blocks is not None + assert len(result.choices[0].delta.thinking_blocks) == 1 + assert result.choices[0].delta.thinking_blocks[0]["type"] == "redacted_thinking" + + def test_streaming_provider_specific_fields(self): + """Test that provider_specific_fields are populated in streaming responses.""" + from litellm.llms.bedrock.chat.invoke_handler import AWSEventStreamDecoder + + handler = AWSEventStreamDecoder(model="amazon.nova-2-lite-v1:0") + + # Simulate a delta event with reasoning content + chunk_data = { + "delta": {"reasoningContent": {"text": "Reasoning text"}}, + "contentBlockIndex": 0, + } + + result = handler.converse_chunk_parser(chunk_data) + + # Verify provider_specific_fields are populated + assert result.choices[0].delta.provider_specific_fields is not None + assert "reasoningContent" in result.choices[0].delta.provider_specific_fields + assert ( + result.choices[0].delta.provider_specific_fields["reasoningContent"]["text"] + == "Reasoning text" + ) + + def test_streaming_mixed_content_blocks(self): + """Test streaming with mixed content blocks (reasoning, text, tool calls).""" + from litellm.llms.bedrock.chat.invoke_handler import AWSEventStreamDecoder + + handler = AWSEventStreamDecoder(model="amazon.nova-2-lite-v1:0") + + # Simulate a complex streaming scenario + chunks = [ + # Start with reasoning + { + "delta": { + "reasoningContent": { + "text": "I need to call a tool to get information." + } + }, + "contentBlockIndex": 0, + }, + # Tool use start + { + "start": {"toolUse": {"toolUseId": "tool-123", "name": "get_weather"}}, + "contentBlockIndex": 1, + }, + # Tool use delta + { + "delta": {"toolUse": {"input": '{"location": "NYC"}'}}, + "contentBlockIndex": 1, + }, + # Text response + {"delta": {"text": "The weather is sunny."}, "contentBlockIndex": 2}, + ] + + results = [] + for chunk_data in chunks: + result = handler.converse_chunk_parser(chunk_data) + results.append(result) + + # Verify reasoning content in first chunk + assert ( + results[0].choices[0].delta.reasoning_content + == "I need to call a tool to get information." + ) + + # Verify tool call in second and third chunks + assert results[1].choices[0].delta.tool_calls is not None + assert ( + results[1].choices[0].delta.tool_calls[0]["function"]["name"] + == "get_weather" + ) + assert results[2].choices[0].delta.tool_calls is not None + + # Verify text content in fourth chunk + assert results[3].choices[0].delta.content == "The weather is sunny." + + def test_extract_reasoning_content_str_with_text(self): + """Test extract_reasoning_content_str method with text.""" + from litellm.llms.bedrock.chat.invoke_handler import AWSEventStreamDecoder + + handler = AWSEventStreamDecoder(model="amazon.nova-2-lite-v1:0") + + reasoning_block = {"text": "This is reasoning text"} + + result = handler.extract_reasoning_content_str(reasoning_block) + + assert result == "This is reasoning text" + + def test_extract_reasoning_content_str_without_text(self): + """Test extract_reasoning_content_str method without text (e.g., signature only).""" + from litellm.llms.bedrock.chat.invoke_handler import AWSEventStreamDecoder + + handler = AWSEventStreamDecoder(model="amazon.nova-2-lite-v1:0") + + reasoning_block = {"signature": "sig-123"} + + result = handler.extract_reasoning_content_str(reasoning_block) + + assert result is None + + def test_translate_thinking_blocks_streaming_text(self): + """Test translate_thinking_blocks method with text.""" + from litellm.llms.bedrock.chat.invoke_handler import AWSEventStreamDecoder + + handler = AWSEventStreamDecoder(model="amazon.nova-2-lite-v1:0") + + thinking_block = {"text": "Thinking content"} + + result = handler.translate_thinking_blocks(thinking_block) + + assert result is not None + assert len(result) == 1 + assert result[0]["type"] == "thinking" + assert result[0]["thinking"] == "Thinking content" + + def test_translate_thinking_blocks_streaming_signature(self): + """Test translate_thinking_blocks method with signature.""" + from litellm.llms.bedrock.chat.invoke_handler import AWSEventStreamDecoder + + handler = AWSEventStreamDecoder(model="amazon.nova-2-lite-v1:0") + + thinking_block = {"signature": "sig-abc"} + + result = handler.translate_thinking_blocks(thinking_block) + + assert result is not None + assert len(result) == 1 + assert result[0]["type"] == "thinking" + assert result[0]["signature"] == "sig-abc" + assert ( + result[0]["thinking"] == "" + ) # Empty string for consistency with Anthropic + + def test_translate_thinking_blocks_streaming_redacted(self): + """Test translate_thinking_blocks method with redacted content.""" + from litellm.llms.bedrock.chat.invoke_handler import AWSEventStreamDecoder + + handler = AWSEventStreamDecoder(model="amazon.nova-2-lite-v1:0") + + thinking_block = {"redactedContent": {}} + + result = handler.translate_thinking_blocks(thinking_block) + + assert result is not None + assert len(result) == 1 + assert result[0]["type"] == "redacted_thinking" From 4063501d6981a7b0606f4942deb2b7d0cc43dc06 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Tue, 2 Dec 2025 17:08:17 -0800 Subject: [PATCH 028/107] Show all credential values on Edit Credential Modal --- .../model_add/AddCredentialModal.test.tsx | 108 +++++++++++++++ .../model_add/AddCredentialModal.tsx | 118 +++++++++++++++++ .../model_add/EditCredentialModal.test.tsx | 123 ++++++++++++++++++ ...ntials_tab.tsx => EditCredentialModal.tsx} | 60 ++++----- .../src/components/model_add/credentials.tsx | 22 ++-- 5 files changed, 386 insertions(+), 45 deletions(-) create mode 100644 ui/litellm-dashboard/src/components/model_add/AddCredentialModal.test.tsx create mode 100644 ui/litellm-dashboard/src/components/model_add/AddCredentialModal.tsx create mode 100644 ui/litellm-dashboard/src/components/model_add/EditCredentialModal.test.tsx rename ui/litellm-dashboard/src/components/model_add/{add_credentials_tab.tsx => EditCredentialModal.tsx} (78%) diff --git a/ui/litellm-dashboard/src/components/model_add/AddCredentialModal.test.tsx b/ui/litellm-dashboard/src/components/model_add/AddCredentialModal.test.tsx new file mode 100644 index 00000000000..aee7a0cdd1d --- /dev/null +++ b/ui/litellm-dashboard/src/components/model_add/AddCredentialModal.test.tsx @@ -0,0 +1,108 @@ +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { render, screen, waitFor } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import { Providers } from "../provider_info_helpers"; +import AddCredentialModal from "./AddCredentialModal"; + +vi.mock("../networking", async () => { + const actual = await vi.importActual("../networking"); + return { + ...actual, + getProviderCreateMetadata: vi.fn().mockResolvedValue([ + { + provider: "OpenAI", + provider_display_name: Providers.OpenAI, + litellm_provider: "openai", + default_model_placeholder: "gpt-3.5-turbo", + credential_fields: [ + { + key: "api_key", + label: "OpenAI API Key", + field_type: "password", + required: true, + }, + { + key: "api_base", + label: "API Base", + field_type: "text", + placeholder: "https://api.openai.com/v1", + }, + ], + }, + { + provider: "Anthropic", + provider_display_name: Providers.Anthropic, + litellm_provider: "anthropic", + default_model_placeholder: "claude-3-opus-20240229", + credential_fields: [ + { + key: "api_key", + label: "Anthropic API Key", + field_type: "password", + required: true, + }, + ], + }, + ]), + }; +}); + +const createQueryClient = () => + new QueryClient({ + defaultOptions: { + queries: { + retry: false, + gcTime: 0, + }, + }, + }); + +const mockUploadProps = { + beforeUpload: vi.fn(), + onChange: vi.fn(), +}; + +describe("AddCredentialModal", () => { + it("should render", () => { + const queryClient = createQueryClient(); + const onCancel = vi.fn(); + const onAddCredential = vi.fn(); + + render( + + + , + ); + + expect(screen.getByText("Add New Credential")).toBeInTheDocument(); + expect(screen.getByLabelText("Credential Name:")).toBeInTheDocument(); + expect(screen.getByLabelText("Provider:")).toBeInTheDocument(); + }); + + it("should show the correct provider fields", async () => { + const queryClient = createQueryClient(); + const onCancel = vi.fn(); + const onAddCredential = vi.fn(); + + render( + + + , + ); + + await waitFor(() => { + expect(screen.getByLabelText("OpenAI API Key")).toBeInTheDocument(); + expect(screen.getByPlaceholderText("https://api.openai.com/v1")).toBeInTheDocument(); + }); + }); +}); diff --git a/ui/litellm-dashboard/src/components/model_add/AddCredentialModal.tsx b/ui/litellm-dashboard/src/components/model_add/AddCredentialModal.tsx new file mode 100644 index 00000000000..694a98201c6 --- /dev/null +++ b/ui/litellm-dashboard/src/components/model_add/AddCredentialModal.tsx @@ -0,0 +1,118 @@ +import { TextInput } from "@tremor/react"; +import { Select as AntdSelect, Button, Form, Modal, Tooltip, Typography } from "antd"; +import type { UploadProps } from "antd/es/upload"; +import React, { useState } from "react"; +import ProviderSpecificFields from "../add_model/provider_specific_fields"; +import { Providers, providerLogoMap } from "../provider_info_helpers"; +const { Link } = Typography; + +interface AddCredentialsModalProps { + open: boolean; + onCancel: () => void; + onAddCredential: (values: any) => void; + uploadProps: UploadProps; +} + +const AddCredentialsModal: React.FC = ({ open, onCancel, onAddCredential, uploadProps }) => { + const [form] = Form.useForm(); + const [selectedProvider, setSelectedProvider] = useState(Providers.OpenAI); + + const handleSubmit = (values: any) => { + const filteredValues = Object.entries(values).reduce((acc, [key, value]) => { + if (value !== "" && value !== undefined && value !== null) { + acc[key] = value; + } + return acc; + }, {} as any); + onAddCredential(filteredValues); + form.resetFields(); + }; + + return ( + { + onCancel(); + form.resetFields(); + }} + footer={null} + width={600} + > +
    + {/* Credential Name */} + + + + + {/* Provider Selection */} + + { + setSelectedProvider(value as Providers); + form.setFieldValue("custom_llm_provider", value); + }} + > + {Object.entries(Providers).map(([providerEnum, providerDisplayName]) => ( + +
    + {`${providerEnum} { + const target = e.target as HTMLImageElement; + const parent = target.parentElement; + if (parent) { + const fallbackDiv = document.createElement("div"); + fallbackDiv.className = + "w-5 h-5 rounded-full bg-gray-200 flex items-center justify-center text-xs"; + fallbackDiv.textContent = providerDisplayName.charAt(0); + parent.replaceChild(fallbackDiv, target); + } + }} + /> + {providerDisplayName} +
    +
    + ))} +
    +
    + + + + {/* Modal Footer */} +
    + + Need Help? + + +
    + + +
    +
    + +
    + ); +}; + +export default AddCredentialsModal; diff --git a/ui/litellm-dashboard/src/components/model_add/EditCredentialModal.test.tsx b/ui/litellm-dashboard/src/components/model_add/EditCredentialModal.test.tsx new file mode 100644 index 00000000000..def3b4f6cd7 --- /dev/null +++ b/ui/litellm-dashboard/src/components/model_add/EditCredentialModal.test.tsx @@ -0,0 +1,123 @@ +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { render, screen, waitFor } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import { Providers } from "../provider_info_helpers"; +import { CredentialItem } from "../networking"; +import EditCredentialModal from "./EditCredentialModal"; + +vi.mock("../networking", async () => { + const actual = await vi.importActual("../networking"); + return { + ...actual, + getProviderCreateMetadata: vi.fn().mockResolvedValue([ + { + provider: "OpenAI", + provider_display_name: Providers.OpenAI, + litellm_provider: "openai", + default_model_placeholder: "gpt-3.5-turbo", + credential_fields: [ + { + key: "api_key", + label: "OpenAI API Key", + field_type: "password", + required: true, + }, + { + key: "api_base", + label: "API Base", + field_type: "text", + placeholder: "https://api.openai.com/v1", + }, + ], + }, + { + provider: "Anthropic", + provider_display_name: Providers.Anthropic, + litellm_provider: "anthropic", + default_model_placeholder: "claude-3-opus-20240229", + credential_fields: [ + { + key: "api_key", + label: "Anthropic API Key", + field_type: "password", + required: true, + }, + ], + }, + ]), + }; +}); + +const createQueryClient = () => + new QueryClient({ + defaultOptions: { + queries: { + retry: false, + gcTime: 0, + }, + }, + }); + +const mockUploadProps = { + beforeUpload: vi.fn(), + onChange: vi.fn(), +}; + +const mockCredential: CredentialItem = { + credential_name: "test-credential", + credential_values: { + api_key: "test-api-key", + api_base: "https://api.test.com", + }, + credential_info: { + custom_llm_provider: Providers.OpenAI, + }, +}; + +describe("EditCredentialModal", () => { + it("should render", () => { + const queryClient = createQueryClient(); + const onCancel = vi.fn(); + const onUpdateCredential = vi.fn(); + + render( + + + , + ); + + expect(screen.getByText("Edit Credential")).toBeInTheDocument(); + expect(screen.getByLabelText("Credential Name:")).toBeInTheDocument(); + expect(screen.getByLabelText("Provider:")).toBeInTheDocument(); + }); + + it("should render initial values", async () => { + const queryClient = createQueryClient(); + const onCancel = vi.fn(); + const onUpdateCredential = vi.fn(); + + render( + + + , + ); + + await waitFor(() => { + const credentialNameInput = screen.getByLabelText("Credential Name:") as HTMLInputElement; + expect(credentialNameInput.value).toBe("test-credential"); + expect(credentialNameInput.disabled).toBe(true); + }); + }); +}); diff --git a/ui/litellm-dashboard/src/components/model_add/add_credentials_tab.tsx b/ui/litellm-dashboard/src/components/model_add/EditCredentialModal.tsx similarity index 78% rename from ui/litellm-dashboard/src/components/model_add/add_credentials_tab.tsx rename to ui/litellm-dashboard/src/components/model_add/EditCredentialModal.tsx index 9c061eb121a..b206ed6c91d 100644 --- a/ui/litellm-dashboard/src/components/model_add/add_credentials_tab.tsx +++ b/ui/litellm-dashboard/src/components/model_add/EditCredentialModal.tsx @@ -1,34 +1,29 @@ -import React, { useEffect, useState } from "react"; -import { Form, Button, Tooltip, Typography, Select as AntdSelect, Modal } from "antd"; -import type { UploadProps } from "antd/es/upload"; -import { Providers, providerLogoMap } from "../provider_info_helpers"; -import ProviderSpecificFields from "../add_model/provider_specific_fields"; import { TextInput } from "@tremor/react"; +import { Select as AntdSelect, Button, Form, Modal, Tooltip, Typography } from "antd"; +import type { UploadProps } from "antd/es/upload"; +import { useEffect, useState } from "react"; +import ProviderSpecificFields from "../add_model/provider_specific_fields"; import { CredentialItem } from "../networking"; -const { Title, Link } = Typography; +import { Providers, providerLogoMap } from "../provider_info_helpers"; +const { Link } = Typography; -interface AddCredentialsModalProps { - isVisible: boolean; +interface EditCredentialsModalProps { + open: boolean; onCancel: () => void; - onAddCredential: (values: any) => void; onUpdateCredential: (values: any) => void; uploadProps: UploadProps; - addOrEdit: "add" | "edit"; existingCredential: CredentialItem | null; } -const AddCredentialsModal: React.FC = ({ - isVisible, +export default function EditCredentialsModal({ + open, onCancel, - onAddCredential, onUpdateCredential, uploadProps, - addOrEdit, existingCredential, -}) => { +}: EditCredentialsModalProps) { const [form] = Form.useForm(); - const [selectedProvider, setSelectedProvider] = useState(Providers.OpenAI); - const [showAdvancedSettings, setShowAdvancedSettings] = useState(false); + const [selectedProvider, setSelectedProvider] = useState(Providers.Anthropic); const handleSubmit = (values: any) => { const filteredValues = Object.entries(values).reduce((acc, [key, value]) => { @@ -37,23 +32,25 @@ const AddCredentialsModal: React.FC = ({ } return acc; }, {} as any); - if (addOrEdit === "add") { - onAddCredential(filteredValues); - } else { - onUpdateCredential(filteredValues); - } + onUpdateCredential(filteredValues); form.resetFields(); }; useEffect(() => { if (existingCredential) { + // Spread all credential_values dynamically, converting undefined/null to null for form compatibility + const credentialValues = Object.entries(existingCredential.credential_values || {}).reduce( + (acc, [key, value]) => { + acc[key] = value ?? null; + return acc; + }, + {} as Record, + ); + form.setFieldsValue({ credential_name: existingCredential.credential_name, custom_llm_provider: existingCredential.credential_info.custom_llm_provider, - api_base: existingCredential.credential_values.api_base, - api_version: existingCredential.credential_values.api_version, - base_model: existingCredential.credential_values.base_model, - api_key: existingCredential.credential_values.api_key, + ...credentialValues, }); setSelectedProvider(existingCredential.credential_info.custom_llm_provider as Providers); } @@ -61,14 +58,15 @@ const AddCredentialsModal: React.FC = ({ return ( { onCancel(); form.resetFields(); }} footer={null} width={600} + destroyOnHidden={true} >
    {/* Credential Name */} @@ -142,12 +140,10 @@ const AddCredentialsModal: React.FC = ({ > Cancel - +
    ); -}; - -export default AddCredentialsModal; +} diff --git a/ui/litellm-dashboard/src/components/model_add/credentials.tsx b/ui/litellm-dashboard/src/components/model_add/credentials.tsx index e36a759294b..eecd26db25f 100644 --- a/ui/litellm-dashboard/src/components/model_add/credentials.tsx +++ b/ui/litellm-dashboard/src/components/model_add/credentials.tsx @@ -19,10 +19,11 @@ import { credentialUpdateCall, CredentialItem, } from "@/components/networking"; // Assume this is your networking function -import AddCredentialsTab from "./add_credentials_tab"; +import AddCredentialsTab from "./AddCredentialModal"; import CredentialDeleteModal from "./CredentialDeleteModal"; import { Form } from "antd"; import NotificationsManager from "../molecules/notifications_manager"; +import EditCredentialsModal from "./EditCredentialModal"; interface CredentialsPanelProps { accessToken: string | null; uploadProps: UploadProps; @@ -60,10 +61,10 @@ const CredentialsPanel: React.FC = ({ }, }; - const response = await credentialUpdateCall(accessToken, values.credential_name, newCredential); + await credentialUpdateCall(accessToken, values.credential_name, newCredential); NotificationsManager.success("Credential updated successfully"); setIsUpdateModalOpen(false); - fetchCredentials(accessToken); + await fetchCredentials(accessToken); }; const handleAddCredential = async (values: any) => { @@ -84,10 +85,10 @@ const CredentialsPanel: React.FC = ({ }; // Add to list and close modal - const response = await credentialCreateCall(accessToken, newCredential); + await credentialCreateCall(accessToken, newCredential); NotificationsManager.success("Credential added successfully"); setIsAddModalOpen(false); - fetchCredentials(accessToken); + await fetchCredentials(accessToken); }; useEffect(() => { @@ -189,23 +190,18 @@ const CredentialsPanel: React.FC = ({ {isAddModalOpen && ( setIsAddModalOpen(false)} uploadProps={uploadProps} - addOrEdit="add" - onUpdateCredential={handleUpdateCredential} - existingCredential={null} /> )} {isUpdateModalOpen && ( - setIsUpdateModalOpen(false)} - addOrEdit="edit" /> )} From 427074ac6e80ffe3b30054dd10c2a7427cfbf96f Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Tue, 2 Dec 2025 17:27:50 -0800 Subject: [PATCH 029/107] Fix: Datadog callback regression when ddtrace is installed (#17393) * fix DD agent host logging * docs fix * test_datadog_agent_configuration * test_datadog_ignores_ddtrace_agent_host --- docs/my-website/docs/observability/datadog.md | 18 +++---- litellm/integrations/datadog/datadog.py | 13 ++--- tests/logging_callback_tests/test_datadog.py | 47 +++++++++++++++++-- 3 files changed, 60 insertions(+), 18 deletions(-) diff --git a/docs/my-website/docs/observability/datadog.md b/docs/my-website/docs/observability/datadog.md index 5cb5ab3af2d..b2901650ea6 100644 --- a/docs/my-website/docs/observability/datadog.md +++ b/docs/my-website/docs/observability/datadog.md @@ -71,17 +71,19 @@ DD_SOURCE="litellm_dev" # [OPTIONAL] your datadog source. use to different Send logs through a local DataDog agent (useful for containerized environments): ```shell -DD_AGENT_HOST="localhost" # hostname or IP of DataDog agent -DD_AGENT_PORT="10518" # [OPTIONAL] port of DataDog agent (default: 10518) -DD_API_KEY="5f2d0f310***********" # [OPTIONAL] your datadog API Key (agent handles auth) -DD_SOURCE="litellm_dev" # [OPTIONAL] your datadog source +LITELLM_DD_AGENT_HOST="localhost" # hostname or IP of DataDog agent +LITELLM_DD_AGENT_PORT="10518" # [OPTIONAL] port of DataDog agent (default: 10518) +DD_API_KEY="5f2d0f310***********" # [OPTIONAL] your datadog API Key (agent handles auth) +DD_SOURCE="litellm_dev" # [OPTIONAL] your datadog source ``` -When `DD_AGENT_HOST` is set, logs are sent to the agent instead of directly to DataDog API. This is useful for: +When `LITELLM_DD_AGENT_HOST` is set, logs are sent to the agent instead of directly to DataDog API. This is useful for: - Centralized log shipping in containerized environments - Reducing direct API calls from multiple services - Leveraging agent-side processing and filtering +**Note:** We use `LITELLM_DD_AGENT_HOST` instead of `DD_AGENT_HOST` to avoid conflicts with `ddtrace` which automatically sets `DD_AGENT_HOST` for APM tracing. + **Step 3**: Start the proxy, make a test request Start proxy @@ -191,8 +193,8 @@ LiteLLM supports customizing the following Datadog environment variables |---------------------|-------------|---------------|----------| | `DD_API_KEY` | Your Datadog API key for authentication (required for direct API, optional for agent) | None | Conditional* | | `DD_SITE` | Your Datadog site (e.g., "us5.datadoghq.com") (required for direct API) | None | Conditional* | -| `DD_AGENT_HOST` | Hostname or IP of DataDog agent (e.g., "localhost"). When set, logs are sent to agent instead of direct API | None | ❌ No | -| `DD_AGENT_PORT` | Port of DataDog agent for log intake | "10518" | ❌ No | +| `LITELLM_DD_AGENT_HOST` | Hostname or IP of DataDog agent (e.g., "localhost"). When set, logs are sent to agent instead of direct API | None | ❌ No | +| `LITELLM_DD_AGENT_PORT` | Port of DataDog agent for log intake | "10518" | ❌ No | | `DD_ENV` | Environment tag for your logs (e.g., "production", "staging") | "unknown" | ❌ No | | `DD_SERVICE` | Service name for your logs | "litellm-server" | ❌ No | | `DD_SOURCE` | Source name for your logs | "litellm" | ❌ No | @@ -201,5 +203,5 @@ LiteLLM supports customizing the following Datadog environment variables | `POD_NAME` | Pod name tag (useful for Kubernetes deployments) | "unknown" | ❌ No | \* **Required when using Direct API** (default): `DD_API_KEY` and `DD_SITE` are required -\* **Optional when using DataDog Agent**: Set `DD_AGENT_HOST` to use agent mode; `DD_API_KEY` and `DD_SITE` are not required +\* **Optional when using DataDog Agent**: Set `LITELLM_DD_AGENT_HOST` to use agent mode; `DD_API_KEY` and `DD_SITE` are not required diff --git a/litellm/integrations/datadog/datadog.py b/litellm/integrations/datadog/datadog.py index 46e1a2c201f..21e1d562224 100644 --- a/litellm/integrations/datadog/datadog.py +++ b/litellm/integrations/datadog/datadog.py @@ -65,11 +65,11 @@ class DataDogLogger( `DD_SITE` - your datadog site, example = `"us5.datadoghq.com"` Optional environment variables (DataDog Agent): - `DD_AGENT_HOST` - hostname or IP of DataDog agent, example = `"localhost"` - `DD_AGENT_PORT` - port of DataDog agent (default: 10518 for logs) + `LITELLM_DD_AGENT_HOST` - hostname or IP of DataDog agent, example = `"localhost"` + `LITELLM_DD_AGENT_PORT` - port of DataDog agent (default: 10518 for logs) - Note: If DD_AGENT_HOST is set, logs will be sent to the agent instead of directly to DataDog API. - In this case, DD_API_KEY and DD_SITE are not required (agent handles authentication). + Note: We use LITELLM_DD_AGENT_HOST instead of DD_AGENT_HOST to avoid conflicts + with ddtrace which automatically sets DD_AGENT_HOST for APM tracing. """ try: verbose_logger.debug("Datadog: in init datadog logger") @@ -85,7 +85,8 @@ class DataDogLogger( ) # Configure DataDog endpoint (Agent or Direct API) - dd_agent_host = os.getenv("DD_AGENT_HOST") + # Use LITELLM_DD_AGENT_HOST to avoid conflicts with ddtrace's DD_AGENT_HOST + dd_agent_host = os.getenv("LITELLM_DD_AGENT_HOST") if dd_agent_host: self._configure_dd_agent(dd_agent_host=dd_agent_host) else: @@ -127,7 +128,7 @@ class DataDogLogger( Args: dd_agent_host: Hostname or IP of DataDog agent """ - dd_agent_port = os.getenv("DD_AGENT_PORT", "10518") # default port for logs + dd_agent_port = os.getenv("LITELLM_DD_AGENT_PORT", "10518") # default port for logs self.intake_url = f"http://{dd_agent_host}:{dd_agent_port}/api/v2/logs" self.DD_API_KEY = os.getenv("DD_API_KEY") # Optional when using agent verbose_logger.debug(f"Datadog: Using DD Agent at {self.intake_url}") diff --git a/tests/logging_callback_tests/test_datadog.py b/tests/logging_callback_tests/test_datadog.py index 13125aa4952..c877f34ac03 100644 --- a/tests/logging_callback_tests/test_datadog.py +++ b/tests/logging_callback_tests/test_datadog.py @@ -633,11 +633,14 @@ async def test_datadog_message_redaction(): def test_datadog_agent_configuration(): """ - Test that DataDog logger correctly configures agent endpoint when DD_AGENT_HOST is set + Test that DataDog logger correctly configures agent endpoint when LITELLM_DD_AGENT_HOST is set. + + Note: We use LITELLM_DD_AGENT_HOST instead of DD_AGENT_HOST to avoid conflicts + with ddtrace which automatically sets DD_AGENT_HOST for APM tracing. """ test_env = { - "DD_AGENT_HOST": "localhost", - "DD_AGENT_PORT": "10518", + "LITELLM_DD_AGENT_HOST": "localhost", + "LITELLM_DD_AGENT_PORT": "10518", } # Remove DD_SITE and DD_API_KEY to verify they're not required for agent mode @@ -654,4 +657,40 @@ def test_datadog_agent_configuration(): assert dd_logger.intake_url == "http://localhost:10518/api/v2/logs", f"Expected agent URL, got {dd_logger.intake_url}" # Verify DD_API_KEY is optional (can be None) - assert dd_logger.DD_API_KEY is None or isinstance(dd_logger.DD_API_KEY, str) \ No newline at end of file + assert dd_logger.DD_API_KEY is None or isinstance(dd_logger.DD_API_KEY, str) + + +def test_datadog_ignores_ddtrace_agent_host(): + """ + Regression test: Ensure DD_AGENT_HOST set by ddtrace doesn't interfere with LiteLLM logging. + + When users have ddtrace installed for APM tracing, it automatically sets DD_AGENT_HOST. + LiteLLM should ignore DD_AGENT_HOST and only use LITELLM_DD_AGENT_HOST for agent mode. + + This prevents the 404 error when ddtrace's DD_AGENT_HOST points to an APM endpoint + that doesn't support /api/v2/logs. + + Regression test for: https://github.com/BerriAI/litellm/issues/16379 + """ + test_env = { + # User's explicit config for LiteLLM logging (direct API) + "DD_API_KEY": "fake-api-key", + "DD_SITE": "us5.datadoghq.com", + # ddtrace automatically sets these for APM tracing + "DD_AGENT_HOST": "10.176.100.40", + "DD_AGENT_PORT": "8126", + } + + with patch.dict(os.environ, test_env, clear=False): + with patch("asyncio.create_task"): + dd_logger = DataDogLogger() + + # Verify direct API endpoint is used (DD_AGENT_HOST should be ignored) + expected_url = "https://http-intake.logs.us5.datadoghq.com/api/v2/logs" + assert dd_logger.intake_url == expected_url, ( + f"Expected direct API URL '{expected_url}', got '{dd_logger.intake_url}'. " + "DD_AGENT_HOST (set by ddtrace) should be ignored - only LITELLM_DD_AGENT_HOST should trigger agent mode." + ) + + # Verify API key is set correctly + assert dd_logger.DD_API_KEY == "fake-api-key" \ No newline at end of file From 8f565a811c2b74b54106c015febf5f2aa77b5656 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Tue, 2 Dec 2025 17:31:03 -0800 Subject: [PATCH 030/107] Merge with main --- ui/litellm-dashboard/src/components/model_add/credentials.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ui/litellm-dashboard/src/components/model_add/credentials.tsx b/ui/litellm-dashboard/src/components/model_add/credentials.tsx index 73e5ce80585..1bba14bef97 100644 --- a/ui/litellm-dashboard/src/components/model_add/credentials.tsx +++ b/ui/litellm-dashboard/src/components/model_add/credentials.tsx @@ -19,6 +19,8 @@ import { } from "@tremor/react"; import { Form } from "antd"; import { UploadProps } from "antd/es/upload"; +import { useEffect, useState } from "react"; +import DeleteResourceModal from "../common_components/DeleteResourceModal"; import NotificationsManager from "../molecules/notifications_manager"; import AddCredentialsTab from "./AddCredentialModal"; import EditCredentialsModal from "./EditCredentialModal"; From afedc9aac33fc41c7b54af5c3420384774472007 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Tue, 2 Dec 2025 17:34:53 -0800 Subject: [PATCH 031/107] Fixing test --- ui/litellm-dashboard/src/components/view_users/table.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 c688d5749d0..82f49d9618e 100644 --- a/ui/litellm-dashboard/src/components/view_users/table.test.tsx +++ b/ui/litellm-dashboard/src/components/view_users/table.test.tsx @@ -105,7 +105,7 @@ describe("UserDataTable", () => { "Spend (USD)", "Budget (USD)", "SSO ID", - "API Keys", + "Virtual Keys", "Created At", "Updated At", "Actions", From 7fea97a0c0ca7def95d9c048931fe2b9b6b3a3e2 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Tue, 2 Dec 2025 17:53:16 -0800 Subject: [PATCH 032/107] Add is_sso_configured to UI Config --- .../ui_discovery_endpoints.py | 3 +- .../ui_discovery_endpoints.py | 1 + .../test_ui_discovery_endpoints.py | 86 +++++++++++++++++++ 3 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 tests/test_litellm/proxy/discovery_endpoints/test_ui_discovery_endpoints.py diff --git a/litellm/proxy/discovery_endpoints/ui_discovery_endpoints.py b/litellm/proxy/discovery_endpoints/ui_discovery_endpoints.py index 9a6032fd084..b297634d6be 100644 --- a/litellm/proxy/discovery_endpoints/ui_discovery_endpoints.py +++ b/litellm/proxy/discovery_endpoints/ui_discovery_endpoints.py @@ -14,8 +14,9 @@ router = APIRouter() ) # if mounted at root path async def get_ui_config(): from litellm.proxy.utils import get_proxy_base_url, get_server_root_path - + from litellm.proxy.auth.auth_utils import _has_user_setup_sso return UiDiscoveryEndpoints( server_root_path=get_server_root_path(), proxy_base_url=get_proxy_base_url(), + is_sso_configured=_has_user_setup_sso(), ) diff --git a/litellm/types/proxy/discovery_endpoints/ui_discovery_endpoints.py b/litellm/types/proxy/discovery_endpoints/ui_discovery_endpoints.py index 7132077b5fa..88f0627f533 100644 --- a/litellm/types/proxy/discovery_endpoints/ui_discovery_endpoints.py +++ b/litellm/types/proxy/discovery_endpoints/ui_discovery_endpoints.py @@ -6,3 +6,4 @@ from pydantic import BaseModel class UiDiscoveryEndpoints(BaseModel): server_root_path: str proxy_base_url: Optional[str] + is_sso_configured: bool diff --git a/tests/test_litellm/proxy/discovery_endpoints/test_ui_discovery_endpoints.py b/tests/test_litellm/proxy/discovery_endpoints/test_ui_discovery_endpoints.py new file mode 100644 index 00000000000..b6e064823c5 --- /dev/null +++ b/tests/test_litellm/proxy/discovery_endpoints/test_ui_discovery_endpoints.py @@ -0,0 +1,86 @@ +import os +import sys +from unittest.mock import patch + +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient + +sys.path.insert( + 0, os.path.abspath("../../..") +) + +from litellm.proxy.discovery_endpoints.ui_discovery_endpoints import router + + +def test_ui_discovery_endpoints_with_defaults(): + app = FastAPI() + app.include_router(router) + client = TestClient(app) + + with patch("litellm.proxy.utils.get_server_root_path", return_value="/"), \ + patch("litellm.proxy.utils.get_proxy_base_url", return_value=None), \ + patch("litellm.proxy.auth.auth_utils._has_user_setup_sso", return_value=False): + + response = client.get("/.well-known/litellm-ui-config") + + assert response.status_code == 200 + data = response.json() + assert data["server_root_path"] == "/" + assert data["proxy_base_url"] is None + assert data["is_sso_configured"] is False + + +def test_ui_discovery_endpoints_with_custom_server_root_path(): + app = FastAPI() + app.include_router(router) + client = TestClient(app) + + with patch("litellm.proxy.utils.get_server_root_path", return_value="/litellm"), \ + patch("litellm.proxy.utils.get_proxy_base_url", return_value=None), \ + patch("litellm.proxy.auth.auth_utils._has_user_setup_sso", return_value=False): + + response = client.get("/.well-known/litellm-ui-config") + + assert response.status_code == 200 + data = response.json() + assert data["server_root_path"] == "/litellm" + assert data["proxy_base_url"] is None + assert data["is_sso_configured"] is False + + +def test_ui_discovery_endpoints_with_proxy_base_url_when_set(): + app = FastAPI() + app.include_router(router) + client = TestClient(app) + + with patch("litellm.proxy.utils.get_server_root_path", return_value="/"), \ + patch("litellm.proxy.utils.get_proxy_base_url", return_value="https://proxy.example.com"), \ + patch("litellm.proxy.auth.auth_utils._has_user_setup_sso", return_value=False): + + response = client.get("/litellm/.well-known/litellm-ui-config") + + assert response.status_code == 200 + data = response.json() + assert data["server_root_path"] == "/" + assert data["proxy_base_url"] == "https://proxy.example.com" + assert data["is_sso_configured"] is False + + +def test_ui_discovery_endpoints_with_sso_configured_when_sso_is_setup(): + app = FastAPI() + app.include_router(router) + client = TestClient(app) + + with patch("litellm.proxy.utils.get_server_root_path", return_value="/litellm"), \ + patch("litellm.proxy.utils.get_proxy_base_url", return_value="https://proxy.example.com"), \ + patch("litellm.proxy.auth.auth_utils._has_user_setup_sso", return_value=True): + + response = client.get("/.well-known/litellm-ui-config") + + assert response.status_code == 200 + data = response.json() + assert data["server_root_path"] == "/litellm" + assert data["proxy_base_url"] == "https://proxy.example.com" + assert data["is_sso_configured"] is True + From 209e9e05aa3a5543639df154fe54974e0b708e9a Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Wed, 3 Dec 2025 10:13:59 +0530 Subject: [PATCH 033/107] Fix gemini 3 last chunk thinking block --- .../vertex_and_google_ai_studio_gemini.py | 5 +++++ .../test_gemini_reasoning_content.py | 20 +++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 tests/local_testing/test_gemini_reasoning_content.py diff --git a/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py b/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py index 5fef8c1ec49..cfc13f5601d 100644 --- a/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py +++ b/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py @@ -1091,6 +1091,11 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): if "thoughtSignature" in part: part_copy = part.copy() part_copy.pop("thoughtSignature") + + text_content = part_copy.get("text") + if isinstance(text_content, str) and text_content.strip() == "": + continue + thinking_blocks.append( ChatCompletionThinkingBlock( type="thinking", diff --git a/tests/local_testing/test_gemini_reasoning_content.py b/tests/local_testing/test_gemini_reasoning_content.py new file mode 100644 index 00000000000..7e516ae8439 --- /dev/null +++ b/tests/local_testing/test_gemini_reasoning_content.py @@ -0,0 +1,20 @@ +import json +from litellm.llms.vertex_ai.gemini.vertex_and_google_ai_studio_gemini import VertexGeminiConfig + + +def test_empty_part_does_not_create_thinking_block(): + parts = [{"text": "", "thoughtSignature": "sig-1"}] + config = VertexGeminiConfig() + thinking_blocks = config._extract_thinking_blocks_from_parts(parts) + assert thinking_blocks == [] + + +def test_non_empty_part_creates_thinking_block(): + parts = [{"text": "Some thinking", "thoughtSignature": "sig-2"}] + config = VertexGeminiConfig() + thinking_blocks = config._extract_thinking_blocks_from_parts(parts) + assert len(thinking_blocks) == 1 + block = thinking_blocks[0] + # thinking should be valid JSON containing the text + parsed = json.loads(block["thinking"]) if isinstance(block["thinking"], str) else None + assert parsed is not None and parsed.get("text") == "Some thinking" From 40c203e32b5991cefa14d9d7befaac6385ee0538 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Wed, 3 Dec 2025 11:04:53 +0530 Subject: [PATCH 034/107] Make thought sign in tool call id as a beta feat --- .../vertex_and_google_ai_studio_gemini.py | 12 +- .../test_thought_signature_in_tool_call_id.py | 277 ++++++++++++------ 2 files changed, 188 insertions(+), 101 deletions(-) diff --git a/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py b/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py index 5fef8c1ec49..1c2efe1eaa7 100644 --- a/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py +++ b/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py @@ -1205,14 +1205,16 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): } # Embed thought signature in ID for OpenAI client compatibility if thought_signature: - _tool_response_chunk[ - "id" - ] = _encode_tool_call_id_with_signature( - _tool_response_chunk["id"] or "", thought_signature - ) _tool_response_chunk["provider_specific_fields"] = { # type: ignore "thought_signature": thought_signature } + # Only embed in ID if preview features are enabled + if litellm.enable_preview_features: + _tool_response_chunk[ + "id" + ] = _encode_tool_call_id_with_signature( + _tool_response_chunk["id"] or "", thought_signature + ) _tools.append(_tool_response_chunk) cumulative_tool_call_idx += 1 if len(_tools) == 0: diff --git a/tests/test_litellm/llms/vertex_ai/gemini/test_thought_signature_in_tool_call_id.py b/tests/test_litellm/llms/vertex_ai/gemini/test_thought_signature_in_tool_call_id.py index 68c0f3bdbfc..46bb8930a7a 100644 --- a/tests/test_litellm/llms/vertex_ai/gemini/test_thought_signature_in_tool_call_id.py +++ b/tests/test_litellm/llms/vertex_ai/gemini/test_thought_signature_in_tool_call_id.py @@ -4,9 +4,13 @@ Tests for embedding thought signatures in tool call IDs for OpenAI client compat When using OpenAI clients (instead of LiteLLM SDK), provider_specific_fields are not preserved. This test suite validates that thought signatures can be embedded in tool call IDs and extracted when converting back to Gemini format. + +Note: Embedding signatures in tool call IDs is a beta feature that requires +enable_preview_features=True to be enabled. """ import pytest +import litellm from litellm.llms.vertex_ai.gemini.vertex_and_google_ai_studio_gemini import ( VertexGeminiConfig, ) @@ -62,36 +66,57 @@ def test_encode_tool_call_id_without_signature(): assert decoded_signature is None -def test_tool_call_id_includes_signature_in_response(): - """Test that tool call IDs in responses include embedded thought signatures""" +@pytest.mark.parametrize("enable_preview_features", [True, False]) +def test_tool_call_id_includes_signature_in_response(enable_preview_features): + """Test that tool call IDs in responses include embedded thought signatures only when preview features are enabled""" test_signature = "Co4CAdHtim/rWgXbz2Ghp4tShzLeMASrPw6JJyYIC3cbVyZnKzU3uv8/wVzyS2sKRPL2m8QQHHXbNQhEEz500G7n/4ZMmksdTtfQcJMoT76S1DGwhnAiLwTgWCNXs3lEb4M19EVYoWFxhrH5Lr9YMIquoU9U4paydGwvZyIyigamIg4B6WnxrRsf0KZV12gJed0DZuKczvOFtHz3zUnmZRlOiTzd5gBVyQM+5jv1VI8m4WUKd6cN/5a5ZvaA0ggiO6kdVhlpIVs7GczSEVJD8KH4u02X7VSnb7CvykqDntZzV0y8rZFBEFGKrChmeHlWXP4D1IB3F9KQyhuLgWImMzg4BajKVxxMU737JGnNISy5" - parts_with_signature = [ - HttpxPartType( - functionCall={ - "name": "get_current_temperature", - "args": {"location": "Paris"}, - }, - thoughtSignature=test_signature, + # Save original state + original_flag = litellm.enable_preview_features + litellm.enable_preview_features = enable_preview_features + + try: + parts_with_signature = [ + HttpxPartType( + functionCall={ + "name": "get_current_temperature", + "args": {"location": "Paris"}, + }, + thoughtSignature=test_signature, + ) + ] + + function, tools, _ = VertexGeminiConfig._transform_parts( + parts=parts_with_signature, + cumulative_tool_call_idx=0, + is_function_call=False, ) - ] - function, tools, _ = VertexGeminiConfig._transform_parts( - parts=parts_with_signature, - cumulative_tool_call_idx=0, - is_function_call=False, - ) + # Verify tool call exists + assert tools is not None + assert len(tools) == 1 + tool_call_id = tools[0]["id"] + + # Verify signature is always in provider_specific_fields + assert tools[0].get("provider_specific_fields", {}).get("thought_signature") == test_signature - # Verify tool call ID includes thought signature - assert tools is not None - assert len(tools) == 1 - tool_call_id = tools[0]["id"] - assert THOUGHT_SIGNATURE_SEPARATOR in tool_call_id - - # Verify we can decode it using the factory function - tool_obj = {"id": tool_call_id, "type": "function"} - decoded_sig = _get_thought_signature_from_tool(tool_obj) - assert decoded_sig == test_signature + if enable_preview_features: + # When preview features enabled, signature should be embedded in ID + assert THOUGHT_SIGNATURE_SEPARATOR in tool_call_id + # Verify we can decode it using the factory function + tool_obj = {"id": tool_call_id, "type": "function"} + decoded_sig = _get_thought_signature_from_tool(tool_obj) + assert decoded_sig == test_signature + else: + # When preview features disabled, signature should NOT be embedded in ID + assert THOUGHT_SIGNATURE_SEPARATOR not in tool_call_id + # But we can still extract from provider_specific_fields + tool_obj = {"id": tool_call_id, "type": "function", "provider_specific_fields": {"thought_signature": test_signature}} + decoded_sig = _get_thought_signature_from_tool(tool_obj) + assert decoded_sig == test_signature + finally: + # Restore original state + litellm.enable_preview_features = original_flag def test_get_thought_signature_backward_compatibility(): @@ -168,97 +193,157 @@ def test_convert_to_gemini_with_embedded_signature(): assert gemini_parts[0]["thoughtSignature"] == test_signature -def test_openai_client_e2e_flow(): +@pytest.mark.parametrize("enable_preview_features", [True, False]) +def test_openai_client_e2e_flow(enable_preview_features): """ End-to-end test simulating OpenAI client usage: 1. LiteLLM receives response from Gemini with thought signature - 2. LiteLLM embeds signature in tool call ID + 2. LiteLLM embeds signature in tool call ID (if preview features enabled) 3. OpenAI client sends message back with same tool call ID - 4. LiteLLM extracts signature from ID and sends to Gemini + 4. LiteLLM extracts signature from ID/provider_specific_fields and sends to Gemini """ test_signature = "Co4CAdHtim/rWgXbz2Ghp4tShzLeMASrPw6JJyYIC3cbVyZnKzU3uv8/wVzyS2sKRPL2m8QQHHXbNQhEEz500G7n/4ZMmksdTtfQcJMoT76S1DGwhnAiLwTgWCNXs3lEb4M19EVYoWFxhrH5Lr9YMIquoU9U4paydGwvZyIyigamIg4B6WnxrRsf0KZV12gJed0DZuKczvOFtHz3zUnmZRlOiTzd5gBVyQM+5jv1VI8m4WUKd6cN/5a5ZvaA0ggiO6kdVhlpIVs7GczSEVJD8KH4u02X7VSnb7CvykqDntZzV0y8rZFBEFGKrChmeHlWXP4D1IB3F9KQyhuLgWImMzg4BajKVxxMU737JGnNISy5" - # Step 1: Gemini returns function call with thought signature - gemini_parts = [ - HttpxPartType( - functionCall={ - "name": "get_current_temperature", - "args": {"location": "Paris"}, - }, - thoughtSignature=test_signature, - ) - ] + # Save original state + original_flag = litellm.enable_preview_features + litellm.enable_preview_features = enable_preview_features - # Step 2: LiteLLM transforms to OpenAI format with embedded signature - function, tools, _ = VertexGeminiConfig._transform_parts( - parts=gemini_parts, - cumulative_tool_call_idx=0, - is_function_call=False, - ) - - assert tools is not None - assert len(tools) == 1 - tool_call_id = tools[0]["id"] - assert THOUGHT_SIGNATURE_SEPARATOR in tool_call_id - - # Step 3: OpenAI client sends back assistant message (preserves tool_call_id) - openai_assistant_message = { - "role": "assistant", - "content": "", - "tool_calls": [ - { - "id": tool_call_id, # Preserved from response - "type": "function", - "function": { + try: + # Step 1: Gemini returns function call with thought signature + gemini_parts = [ + HttpxPartType( + functionCall={ "name": "get_current_temperature", - "arguments": '{"location": "Paris"}', + "args": {"location": "Paris"}, }, + thoughtSignature=test_signature, + ) + ] + + # Step 2: LiteLLM transforms to OpenAI format + function, tools, _ = VertexGeminiConfig._transform_parts( + parts=gemini_parts, + cumulative_tool_call_idx=0, + is_function_call=False, + ) + + assert tools is not None + assert len(tools) == 1 + tool_call_id = tools[0]["id"] + + if enable_preview_features: + # When preview features enabled, signature should be embedded in ID + assert THOUGHT_SIGNATURE_SEPARATOR in tool_call_id + else: + # When preview features disabled, signature should NOT be embedded in ID + assert THOUGHT_SIGNATURE_SEPARATOR not in tool_call_id + + # Step 3: OpenAI client sends back assistant message + # For the disabled case, we simulate that the client might have provider_specific_fields + # or we use the embedded ID if preview features were enabled + if enable_preview_features: + openai_assistant_message = { + "role": "assistant", + "content": "", + "tool_calls": [ + { + "id": tool_call_id, # Preserved from response (with embedded signature) + "type": "function", + "function": { + "name": "get_current_temperature", + "arguments": '{"location": "Paris"}', + }, + } + ], + } + else: + # When preview features disabled, simulate that provider_specific_fields might be preserved + # (though in real OpenAI client usage, this might not happen) + # For this test, we'll use provider_specific_fields to show extraction still works + openai_assistant_message = { + "role": "assistant", + "content": "", + "tool_calls": [ + { + "id": tool_call_id, # ID without embedded signature + "type": "function", + "function": { + "name": "get_current_temperature", + "arguments": '{"location": "Paris"}', + }, + "provider_specific_fields": {"thought_signature": test_signature}, + } + ], } - ], - } - # Step 4: LiteLLM converts back to Gemini format, extracting signature - gemini_parts_converted = convert_to_gemini_tool_call_invoke( - openai_assistant_message - ) + # Step 4: LiteLLM converts back to Gemini format, extracting signature + gemini_parts_converted = convert_to_gemini_tool_call_invoke( + openai_assistant_message + ) - # Verify signature is preserved through the round trip - assert len(gemini_parts_converted) == 1 - assert "thoughtSignature" in gemini_parts_converted[0] - assert gemini_parts_converted[0]["thoughtSignature"] == test_signature + # Verify signature is preserved through the round trip + assert len(gemini_parts_converted) == 1 + assert "thoughtSignature" in gemini_parts_converted[0] + assert gemini_parts_converted[0]["thoughtSignature"] == test_signature + finally: + # Restore original state + litellm.enable_preview_features = original_flag -def test_parallel_tool_calls_with_signatures(): +@pytest.mark.parametrize("enable_preview_features", [True, False]) +def test_parallel_tool_calls_with_signatures(enable_preview_features): """Test that parallel tool calls preserve signatures correctly""" signature1 = "signature_for_first_call" # Only first call has signature (Gemini behavior for parallel calls) - gemini_parts = [ - HttpxPartType( - functionCall={"name": "get_temperature", "args": {"location": "Paris"}}, - thoughtSignature=signature1, - ), - HttpxPartType( - functionCall={"name": "get_temperature", "args": {"location": "London"}}, - # No signature for second parallel call - ), - ] + # Save original state + original_flag = litellm.enable_preview_features + litellm.enable_preview_features = enable_preview_features - function, tools, _ = VertexGeminiConfig._transform_parts( - parts=gemini_parts, - cumulative_tool_call_idx=0, - is_function_call=False, - ) + try: + gemini_parts = [ + HttpxPartType( + functionCall={"name": "get_temperature", "args": {"location": "Paris"}}, + thoughtSignature=signature1, + ), + HttpxPartType( + functionCall={"name": "get_temperature", "args": {"location": "London"}}, + # No signature for second parallel call + ), + ] - assert tools is not None - assert len(tools) == 2 + function, tools, _ = VertexGeminiConfig._transform_parts( + parts=gemini_parts, + cumulative_tool_call_idx=0, + is_function_call=False, + ) - # First tool call has signature in ID - assert THOUGHT_SIGNATURE_SEPARATOR in tools[0]["id"] - sig1 = _get_thought_signature_from_tool({"id": tools[0]["id"], "type": "function"}) - assert sig1 == signature1 + assert tools is not None + assert len(tools) == 2 - # Second tool call has no signature in ID - assert THOUGHT_SIGNATURE_SEPARATOR not in tools[1]["id"] - sig2 = _get_thought_signature_from_tool({"id": tools[1]["id"], "type": "function"}) - assert sig2 is None + # First tool call should have signature in provider_specific_fields + assert tools[0].get("provider_specific_fields", {}).get("thought_signature") == signature1 + + if enable_preview_features: + # When preview features enabled, first tool call has signature in ID + assert THOUGHT_SIGNATURE_SEPARATOR in tools[0]["id"] + sig1 = _get_thought_signature_from_tool({"id": tools[0]["id"], "type": "function"}) + assert sig1 == signature1 + else: + # When preview features disabled, signature should NOT be in ID + assert THOUGHT_SIGNATURE_SEPARATOR not in tools[0]["id"] + # But we can extract from provider_specific_fields + sig1 = _get_thought_signature_from_tool({ + "id": tools[0]["id"], + "type": "function", + "provider_specific_fields": {"thought_signature": signature1} + }) + assert sig1 == signature1 + + # Second tool call has no signature in ID (regardless of flag) + assert THOUGHT_SIGNATURE_SEPARATOR not in tools[1]["id"] + sig2 = _get_thought_signature_from_tool({"id": tools[1]["id"], "type": "function"}) + assert sig2 is None + finally: + # Restore original state + litellm.enable_preview_features = original_flag From 099ccf56a746c9bca8afa4c0c62fdb52fa5abfa9 Mon Sep 17 00:00:00 2001 From: Richard Song <9144514+richardmcsong@users.noreply.github.com> Date: Wed, 3 Dec 2025 00:57:07 -0500 Subject: [PATCH 035/107] Refactor add_schema_to_components to move definitions to components/schemas and add corresponding unit test (#17389) --- .../proxy/common_utils/custom_openapi_spec.py | 2 +- .../common_utils/test_custom_openapi_spec.py | 29 +++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/litellm/proxy/common_utils/custom_openapi_spec.py b/litellm/proxy/common_utils/custom_openapi_spec.py index c448742f6dc..69472c2cda4 100644 --- a/litellm/proxy/common_utils/custom_openapi_spec.py +++ b/litellm/proxy/common_utils/custom_openapi_spec.py @@ -72,7 +72,7 @@ class CustomOpenAPISpec: openapi_schema["components"]["schemas"] = {} # Add the schema - openapi_schema["components"]["schemas"][schema_name] = schema_def + CustomOpenAPISpec._move_defs_to_components(openapi_schema, {schema_name: schema_def}) @staticmethod def add_request_body_to_paths(openapi_schema: Dict[str, Any], paths: List[str], schema_ref: str) -> None: diff --git a/tests/test_litellm/proxy/common_utils/test_custom_openapi_spec.py b/tests/test_litellm/proxy/common_utils/test_custom_openapi_spec.py index 7549b4259af..5fef35eb821 100644 --- a/tests/test_litellm/proxy/common_utils/test_custom_openapi_spec.py +++ b/tests/test_litellm/proxy/common_utils/test_custom_openapi_spec.py @@ -90,6 +90,35 @@ class TestCustomOpenAPISpec: ) assert result == base_openapi_schema +def test_defs_rewritten_in_add_schema_to_components(): + """ + Test that defs are rewritten to components/schemas in add_schema_to_components. + """ + + openapi_schema = {} + schema_name = "SchemaName" + schema_def = { + "type": "object", + "properties": { + "messages": { + "type": "array", + "items": { + "anyOf": [ + {"$ref": "#/$defs/UserMessage"}, + {"$ref": "#/$defs/AssistantMessage"} + ] + } + } + }, + "$defs": { + "UserMessage": {"type": "object"}, + "AssistantMessage": {"type": "object"} + } + } + CustomOpenAPISpec.add_schema_to_components(openapi_schema=openapi_schema, schema_name=schema_name, schema_def=schema_def) + assert "$defs" not in openapi_schema + assert openapi_schema["components"]["schemas"]["SchemaName"]["properties"]["messages"]["items"]["anyOf"][0]["$ref"] == "#/components/schemas/UserMessage" + assert openapi_schema["components"]["schemas"]["SchemaName"]["properties"]["messages"]["items"]["anyOf"][1]["$ref"] == "#/components/schemas/AssistantMessage" def test_move_defs_to_components(): """ From f22bc0aab20e9b5336e73d67ba1631176cbacfd6 Mon Sep 17 00:00:00 2001 From: Matt Greathouse Date: Wed, 3 Dec 2025 01:00:19 -0500 Subject: [PATCH 036/107] Support Deepseek 3.2 with Reasoning (#17384) * Add openrouter/deepseek/deepseek-v3.2 * Added deepseek-provided v3.2 * Allow reasoning effort param for openrouter models that support it * Added tests --- .../llms/openrouter/chat/transformation.py | 15 ++++++++++ model_prices_and_context_window.json | 30 +++++++++++++++++++ .../test_openrouter_chat_transformation.py | 27 +++++++++++++++++ 3 files changed, 72 insertions(+) diff --git a/litellm/llms/openrouter/chat/transformation.py b/litellm/llms/openrouter/chat/transformation.py index f1eafe4e294..b5610852fd2 100644 --- a/litellm/llms/openrouter/chat/transformation.py +++ b/litellm/llms/openrouter/chat/transformation.py @@ -10,6 +10,7 @@ from enum import Enum from typing import Any, AsyncIterator, Iterator, List, Optional, Tuple, Union, cast import httpx +import litellm from litellm.llms.base_llm.base_model_iterator import BaseModelResponseIterator from litellm.llms.base_llm.chat.transformation import BaseLLMException @@ -28,6 +29,20 @@ class CacheControlSupportedModels(str, Enum): class OpenrouterConfig(OpenAIGPTConfig): + def get_supported_openai_params(self, model: str) -> list: + """ + Allow reasoning parameters for models flagged as reasoning-capable. + """ + supported_params = super().get_supported_openai_params(model=model) + try: + if litellm.supports_reasoning( + model=model, custom_llm_provider="openrouter" + ) or litellm.supports_reasoning(model=model): + supported_params.append("reasoning_effort") + except Exception: + pass + return list(dict.fromkeys(supported_params)) + def map_openai_params( self, non_default_params: dict, diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 19ed734c5f8..f82abce525f 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -9629,6 +9629,21 @@ "supports_prompt_caching": true, "supports_tool_choice": true }, + "deepseek/deepseek-v3.2": { + "input_cost_per_token": 2.8e-07, + "input_cost_per_token_cache_hit": 2.8e-08, + "litellm_provider": "deepseek", + "max_input_tokens": 163840, + "max_output_tokens": 163840, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 4e-07, + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, "deepseek.v3-v1:0": { "input_cost_per_token": 5.8e-07, "litellm_provider": "bedrock_converse", @@ -20565,6 +20580,21 @@ "supports_reasoning": true, "supports_tool_choice": true }, + "openrouter/deepseek/deepseek-v3.2": { + "input_cost_per_token": 2.8e-07, + "input_cost_per_token_cache_hit": 2.8e-08, + "litellm_provider": "openrouter", + "max_input_tokens": 163840, + "max_output_tokens": 163840, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 4e-07, + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, "openrouter/deepseek/deepseek-v3.2-exp": { "input_cost_per_token": 2e-07, "input_cost_per_token_cache_hit": 2e-08, diff --git a/tests/test_litellm/llms/openrouter/chat/test_openrouter_chat_transformation.py b/tests/test_litellm/llms/openrouter/chat/test_openrouter_chat_transformation.py index 64ac299fd79..d5a73b3fd12 100644 --- a/tests/test_litellm/llms/openrouter/chat/test_openrouter_chat_transformation.py +++ b/tests/test_litellm/llms/openrouter/chat/test_openrouter_chat_transformation.py @@ -489,3 +489,30 @@ def test_openrouter_cost_tracking_streaming(): # Verify cost field is preserved in the Usage object - this is the key data for cost tracking # The chunk_parser converts the dict to a Usage Pydantic model which includes the cost field assert result2.usage.cost == 0.0001 + + +def test_openrouter_reasoning_models_allow_reasoning_effort_param(): + """ + OpenRouter reasoning-capable models should accept the reasoning_effort param. + """ + config = OpenrouterConfig() + + supported_params = config.get_supported_openai_params( + model="openrouter/deepseek/deepseek-v3.2" + ) + + assert "reasoning_effort" in supported_params + assert supported_params.count("reasoning_effort") == 1 + + +def test_openrouter_non_reasoning_models_do_not_add_reasoning_effort(): + """ + Models without reasoning support should not gain reasoning-specific params. + """ + config = OpenrouterConfig() + + supported_params = config.get_supported_openai_params( + model="openrouter/anthropic/claude-3-5-haiku" + ) + + assert "reasoning_effort" not in supported_params From ae633184f72d8a12ae442a03ce86f51fc4f649fc Mon Sep 17 00:00:00 2001 From: mossbanay <2216177+mossbanay@users.noreply.github.com> Date: Wed, 3 Dec 2025 17:02:57 +1100 Subject: [PATCH 037/107] Add model price & details for Bedrock model global.anthropic.claude-opus-4-5-20251101-v1:0 (#17380) --- model_prices_and_context_window.json | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index f82abce525f..999c88dde05 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -23850,6 +23850,32 @@ "supports_vision": true, "tool_use_system_prompt_tokens": 159 }, + "global.anthropic.claude-opus-4-5-20251101-v1:0": { + "cache_creation_input_token_cost": 6.25e-06, + "cache_read_input_token_cost": 5e-07, + "input_cost_per_token": 5e-06, + "litellm_provider": "bedrock_converse", + "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 + }, "us.anthropic.claude-sonnet-4-20250514-v1:0": { "cache_creation_input_token_cost": 3.75e-06, "cache_read_input_token_cost": 3e-07, From 6b5ad5d5a6c9cce7008d0eb57258a826e6d82808 Mon Sep 17 00:00:00 2001 From: Ali Saleh Date: Wed, 3 Dec 2025 11:03:54 +0500 Subject: [PATCH 038/107] docs: Update Instructions For Phoenix Integration (#17373) --- .../docs/observability/phoenix_integration.md | 79 +++++++++++++++---- 1 file changed, 64 insertions(+), 15 deletions(-) diff --git a/docs/my-website/docs/observability/phoenix_integration.md b/docs/my-website/docs/observability/phoenix_integration.md index ad337439934..898d780668d 100644 --- a/docs/my-website/docs/observability/phoenix_integration.md +++ b/docs/my-website/docs/observability/phoenix_integration.md @@ -6,7 +6,7 @@ Open source tracing and evaluation platform :::tip -This is community maintained, Please make an issue if you run into a bug +This is community maintained. Please make an issue if you run into a bug: https://github.com/BerriAI/litellm ::: @@ -31,19 +31,16 @@ litellm.callbacks = ["arize_phoenix"] import litellm import os -os.environ["PHOENIX_API_KEY"] = "" # Necessary only using Phoenix Cloud -os.environ["PHOENIX_COLLECTOR_HTTP_ENDPOINT"] = "" # The URL of your Phoenix OSS instance e.g. http://localhost:6006/v1/traces -os.environ["PHOENIX_PROJECT_NAME"]="litellm" # OPTIONAL: you can configure project names, otherwise traces would go to "default" project +# Set env variables +os.environ["PHOENIX_API_KEY"] = "d0*****" # Set the Phoenix API key here. It is necessary only when using Phoenix Cloud. +os.environ["PHOENIX_COLLECTOR_HTTP_ENDPOINT"] = "https://app.phoenix.arize.com/s//v1/traces" # Set the URL of your Phoenix OSS instance, otherwise tracer would use https://app.phoenix.arize.com/v1/traces for Phoenix Cloud. +os.environ["PHOENIX_PROJECT_NAME"] = "litellm" # Configure the project name, otherwise traces would go to "default" project. +os.environ['OPENAI_API_KEY'] = "fake-key" # Set the OpenAI API key here. -# This defaults to https://app.phoenix.arize.com/v1/traces for Phoenix Cloud - -# LLM API Keys -os.environ['OPENAI_API_KEY']="" - -# set arize as a callback, litellm will send the data to arize +# Set arize_phoenix as a callback & LiteLLM will send the data to Phoenix. litellm.callbacks = ["arize_phoenix"] - -# openai call + +# OpenAI call response = litellm.completion( model="gpt-3.5-turbo", messages=[ @@ -52,8 +49,9 @@ response = litellm.completion( ) ``` -### Using with LiteLLM Proxy +## Using with LiteLLM Proxy +1. Setup config.yaml ```yaml model_list: @@ -66,12 +64,63 @@ model_list: litellm_settings: callbacks: ["arize_phoenix"] +general_settings: + master_key: "sk-1234" + environment_variables: PHOENIX_API_KEY: "d0*****" - PHOENIX_COLLECTOR_ENDPOINT: "https://app.phoenix.arize.com/v1/traces" # OPTIONAL, for setting the GRPC endpoint - PHOENIX_COLLECTOR_HTTP_ENDPOINT: "https://app.phoenix.arize.com/v1/traces" # OPTIONAL, for setting the HTTP endpoint + PHOENIX_COLLECTOR_ENDPOINT: "https://app.phoenix.arize.com/s//v1/traces" # OPTIONAL - For setting the gRPC endpoint + PHOENIX_COLLECTOR_HTTP_ENDPOINT: "https://app.phoenix.arize.com/s//v1/traces" # OPTIONAL - For setting the HTTP endpoint ``` +2. Start the proxy + +```bash +litellm --config config.yaml +``` + +3. Test it! + +```bash +curl -X POST 'http://0.0.0.0:4000/chat/completions' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: Bearer sk-1234' \ +-d '{ "model": "gpt-4o", "messages": [{"role": "user", "content": "Hi 👋 - i'm openai"}]}' +``` + +## Supported Phoenix Endpoints +Phoenix now supports multiple deployment types. The correct endpoint depends on which version of Phoenix Cloud you are using. + +**Phoenix Cloud (With Spaces - New Version)** +Use this if your Phoenix URL contains `/s/` path. + +```bash +https://app.phoenix.arize.com/s//v1/traces +``` + +**Phoenix Cloud (Legacy - Deprecated)** +Use this only if your deployment still shows the `/legacy` pattern. + +```bash +https://app.phoenix.arize.com/legacy/v1/traces +``` + +**Phoenix Cloud (Without Spaces - Old Version)** +Use this if your Phoenix Cloud URL does not contain `/s/` or `/legacy` path. + +```bash +https://app.phoenix.arize.com/v1/traces +``` + +**Self-Hosted Phoenix (Local Instance)** +Use this when running Phoenix on your machine or a private server. + +```bash +http://localhost:6006/v1/traces +``` + +Depending on which Phoenix Cloud version or deployment you are using, you should set the corresponding endpoint in `PHOENIX_COLLECTOR_HTTP_ENDPOINT` or `PHOENIX_COLLECTOR_ENDPOINT`. + ## Support & Talk to Founders - [Schedule Demo 👋](https://calendly.com/d/4mp-gd3-k5k/berriai-1-1-onboarding-litellm-hosted-version) From 566adebdec41d3f2790de33e43d93892f529f597 Mon Sep 17 00:00:00 2001 From: Mariano Hielpos <108539968+mhielpos-asapp@users.noreply.github.com> Date: Wed, 3 Dec 2025 03:06:51 -0300 Subject: [PATCH 039/107] update model_prices_and_context_window.json (#17376) * update model_prices_and_context_window.json * update * update --- ...odel_prices_and_context_window_backup.json | 88 +++++++++++++++++-- model_prices_and_context_window.json | 88 +++++++++++++++++-- 2 files changed, 160 insertions(+), 16 deletions(-) diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 932508824af..464f9c185f9 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -10772,25 +10772,25 @@ "supports_tool_choice": true }, "ft:babbage-002": { - "input_cost_per_token": 4e-07, + "input_cost_per_token": 1.6e-06, "input_cost_per_token_batches": 2e-07, "litellm_provider": "text-completion-openai", "max_input_tokens": 16384, "max_output_tokens": 4096, "max_tokens": 16384, "mode": "completion", - "output_cost_per_token": 4e-07, + "output_cost_per_token": 1.6e-06, "output_cost_per_token_batches": 2e-07 }, "ft:davinci-002": { - "input_cost_per_token": 2e-06, + "input_cost_per_token": 1.2e-05, "input_cost_per_token_batches": 1e-06, "litellm_provider": "text-completion-openai", "max_input_tokens": 16384, "max_output_tokens": 4096, "max_tokens": 16384, "mode": "completion", - "output_cost_per_token": 2e-06, + "output_cost_per_token": 1.2e-05, "output_cost_per_token_batches": 1e-06 }, "ft:gpt-3.5-turbo": { @@ -10853,6 +10853,7 @@ "supports_tool_choice": true }, "ft:gpt-4o-2024-08-06": { + "cache_read_input_token_cost": 1.875e-06, "input_cost_per_token": 3.75e-06, "input_cost_per_token_batches": 1.875e-06, "litellm_provider": "openai", @@ -10865,6 +10866,7 @@ "supports_function_calling": true, "supports_parallel_function_calling": true, "supports_pdf_input": true, + "supports_prompt_caching": true, "supports_response_schema": true, "supports_system_messages": true, "supports_tool_choice": true, @@ -10885,8 +10887,7 @@ "supports_prompt_caching": true, "supports_response_schema": true, "supports_system_messages": true, - "supports_tool_choice": true, - "supports_vision": true + "supports_tool_choice": true }, "ft:gpt-4o-mini-2024-07-18": { "cache_read_input_token_cost": 1.5e-07, @@ -10905,8 +10906,79 @@ "supports_prompt_caching": true, "supports_response_schema": true, "supports_system_messages": true, - "supports_tool_choice": true, - "supports_vision": true + "supports_tool_choice": true + }, + "ft:gpt-4.1-2025-04-14": { + "cache_read_input_token_cost": 7.5e-07, + "input_cost_per_token": 3e-06, + "input_cost_per_token_batches": 1.5e-06, + "litellm_provider": "openai", + "max_input_tokens": 1047576, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 1.2e-05, + "output_cost_per_token_batches": 6e-06, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "ft:gpt-4.1-mini-2025-04-14": { + "cache_read_input_token_cost": 2e-07, + "input_cost_per_token": 8e-07, + "input_cost_per_token_batches": 4e-07, + "litellm_provider": "openai", + "max_input_tokens": 1047576, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 3.2e-06, + "output_cost_per_token_batches": 1.6e-06, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "ft:gpt-4.1-nano-2025-04-14": { + "cache_read_input_token_cost": 5e-08, + "input_cost_per_token": 2e-07, + "input_cost_per_token_batches": 1e-07, + "litellm_provider": "openai", + "max_input_tokens": 1047576, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 8e-07, + "output_cost_per_token_batches": 4e-07, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "ft:o4-mini-2025-04-16": { + "cache_read_input_token_cost": 1e-06, + "input_cost_per_token": 4e-06, + "input_cost_per_token_batches": 2e-06, + "litellm_provider": "openai", + "max_input_tokens": 200000, + "max_output_tokens": 100000, + "max_tokens": 100000, + "mode": "chat", + "output_cost_per_token": 1.6e-05, + "output_cost_per_token_batches": 8e-06, + "supports_function_calling": true, + "supports_parallel_function_calling": false, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true }, "gemini-1.0-pro": { "input_cost_per_character": 1.25e-07, diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 999c88dde05..dbaa60e0f1d 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -10852,25 +10852,25 @@ "supports_tool_choice": true }, "ft:babbage-002": { - "input_cost_per_token": 4e-07, + "input_cost_per_token": 1.6e-06, "input_cost_per_token_batches": 2e-07, "litellm_provider": "text-completion-openai", "max_input_tokens": 16384, "max_output_tokens": 4096, "max_tokens": 16384, "mode": "completion", - "output_cost_per_token": 4e-07, + "output_cost_per_token": 1.6e-06, "output_cost_per_token_batches": 2e-07 }, "ft:davinci-002": { - "input_cost_per_token": 2e-06, + "input_cost_per_token": 1.2e-05, "input_cost_per_token_batches": 1e-06, "litellm_provider": "text-completion-openai", "max_input_tokens": 16384, "max_output_tokens": 4096, "max_tokens": 16384, "mode": "completion", - "output_cost_per_token": 2e-06, + "output_cost_per_token": 1.2e-05, "output_cost_per_token_batches": 1e-06 }, "ft:gpt-3.5-turbo": { @@ -10933,6 +10933,7 @@ "supports_tool_choice": true }, "ft:gpt-4o-2024-08-06": { + "cache_read_input_token_cost": 1.875e-06, "input_cost_per_token": 3.75e-06, "input_cost_per_token_batches": 1.875e-06, "litellm_provider": "openai", @@ -10945,6 +10946,7 @@ "supports_function_calling": true, "supports_parallel_function_calling": true, "supports_pdf_input": true, + "supports_prompt_caching": true, "supports_response_schema": true, "supports_system_messages": true, "supports_tool_choice": true, @@ -10965,8 +10967,7 @@ "supports_prompt_caching": true, "supports_response_schema": true, "supports_system_messages": true, - "supports_tool_choice": true, - "supports_vision": true + "supports_tool_choice": true }, "ft:gpt-4o-mini-2024-07-18": { "cache_read_input_token_cost": 1.5e-07, @@ -10985,8 +10986,79 @@ "supports_prompt_caching": true, "supports_response_schema": true, "supports_system_messages": true, - "supports_tool_choice": true, - "supports_vision": true + "supports_tool_choice": true + }, + "ft:gpt-4.1-2025-04-14": { + "cache_read_input_token_cost": 7.5e-07, + "input_cost_per_token": 3e-06, + "input_cost_per_token_batches": 1.5e-06, + "litellm_provider": "openai", + "max_input_tokens": 1047576, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 1.2e-05, + "output_cost_per_token_batches": 6e-06, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "ft:gpt-4.1-mini-2025-04-14": { + "cache_read_input_token_cost": 2e-07, + "input_cost_per_token": 8e-07, + "input_cost_per_token_batches": 4e-07, + "litellm_provider": "openai", + "max_input_tokens": 1047576, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 3.2e-06, + "output_cost_per_token_batches": 1.6e-06, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "ft:gpt-4.1-nano-2025-04-14": { + "cache_read_input_token_cost": 5e-08, + "input_cost_per_token": 2e-07, + "input_cost_per_token_batches": 1e-07, + "litellm_provider": "openai", + "max_input_tokens": 1047576, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 8e-07, + "output_cost_per_token_batches": 4e-07, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "ft:o4-mini-2025-04-16": { + "cache_read_input_token_cost": 1e-06, + "input_cost_per_token": 4e-06, + "input_cost_per_token_batches": 2e-06, + "litellm_provider": "openai", + "max_input_tokens": 200000, + "max_output_tokens": 100000, + "max_tokens": 100000, + "mode": "chat", + "output_cost_per_token": 1.6e-05, + "output_cost_per_token_batches": 8e-06, + "supports_function_calling": true, + "supports_parallel_function_calling": false, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true }, "gemini-1.0-pro": { "input_cost_per_character": 1.25e-07, From 17faea96bb75be88f6761398e3ad2130a0d29cfc Mon Sep 17 00:00:00 2001 From: Jonathan Yang Date: Wed, 3 Dec 2025 07:09:57 +0100 Subject: [PATCH 040/107] fix: conditionally pass enable_cleanup_closed to aiohttp TCPConnector (#17367) * fix: conditionally pass enable_cleanup_closed to aiohttp TCPConnector Fixes deprecation warning on Python 3.12.7+ and 3.13.1+ where enable_cleanup_closed is no longer needed since the underlying CPython SSL connection leak bug was fixed. See: https://github.com/python/cpython/pull/118960 * chore: add aiohttp source reference to AIOHTTP_NEEDS_CLEANUP_CLOSED --- litellm/constants.py | 7 +++++++ litellm/llms/custom_httpx/http_handler.py | 3 ++- litellm/proxy/proxy_server.py | 3 ++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/litellm/constants.py b/litellm/constants.py index 6a67a9a0e18..db617a2e475 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -1,4 +1,5 @@ import os +import sys from typing import List, Literal DEFAULT_HEALTH_CHECK_PROMPT = str( @@ -103,6 +104,12 @@ _DEFAULT_TTL_FOR_HTTPX_CLIENTS = 3600 # 1 hour, re-use the same httpx client fo AIOHTTP_CONNECTOR_LIMIT = int(os.getenv("AIOHTTP_CONNECTOR_LIMIT", 0)) AIOHTTP_KEEPALIVE_TIMEOUT = int(os.getenv("AIOHTTP_KEEPALIVE_TIMEOUT", 120)) AIOHTTP_TTL_DNS_CACHE = int(os.getenv("AIOHTTP_TTL_DNS_CACHE", 300)) +# enable_cleanup_closed is only needed for Python versions with the SSL leak bug +# Fixed in Python 3.12.7+ and 3.13.1+ (see https://github.com/python/cpython/pull/118960) +# Reference: https://github.com/aio-libs/aiohttp/blob/master/aiohttp/connector.py#L74-L78 +AIOHTTP_NEEDS_CLEANUP_CLOSED = ( + (3, 13, 0) <= sys.version_info < (3, 13, 1) or sys.version_info < (3, 12, 7) +) # WebSocket constants # Default to None (unlimited) to match OpenAI's official agents SDK behavior diff --git a/litellm/llms/custom_httpx/http_handler.py b/litellm/llms/custom_httpx/http_handler.py index c35e910ab08..b06e8463abd 100644 --- a/litellm/llms/custom_httpx/http_handler.py +++ b/litellm/llms/custom_httpx/http_handler.py @@ -17,6 +17,7 @@ from litellm.constants import ( _DEFAULT_TTL_FOR_HTTPX_CLIENTS, AIOHTTP_CONNECTOR_LIMIT, AIOHTTP_KEEPALIVE_TIMEOUT, + AIOHTTP_NEEDS_CLEANUP_CLOSED, AIOHTTP_TTL_DNS_CACHE, DEFAULT_SSL_CIPHERS, ) @@ -798,7 +799,7 @@ class AsyncHTTPHandler: limit=AIOHTTP_CONNECTOR_LIMIT, keepalive_timeout=AIOHTTP_KEEPALIVE_TIMEOUT, ttl_dns_cache=AIOHTTP_TTL_DNS_CACHE, - enable_cleanup_closed=True, + enable_cleanup_closed=AIOHTTP_NEEDS_CLEANUP_CLOSED, **connector_kwargs, ), trust_env=trust_env, diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 99789162318..a1e01caddc1 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -32,6 +32,7 @@ from litellm._uuid import uuid from litellm.constants import ( AIOHTTP_CONNECTOR_LIMIT, AIOHTTP_KEEPALIVE_TIMEOUT, + AIOHTTP_NEEDS_CLEANUP_CLOSED, AIOHTTP_TTL_DNS_CACHE, AUDIO_SPEECH_CHUNK_SIZE, BASE_MCP_ROUTE, @@ -635,7 +636,7 @@ async def _initialize_shared_aiohttp_session(): limit=AIOHTTP_CONNECTOR_LIMIT, keepalive_timeout=AIOHTTP_KEEPALIVE_TIMEOUT, ttl_dns_cache=AIOHTTP_TTL_DNS_CACHE, - enable_cleanup_closed=True, + enable_cleanup_closed=AIOHTTP_NEEDS_CLEANUP_CLOSED, ) session = ClientSession(connector=connector) From 43dd9e4a90281e42fd1015cd72cbca5280fc4e4c Mon Sep 17 00:00:00 2001 From: Jonathan Yang Date: Wed, 3 Dec 2025 07:12:55 +0100 Subject: [PATCH 041/107] fix: replace deprecated .dict() with .model_dump() in streaming_handler (#17359) Replace Pydantic v1 `.dict()` method with v2 `.model_dump()` to fix PydanticDeprecatedSince20 warnings. The `.dict()` method is deprecated in Pydantic v2 and will be removed in v3. Fixes #5987 --- .../litellm_core_utils/streaming_handler.py | 53 +++++++++++-------- 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/litellm/litellm_core_utils/streaming_handler.py b/litellm/litellm_core_utils/streaming_handler.py index 4d8e109d882..a7f460fab59 100644 --- a/litellm/litellm_core_utils/streaming_handler.py +++ b/litellm/litellm_core_utils/streaming_handler.py @@ -96,9 +96,9 @@ class CustomStreamWrapper: self.system_fingerprint: Optional[str] = None self.received_finish_reason: Optional[str] = None - self.intermittent_finish_reason: Optional[str] = ( - None # finish reasons that show up mid-stream - ) + self.intermittent_finish_reason: Optional[ + str + ] = None # finish reasons that show up mid-stream self.special_tokens = [ "<|assistant|>", "<|system|>", @@ -735,7 +735,7 @@ class CustomStreamWrapper: and completion_obj["function_call"] is not None ) or ( - "tool_calls" in model_response.choices[0].delta + "tool_calls" in model_response.choices[0].delta and model_response.choices[0].delta["tool_calls"] is not None ) or ( @@ -889,7 +889,6 @@ class CustomStreamWrapper: ## check if openai/azure chunk original_chunk = response_obj.get("original_chunk", None) if original_chunk: - if len(original_chunk.choices) > 0: choices = [] for choice in original_chunk.choices: @@ -906,7 +905,6 @@ class CustomStreamWrapper: print_verbose(f"choices in streaming: {choices}") setattr(model_response, "choices", choices) else: - return model_response.system_fingerprint = ( original_chunk.system_fingerprint @@ -1435,9 +1433,9 @@ class CustomStreamWrapper: _json_delta = delta.model_dump() print_verbose(f"_json_delta: {_json_delta}") if "role" not in _json_delta or _json_delta["role"] is None: - _json_delta["role"] = ( - "assistant" # mistral's api returns role as None - ) + _json_delta[ + "role" + ] = "assistant" # mistral's api returns role as None if "tool_calls" in _json_delta and isinstance( _json_delta["tool_calls"], list ): @@ -1533,7 +1531,7 @@ class CustomStreamWrapper: async def _call_post_streaming_deployment_hook(self, chunk): """ Call the post-call streaming deployment hook for callbacks. - + This allows callbacks to modify streaming chunks before they're returned. """ try: @@ -1544,15 +1542,17 @@ class CustomStreamWrapper: # Get request kwargs from logging object request_data = self.logging_obj.model_call_details call_type_str = self.logging_obj.call_type - + try: typed_call_type = CallTypes(call_type_str) except ValueError: typed_call_type = None - + # Call hooks for all callbacks for callback in litellm.callbacks: - if isinstance(callback, CustomLogger) and hasattr(callback, "async_post_call_streaming_deployment_hook"): + if isinstance(callback, CustomLogger) and hasattr( + callback, "async_post_call_streaming_deployment_hook" + ): result = await callback.async_post_call_streaming_deployment_hook( request_data=request_data, response_chunk=chunk, @@ -1560,11 +1560,14 @@ class CustomStreamWrapper: ) if result is not None: chunk = result - + return chunk except Exception as e: from litellm._logging import verbose_logger - verbose_logger.exception(f"Error in post-call streaming deployment hook: {str(e)}") + + verbose_logger.exception( + f"Error in post-call streaming deployment hook: {str(e)}" + ) return chunk def cache_streaming_response(self, processed_chunk, cache_hit: bool): @@ -1687,7 +1690,7 @@ class CustomStreamWrapper: response, "usage" ): # remove usage from chunk, only send on final chunk # Convert the object to a dictionary - obj_dict = response.dict() + obj_dict = response.model_dump() # Remove an attribute (e.g., 'attr2') if "usage" in obj_dict: @@ -1852,7 +1855,7 @@ class CustomStreamWrapper: processed_chunk, "usage" ): # remove usage from chunk, only send on final chunk # Convert the object to a dictionary - obj_dict = processed_chunk.dict() + obj_dict = processed_chunk.model_dump() # Remove an attribute (e.g., 'attr2') if "usage" in obj_dict: @@ -1872,11 +1875,15 @@ class CustomStreamWrapper: if self.sent_last_chunk is True and self.stream_options is None: usage = calculate_total_usage(chunks=self.chunks) processed_chunk._hidden_params["usage"] = usage - + # Call post-call streaming deployment hook for final chunk if self.sent_last_chunk is True: - processed_chunk = await self._call_post_streaming_deployment_hook(processed_chunk) - + processed_chunk = ( + await self._call_post_streaming_deployment_hook( + processed_chunk + ) + ) + return processed_chunk raise StopAsyncIteration else: # temporary patch for non-aiohttp async calls @@ -1890,9 +1897,9 @@ class CustomStreamWrapper: chunk = next(self.completion_stream) if chunk is not None and chunk != b"": print_verbose(f"PROCESSED CHUNK PRE CHUNK CREATOR: {chunk}") - processed_chunk: Optional[ModelResponseStream] = ( - self.chunk_creator(chunk=chunk) - ) + processed_chunk: Optional[ + ModelResponseStream + ] = self.chunk_creator(chunk=chunk) print_verbose( f"PROCESSED CHUNK POST CHUNK CREATOR: {processed_chunk}" ) From e289f5e454140042eda28ca27b8797078e2fe9f6 Mon Sep 17 00:00:00 2001 From: Deepak Tammali <45919384+deepaktammali@users.noreply.github.com> Date: Wed, 3 Dec 2025 11:44:48 +0530 Subject: [PATCH 042/107] feat: make streaming chunk size configurable in bedrock converse and invoke handlers (#17357) --- litellm/llms/bedrock/chat/converse_handler.py | 10 ++++++++-- litellm/llms/bedrock/chat/invoke_handler.py | 20 ++++++++++++------- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/litellm/llms/bedrock/chat/converse_handler.py b/litellm/llms/bedrock/chat/converse_handler.py index fd1f6f0c893..d5bd054118d 100644 --- a/litellm/llms/bedrock/chat/converse_handler.py +++ b/litellm/llms/bedrock/chat/converse_handler.py @@ -29,6 +29,7 @@ def make_sync_call( logging_obj: LiteLLMLoggingObject, json_mode: Optional[bool] = False, fake_stream: bool = False, + stream_chunk_size: int = 1024, ): if client is None: client = _get_httpx_client() # Create a new client if none provided @@ -66,7 +67,7 @@ def make_sync_call( ) else: decoder = AWSEventStreamDecoder(model=model) - completion_stream = decoder.iter_bytes(response.iter_bytes(chunk_size=1024)) + completion_stream = decoder.iter_bytes(response.iter_bytes(chunk_size=stream_chunk_size)) # LOGGING logging_obj.post_call( @@ -102,6 +103,7 @@ class BedrockConverseLLM(BaseAWSLLM): fake_stream: bool = False, json_mode: Optional[bool] = False, api_key: Optional[str] = None, + stream_chunk_size: int = 1024, ) -> CustomStreamWrapper: request_data = await litellm.AmazonConverseConfig()._async_transform_request( model=model, @@ -143,6 +145,7 @@ class BedrockConverseLLM(BaseAWSLLM): logging_obj=logging_obj, fake_stream=fake_stream, json_mode=json_mode, + stream_chunk_size=stream_chunk_size, ) streaming_response = CustomStreamWrapper( completion_stream=completion_stream, @@ -260,6 +263,7 @@ class BedrockConverseLLM(BaseAWSLLM): ): ## SETUP ## stream = optional_params.pop("stream", None) + stream_chunk_size = optional_params.pop("stream_chunk_size", 1024) unencoded_model_id = optional_params.pop("model_id", None) fake_stream = optional_params.pop("fake_stream", False) json_mode = optional_params.get("json_mode", False) @@ -356,7 +360,8 @@ class BedrockConverseLLM(BaseAWSLLM): json_mode=json_mode, fake_stream=fake_stream, credentials=credentials, - api_key=api_key + api_key=api_key, + stream_chunk_size=stream_chunk_size, ) # type: ignore ### ASYNC COMPLETION return self.async_completion( @@ -433,6 +438,7 @@ class BedrockConverseLLM(BaseAWSLLM): logging_obj=logging_obj, json_mode=json_mode, fake_stream=fake_stream, + stream_chunk_size=stream_chunk_size, ) streaming_response = CustomStreamWrapper( completion_stream=completion_stream, diff --git a/litellm/llms/bedrock/chat/invoke_handler.py b/litellm/llms/bedrock/chat/invoke_handler.py index 7a960fd45d2..5e33a266449 100644 --- a/litellm/llms/bedrock/chat/invoke_handler.py +++ b/litellm/llms/bedrock/chat/invoke_handler.py @@ -192,6 +192,7 @@ async def make_call( fake_stream: bool = False, json_mode: Optional[bool] = False, bedrock_invoke_provider: Optional[litellm.BEDROCK_INVOKE_PROVIDERS_LITERAL] = None, + stream_chunk_size: int = 1024, ): try: if client is None: @@ -235,7 +236,7 @@ async def make_call( json_mode=json_mode, ) completion_stream = decoder.aiter_bytes( - response.aiter_bytes(chunk_size=1024) + response.aiter_bytes(chunk_size=stream_chunk_size) ) elif bedrock_invoke_provider == "deepseek_r1": decoder = AmazonDeepSeekR1StreamDecoder( @@ -243,12 +244,12 @@ async def make_call( sync_stream=False, ) completion_stream = decoder.aiter_bytes( - response.aiter_bytes(chunk_size=1024) + response.aiter_bytes(chunk_size=stream_chunk_size) ) else: decoder = AWSEventStreamDecoder(model=model) completion_stream = decoder.aiter_bytes( - response.aiter_bytes(chunk_size=1024) + response.aiter_bytes(chunk_size=stream_chunk_size) ) # LOGGING @@ -281,6 +282,7 @@ def make_sync_call( fake_stream: bool = False, json_mode: Optional[bool] = False, bedrock_invoke_provider: Optional[litellm.BEDROCK_INVOKE_PROVIDERS_LITERAL] = None, + stream_chunk_size: int = 1024, ): try: if client is None: @@ -321,16 +323,16 @@ def make_sync_call( sync_stream=True, json_mode=json_mode, ) - completion_stream = decoder.iter_bytes(response.iter_bytes(chunk_size=1024)) + completion_stream = decoder.iter_bytes(response.iter_bytes(chunk_size=stream_chunk_size)) elif bedrock_invoke_provider == "deepseek_r1": decoder = AmazonDeepSeekR1StreamDecoder( model=model, sync_stream=True, ) - completion_stream = decoder.iter_bytes(response.iter_bytes(chunk_size=1024)) + completion_stream = decoder.iter_bytes(response.iter_bytes(chunk_size=stream_chunk_size)) else: decoder = AWSEventStreamDecoder(model=model) - completion_stream = decoder.iter_bytes(response.iter_bytes(chunk_size=1024)) + completion_stream = decoder.iter_bytes(response.iter_bytes(chunk_size=stream_chunk_size)) # LOGGING logging_obj.post_call( @@ -729,6 +731,7 @@ class BedrockLLM(BaseAWSLLM): ## SETUP ## stream = optional_params.pop("stream", None) + stream_chunk_size = optional_params.pop("stream_chunk_size", 1024) provider = self.get_bedrock_invoke_provider(model) modelId = self.get_bedrock_model_id( @@ -1003,6 +1006,7 @@ class BedrockLLM(BaseAWSLLM): headers=prepped.headers, timeout=timeout, client=client, + stream_chunk_size=stream_chunk_size, ) # type: ignore ### ASYNC COMPLETION return self.async_completion( @@ -1048,7 +1052,7 @@ class BedrockLLM(BaseAWSLLM): decoder = AWSEventStreamDecoder(model=model) - completion_stream = decoder.iter_bytes(response.iter_bytes(chunk_size=1024)) + completion_stream = decoder.iter_bytes(response.iter_bytes(chunk_size=stream_chunk_size)) streaming_response = CustomStreamWrapper( completion_stream=completion_stream, model=model, @@ -1168,6 +1172,7 @@ class BedrockLLM(BaseAWSLLM): logger_fn=None, headers={}, client: Optional[AsyncHTTPHandler] = None, + stream_chunk_size: int = 1024, ) -> CustomStreamWrapper: # The call is not made here; instead, we prepare the necessary objects for the stream. @@ -1183,6 +1188,7 @@ class BedrockLLM(BaseAWSLLM): messages=messages, logging_obj=logging_obj, fake_stream=True if "ai21" in api_base else False, + stream_chunk_size=stream_chunk_size, ), model=model, custom_llm_provider="bedrock", From 4c6604b0da6bcdd9c230632f203921047163d636 Mon Sep 17 00:00:00 2001 From: Cesar Garcia <128240629+Chesars@users.noreply.github.com> Date: Wed, 3 Dec 2025 03:25:26 -0300 Subject: [PATCH 043/107] Cleanup: Remove orphan docs pages and Docusaurus template files (#17356) * docs: update getting started page - Add Core Functions table with link to full list - Add Responses API section - Add Async section with acompletion() example - Add "Switch Providers with One Line" example - Clarify Basic Usage supports multiple endpoints - Update models to current versions (openai/gpt-4o, anthropic/claude-sonnet-4) - Use provider/model format throughout - Fix deprecated import: from openai.error -> from openai - Keep original structure: community key, More details links, observability env vars * Cleanup: Remove orphan docs pages and Docusaurus template files - Remove orphan getting_started.md (not linked in sidebar) - Remove Docusaurus template intro.md - Remove tutorial-basics/ directory (Docusaurus template) - Remove tutorial-extras/ directory (Docusaurus template) --- docs/my-website/docs/getting_started.md | 108 ------------- docs/my-website/src/pages/intro.md | 47 ------ .../src/pages/tutorial-basics/_category_.json | 8 - .../pages/tutorial-basics/congratulations.md | 23 --- .../tutorial-basics/create-a-blog-post.md | 34 ---- .../tutorial-basics/create-a-document.md | 57 ------- .../pages/tutorial-basics/create-a-page.md | 43 ----- .../pages/tutorial-basics/deploy-your-site.md | 31 ---- .../tutorial-basics/markdown-features.mdx | 150 ------------------ .../src/pages/tutorial-extras/_category_.json | 7 - .../img/docsVersionDropdown.png | Bin 25427 -> 0 bytes .../tutorial-extras/img/localeDropdown.png | Bin 27841 -> 0 bytes .../tutorial-extras/manage-docs-versions.md | 55 ------- .../tutorial-extras/translate-your-site.md | 88 ---------- 14 files changed, 651 deletions(-) delete mode 100644 docs/my-website/docs/getting_started.md delete mode 100644 docs/my-website/src/pages/intro.md delete mode 100644 docs/my-website/src/pages/tutorial-basics/_category_.json delete mode 100644 docs/my-website/src/pages/tutorial-basics/congratulations.md delete mode 100644 docs/my-website/src/pages/tutorial-basics/create-a-blog-post.md delete mode 100644 docs/my-website/src/pages/tutorial-basics/create-a-document.md delete mode 100644 docs/my-website/src/pages/tutorial-basics/create-a-page.md delete mode 100644 docs/my-website/src/pages/tutorial-basics/deploy-your-site.md delete mode 100644 docs/my-website/src/pages/tutorial-basics/markdown-features.mdx delete mode 100644 docs/my-website/src/pages/tutorial-extras/_category_.json delete mode 100644 docs/my-website/src/pages/tutorial-extras/img/docsVersionDropdown.png delete mode 100644 docs/my-website/src/pages/tutorial-extras/img/localeDropdown.png delete mode 100644 docs/my-website/src/pages/tutorial-extras/manage-docs-versions.md delete mode 100644 docs/my-website/src/pages/tutorial-extras/translate-your-site.md diff --git a/docs/my-website/docs/getting_started.md b/docs/my-website/docs/getting_started.md deleted file mode 100644 index 6b2c1fd531e..00000000000 --- a/docs/my-website/docs/getting_started.md +++ /dev/null @@ -1,108 +0,0 @@ -# Getting Started - -import QuickStart from '../src/components/QuickStart.js' - -LiteLLM simplifies LLM API calls by mapping them all to the [OpenAI ChatCompletion format](https://platform.openai.com/docs/api-reference/chat). - -## basic usage - -By default we provide a free $10 community-key to try all providers supported on LiteLLM. - -```python -from litellm import completion - -## set ENV variables -os.environ["OPENAI_API_KEY"] = "your-api-key" -os.environ["COHERE_API_KEY"] = "your-api-key" - -messages = [{ "content": "Hello, how are you?","role": "user"}] - -# openai call -response = completion(model="gpt-3.5-turbo", messages=messages) - -# cohere call -response = completion("command-nightly", messages) -``` - -**Need a dedicated key?** -Email us @ krrish@berri.ai - -Next Steps 👉 [Call all supported models - e.g. Claude-2, Llama2-70b, etc.](./proxy_api.md#supported-models) - -More details 👉 - -- [Completion() function details](./completion/) -- [Overview of supported models / providers on LiteLLM](./providers/) -- [Search all models / providers](https://models.litellm.ai/) -- [Build your own OpenAI proxy](https://github.com/BerriAI/liteLLM-proxy/tree/main) - -## streaming - -Same example from before. Just pass in `stream=True` in the completion args. - -```python -from litellm import completion - -## set ENV variables -os.environ["OPENAI_API_KEY"] = "openai key" -os.environ["COHERE_API_KEY"] = "cohere key" - -messages = [{ "content": "Hello, how are you?","role": "user"}] - -# openai call -response = completion(model="gpt-3.5-turbo", messages=messages, stream=True) - -# cohere call -response = completion("command-nightly", messages, stream=True) - -print(response) -``` - -More details 👉 - -- [streaming + async](./completion/stream.md) -- [tutorial for streaming Llama2 on TogetherAI](./tutorials/TogetherAI_liteLLM.md) - -## exception handling - -LiteLLM maps exceptions across all supported providers to the OpenAI exceptions. All our exceptions inherit from OpenAI's exception types, so any error-handling you have for that, should work out of the box with LiteLLM. - -```python -from openai.error import OpenAIError -from litellm import completion - -os.environ["ANTHROPIC_API_KEY"] = "bad-key" -try: - # some code - completion(model="claude-instant-1", messages=[{"role": "user", "content": "Hey, how's it going?"}]) -except OpenAIError as e: - print(e) -``` - -## Logging Observability - Log LLM Input/Output ([Docs](https://docs.litellm.ai/docs/observability/callbacks)) - -LiteLLM exposes pre defined callbacks to send data to MLflow, Lunary, Langfuse, Helicone, Promptlayer, Traceloop, Slack - -```python -from litellm import completion - -## set env variables for logging tools (API key set up is not required when using MLflow) -os.environ["LUNARY_PUBLIC_KEY"] = "your-lunary-public-key" # get your public key at https://app.lunary.ai/settings -os.environ["HELICONE_API_KEY"] = "your-helicone-key" -os.environ["LANGFUSE_PUBLIC_KEY"] = "" -os.environ["LANGFUSE_SECRET_KEY"] = "" - -os.environ["OPENAI_API_KEY"] - -# set callbacks -litellm.success_callback = ["lunary", "mlflow", "langfuse", "helicone"] # log input/output to MLflow, langfuse, lunary, helicone - -#openai call -response = completion(model="gpt-3.5-turbo", messages=[{"role": "user", "content": "Hi 👋 - i'm openai"}]) -``` - -More details 👉 - -- [exception mapping](./exception_mapping.md) -- [retries + model fallbacks for completion()](./completion/reliable_completions.md) -- [tutorial for model fallbacks with completion()](./tutorials/fallbacks.md) diff --git a/docs/my-website/src/pages/intro.md b/docs/my-website/src/pages/intro.md deleted file mode 100644 index 8a2e69d95f9..00000000000 --- a/docs/my-website/src/pages/intro.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -sidebar_position: 1 ---- - -# Tutorial Intro - -Let's discover **Docusaurus in less than 5 minutes**. - -## Getting Started - -Get started by **creating a new site**. - -Or **try Docusaurus immediately** with **[docusaurus.new](https://docusaurus.new)**. - -### What you'll need - -- [Node.js](https://nodejs.org/en/download/) version 16.14 or above: - - When installing Node.js, you are recommended to check all checkboxes related to dependencies. - -## Generate a new site - -Generate a new Docusaurus site using the **classic template**. - -The classic template will automatically be added to your project after you run the command: - -```bash -npm init docusaurus@latest my-website classic -``` - -You can type this command into Command Prompt, Powershell, Terminal, or any other integrated terminal of your code editor. - -The command also installs all necessary dependencies you need to run Docusaurus. - -## Start your site - -Run the development server: - -```bash -cd my-website -npm run start -``` - -The `cd` command changes the directory you're working with. In order to work with your newly created Docusaurus site, you'll need to navigate the terminal there. - -The `npm run start` command builds your website locally and serves it through a development server, ready for you to view at http://localhost:3000/. - -Open `docs/intro.md` (this page) and edit some lines: the site **reloads automatically** and displays your changes. diff --git a/docs/my-website/src/pages/tutorial-basics/_category_.json b/docs/my-website/src/pages/tutorial-basics/_category_.json deleted file mode 100644 index 2e6db55b1eb..00000000000 --- a/docs/my-website/src/pages/tutorial-basics/_category_.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "label": "Tutorial - Basics", - "position": 2, - "link": { - "type": "generated-index", - "description": "5 minutes to learn the most important Docusaurus concepts." - } -} diff --git a/docs/my-website/src/pages/tutorial-basics/congratulations.md b/docs/my-website/src/pages/tutorial-basics/congratulations.md deleted file mode 100644 index 04771a00b72..00000000000 --- a/docs/my-website/src/pages/tutorial-basics/congratulations.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -sidebar_position: 6 ---- - -# Congratulations! - -You have just learned the **basics of Docusaurus** and made some changes to the **initial template**. - -Docusaurus has **much more to offer**! - -Have **5 more minutes**? Take a look at **[versioning](../tutorial-extras/manage-docs-versions.md)** and **[i18n](../tutorial-extras/translate-your-site.md)**. - -Anything **unclear** or **buggy** in this tutorial? [Please report it!](https://github.com/facebook/docusaurus/discussions/4610) - -## What's next? - -- Read the [official documentation](https://docusaurus.io/) -- Modify your site configuration with [`docusaurus.config.js`](https://docusaurus.io/docs/api/docusaurus-config) -- Add navbar and footer items with [`themeConfig`](https://docusaurus.io/docs/api/themes/configuration) -- Add a custom [Design and Layout](https://docusaurus.io/docs/styling-layout) -- Add a [search bar](https://docusaurus.io/docs/search) -- Find inspirations in the [Docusaurus showcase](https://docusaurus.io/showcase) -- Get involved in the [Docusaurus Community](https://docusaurus.io/community/support) diff --git a/docs/my-website/src/pages/tutorial-basics/create-a-blog-post.md b/docs/my-website/src/pages/tutorial-basics/create-a-blog-post.md deleted file mode 100644 index ea472bbaf87..00000000000 --- a/docs/my-website/src/pages/tutorial-basics/create-a-blog-post.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -sidebar_position: 3 ---- - -# Create a Blog Post - -Docusaurus creates a **page for each blog post**, but also a **blog index page**, a **tag system**, an **RSS** feed... - -## Create your first Post - -Create a file at `blog/2021-02-28-greetings.md`: - -```md title="blog/2021-02-28-greetings.md" ---- -slug: greetings -title: Greetings! -authors: - - name: Joel Marcey - title: Co-creator of Docusaurus 1 - url: https://github.com/JoelMarcey - image_url: https://github.com/JoelMarcey.png - - name: Sébastien Lorber - title: Docusaurus maintainer - url: https://sebastienlorber.com - image_url: https://github.com/slorber.png -tags: [greetings] ---- - -Congratulations, you have made your first post! - -Feel free to play around and edit this post as much you like. -``` - -A new blog post is now available at [http://localhost:3000/blog/greetings](http://localhost:3000/blog/greetings). diff --git a/docs/my-website/src/pages/tutorial-basics/create-a-document.md b/docs/my-website/src/pages/tutorial-basics/create-a-document.md deleted file mode 100644 index ffddfa8eb8a..00000000000 --- a/docs/my-website/src/pages/tutorial-basics/create-a-document.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -sidebar_position: 2 ---- - -# Create a Document - -Documents are **groups of pages** connected through: - -- a **sidebar** -- **previous/next navigation** -- **versioning** - -## Create your first Doc - -Create a Markdown file at `docs/hello.md`: - -```md title="docs/hello.md" -# Hello - -This is my **first Docusaurus document**! -``` - -A new document is now available at [http://localhost:3000/docs/hello](http://localhost:3000/docs/hello). - -## Configure the Sidebar - -Docusaurus automatically **creates a sidebar** from the `docs` folder. - -Add metadata to customize the sidebar label and position: - -```md title="docs/hello.md" {1-4} ---- -sidebar_label: 'Hi!' -sidebar_position: 3 ---- - -# Hello - -This is my **first Docusaurus document**! -``` - -It is also possible to create your sidebar explicitly in `sidebars.js`: - -```js title="sidebars.js" -module.exports = { - tutorialSidebar: [ - 'intro', - // highlight-next-line - 'hello', - { - type: 'category', - label: 'Tutorial', - items: ['tutorial-basics/create-a-document'], - }, - ], -}; -``` diff --git a/docs/my-website/src/pages/tutorial-basics/create-a-page.md b/docs/my-website/src/pages/tutorial-basics/create-a-page.md deleted file mode 100644 index 20e2ac30055..00000000000 --- a/docs/my-website/src/pages/tutorial-basics/create-a-page.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -sidebar_position: 1 ---- - -# Create a Page - -Add **Markdown or React** files to `src/pages` to create a **standalone page**: - -- `src/pages/index.js` → `localhost:3000/` -- `src/pages/foo.md` → `localhost:3000/foo` -- `src/pages/foo/bar.js` → `localhost:3000/foo/bar` - -## Create your first React Page - -Create a file at `src/pages/my-react-page.js`: - -```jsx title="src/pages/my-react-page.js" -import React from 'react'; -import Layout from '@theme/Layout'; - -export default function MyReactPage() { - return ( - -

    My React page

    -

    This is a React page

    -
    - ); -} -``` - -A new page is now available at [http://localhost:3000/my-react-page](http://localhost:3000/my-react-page). - -## Create your first Markdown Page - -Create a file at `src/pages/my-markdown-page.md`: - -```mdx title="src/pages/my-markdown-page.md" -# My Markdown page - -This is a Markdown page -``` - -A new page is now available at [http://localhost:3000/my-markdown-page](http://localhost:3000/my-markdown-page). diff --git a/docs/my-website/src/pages/tutorial-basics/deploy-your-site.md b/docs/my-website/src/pages/tutorial-basics/deploy-your-site.md deleted file mode 100644 index 1c50ee063ef..00000000000 --- a/docs/my-website/src/pages/tutorial-basics/deploy-your-site.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -sidebar_position: 5 ---- - -# Deploy your site - -Docusaurus is a **static-site-generator** (also called **[Jamstack](https://jamstack.org/)**). - -It builds your site as simple **static HTML, JavaScript and CSS files**. - -## Build your site - -Build your site **for production**: - -```bash -npm run build -``` - -The static files are generated in the `build` folder. - -## Deploy your site - -Test your production build locally: - -```bash -npm run serve -``` - -The `build` folder is now served at [http://localhost:3000/](http://localhost:3000/). - -You can now deploy the `build` folder **almost anywhere** easily, **for free** or very small cost (read the **[Deployment Guide](https://docusaurus.io/docs/deployment)**). diff --git a/docs/my-website/src/pages/tutorial-basics/markdown-features.mdx b/docs/my-website/src/pages/tutorial-basics/markdown-features.mdx deleted file mode 100644 index 0337f34d6a5..00000000000 --- a/docs/my-website/src/pages/tutorial-basics/markdown-features.mdx +++ /dev/null @@ -1,150 +0,0 @@ ---- -sidebar_position: 4 ---- - -# Markdown Features - -Docusaurus supports **[Markdown](https://daringfireball.net/projects/markdown/syntax)** and a few **additional features**. - -## Front Matter - -Markdown documents have metadata at the top called [Front Matter](https://jekyllrb.com/docs/front-matter/): - -```text title="my-doc.md" -// highlight-start ---- -id: my-doc-id -title: My document title -description: My document description -slug: /my-custom-url ---- -// highlight-end - -## Markdown heading - -Markdown text with [links](./hello.md) -``` - -## Links - -Regular Markdown links are supported, using url paths or relative file paths. - -```md -Let's see how to [Create a page](/create-a-page). -``` - -```md -Let's see how to [Create a page](./create-a-page.md). -``` - -**Result:** Let's see how to [Create a page](./create-a-page.md). - -## Images - -Regular Markdown images are supported. - -You can use absolute paths to reference images in the static directory (`static/img/docusaurus.png`): - -```md -![Docusaurus logo](/img/docusaurus.png) -``` - -![Docusaurus logo](/img/docusaurus.png) - -You can reference images relative to the current file as well. This is particularly useful to colocate images close to the Markdown files using them: - -```md -![Docusaurus logo](./img/docusaurus.png) -``` - -## Code Blocks - -Markdown code blocks are supported with Syntax highlighting. - - ```jsx title="src/components/HelloDocusaurus.js" - function HelloDocusaurus() { - return ( -

    Hello, Docusaurus!

    - ) - } - ``` - -```jsx title="src/components/HelloDocusaurus.js" -function HelloDocusaurus() { - return

    Hello, Docusaurus!

    ; -} -``` - -## Admonitions - -Docusaurus has a special syntax to create admonitions and callouts: - - :::tip My tip - - Use this awesome feature option - - ::: - - :::danger Take care - - This action is dangerous - - ::: - -:::tip My tip - -Use this awesome feature option - -::: - -:::danger Take care - -This action is dangerous - -::: - -## MDX and React Components - -[MDX](https://mdxjs.com/) can make your documentation more **interactive** and allows using any **React components inside Markdown**: - -```jsx -export const Highlight = ({children, color}) => ( - { - alert(`You clicked the color ${color} with label ${children}`) - }}> - {children} - -); - -This is Docusaurus green ! - -This is Facebook blue ! -``` - -export const Highlight = ({children, color}) => ( - { - alert(`You clicked the color ${color} with label ${children}`); - }}> - {children} - -); - -This is Docusaurus green ! - -This is Facebook blue ! diff --git a/docs/my-website/src/pages/tutorial-extras/_category_.json b/docs/my-website/src/pages/tutorial-extras/_category_.json deleted file mode 100644 index a8ffcc19300..00000000000 --- a/docs/my-website/src/pages/tutorial-extras/_category_.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "label": "Tutorial - Extras", - "position": 3, - "link": { - "type": "generated-index" - } -} diff --git a/docs/my-website/src/pages/tutorial-extras/img/docsVersionDropdown.png b/docs/my-website/src/pages/tutorial-extras/img/docsVersionDropdown.png deleted file mode 100644 index 97e4164618b5f8beda34cfa699720aba0ad2e342..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25427 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 diff --git a/docs/my-website/src/pages/tutorial-extras/img/localeDropdown.png b/docs/my-website/src/pages/tutorial-extras/img/localeDropdown.png deleted file mode 100644 index e257edc1f932985396bf59584c7ccfaddf955779..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 27841 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 JrC|B>e*nH7H{}2T diff --git a/docs/my-website/src/pages/tutorial-extras/manage-docs-versions.md b/docs/my-website/src/pages/tutorial-extras/manage-docs-versions.md deleted file mode 100644 index e12c3f3444f..00000000000 --- a/docs/my-website/src/pages/tutorial-extras/manage-docs-versions.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -sidebar_position: 1 ---- - -# Manage Docs Versions - -Docusaurus can manage multiple versions of your docs. - -## Create a docs version - -Release a version 1.0 of your project: - -```bash -npm run docusaurus docs:version 1.0 -``` - -The `docs` folder is copied into `versioned_docs/version-1.0` and `versions.json` is created. - -Your docs now have 2 versions: - -- `1.0` at `http://localhost:3000/docs/` for the version 1.0 docs -- `current` at `http://localhost:3000/docs/next/` for the **upcoming, unreleased docs** - -## Add a Version Dropdown - -To navigate seamlessly across versions, add a version dropdown. - -Modify the `docusaurus.config.js` file: - -```js title="docusaurus.config.js" -module.exports = { - themeConfig: { - navbar: { - items: [ - // highlight-start - { - type: 'docsVersionDropdown', - }, - // highlight-end - ], - }, - }, -}; -``` - -The docs version dropdown appears in your navbar: - -![Docs Version Dropdown](./img/docsVersionDropdown.png) - -## Update an existing version - -It is possible to edit versioned docs in their respective folder: - -- `versioned_docs/version-1.0/hello.md` updates `http://localhost:3000/docs/hello` -- `docs/hello.md` updates `http://localhost:3000/docs/next/hello` diff --git a/docs/my-website/src/pages/tutorial-extras/translate-your-site.md b/docs/my-website/src/pages/tutorial-extras/translate-your-site.md deleted file mode 100644 index caeaffb0554..00000000000 --- a/docs/my-website/src/pages/tutorial-extras/translate-your-site.md +++ /dev/null @@ -1,88 +0,0 @@ ---- -sidebar_position: 2 ---- - -# Translate your site - -Let's translate `docs/intro.md` to French. - -## Configure i18n - -Modify `docusaurus.config.js` to add support for the `fr` locale: - -```js title="docusaurus.config.js" -module.exports = { - i18n: { - defaultLocale: 'en', - locales: ['en', 'fr'], - }, -}; -``` - -## Translate a doc - -Copy the `docs/intro.md` file to the `i18n/fr` folder: - -```bash -mkdir -p i18n/fr/docusaurus-plugin-content-docs/current/ - -cp docs/intro.md i18n/fr/docusaurus-plugin-content-docs/current/intro.md -``` - -Translate `i18n/fr/docusaurus-plugin-content-docs/current/intro.md` in French. - -## Start your localized site - -Start your site on the French locale: - -```bash -npm run start -- --locale fr -``` - -Your localized site is accessible at [http://localhost:3000/fr/](http://localhost:3000/fr/) and the `Getting Started` page is translated. - -:::caution - -In development, you can only use one locale at a same time. - -::: - -## Add a Locale Dropdown - -To navigate seamlessly across languages, add a locale dropdown. - -Modify the `docusaurus.config.js` file: - -```js title="docusaurus.config.js" -module.exports = { - themeConfig: { - navbar: { - items: [ - // highlight-start - { - type: 'localeDropdown', - }, - // highlight-end - ], - }, - }, -}; -``` - -The locale dropdown now appears in your navbar: - -![Locale Dropdown](./img/localeDropdown.png) - -## Build your localized site - -Build your site for a specific locale: - -```bash -npm run build -- --locale fr -``` - -Or build your site to include all the locales at once: - -```bash -npm run build -``` From 86350fe6d70dc62ecb5331a7df5f96a35b853305 Mon Sep 17 00:00:00 2001 From: Cesar Garcia <128240629+Chesars@users.noreply.github.com> Date: Wed, 3 Dec 2025 03:27:04 -0300 Subject: [PATCH 044/107] docs: add Google ADK and Harbor to projects (#17352) Both frameworks integrate with LiteLLM: - Google ADK uses LiteLLM for model-agnostic agent building - Harbor uses LiteLLM for agent evaluation across providers --- docs/my-website/docs/projects/Google ADK.md | 21 ++++++++++++++++++ docs/my-website/docs/projects/Harbor.md | 24 +++++++++++++++++++++ docs/my-website/sidebars.js | 4 +++- 3 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 docs/my-website/docs/projects/Google ADK.md create mode 100644 docs/my-website/docs/projects/Harbor.md diff --git a/docs/my-website/docs/projects/Google ADK.md b/docs/my-website/docs/projects/Google ADK.md new file mode 100644 index 00000000000..25e910dcbad --- /dev/null +++ b/docs/my-website/docs/projects/Google ADK.md @@ -0,0 +1,21 @@ + +# Google ADK (Agent Development Kit) + +[Google ADK](https://github.com/google/adk-python) is an open-source, code-first Python framework for building, evaluating, and deploying sophisticated AI agents. While optimized for Gemini, ADK is model-agnostic and supports LiteLLM for using 100+ providers. + +```python +from google.adk.agents.llm_agent import Agent +from google.adk.models.lite_llm import LiteLlm + +root_agent = Agent( + model=LiteLlm(model="openai/gpt-4o"), # Or any LiteLLM-supported model + name="my_agent", + description="An agent using LiteLLM", + instruction="You are a helpful assistant.", + tools=[your_tools], +) +``` + +- [GitHub](https://github.com/google/adk-python) +- [Documentation](https://google.github.io/adk-docs) +- [LiteLLM Samples](https://github.com/google/adk-python/tree/main/contributing/samples/hello_world_litellm) diff --git a/docs/my-website/docs/projects/Harbor.md b/docs/my-website/docs/projects/Harbor.md new file mode 100644 index 00000000000..684dfa93720 --- /dev/null +++ b/docs/my-website/docs/projects/Harbor.md @@ -0,0 +1,24 @@ + +# Harbor + +[Harbor](https://github.com/laude-institute/harbor) is a framework from the creators of Terminal-Bench for evaluating and optimizing agents and language models. It uses LiteLLM to call 100+ LLM providers. + +```bash +# Install +pip install harbor + +# Run a benchmark with any LiteLLM-supported model +harbor run --dataset terminal-bench@2.0 \ + --agent claude-code \ + --model anthropic/claude-opus-4-1 \ + --n-concurrent 4 +``` + +Key features: +- Evaluate agents like Claude Code, OpenHands, Codex CLI +- Build and share benchmarks and environments +- Run experiments in parallel across cloud providers (Daytona, Modal) +- Generate rollouts for RL optimization + +- [GitHub](https://github.com/laude-institute/harbor) +- [Documentation](https://harborframework.com/docs) diff --git a/docs/my-website/sidebars.js b/docs/my-website/sidebars.js index 983816ed216..a9790547e84 100644 --- a/docs/my-website/sidebars.js +++ b/docs/my-website/sidebars.js @@ -820,10 +820,12 @@ const sidebars = { "Learn how to deploy + call models from different providers on LiteLLM", slug: "/project", }, - items: [ + items: [ "projects/smolagents", "projects/mini-swe-agent", "projects/openai-agents", + "projects/Google ADK", + "projects/Harbor", "projects/Docq.AI", "projects/PDL", "projects/OpenInterpreter", From c173a4a27594b0a435f58a0be7633514bbeee440 Mon Sep 17 00:00:00 2001 From: Fabian Reinold <32450519+freinold@users.noreply.github.com> Date: Wed, 3 Dec 2025 07:30:54 +0100 Subject: [PATCH 045/107] Helm Chart: add ingress-only labels (#17348) * feat(helm): add ingress-only labels * feat(helm): add ingress configuration tests * chore(helm): bump chart version --- deploy/charts/litellm-helm/Chart.yaml | 4 +- deploy/charts/litellm-helm/README.md | 125 +++++++++--------- .../litellm-helm/templates/ingress.yaml | 3 + .../litellm-helm/tests/ingress_tests.yaml | 45 +++++++ deploy/charts/litellm-helm/values.yaml | 45 ++++--- 5 files changed, 140 insertions(+), 82 deletions(-) create mode 100644 deploy/charts/litellm-helm/tests/ingress_tests.yaml diff --git a/deploy/charts/litellm-helm/Chart.yaml b/deploy/charts/litellm-helm/Chart.yaml index eedadebaa8e..7f14af7db5d 100644 --- a/deploy/charts/litellm-helm/Chart.yaml +++ b/deploy/charts/litellm-helm/Chart.yaml @@ -18,7 +18,7 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.4.8 +version: 0.4.9 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to @@ -33,5 +33,5 @@ dependencies: condition: db.deployStandalone - name: redis version: ">=18.0.0" - repository: oci://registry-1.docker.io/bitnamicharts + repository: oci://registry-1.docker.io/bitnamicharts condition: redis.enabled diff --git a/deploy/charts/litellm-helm/README.md b/deploy/charts/litellm-helm/README.md index 352c3e9ddff..6fdc423a177 100644 --- a/deploy/charts/litellm-helm/README.md +++ b/deploy/charts/litellm-helm/README.md @@ -10,46 +10,48 @@ - Helm 3.8.0+ If `db.deployStandalone` is used: + - PV provisioner support in the underlying infrastructure If `db.useStackgresOperator` is used (not yet implemented): -- The Stackgres Operator must already be installed in the Kubernetes Cluster. This chart will **not** install the operator if it is missing. + +- The Stackgres Operator must already be installed in the Kubernetes Cluster. This chart will **not** install the operator if it is missing. ## Parameters ### LiteLLM Proxy Deployment Settings -| Name | Description | Value | -| ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- | -| `replicaCount` | The number of LiteLLM Proxy pods to be deployed | `1` | -| `masterkeySecretName` | The name of the Kubernetes Secret that contains the Master API Key for LiteLLM. If not specified, use the generated secret name. | N/A | -| `masterkeySecretKey` | The key within the Kubernetes Secret that contains the Master API Key for LiteLLM. If not specified, use `masterkey` as the key. | N/A | -| `masterkey` | The Master API Key for LiteLLM. If not specified, a random key in the `sk-...` format is generated. | N/A | -| `environmentSecrets` | An optional array of Secret object names. The keys and values in these secrets will be presented to the LiteLLM proxy pod as environment variables. See below for an example Secret object. | `[]` | -| `environmentConfigMaps` | An optional array of ConfigMap object names. The keys and values in these configmaps will be presented to the LiteLLM proxy pod as environment variables. See below for an example Secret object. | `[]` | -| `image.repository` | LiteLLM Proxy image repository | `ghcr.io/berriai/litellm` | -| `image.pullPolicy` | LiteLLM Proxy image pull policy | `IfNotPresent` | -| `image.tag` | Overrides the image tag whose default the latest version of LiteLLM at the time this chart was published. | `""` | -| `imagePullSecrets` | Registry credentials for the LiteLLM and initContainer images. | `[]` | -| `serviceAccount.create` | Whether or not to create a Kubernetes Service Account for this deployment. The default is `false` because LiteLLM has no need to access the Kubernetes API. | `false` | -| `service.type` | Kubernetes Service type (e.g. `LoadBalancer`, `ClusterIP`, etc.) | `ClusterIP` | -| `service.port` | TCP port that the Kubernetes Service will listen on. Also the TCP port within the Pod that the proxy will listen on. | `4000` | -| `service.loadBalancerClass` | Optional LoadBalancer implementation class (only used when `service.type` is `LoadBalancer`) | `""` | -| `ingress.*` | See [values.yaml](./values.yaml) for example settings | N/A | -| `proxyConfigMap.create` | When `true`, render a ConfigMap from `.Values.proxy_config` and mount it. | `true` | -| `proxyConfigMap.name` | When `create=false`, name of the existing ConfigMap to mount. | `""` | -| `proxyConfigMap.key` | Key in the ConfigMap that contains the proxy config file. | `"config.yaml"` | -| `proxy_config.*` | See [values.yaml](./values.yaml) for default settings. Rendered into the ConfigMap’s `config.yaml` only when `proxyConfigMap.create=true`. See [example_config_yaml](../../../litellm/proxy/example_config_yaml/) for configuration examples. | `N/A` | -| `extraContainers[]` | An array of additional containers to be deployed as sidecars alongside the LiteLLM Proxy. -| `pdb.enabled` | Enable a PodDisruptionBudget for the LiteLLM proxy Deployment | `false` | -| `pdb.minAvailable` | Minimum number/percentage of pods that must be available during **voluntary** disruptions (choose **one** of minAvailable/maxUnavailable) | `null` | -| `pdb.maxUnavailable` | Maximum number/percentage of pods that can be unavailable during **voluntary** disruptions (choose **one** of minAvailable/maxUnavailable) | `null` | -| `pdb.annotations` | Extra metadata annotations to add to the PDB | `{}` | -| `pdb.labels` | Extra metadata labels to add to the PDB | `{}` | +| Name | Description | Value | +| --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | +| `replicaCount` | The number of LiteLLM Proxy pods to be deployed | `1` | +| `masterkeySecretName` | The name of the Kubernetes Secret that contains the Master API Key for LiteLLM. If not specified, use the generated secret name. | N/A | +| `masterkeySecretKey` | The key within the Kubernetes Secret that contains the Master API Key for LiteLLM. If not specified, use `masterkey` as the key. | N/A | +| `masterkey` | The Master API Key for LiteLLM. If not specified, a random key in the `sk-...` format is generated. | N/A | +| `environmentSecrets` | An optional array of Secret object names. The keys and values in these secrets will be presented to the LiteLLM proxy pod as environment variables. See below for an example Secret object. | `[]` | +| `environmentConfigMaps` | An optional array of ConfigMap object names. The keys and values in these configmaps will be presented to the LiteLLM proxy pod as environment variables. See below for an example Secret object. | `[]` | +| `image.repository` | LiteLLM Proxy image repository | `ghcr.io/berriai/litellm` | +| `image.pullPolicy` | LiteLLM Proxy image pull policy | `IfNotPresent` | +| `image.tag` | Overrides the image tag whose default the latest version of LiteLLM at the time this chart was published. | `""` | +| `imagePullSecrets` | Registry credentials for the LiteLLM and initContainer images. | `[]` | +| `serviceAccount.create` | Whether or not to create a Kubernetes Service Account for this deployment. The default is `false` because LiteLLM has no need to access the Kubernetes API. | `false` | +| `service.type` | Kubernetes Service type (e.g. `LoadBalancer`, `ClusterIP`, etc.) | `ClusterIP` | +| `service.port` | TCP port that the Kubernetes Service will listen on. Also the TCP port within the Pod that the proxy will listen on. | `4000` | +| `service.loadBalancerClass` | Optional LoadBalancer implementation class (only used when `service.type` is `LoadBalancer`) | `""` | +| `ingress.labels` | Additional labels for the Ingress resource | `{}` | +| `ingress.*` | See [values.yaml](./values.yaml) for example settings | N/A | +| `proxyConfigMap.create` | When `true`, render a ConfigMap from `.Values.proxy_config` and mount it. | `true` | +| `proxyConfigMap.name` | When `create=false`, name of the existing ConfigMap to mount. | `""` | +| `proxyConfigMap.key` | Key in the ConfigMap that contains the proxy config file. | `"config.yaml"` | +| `proxy_config.*` | See [values.yaml](./values.yaml) for default settings. Rendered into the ConfigMap’s `config.yaml` only when `proxyConfigMap.create=true`. See [example_config_yaml](../../../litellm/proxy/example_config_yaml/) for configuration examples. | `N/A` | +| `extraContainers[]` | An array of additional containers to be deployed as sidecars alongside the LiteLLM Proxy. | +| `pdb.enabled` | Enable a PodDisruptionBudget for the LiteLLM proxy Deployment | `false` | +| `pdb.minAvailable` | Minimum number/percentage of pods that must be available during **voluntary** disruptions (choose **one** of minAvailable/maxUnavailable) | `null` | +| `pdb.maxUnavailable` | Maximum number/percentage of pods that can be unavailable during **voluntary** disruptions (choose **one** of minAvailable/maxUnavailable) | `null` | +| `pdb.annotations` | Extra metadata annotations to add to the PDB | `{}` | +| `pdb.labels` | Extra metadata labels to add to the PDB | `{}` | #### Example `proxy_config` ConfigMap from values (default): - ``` proxyConfigMap: create: true @@ -67,7 +69,6 @@ proxy_config: #### Example using existing `proxyConfigMap` instead of creating it: - ``` proxyConfigMap: create: false @@ -77,8 +78,7 @@ proxyConfigMap: # proxy_config is ignored in this mode ``` -#### Example `environmentSecrets` Secret - +#### Example `environmentSecrets` Secret ``` apiVersion: v1 @@ -91,21 +91,23 @@ type: Opaque ``` ### Database Settings -| Name | Description | Value | -| ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- | -| `db.useExisting` | Use an existing Postgres database. A Kubernetes Secret object must exist that contains credentials for connecting to the database. An example secret object definition is provided below. | `false` | -| `db.endpoint` | If `db.useExisting` is `true`, this is the IP, Hostname or Service Name of the Postgres server to connect to. | `localhost` | -| `db.database` | If `db.useExisting` is `true`, the name of the existing database to connect to. | `litellm` | -| `db.url` | If `db.useExisting` is `true`, the connection url of the existing database to connect to can be overwritten with this value. | `postgresql://$(DATABASE_USERNAME):$(DATABASE_PASSWORD)@$(DATABASE_HOST)/$(DATABASE_NAME)` | -| `db.secret.name` | If `db.useExisting` is `true`, the name of the Kubernetes Secret that contains credentials. | `postgres` | -| `db.secret.usernameKey` | If `db.useExisting` is `true`, the name of the key within the Kubernetes Secret that holds the username for authenticating with the Postgres instance. | `username` | -| `db.secret.passwordKey` | If `db.useExisting` is `true`, the name of the key within the Kubernetes Secret that holds the password associates with the above user. | `password` | -| `db.useStackgresOperator` | Not yet implemented. | `false` | -| `db.deployStandalone` | Deploy a standalone, single instance deployment of Postgres, using the Bitnami postgresql chart. This is useful for getting started but doesn't provide HA or (by default) data backups. | `true` | -| `postgresql.*` | If `db.deployStandalone` is `true`, configuration passed to the Bitnami postgresql chart. See the [Bitnami Documentation](https://github.com/bitnami/charts/tree/main/bitnami/postgresql) for full configuration details. See [values.yaml](./values.yaml) for the default configuration. | See [values.yaml](./values.yaml) | -| `postgresql.auth.*` | If `db.deployStandalone` is `true`, care should be taken to ensure the default `password` and `postgres-password` values are **NOT** used. | `NoTaGrEaTpAsSwOrD` | + +| Name | Description | Value | +| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | +| `db.useExisting` | Use an existing Postgres database. A Kubernetes Secret object must exist that contains credentials for connecting to the database. An example secret object definition is provided below. | `false` | +| `db.endpoint` | If `db.useExisting` is `true`, this is the IP, Hostname or Service Name of the Postgres server to connect to. | `localhost` | +| `db.database` | If `db.useExisting` is `true`, the name of the existing database to connect to. | `litellm` | +| `db.url` | If `db.useExisting` is `true`, the connection url of the existing database to connect to can be overwritten with this value. | `postgresql://$(DATABASE_USERNAME):$(DATABASE_PASSWORD)@$(DATABASE_HOST)/$(DATABASE_NAME)` | +| `db.secret.name` | If `db.useExisting` is `true`, the name of the Kubernetes Secret that contains credentials. | `postgres` | +| `db.secret.usernameKey` | If `db.useExisting` is `true`, the name of the key within the Kubernetes Secret that holds the username for authenticating with the Postgres instance. | `username` | +| `db.secret.passwordKey` | If `db.useExisting` is `true`, the name of the key within the Kubernetes Secret that holds the password associates with the above user. | `password` | +| `db.useStackgresOperator` | Not yet implemented. | `false` | +| `db.deployStandalone` | Deploy a standalone, single instance deployment of Postgres, using the Bitnami postgresql chart. This is useful for getting started but doesn't provide HA or (by default) data backups. | `true` | +| `postgresql.*` | If `db.deployStandalone` is `true`, configuration passed to the Bitnami postgresql chart. See the [Bitnami Documentation](https://github.com/bitnami/charts/tree/main/bitnami/postgresql) for full configuration details. See [values.yaml](./values.yaml) for the default configuration. | See [values.yaml](./values.yaml) | +| `postgresql.auth.*` | If `db.deployStandalone` is `true`, care should be taken to ensure the default `password` and `postgres-password` values are **NOT** used. | `NoTaGrEaTpAsSwOrD` | #### Example Postgres `db.useExisting` Secret + ```yaml apiVersion: v1 kind: Secret @@ -143,7 +145,7 @@ metadata: name: litellm-env-secret type: Opaque data: - SOME_PASSWORD: cDZbUGVXeU5e0ZW # base64 encoded + SOME_PASSWORD: cDZbUGVXeU5e0ZW # base64 encoded ANOTHER_PASSWORD: AAZbUGVXeU5e0ZB # base64 encoded ``` @@ -153,23 +155,23 @@ Source: [GitHub Gist from troyharvey](https://gist.github.com/troyharvey/4506472 The migration job supports both ArgoCD and Helm hooks to ensure database migrations run at the appropriate time during deployments. -| Name | Description | Value | -| ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- | -| `migrationJob.enabled` | Enable or disable the schema migration Job | `true` | -| `migrationJob.backoffLimit` | Backoff limit for Job restarts | `4` | -| `migrationJob.ttlSecondsAfterFinished` | TTL for completed migration jobs | `120` | -| `migrationJob.annotations` | Additional annotations for the migration job pod | `{}` | -| `migrationJob.extraContainers` | Additional containers to run alongside the migration job | `[]` | -| `migrationJob.hooks.argocd.enabled` | Enable ArgoCD hooks for the migration job (uses PreSync hook with BeforeHookCreation delete policy) | `true` | -| `migrationJob.hooks.helm.enabled` | Enable Helm hooks for the migration job (uses pre-install,pre-upgrade hooks with before-hook-creation delete policy) | `false` | -| `migrationJob.hooks.helm.weight` | Helm hook execution order (lower weights executed first). Optional - defaults to "1" if not specified. | N/A | - +| Name | Description | Value | +| -------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | ------- | +| `migrationJob.enabled` | Enable or disable the schema migration Job | `true` | +| `migrationJob.backoffLimit` | Backoff limit for Job restarts | `4` | +| `migrationJob.ttlSecondsAfterFinished` | TTL for completed migration jobs | `120` | +| `migrationJob.annotations` | Additional annotations for the migration job pod | `{}` | +| `migrationJob.extraContainers` | Additional containers to run alongside the migration job | `[]` | +| `migrationJob.hooks.argocd.enabled` | Enable ArgoCD hooks for the migration job (uses PreSync hook with BeforeHookCreation delete policy) | `true` | +| `migrationJob.hooks.helm.enabled` | Enable Helm hooks for the migration job (uses pre-install,pre-upgrade hooks with before-hook-creation delete policy) | `false` | +| `migrationJob.hooks.helm.weight` | Helm hook execution order (lower weights executed first). Optional - defaults to "1" if not specified. | N/A | ## Accessing the Admin UI + When browsing to the URL published per the settings in `ingress.*`, you will -be prompted for **Admin Configuration**. The **Proxy Endpoint** is the internal +be prompted for **Admin Configuration**. The **Proxy Endpoint** is the internal (from the `litellm` pod's perspective) URL published by the `-litellm` -Kubernetes Service. If the deployment uses the default settings for this +Kubernetes Service. If the deployment uses the default settings for this service, the **Proxy Endpoint** should be set to `http://-litellm:4000`. The **Proxy Key** is the value specified for `masterkey` or, if a `masterkey` @@ -181,7 +183,8 @@ kubectl -n litellm get secret -litellm-masterkey -o jsonpath="{.data.ma ``` ## Admin UI Limitations -At the time of writing, the Admin UI is unable to add models. This is because + +At the time of writing, the Admin UI is unable to add models. This is because it would need to update the `config.yaml` file which is a exposed ConfigMap, and -therefore, read-only. This is a limitation of this helm chart, not the Admin UI +therefore, read-only. This is a limitation of this helm chart, not the Admin UI itself. diff --git a/deploy/charts/litellm-helm/templates/ingress.yaml b/deploy/charts/litellm-helm/templates/ingress.yaml index 09e8d715ab8..ea9ffcbb54c 100644 --- a/deploy/charts/litellm-helm/templates/ingress.yaml +++ b/deploy/charts/litellm-helm/templates/ingress.yaml @@ -18,6 +18,9 @@ metadata: name: {{ $fullName }} labels: {{- include "litellm.labels" . | nindent 4 }} + {{- with .Values.ingress.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} {{- with .Values.ingress.annotations }} annotations: {{- toYaml . | nindent 4 }} diff --git a/deploy/charts/litellm-helm/tests/ingress_tests.yaml b/deploy/charts/litellm-helm/tests/ingress_tests.yaml new file mode 100644 index 00000000000..aad6ecfcee8 --- /dev/null +++ b/deploy/charts/litellm-helm/tests/ingress_tests.yaml @@ -0,0 +1,45 @@ +suite: Ingress Configuration Tests +templates: + - ingress.yaml +tests: + - it: should not create Ingress by default + asserts: + - hasDocuments: + count: 0 + + - it: should create Ingress when enabled + set: + ingress.enabled: true + asserts: + - hasDocuments: + count: 1 + - isKind: + of: Ingress + + - it: should add custom labels + set: + ingress.enabled: true + ingress.labels: + custom-label: "true" + another-label: "value" + asserts: + - isKind: + of: Ingress + - equal: + path: metadata.labels.custom-label + value: "true" + - equal: + path: metadata.labels.another-label + value: "value" + + - it: should add annotations + set: + ingress.enabled: true + ingress.annotations: + kubernetes.io/ingress.class: "nginx" + asserts: + - isKind: + of: Ingress + - equal: + path: metadata.annotations["kubernetes.io/ingress.class"] + value: "nginx" diff --git a/deploy/charts/litellm-helm/values.yaml b/deploy/charts/litellm-helm/values.yaml index acb8c9ca32f..35021157826 100644 --- a/deploy/charts/litellm-helm/values.yaml +++ b/deploy/charts/litellm-helm/values.yaml @@ -35,7 +35,8 @@ podAnnotations: {} podLabels: {} terminationGracePeriodSeconds: 90 -topologySpreadConstraints: [] +topologySpreadConstraints: + [] # - maxSkew: 1 # topologyKey: kubernetes.io/hostname # whenUnsatisfiable: DoNotSchedule @@ -46,7 +47,8 @@ topologySpreadConstraints: [] # At the time of writing, the litellm docker image requires write access to the # filesystem on startup so that prisma can install some dependencies. podSecurityContext: {} -securityContext: {} +securityContext: + {} # capabilities: # drop: # - ALL @@ -57,13 +59,15 @@ securityContext: {} # A list of Kubernetes Secret objects that will be exported to the LiteLLM proxy # pod as environment variables. These secrets can then be referenced in the # configuration file (or "litellm" ConfigMap) with `os.environ/` -environmentSecrets: [] +environmentSecrets: + [] # - litellm-env-secret # A list of Kubernetes ConfigMap objects that will be exported to the LiteLLM proxy # pod as environment variables. The ConfigMap kv-pairs can then be referenced in the # configuration file (or "litellm" ConfigMap) with `os.environ/` -environmentConfigMaps: [] +environmentConfigMaps: + [] # - litellm-env-configmap service: @@ -82,7 +86,9 @@ separateHealthPort: 8081 ingress: enabled: false className: "nginx" - annotations: {} + labels: {} + annotations: + {} # kubernetes.io/ingress.class: nginx # kubernetes.io/tls-acme: "true" hosts: @@ -129,7 +135,8 @@ proxy_config: general_settings: master_key: os.environ/PROXY_MASTER_KEY -resources: {} +resources: + {} # We usually recommend not to specify default resources and to leave this as a conscious # choice for the user. This also increases chances charts run on environments with little # resources, such as Minikube. If you do want to specify resources, uncomment the following @@ -231,7 +238,7 @@ migrationJob: # cpu: 100m # memory: 100Mi extraContainers: [] - + # Hook configuration hooks: argocd: @@ -240,30 +247,30 @@ migrationJob: enabled: false # Additional environment variables to be added to the deployment as a map of key-value pairs -envVars: { - # USE_DDTRACE: "true" -} +envVars: {} +# USE_DDTRACE: "true" # Additional environment variables to be added to the deployment as a list of k8s env vars -extraEnvVars: { - # - name: EXTRA_ENV_VAR - # value: EXTRA_ENV_VAR_VALUE -} +extraEnvVars: {} +# - name: EXTRA_ENV_VAR +# value: EXTRA_ENV_VAR_VALUE # Pod Disruption Budget pdb: enabled: false # Set exactly one of the following. If both are set, minAvailable takes precedence. - minAvailable: null # e.g. "50%" or 1 - maxUnavailable: null # e.g. 1 or "20%" + minAvailable: null # e.g. "50%" or 1 + maxUnavailable: null # e.g. 1 or "20%" annotations: {} labels: {} serviceMonitor: enabled: false - labels: {} + labels: + {} # test: test - annotations: {} + annotations: + {} # kubernetes.io/test: test interval: 15s scrapeTimeout: 10s @@ -273,4 +280,4 @@ serviceMonitor: # action: replace namespaceSelector: matchNames: [] - # - test-namespace \ No newline at end of file + # - test-namespace From 1ac2655b17f006f738632c27194153b93e9faa0c Mon Sep 17 00:00:00 2001 From: rioiart Date: Wed, 3 Dec 2025 07:46:03 +0100 Subject: [PATCH 046/107] Fix/organization max budget not enforced (#17334) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test: add failing tests for organization budget enforcement bug Add comprehensive tests exposing that organization-level budgets are retrieved but never enforced during request authentication. Tests verify: 1. Basic org budget exceeded scenario (team under budget, org over) 2. Multiple teams collectively exceeding org budget 3. Organization budget fields exist but are never checked 4. Inconsistency between team budget enforcement (works) and org (doesn't) Tests intentionally fail to document the bug. Will be fixed in next commit. Related to organization_max_budget not being enforced in auth_checks.py * fix: enforce organization budget in auth checks Add organization budget enforcement to common_checks() in auth_checks.py. Previously, organization_max_budget was retrieved from DB but never checked, allowing teams to collectively exceed their organization's budget limit. Changes: - Add _organization_max_budget_check() function following team budget pattern - Call org budget check after team budget check in common_checks() - Add "organization_budget" to budget_alerts type literals - Update tests to verify org budget is enforced Budget hierarchy is now properly enforced: Organization Budget (hard ceiling) └─ Team Budget (sub-allocation) └─ Team Member Budget (per-user within team) └─ Key Budget (per-key) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * fix: add organization_id to budget alerts, fix enum comparison and linting of newly added code - Add organization_id field to CallInfo class for better alert context - Include organization_id in budget alerts (token, soft, team, org) - Fix event_group enum comparison (was comparing enum to string) - Add OrganizationBudgetAlert class for organization budget alerting - Add organization_budget to test parameterizations - Apply Black formatting to slack_alerting.py --------- Co-authored-by: Claude --- .../SlackAlerting/budget_alert_types.py | 10 + .../SlackAlerting/slack_alerting.py | 19 +- litellm/proxy/_types.py | 1 + litellm/proxy/auth/auth_checks.py | 70 ++++ litellm/proxy/utils.py | 1 + tests/logging_callback_tests/test_alerting.py | 2 + .../test_organization_budget_enforcement.py | 344 ++++++++++++++++++ 7 files changed, 441 insertions(+), 6 deletions(-) create mode 100644 tests/test_litellm/proxy/auth/test_organization_budget_enforcement.py diff --git a/litellm/integrations/SlackAlerting/budget_alert_types.py b/litellm/integrations/SlackAlerting/budget_alert_types.py index 1e9ad286e37..dadfef3fc40 100644 --- a/litellm/integrations/SlackAlerting/budget_alert_types.py +++ b/litellm/integrations/SlackAlerting/budget_alert_types.py @@ -50,6 +50,14 @@ class TeamBudgetAlert(BaseBudgetAlertType): return user_info.team_id or "default_id" +class OrganizationBudgetAlert(BaseBudgetAlertType): + def get_event_message(self) -> str: + return "Organization Budget: " + + def get_id(self, user_info: CallInfo) -> str: + return user_info.organization_id or "default_id" + + class TokenBudgetAlert(BaseBudgetAlertType): def get_event_message(self) -> str: return "Key Budget: " @@ -72,6 +80,7 @@ def get_budget_alert_type( "soft_budget", "user_budget", "team_budget", + "organization_budget", "proxy_budget", "projected_limit_exceeded", ], @@ -83,6 +92,7 @@ def get_budget_alert_type( "soft_budget": SoftBudgetAlert(), "user_budget": UserBudgetAlert(), "team_budget": TeamBudgetAlert(), + "organization_budget": OrganizationBudgetAlert(), "token_budget": TokenBudgetAlert(), "projected_limit_exceeded": ProjectedLimitExceededAlert(), } diff --git a/litellm/integrations/SlackAlerting/slack_alerting.py b/litellm/integrations/SlackAlerting/slack_alerting.py index 3efe5873786..0e691e2c43f 100644 --- a/litellm/integrations/SlackAlerting/slack_alerting.py +++ b/litellm/integrations/SlackAlerting/slack_alerting.py @@ -134,19 +134,25 @@ class SlackAlerting(CustomBatchLogger): if llm_router is not None: self.llm_router = llm_router - def _prepare_outage_value_for_cache(self, outage_value: Union[dict, ProviderRegionOutageModel, OutageModel]) -> dict: + def _prepare_outage_value_for_cache( + self, outage_value: Union[dict, ProviderRegionOutageModel, OutageModel] + ) -> dict: """ Helper method to prepare outage value for Redis caching. Converts set objects to lists for JSON serialization. """ # Convert to dict for processing cache_value = dict(outage_value) - - if "deployment_ids" in cache_value and isinstance(cache_value["deployment_ids"], set): + + if "deployment_ids" in cache_value and isinstance( + cache_value["deployment_ids"], set + ): cache_value["deployment_ids"] = list(cache_value["deployment_ids"]) return cache_value - def _restore_outage_value_from_cache(self, outage_value: Optional[dict]) -> Optional[dict]: + def _restore_outage_value_from_cache( + self, outage_value: Optional[dict] + ) -> Optional[dict]: """ Helper method to restore outage value after retrieving from cache. Converts list objects back to sets for proper handling. @@ -528,6 +534,7 @@ class SlackAlerting(CustomBatchLogger): "soft_budget", "user_budget", "team_budget", + "organization_budget", "proxy_budget", "projected_limit_exceeded", ], @@ -1338,7 +1345,7 @@ Model Info: subject=email_event["subject"], html=email_event["html"], ) - if webhook_event.event_group == "team": + if webhook_event.event_group == Litellm_EntityType.TEAM: from litellm.integrations.email_alerting import send_team_budget_alert await send_team_budget_alert(webhook_event=webhook_event) @@ -1399,7 +1406,7 @@ Model Info: current_time = datetime.now().strftime("%H:%M:%S") _proxy_base_url = os.getenv("PROXY_BASE_URL", None) # Use .name if it's an enum, otherwise use as is - alert_type_name = getattr(alert_type, 'name', alert_type) + alert_type_name = getattr(alert_type, "name", alert_type) alert_type_formatted = f"Alert type: `{alert_type_name}`" if alert_type == "daily_reports" or alert_type == "new_model_added": formatted_message = alert_type_formatted + message diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 7e7d4049818..53a8627bc8f 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -2444,6 +2444,7 @@ class CallInfo(LiteLLMPydanticObjectBase): user_id: Optional[str] = None team_id: Optional[str] = None team_alias: Optional[str] = None + organization_id: Optional[str] = None user_email: Optional[str] = None key_alias: Optional[str] = None projected_exceeded_date: Optional[str] = None diff --git a/litellm/proxy/auth/auth_checks.py b/litellm/proxy/auth/auth_checks.py index c9774b18b88..45b0752d4cd 100644 --- a/litellm/proxy/auth/auth_checks.py +++ b/litellm/proxy/auth/auth_checks.py @@ -143,6 +143,14 @@ async def common_checks( valid_token=valid_token, ) + # 3.1. If organization is in budget + await _organization_max_budget_check( + valid_token=valid_token, + prisma_client=prisma_client, + user_api_key_cache=user_api_key_cache, + proxy_logging_obj=proxy_logging_obj, + ) + await _tag_max_budget_check( request_body=request_body, prisma_client=prisma_client, @@ -1893,6 +1901,7 @@ async def _virtual_key_max_budget_check( max_budget=valid_token.max_budget, user_id=valid_token.user_id, team_id=valid_token.team_id, + organization_id=valid_token.org_id, user_email=user_email, key_alias=valid_token.key_alias, event_group=Litellm_EntityType.KEY, @@ -1939,6 +1948,7 @@ async def _virtual_key_soft_budget_check( user_id=valid_token.user_id, team_id=valid_token.team_id, team_alias=valid_token.team_alias, + organization_id=valid_token.org_id, user_email=None, key_alias=valid_token.key_alias, event_group=Litellm_EntityType.KEY, @@ -1977,6 +1987,7 @@ async def _team_max_budget_check( user_id=valid_token.user_id, team_id=valid_token.team_id, team_alias=valid_token.team_alias, + organization_id=valid_token.org_id, event_group=Litellm_EntityType.TEAM, ) asyncio.create_task( @@ -1993,6 +2004,65 @@ async def _team_max_budget_check( ) +async def _organization_max_budget_check( + valid_token: Optional[UserAPIKeyAuth], + prisma_client: Optional[PrismaClient], + user_api_key_cache: DualCache, + proxy_logging_obj: ProxyLogging, +): + """ + Check if the organization is over its max budget. + + Raises: + BudgetExceededError if the organization is over its max budget. + Triggers a budget alert if the organization is over its max budget. + """ + # Only check if token has organization info and organization_max_budget is set + if ( + valid_token is None + or valid_token.org_id is None + or valid_token.organization_max_budget is None + or valid_token.organization_max_budget <= 0 + ): + return + + # Get organization object to check current spend + if prisma_client is not None: + org_table = await get_org_object( + org_id=valid_token.org_id, + prisma_client=prisma_client, + user_api_key_cache=user_api_key_cache, + ) + + if ( + org_table is not None + and org_table.spend >= valid_token.organization_max_budget + ): + # Trigger budget alert + call_info = CallInfo( + token=valid_token.token, + spend=org_table.spend, + max_budget=valid_token.organization_max_budget, + user_id=valid_token.user_id, + team_id=valid_token.team_id, + team_alias=valid_token.team_alias, + organization_id=valid_token.org_id, + event_group=Litellm_EntityType.ORGANIZATION, + ) + asyncio.create_task( + proxy_logging_obj.budget_alerts( + type="organization_budget", + user_info=call_info, + ) + ) + + raise litellm.BudgetExceededError( + current_cost=org_table.spend, + max_budget=valid_token.organization_max_budget, + message=f"Budget has been exceeded! Organization={valid_token.org_id} Current cost: {org_table.spend}, Max budget: {valid_token.organization_max_budget}", + ) + + async def _tag_max_budget_check( request_body: dict, prisma_client: Optional[PrismaClient], diff --git a/litellm/proxy/utils.py b/litellm/proxy/utils.py index 9594a55962c..3b746b757ec 100644 --- a/litellm/proxy/utils.py +++ b/litellm/proxy/utils.py @@ -1072,6 +1072,7 @@ class ProxyLogging: "user_budget", "soft_budget", "team_budget", + "organization_budget", "proxy_budget", "projected_limit_exceeded", ], diff --git a/tests/logging_callback_tests/test_alerting.py b/tests/logging_callback_tests/test_alerting.py index b9ecfaeb3f0..ac7f5cd6aa1 100644 --- a/tests/logging_callback_tests/test_alerting.py +++ b/tests/logging_callback_tests/test_alerting.py @@ -477,6 +477,7 @@ async def test_send_daily_reports_all_zero_or_none(): "token_budget", "user_budget", "team_budget", + "organization_budget", "proxy_budget", "projected_limit_exceeded", ], @@ -514,6 +515,7 @@ async def test_send_token_budget_crossed_alerts(alerting_type): "token_budget", "user_budget", "team_budget", + "organization_budget", "proxy_budget", "projected_limit_exceeded", ], diff --git a/tests/test_litellm/proxy/auth/test_organization_budget_enforcement.py b/tests/test_litellm/proxy/auth/test_organization_budget_enforcement.py new file mode 100644 index 00000000000..9c2adca9cd3 --- /dev/null +++ b/tests/test_litellm/proxy/auth/test_organization_budget_enforcement.py @@ -0,0 +1,344 @@ +""" +Tests for organization budget enforcement. + +These tests verify that organization-level budgets are properly enforced during +request authentication. When an organization's spend exceeds its max_budget, +requests should fail with BudgetExceededError. + +This prevents teams within an organization from collectively exceeding the +organization's budget limit. +""" + +import asyncio +import os +import sys +from typing import Optional +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +sys.path.insert(0, os.path.abspath("../../../")) + +import litellm +from litellm.proxy._types import ( + LiteLLM_BudgetTable, + LiteLLM_OrganizationTable, + LiteLLM_TeamTable, + UserAPIKeyAuth, +) +from litellm.proxy.auth.auth_checks import common_checks +from litellm.proxy.utils import ProxyLogging + + +@pytest.mark.asyncio +async def test_organization_budget_exceeded_blocks_request(): + """ + Bug: Organization budget is retrieved but NEVER enforced. + + When organization spend >= organization_max_budget, requests should fail + with BudgetExceededError. Currently this passes because no check exists. + """ + org_id = "test-org-budget-exceeded" + + # Organization with max_budget of 100, but spend is 150 + org_object = LiteLLM_OrganizationTable( + organization_id=org_id, + budget_id="org-budget-1", + spend=150.0, # Over budget! + models=["gpt-4"], + created_by="test", + updated_by="test", + litellm_budget_table=LiteLLM_BudgetTable( + max_budget=100.0, # Budget is 100 + ), + ) + + # Team within the organization (team itself is under budget) + team_object = LiteLLM_TeamTable( + team_id="test-team-1", + organization_id=org_id, + max_budget=50.0, # Team budget is 50 + spend=10.0, # Team spend is only 10 - under budget + models=["gpt-4"], + ) + + # Valid token with organization info + valid_token = UserAPIKeyAuth( + token="sk-test-123", + team_id="test-team-1", + org_id=org_id, + organization_max_budget=100.0, # This is set but never checked! + ) + + mock_request = MagicMock() + mock_request.url.path = "/v1/chat/completions" + + mock_proxy_logging = MagicMock(spec=ProxyLogging) + mock_proxy_logging.budget_alerts = AsyncMock() + + with patch("litellm.proxy.proxy_server.prisma_client") as mock_prisma: + with patch("litellm.proxy.proxy_server.user_api_key_cache") as mock_cache: + with patch("litellm.proxy.auth.auth_checks.get_org_object", new_callable=AsyncMock) as mock_get_org: + mock_get_org.return_value = org_object + + # BUG: This should raise BudgetExceededError but currently passes + with pytest.raises(litellm.BudgetExceededError) as exc_info: + await common_checks( + request_body={"model": "gpt-4"}, + team_object=team_object, + user_object=None, + end_user_object=None, + global_proxy_spend=None, + general_settings={}, + route="/v1/chat/completions", + llm_router=None, + proxy_logging_obj=mock_proxy_logging, + valid_token=valid_token, + request=mock_request, + ) + + assert "Organization" in str(exc_info.value.message) + assert exc_info.value.current_cost == 150.0 + assert exc_info.value.max_budget == 100.0 + + +@pytest.mark.asyncio +async def test_multiple_teams_exceed_organization_budget(): + """ + Test that organization budget is enforced even when individual teams are under budget. + + Scenario: + - Organization max_budget = $5000, spend = $5000 (at limit) + - Team A spend = $1500 (under team budget of $2000) + - Request via Team A should FAIL because org is at budget limit + + Expected: Request fails with BudgetExceededError + """ + org_id = "multi-team-org" + + # Organization at budget limit + org_object = LiteLLM_OrganizationTable( + organization_id=org_id, + budget_id="org-budget-2", + spend=5000.0, # At $5000 limit + models=["gpt-4"], + created_by="test", + updated_by="test", + litellm_budget_table=LiteLLM_BudgetTable( + max_budget=5000.0, # Org budget is $5000 + ), + ) + + # Team A - under its own budget, but org is almost at limit + team_a = LiteLLM_TeamTable( + team_id="team-a", + organization_id=org_id, + max_budget=2000.0, + spend=1500.0, # Team A has spent $1500 of its $2000 budget + models=["gpt-4"], + ) + + valid_token = UserAPIKeyAuth( + token="sk-team-a-key", + team_id="team-a", + org_id=org_id, + organization_max_budget=5000.0, # Set but never enforced + ) + + mock_request = MagicMock() + mock_request.url.path = "/v1/chat/completions" + + mock_proxy_logging = MagicMock(spec=ProxyLogging) + mock_proxy_logging.budget_alerts = AsyncMock() + + with patch("litellm.proxy.proxy_server.prisma_client") as mock_prisma: + with patch("litellm.proxy.proxy_server.user_api_key_cache") as mock_cache: + with patch("litellm.proxy.auth.auth_checks.get_org_object", new_callable=AsyncMock) as mock_get_org: + mock_get_org.return_value = org_object + + # Org is at budget limit, should raise BudgetExceededError + with pytest.raises(litellm.BudgetExceededError) as exc_info: + await common_checks( + request_body={"model": "gpt-4"}, + team_object=team_a, + user_object=None, + end_user_object=None, + global_proxy_spend=None, + general_settings={}, + route="/v1/chat/completions", + llm_router=None, + proxy_logging_obj=mock_proxy_logging, + valid_token=valid_token, + request=mock_request, + ) + + # Verify the error message mentions organization + assert "Organization" in str(exc_info.value.message) + assert exc_info.value.current_cost == 5000.0 + assert exc_info.value.max_budget == 5000.0 + + +@pytest.mark.asyncio +async def test_organization_budget_fields_are_checked(): + """ + Verify that organization_max_budget is populated in UserAPIKeyAuth + and BudgetExceededError is raised when organization is over budget. + """ + # Token has org budget info + valid_token = UserAPIKeyAuth( + token="sk-test", + team_id="test-team", + org_id="test-org", + organization_max_budget=100.0, # Budget is $100 + ) + + # Verify the field exists and is set + assert valid_token.organization_max_budget == 100.0 + assert valid_token.org_id == "test-org" + + team_object = LiteLLM_TeamTable( + team_id="test-team", + organization_id="test-org", + max_budget=None, + spend=0.0, + models=["gpt-4"], + ) + + mock_request = MagicMock() + mock_request.url.path = "/v1/chat/completions" + + mock_proxy_logging = MagicMock(spec=ProxyLogging) + mock_proxy_logging.budget_alerts = AsyncMock() + + # Organization is over budget + org_over_budget = LiteLLM_OrganizationTable( + organization_id="test-org", + budget_id="budget-1", + spend=150.0, # Over $100 budget + models=["gpt-4"], + created_by="test", + updated_by="test", + litellm_budget_table=LiteLLM_BudgetTable(max_budget=100.0), + ) + + with patch("litellm.proxy.proxy_server.prisma_client") as mock_prisma: + with patch("litellm.proxy.proxy_server.user_api_key_cache") as mock_cache: + with patch("litellm.proxy.auth.auth_checks.get_org_object", new_callable=AsyncMock) as mock_get_org: + mock_get_org.return_value = org_over_budget + + # Organization is over budget, should raise BudgetExceededError + with pytest.raises(litellm.BudgetExceededError) as exc_info: + await common_checks( + request_body={"model": "gpt-4"}, + team_object=team_object, + user_object=None, + end_user_object=None, + global_proxy_spend=None, + general_settings={}, + route="/v1/chat/completions", + llm_router=None, + proxy_logging_obj=mock_proxy_logging, + valid_token=valid_token, + request=mock_request, + ) + + assert exc_info.value.current_cost == 150.0 + assert exc_info.value.max_budget == 100.0 + + +@pytest.mark.asyncio +async def test_both_team_and_org_budget_enforced(): + """ + Verify that both team budget and organization budget are enforced consistently. + + This test verifies: + 1. Team over budget raises BudgetExceededError + 2. Organization over budget also raises BudgetExceededError + """ + mock_request = MagicMock() + mock_request.url.path = "/v1/chat/completions" + + mock_proxy_logging = MagicMock(spec=ProxyLogging) + mock_proxy_logging.budget_alerts = AsyncMock() + + # Scenario A: Team over budget - should raise BudgetExceededError + team_over_budget = LiteLLM_TeamTable( + team_id="team-over", + max_budget=100.0, + spend=150.0, # Over budget + models=["gpt-4"], + ) + + valid_token_team = UserAPIKeyAuth( + token="sk-team-test", + team_id="team-over", + ) + + with patch("litellm.proxy.proxy_server.prisma_client") as mock_prisma: + with patch("litellm.proxy.proxy_server.user_api_key_cache") as mock_cache: + with pytest.raises(litellm.BudgetExceededError) as exc_info: + await common_checks( + request_body={"model": "gpt-4"}, + team_object=team_over_budget, + user_object=None, + end_user_object=None, + global_proxy_spend=None, + general_settings={}, + route="/v1/chat/completions", + llm_router=None, + proxy_logging_obj=mock_proxy_logging, + valid_token=valid_token_team, + request=mock_request, + ) + assert "Team" in str(exc_info.value.message) + + # Scenario B: Org over budget - should also raise BudgetExceededError + org_over_budget = LiteLLM_OrganizationTable( + organization_id="org-over", + budget_id="budget-1", + spend=150.0, # Over $100 budget + models=["gpt-4"], + created_by="test", + updated_by="test", + litellm_budget_table=LiteLLM_BudgetTable(max_budget=100.0), + ) + + team_under_budget = LiteLLM_TeamTable( + team_id="team-under", + organization_id="org-over", + max_budget=50.0, + spend=10.0, # Team is fine + models=["gpt-4"], + ) + + valid_token_org = UserAPIKeyAuth( + token="sk-org-test", + team_id="team-under", + org_id="org-over", + organization_max_budget=100.0, + ) + + with patch("litellm.proxy.proxy_server.prisma_client") as mock_prisma: + with patch("litellm.proxy.proxy_server.user_api_key_cache") as mock_cache: + with patch("litellm.proxy.auth.auth_checks.get_org_object", new_callable=AsyncMock) as mock_get_org: + mock_get_org.return_value = org_over_budget + + # Organization is over budget, should raise BudgetExceededError + with pytest.raises(litellm.BudgetExceededError) as exc_info: + await common_checks( + request_body={"model": "gpt-4"}, + team_object=team_under_budget, + user_object=None, + end_user_object=None, + global_proxy_spend=None, + general_settings={}, + route="/v1/chat/completions", + llm_router=None, + proxy_logging_obj=mock_proxy_logging, + valid_token=valid_token_org, + request=mock_request, + ) + + assert "Organization" in str(exc_info.value.message) + assert exc_info.value.current_cost == 150.0 + assert exc_info.value.max_budget == 100.0 From 54e29e7828f698690963eafac8148aeaf466262f Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Wed, 3 Dec 2025 12:19:21 +0530 Subject: [PATCH 047/107] Enforce support of enforce_user_param to openai post endpoints --- litellm/proxy/auth/auth_checks.py | 13 +- .../proxy/test_enforce_user_param.py | 438 ++++++++++++++++++ 2 files changed, 450 insertions(+), 1 deletion(-) create mode 100644 tests/test_litellm/proxy/test_enforce_user_param.py diff --git a/litellm/proxy/auth/auth_checks.py b/litellm/proxy/auth/auth_checks.py index c9774b18b88..92cffe0ea82 100644 --- a/litellm/proxy/auth/auth_checks.py +++ b/litellm/proxy/auth/auth_checks.py @@ -182,7 +182,18 @@ async def common_checks( general_settings.get("enforce_user_param", None) is not None and general_settings["enforce_user_param"] is True ): - if RouteChecks.is_llm_api_route(route=route) and "user" not in request_body: + # Get HTTP method from request + http_method = request.method if hasattr(request, 'method') else None + + # Check if it's a POST request and if it's an OpenAI route but not MCP + is_post_method = http_method and http_method.upper() == "POST" + is_openai_route = RouteChecks.is_llm_api_route(route=route) + is_mcp_route = route in LiteLLMRoutes.mcp_routes.value or RouteChecks.check_route_access( + route=route, allowed_routes=LiteLLMRoutes.mcp_routes.value + ) + + # Enforce user param only for POST requests on OpenAI routes (excluding MCP routes) + if is_post_method and is_openai_route and not is_mcp_route and "user" not in request_body: raise Exception( f"'user' param not passed in. 'enforce_user_param'={general_settings['enforce_user_param']}" ) diff --git a/tests/test_litellm/proxy/test_enforce_user_param.py b/tests/test_litellm/proxy/test_enforce_user_param.py new file mode 100644 index 00000000000..5d9369d61bb --- /dev/null +++ b/tests/test_litellm/proxy/test_enforce_user_param.py @@ -0,0 +1,438 @@ +""" +Tests for enforce_user_param feature with POST/GET method filtering and MCP route exclusion. + +Tests verify that: +1. enforce_user_param only applies to POST requests +2. GET requests like /v1/models are not affected +3. MCP routes are excluded from enforcement +4. POST requests to completion endpoints still require user param when enforced +""" + +import asyncio +from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch + +import pytest +from fastapi import Request + +from litellm.proxy._types import LiteLLMRoutes, UserAPIKeyAuth +from litellm.proxy.auth.auth_checks import common_checks +from litellm.proxy.auth.route_checks import RouteChecks + + +class MockRequest: + """Mock FastAPI Request object""" + def __init__(self, method: str = "POST"): + self.method = method + + +def get_mock_user_token(): + """Create a mock UserAPIKeyAuth token for testing""" + return UserAPIKeyAuth( + api_key="test-key", + user_id="test-user", + team_id="test-team", + org_id="test-org", + models=["*"], + metadata={} + ) + + +class TestEnforceUserParamPostGetFiltering: + """Test POST/GET method filtering for enforce_user_param""" + + @pytest.mark.asyncio + async def test_post_completion_without_user_param_should_fail(self): + """POST to /v1/chat/completions without user param should raise error""" + request = MockRequest(method="POST") + general_settings = {"enforce_user_param": True} + request_body = { + "model": "gpt-3.5-turbo", + "messages": [{"role": "user", "content": "Hello"}] + } + + with patch('litellm.proxy.auth.auth_checks._is_api_route_allowed', new_callable=AsyncMock, return_value=True): + with pytest.raises(Exception) as exc_info: + await common_checks( + request_body=request_body, + team_object=None, + user_object=None, + end_user_object=None, + global_proxy_spend=None, + general_settings=general_settings, + route="/v1/chat/completions", + llm_router=None, + proxy_logging_obj=MagicMock(), + valid_token=get_mock_user_token(), + request=request, + ) + + assert "user" in str(exc_info.value).lower() + + @pytest.mark.asyncio + async def test_post_completion_with_user_param_should_pass(self): + """POST to /v1/chat/completions with user param should pass""" + request = MockRequest(method="POST") + general_settings = {"enforce_user_param": True} + request_body = { + "model": "gpt-3.5-turbo", + "messages": [{"role": "user", "content": "Hello"}], + "user": "user123" + } + + with patch('litellm.proxy.auth.auth_checks._is_api_route_allowed', new_callable=AsyncMock, return_value=True): + # Should not raise exception + result = await common_checks( + request_body=request_body, + team_object=None, + user_object=None, + end_user_object=None, + global_proxy_spend=None, + general_settings=general_settings, + route="/v1/chat/completions", + llm_router=None, + proxy_logging_obj=MagicMock(), + valid_token=get_mock_user_token(), + request=request, + ) + + assert result is True + + @pytest.mark.asyncio + async def test_get_models_without_user_param_should_pass(self): + """GET to /v1/models without user param should NOT raise error""" + request = MockRequest(method="GET") + general_settings = {"enforce_user_param": True} + request_body = {} # GET requests typically don't have body + + with patch('litellm.proxy.auth.auth_checks._is_api_route_allowed', new_callable=AsyncMock, return_value=True): + # Should not raise exception + result = await common_checks( + request_body=request_body, + team_object=None, + user_object=None, + end_user_object=None, + global_proxy_spend=None, + general_settings=general_settings, + route="/v1/models", + llm_router=None, + proxy_logging_obj=MagicMock(), + valid_token=get_mock_user_token(), + request=request, + ) + + assert result is True + + @pytest.mark.asyncio + async def test_get_files_without_user_param_should_pass(self): + """GET to /v1/files without user param should NOT raise error""" + request = MockRequest(method="GET") + general_settings = {"enforce_user_param": True} + request_body = {} + + with patch('litellm.proxy.auth.auth_checks._is_api_route_allowed', new_callable=AsyncMock, return_value=True): + result = await common_checks( + request_body=request_body, + team_object=None, + user_object=None, + end_user_object=None, + global_proxy_spend=None, + general_settings=general_settings, + route="/v1/files", + llm_router=None, + proxy_logging_obj=MagicMock(), + valid_token=get_mock_user_token(), + request=request, + ) + + assert result is True + + @pytest.mark.asyncio + async def test_post_embeddings_without_user_param_should_fail(self): + """POST to /v1/embeddings without user param should raise error""" + request = MockRequest(method="POST") + general_settings = {"enforce_user_param": True} + request_body = { + "model": "text-embedding-ada-002", + "input": "test" + } + + with patch('litellm.proxy.auth.auth_checks._is_api_route_allowed', new_callable=AsyncMock, return_value=True): + with pytest.raises(Exception) as exc_info: + await common_checks( + request_body=request_body, + team_object=None, + user_object=None, + end_user_object=None, + global_proxy_spend=None, + general_settings=general_settings, + route="/v1/embeddings", + llm_router=None, + proxy_logging_obj=MagicMock(), + valid_token=get_mock_user_token(), + request=request, + ) + + assert "user" in str(exc_info.value).lower() + + @pytest.mark.asyncio + async def test_post_embeddings_with_user_param_should_pass(self): + """POST to /v1/embeddings with user param should pass""" + request = MockRequest(method="POST") + general_settings = {"enforce_user_param": True} + request_body = { + "model": "text-embedding-ada-002", + "input": "test", + "user": "user123" + } + + with patch('litellm.proxy.auth.auth_checks._is_api_route_allowed', new_callable=AsyncMock, return_value=True): + result = await common_checks( + request_body=request_body, + team_object=None, + user_object=None, + end_user_object=None, + global_proxy_spend=None, + general_settings=general_settings, + route="/v1/embeddings", + llm_router=None, + proxy_logging_obj=MagicMock(), + valid_token=get_mock_user_token(), + request=request, + ) + + assert result is True + + +class TestEnforceUserParamMCPExclusion: + """Test MCP route exclusion from enforce_user_param""" + + @pytest.mark.asyncio + async def test_mcp_route_without_user_param_should_pass(self): + """POST to MCP route without user param should NOT raise error""" + request = MockRequest(method="POST") + general_settings = {"enforce_user_param": True} + request_body = {"action": "list_tools"} + + with patch('litellm.proxy.auth.auth_checks._is_api_route_allowed', new_callable=AsyncMock, return_value=True): + # Should not raise exception for MCP routes + result = await common_checks( + request_body=request_body, + team_object=None, + user_object=None, + end_user_object=None, + global_proxy_spend=None, + general_settings=general_settings, + route="/mcp/tools/list", + llm_router=None, + proxy_logging_obj=MagicMock(), + valid_token=get_mock_user_token(), + request=request, + ) + + assert result is True + + @pytest.mark.asyncio + async def test_mcp_root_route_without_user_param_should_pass(self): + """POST to /mcp without user param should NOT raise error""" + request = MockRequest(method="POST") + general_settings = {"enforce_user_param": True} + request_body = {"data": "test"} + + with patch('litellm.proxy.auth.auth_checks._is_api_route_allowed', new_callable=AsyncMock, return_value=True): + result = await common_checks( + request_body=request_body, + team_object=None, + user_object=None, + end_user_object=None, + global_proxy_spend=None, + general_settings=general_settings, + route="/mcp", + llm_router=None, + proxy_logging_obj=MagicMock(), + valid_token=get_mock_user_token(), + request=request, + ) + + assert result is True + + +class TestEnforceUserParamDisabled: + """Test behavior when enforce_user_param is disabled""" + + @pytest.mark.asyncio + async def test_post_without_user_param_when_disabled_should_pass(self): + """POST without user param when enforce_user_param=False should pass""" + request = MockRequest(method="POST") + general_settings = {"enforce_user_param": False} + request_body = { + "model": "gpt-3.5-turbo", + "messages": [{"role": "user", "content": "Hello"}] + } + + with patch('litellm.proxy.auth.auth_checks._is_api_route_allowed', new_callable=AsyncMock, return_value=True): + result = await common_checks( + request_body=request_body, + team_object=None, + user_object=None, + end_user_object=None, + global_proxy_spend=None, + general_settings=general_settings, + route="/v1/chat/completions", + llm_router=None, + proxy_logging_obj=MagicMock(), + valid_token=get_mock_user_token(), + request=request, + ) + + assert result is True + + @pytest.mark.asyncio + async def test_post_without_user_param_when_not_set_should_pass(self): + """POST without user param when enforce_user_param not set should pass""" + request = MockRequest(method="POST") + general_settings = {} # enforce_user_param not set + request_body = { + "model": "gpt-3.5-turbo", + "messages": [{"role": "user", "content": "Hello"}] + } + + with patch('litellm.proxy.auth.auth_checks._is_api_route_allowed', new_callable=AsyncMock, return_value=True): + result = await common_checks( + request_body=request_body, + team_object=None, + user_object=None, + end_user_object=None, + global_proxy_spend=None, + general_settings=general_settings, + route="/v1/chat/completions", + llm_router=None, + proxy_logging_obj=MagicMock(), + valid_token=get_mock_user_token(), + request=request, + ) + + assert result is True + + +class TestEnforceUserParamEdgeCases: + """Test edge cases for enforce_user_param""" + + @pytest.mark.asyncio + async def test_request_without_method_attribute_should_pass(self): + """Request without method attribute should not raise error""" + request = MagicMock() + del request.method # Remove method attribute + request.__hasattr__ = MagicMock(return_value=False) + + general_settings = {"enforce_user_param": True} + request_body = { + "model": "gpt-3.5-turbo", + "messages": [{"role": "user", "content": "Hello"}] + } + + with patch('litellm.proxy.auth.auth_checks._is_api_route_allowed', new_callable=AsyncMock, return_value=True): + # Should not raise error even without method + result = await common_checks( + request_body=request_body, + team_object=None, + user_object=None, + end_user_object=None, + global_proxy_spend=None, + general_settings=general_settings, + route="/v1/chat/completions", + llm_router=None, + proxy_logging_obj=MagicMock(), + valid_token=get_mock_user_token(), + request=request, + ) + + assert result is True + + @pytest.mark.asyncio + async def test_case_insensitive_http_method(self): + """HTTP method comparison should be case-insensitive""" + request = MockRequest(method="post") # lowercase + general_settings = {"enforce_user_param": True} + request_body = { + "model": "gpt-3.5-turbo", + "messages": [{"role": "user", "content": "Hello"}] + } + + with patch('litellm.proxy.auth.auth_checks._is_api_route_allowed', new_callable=AsyncMock, return_value=True): + with pytest.raises(Exception) as exc_info: + await common_checks( + request_body=request_body, + team_object=None, + user_object=None, + end_user_object=None, + global_proxy_spend=None, + general_settings=general_settings, + route="/v1/chat/completions", + llm_router=None, + proxy_logging_obj=MagicMock(), + valid_token=get_mock_user_token(), + request=request, + ) + + assert "user" in str(exc_info.value).lower() + + @pytest.mark.asyncio + async def test_put_method_should_not_enforce_user_param(self): + """PUT method should not enforce user param (only POST)""" + request = MockRequest(method="PUT") + general_settings = {"enforce_user_param": True} + request_body = { + "model": "gpt-3.5-turbo", + "messages": [{"role": "user", "content": "Hello"}] + } + + with patch('litellm.proxy.auth.auth_checks._is_api_route_allowed', new_callable=AsyncMock, return_value=True): + # Should not raise for PUT method + result = await common_checks( + request_body=request_body, + team_object=None, + user_object=None, + end_user_object=None, + global_proxy_spend=None, + general_settings=general_settings, + route="/v1/chat/completions", + llm_router=None, + proxy_logging_obj=MagicMock(), + valid_token=get_mock_user_token(), + request=request, + ) + + assert result is True + + @pytest.mark.asyncio + async def test_patch_method_should_not_enforce_user_param(self): + """PATCH method should not enforce user param (only POST)""" + request = MockRequest(method="PATCH") + general_settings = {"enforce_user_param": True} + request_body = { + "model": "gpt-3.5-turbo", + "messages": [{"role": "user", "content": "Hello"}] + } + + with patch('litellm.proxy.auth.auth_checks._is_api_route_allowed', new_callable=AsyncMock, return_value=True): + # Should not raise for PATCH method + result = await common_checks( + request_body=request_body, + team_object=None, + user_object=None, + end_user_object=None, + global_proxy_spend=None, + general_settings=general_settings, + route="/v1/chat/completions", + llm_router=None, + proxy_logging_obj=MagicMock(), + valid_token=get_mock_user_token(), + request=request, + ) + + assert result is True + + +if __name__ == "__main__": + # Run tests with: pytest tests/test_litellm/proxy/test_enforce_user_param.py -v + pytest.main([__file__, "-v"]) From 74ba18df55906165e5796a5408cbfd1fc8047604 Mon Sep 17 00:00:00 2001 From: Krish Dholakia Date: Tue, 2 Dec 2025 22:50:13 -0800 Subject: [PATCH 048/107] Litellm chainguard fixes 12 02 2025 p1 (#17406) * build: update dockerfile non root * build: update build * build: update non root * build: dockerfile fixes * build: ensure dockerfile + dockerfile.database also work --- Dockerfile | 15 +++++---------- docker/Dockerfile.database | 16 +++++++++------- docker/Dockerfile.non_root | 8 +++++--- 3 files changed, 19 insertions(+), 20 deletions(-) diff --git a/Dockerfile b/Dockerfile index f75706805e0..d8397ec4811 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,8 @@ # Base image for building -ARG LITELLM_BUILD_IMAGE=cgr.dev/chainguard/python:latest-dev +ARG LITELLM_BUILD_IMAGE=cgr.dev/chainguard/wolfi-base # Runtime image -ARG LITELLM_RUNTIME_IMAGE=cgr.dev/chainguard/python:latest-dev +ARG LITELLM_RUNTIME_IMAGE=cgr.dev/chainguard/wolfi-base # Builder stage FROM $LITELLM_BUILD_IMAGE AS builder @@ -12,11 +12,9 @@ WORKDIR /app USER root # Install build dependencies -RUN apk add --no-cache gcc python3-dev openssl openssl-dev +RUN apk add --no-cache bash gcc py3-pip python3 python3-dev openssl openssl-dev - -RUN pip install --upgrade pip>=24.3.1 && \ - pip install build +RUN python -m pip install build # Copy the current directory contents into the container at /app COPY . . @@ -48,10 +46,7 @@ FROM $LITELLM_RUNTIME_IMAGE AS runtime USER root # Install runtime dependencies -RUN apk add --no-cache openssl tzdata nodejs npm - -# Upgrade pip to fix CVE-2025-8869 -RUN pip install --upgrade pip>=24.3.1 +RUN apk add --no-cache bash openssl tzdata nodejs npm python3 py3-pip WORKDIR /app # Copy the current directory contents into the container at /app diff --git a/docker/Dockerfile.database b/docker/Dockerfile.database index 09b5265191b..0e804cbfd12 100644 --- a/docker/Dockerfile.database +++ b/docker/Dockerfile.database @@ -1,8 +1,8 @@ # Base image for building -ARG LITELLM_BUILD_IMAGE=cgr.dev/chainguard/python:latest-dev +ARG LITELLM_BUILD_IMAGE=cgr.dev/chainguard/wolfi-base # Runtime image -ARG LITELLM_RUNTIME_IMAGE=cgr.dev/chainguard/python:latest-dev +ARG LITELLM_RUNTIME_IMAGE=cgr.dev/chainguard/wolfi-base # Builder stage FROM $LITELLM_BUILD_IMAGE AS builder @@ -13,13 +13,15 @@ USER root # Install build dependencies RUN apk add --no-cache \ - build-base \ + bash \ + gcc \ + py3-pip \ + python3 \ python3-dev \ + openssl \ openssl-dev - -RUN pip install --upgrade pip && \ - pip install build +RUN python -m pip install build # Copy the current directory contents into the container at /app COPY . . @@ -46,7 +48,7 @@ FROM $LITELLM_RUNTIME_IMAGE AS runtime USER root # Install runtime dependencies -RUN apk add --no-cache openssl +RUN apk add --no-cache bash openssl tzdata nodejs npm python3 py3-pip WORKDIR /app # Copy the current directory contents into the container at /app diff --git a/docker/Dockerfile.non_root b/docker/Dockerfile.non_root index 8b66a367eeb..cd1633e319c 100644 --- a/docker/Dockerfile.non_root +++ b/docker/Dockerfile.non_root @@ -1,6 +1,6 @@ # Base images -ARG LITELLM_BUILD_IMAGE=cgr.dev/chainguard/python:latest-dev -ARG LITELLM_RUNTIME_IMAGE=cgr.dev/chainguard/python:latest-dev +ARG LITELLM_BUILD_IMAGE=cgr.dev/chainguard/wolfi-base +ARG LITELLM_RUNTIME_IMAGE=cgr.dev/chainguard/wolfi-base # ----------------- # Builder Stage @@ -11,6 +11,8 @@ WORKDIR /app # Install build dependencies including Node.js for UI build USER root RUN apk add --no-cache \ + python3 \ + py3-pip \ clang \ llvm \ lld \ @@ -71,7 +73,7 @@ WORKDIR /app # Install runtime dependencies USER root RUN apk upgrade --no-cache && \ - apk add --no-cache bash libstdc++ ca-certificates openssl supervisor + apk add --no-cache python3 py3-pip bash openssl tzdata nodejs npm supervisor # Copy only necessary artifacts from builder stage for runtime COPY . . From 8edcc4ecc3fc8ca56447e039871176622cf11aba Mon Sep 17 00:00:00 2001 From: Krish Dholakia Date: Tue, 2 Dec 2025 22:52:09 -0800 Subject: [PATCH 049/107] Guardrails API - add streaming support (#17400) * fix(initial-commit): adding a way to get the right response type based on the api route * feat(unified_guardrail.py): support streaming guardrails * test: update tests * fix: fix linting errors * test: update tests --- litellm/batches/main.py | 2 - litellm/constants.py | 18 +- litellm/integrations/custom_guardrail.py | 1 - litellm/litellm_core_utils/README.md | 1 + .../api_route_to_call_types.py | 38 ++ .../guardrail_translation/base_translation.py | 14 + .../chat/guardrail_translation/handler.py | 98 +++++- ...odel_prices_and_context_window_backup.json | 65 ++++ .../unified_guardrail/unified_guardrail.py | 110 +++++- litellm/proxy/utils.py | 21 +- litellm/types/utils.py | 327 +++++++++++++++++- .../test_apply_guardrail_endpoint.py | 116 ++++--- .../test_bedrock_apply_guardrail.py | 161 +++++---- .../rerank/test_rerank_guardrail_handler.py | 72 ++-- .../test_text_completion_guardrail_handler.py | 48 ++- ...test_image_generation_guardrail_handler.py | 30 +- ...test_openai_responses_guardrail_handler.py | 14 +- .../test_text_to_speech_guardrail_handler.py | 73 ++-- ...t_audio_transcription_guardrail_handler.py | 80 +++-- .../content_filter/test_content_filter.py | 173 +++++---- .../guardrail_hooks/test_presidio.py | 51 +-- 21 files changed, 1134 insertions(+), 379 deletions(-) create mode 100644 litellm/litellm_core_utils/api_route_to_call_types.py diff --git a/litellm/batches/main.py b/litellm/batches/main.py index 353b1e25698..b99f4a628dc 100644 --- a/litellm/batches/main.py +++ b/litellm/batches/main.py @@ -18,8 +18,6 @@ from typing import Any, Coroutine, Dict, Literal, Optional, Union, cast import httpx from openai.types.batch import BatchRequestCounts -from openai.types.batch import Metadata -from openai.types.batch import Metadata as OpenAIBatchMetadata import litellm from litellm._logging import verbose_logger diff --git a/litellm/constants.py b/litellm/constants.py index db617a2e475..57029cd2a8c 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -262,7 +262,9 @@ 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 +AUDIO_SPEECH_CHUNK_SIZE = int( + os.getenv("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) ) @@ -285,10 +287,16 @@ REDACTED_BY_LITELM_STRING = "REDACTED_BY_LITELM" MAX_LANGFUSE_INITIALIZED_CLIENTS = int( os.getenv("MAX_LANGFUSE_INITIALIZED_CLIENTS", 50) ) -LOGGING_WORKER_CONCURRENCY = int(os.getenv("LOGGING_WORKER_CONCURRENCY", 100)) # Must be above 0 +LOGGING_WORKER_CONCURRENCY = int( + os.getenv("LOGGING_WORKER_CONCURRENCY", 100) +) # Must be above 0 LOGGING_WORKER_MAX_QUEUE_SIZE = int(os.getenv("LOGGING_WORKER_MAX_QUEUE_SIZE", 50_000)) -LOGGING_WORKER_MAX_TIME_PER_COROUTINE = float(os.getenv("LOGGING_WORKER_MAX_TIME_PER_COROUTINE", 20.0)) -LOGGING_WORKER_CLEAR_PERCENTAGE = int(os.getenv("LOGGING_WORKER_CLEAR_PERCENTAGE", 50)) # Percentage of queue to clear (default: 50%) +LOGGING_WORKER_MAX_TIME_PER_COROUTINE = float( + os.getenv("LOGGING_WORKER_MAX_TIME_PER_COROUTINE", 20.0) +) +LOGGING_WORKER_CLEAR_PERCENTAGE = int( + os.getenv("LOGGING_WORKER_CLEAR_PERCENTAGE", 50) +) # Percentage of queue to clear (default: 50%) MAX_ITERATIONS_TO_CLEAR_QUEUE = int(os.getenv("MAX_ITERATIONS_TO_CLEAR_QUEUE", 200)) MAX_TIME_TO_CLEAR_QUEUE = float(os.getenv("MAX_TIME_TO_CLEAR_QUEUE", 5.0)) LOGGING_WORKER_AGGRESSIVE_CLEAR_COOLDOWN_SECONDS = float( @@ -866,7 +874,7 @@ BEDROCK_INVOKE_PROVIDERS_LITERAL = Literal[ "deepseek_r1", "qwen3", "twelvelabs", - "openai" + "openai", ] BEDROCK_EMBEDDING_PROVIDERS_LITERAL = Literal[ diff --git a/litellm/integrations/custom_guardrail.py b/litellm/integrations/custom_guardrail.py index 7f74f5d2157..507a754a7e0 100644 --- a/litellm/integrations/custom_guardrail.py +++ b/litellm/integrations/custom_guardrail.py @@ -20,7 +20,6 @@ from litellm.types.guardrails import ( GuardrailEventHooks, LitellmParams, Mode, - PiiEntityType, ) from litellm.types.llms.openai import AllMessageValues from litellm.types.proxy.guardrails.guardrail_hooks.base import GuardrailConfigModel diff --git a/litellm/litellm_core_utils/README.md b/litellm/litellm_core_utils/README.md index 6494041291b..b61c8982762 100644 --- a/litellm/litellm_core_utils/README.md +++ b/litellm/litellm_core_utils/README.md @@ -9,4 +9,5 @@ Core files: - `default_encoding.py`: code for loading the default encoding (tiktoken) - `get_llm_provider_logic.py`: code for inferring the LLM provider from a given model name. - `duration_parser.py`: code for parsing durations - e.g. "1d", "1mo", "10s" +- `api_route_to_call_types.py`: mapping of API routes to their corresponding CallTypes (e.g., `/chat/completions` -> [acompletion, completion]) diff --git a/litellm/litellm_core_utils/api_route_to_call_types.py b/litellm/litellm_core_utils/api_route_to_call_types.py new file mode 100644 index 00000000000..35f83de1dd7 --- /dev/null +++ b/litellm/litellm_core_utils/api_route_to_call_types.py @@ -0,0 +1,38 @@ +""" +Dictionary mapping API routes to their corresponding CallTypes in LiteLLM. + +This dictionary maps each API endpoint to the CallTypes that can be used for that route. +Each route can have both async (prefixed with 'a') and sync call types. +""" + +from litellm.types.utils import API_ROUTE_TO_CALL_TYPES, CallTypes + + +def get_call_types_for_route(route: str) -> list: + """ + Get the list of CallTypes for a given API route. + + Args: + route: API route path (e.g., "/chat/completions") + + Returns: + List of CallTypes for that route, or empty list if route not found + """ + return API_ROUTE_TO_CALL_TYPES.get(route, []) + + +def get_routes_for_call_type(call_type: CallTypes) -> list: + """ + Get all routes that use a specific CallType. + + Args: + call_type: The CallType to search for + + Returns: + List of routes that use this CallType + """ + routes = [] + for route, types in API_ROUTE_TO_CALL_TYPES.items(): + if call_type in types: + routes.append(route) + return routes diff --git a/litellm/llms/base_llm/guardrail_translation/base_translation.py b/litellm/llms/base_llm/guardrail_translation/base_translation.py index 5acbf4e9f4f..c1ea3311bd8 100644 --- a/litellm/llms/base_llm/guardrail_translation/base_translation.py +++ b/litellm/llms/base_llm/guardrail_translation/base_translation.py @@ -84,3 +84,17 @@ class BaseTranslation(ABC): user_api_key_dict: User API key metadata (passed separately since response doesn't contain it) """ pass + + async def process_output_streaming_response( + self, + response: Any, + guardrail_to_apply: "CustomGuardrail", + litellm_logging_obj: Optional["LiteLLMLoggingObj"] = None, + user_api_key_dict: Optional["UserAPIKeyAuth"] = None, + ) -> Any: + """ + Process output streaming response with guardrails. + + Optional to override in subclasses. + """ + return response diff --git a/litellm/llms/openai/chat/guardrail_translation/handler.py b/litellm/llms/openai/chat/guardrail_translation/handler.py index 0abc94012ee..29fb12a6f74 100644 --- a/litellm/llms/openai/chat/guardrail_translation/handler.py +++ b/litellm/llms/openai/chat/guardrail_translation/handler.py @@ -14,16 +14,16 @@ Pattern Overview: This pattern can be replicated for other message formats (e.g., Anthropic). """ -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, cast +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union, cast import litellm from litellm._logging import verbose_proxy_logger from litellm.llms.base_llm.guardrail_translation.base_translation import BaseTranslation -from litellm.types.utils import Choices +from litellm.types.utils import Choices, StreamingChoices if TYPE_CHECKING: from litellm.integrations.custom_guardrail import CustomGuardrail - from litellm.types.utils import ModelResponse + from litellm.types.utils import ModelResponse, ModelResponseStream class OpenAIChatCompletionsHandler(BaseTranslation): @@ -241,21 +241,79 @@ class OpenAIChatCompletionsHandler(BaseTranslation): return response - def _has_text_content(self, response: "ModelResponse") -> bool: + async def process_output_streaming_response( + self, + response: "ModelResponseStream", + guardrail_to_apply: "CustomGuardrail", + litellm_logging_obj: Optional[Any] = None, + user_api_key_dict: Optional[Any] = None, + ) -> Any: + """ + Process output streaming response by applying guardrails to text content. + + Args: + response: LiteLLM ModelResponseStream object + guardrail_to_apply: The guardrail instance to apply + litellm_logging_obj: Optional logging object + user_api_key_dict: User API key metadata to pass to guardrails + + Returns: + Modified response with guardrail applied to content + + Response Format Support: + - String content: choice.message.content = "text here" + - List content: choice.message.content = [{"type": "text", "text": "text here"}, ...] + """ + + # Step 0: Check if response has any text content to process + if not self._has_text_content(response): + return response + + texts_to_check: List[str] = [] + images_to_check: List[str] = [] + task_mappings: List[Tuple[int, Optional[int]]] = [] + # Track (choice_index, content_index) for each text + + # Step 1: Extract all text content and images from response choices + for choice_idx, choice in enumerate(response.choices): + + self._extract_output_text_and_images( + choice=choice, + choice_idx=choice_idx, + texts_to_check=texts_to_check, + images_to_check=images_to_check, + task_mappings=task_mappings, + ) + + def _has_text_content( + self, response: Union["ModelResponse", "ModelResponseStream"] + ) -> bool: """ Check if response has any text content to process. Override this method to customize text content detection. """ - for choice in response.choices: - if isinstance(choice, litellm.Choices): - if choice.message.content and isinstance(choice.message.content, str): - return True + from litellm.types.utils import ModelResponse, ModelResponseStream + + if isinstance(response, ModelResponse): + for choice in response.choices: + if isinstance(choice, litellm.Choices): + if choice.message.content and isinstance( + choice.message.content, str + ): + return True + elif isinstance(response, ModelResponseStream): + for choice in response.choices: + if isinstance(choice, litellm.Choices): + if choice.message.content and isinstance( + choice.message.content, str + ): + return True return False def _extract_output_text_and_images( self, - choice: Any, + choice: Union[Choices, StreamingChoices], choice_idx: int, texts_to_check: List[str], images_to_check: List[str], @@ -266,21 +324,29 @@ class OpenAIChatCompletionsHandler(BaseTranslation): Override this method to customize text/image extraction logic. """ - if not isinstance(choice, litellm.Choices): - return - verbose_proxy_logger.debug( "OpenAI Chat Completions: Processing choice: %s", choice ) - if choice.message.content and isinstance(choice.message.content, str): + # Determine content source based on choice type + content = None + if isinstance(choice, litellm.Choices): + content = choice.message.content + elif isinstance(choice, litellm.StreamingChoices): + content = choice.delta.content + else: + # Unknown choice type, skip processing + return + + # Process content if it exists + if content and isinstance(content, str): # Simple string content - texts_to_check.append(choice.message.content) + texts_to_check.append(content) task_mappings.append((choice_idx, None)) - elif choice.message.content and isinstance(choice.message.content, list): + elif content and isinstance(content, list): # List content (e.g., multimodal response) - for content_idx, content_item in enumerate(choice.message.content): + for content_idx, content_item in enumerate(content): # Extract text content_text = content_item.get("text") if content_text: diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 464f9c185f9..0fc97ce7b0a 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -269,6 +269,71 @@ "supports_response_schema": true, "supports_vision": true }, + "amazon.nova-2-lite-v1:0": { + "input_cost_per_token": 3e-07, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 1000000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 2.5e-06, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_video_input": true, + "supports_vision": true + }, + "apac.amazon.nova-2-lite-v1:0": { + "input_cost_per_token": 6e-08, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 1000000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 2.75e-06, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_video_input": true, + "supports_vision": true + }, + "eu.amazon.nova-2-lite-v1:0": { + "input_cost_per_token": 6e-08, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 1000000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 2.75e-06, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_video_input": true, + "supports_vision": true + }, + "us.amazon.nova-2-lite-v1:0": { + "input_cost_per_token": 6e-08, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 1000000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 2.75e-06, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_video_input": true, + "supports_vision": true + }, + "amazon.nova-micro-v1:0": { "input_cost_per_token": 3.5e-08, "litellm_provider": "bedrock_converse", diff --git a/litellm/proxy/guardrails/guardrail_hooks/unified_guardrail/unified_guardrail.py b/litellm/proxy/guardrails/guardrail_hooks/unified_guardrail/unified_guardrail.py index aeae19a8270..0f05696af42 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/unified_guardrail/unified_guardrail.py +++ b/litellm/proxy/guardrails/guardrail_hooks/unified_guardrail/unified_guardrail.py @@ -13,6 +13,7 @@ from litellm.caching.caching import DualCache from litellm.cost_calculator import _infer_call_type from litellm.integrations.custom_guardrail import CustomGuardrail from litellm.integrations.custom_logger import CustomLogger +from litellm.litellm_core_utils.api_route_to_call_types import get_call_types_for_route from litellm.llms import load_guardrail_translation_mappings from litellm.proxy._types import UserAPIKeyAuth from litellm.types.guardrails import GuardrailEventHooks @@ -176,6 +177,113 @@ class UnifiedLLMGuardrails(CustomLogger): See Aim guardrail implementation for an example - https://github.com/BerriAI/litellm/blob/d0e022cfacb8e9ebc5409bb652059b6fd97b45c0/litellm/proxy/guardrails/guardrail_hooks/aim.py#L168 Triggered by mode: 'post_call' + + Supports sampling_rate parameter to control how often chunks are processed. + sampling_rate=1 means every chunk, sampling_rate=5 means every 5th chunk, etc. """ + + global endpoint_guardrail_translation_mappings + from litellm.proxy.common_utils.callback_utils import ( + add_guardrail_to_applied_guardrails_header, + ) + + guardrail_to_apply: CustomGuardrail = request_data.pop( + "guardrail_to_apply", None + ) + + # Get sampling rate from guardrail config or optional_params, default to 5 + sampling_rate = 5 + if guardrail_to_apply is not None: + # Check guardrail config first + guardrail_config = getattr(guardrail_to_apply, "guardrail_config", {}) + sampling_rate = guardrail_config.get( + "streaming_sampling_rate", sampling_rate + ) + + # Also check optional_params as fallback + sampling_rate = self.optional_params.get( + "streaming_sampling_rate", sampling_rate + ) + + if guardrail_to_apply is None: + async for item in response: + yield item + return + + event_type: GuardrailEventHooks = GuardrailEventHooks.post_call + if ( + guardrail_to_apply.should_run_guardrail( + data=request_data, event_type=event_type + ) + is not True + ): + verbose_proxy_logger.debug( + "UnifiedLLMGuardrails: Post-call streaming scanning disabled for %s", + guardrail_to_apply.guardrail_name, + ) + async for item in response: + yield item + return + + # Initialize translation mappings if needed + if endpoint_guardrail_translation_mappings is None: + endpoint_guardrail_translation_mappings = ( + load_guardrail_translation_mappings() + ) + + # Infer call type from first chunk + call_type = None + chunk_counter = 0 + async for item in response: - yield item + chunk_counter += 1 + + # Infer call type from first chunk if not already done + if call_type is None and user_api_key_dict.request_route is not None: + call_types = get_call_types_for_route(user_api_key_dict.request_route) + if call_types is not None: + call_type = call_types[0] + + # If call type not supported, just pass through all chunks + if ( + call_type is None + or CallTypes(call_type) + not in endpoint_guardrail_translation_mappings + ): + yield item + async for remaining_item in response: + yield remaining_item + return + + # Process chunk based on sampling rate + if chunk_counter % sampling_rate == 0: + verbose_proxy_logger.debug( + "Processing streaming chunk %s (sampling_rate=%s) with guardrail %s", + chunk_counter, + sampling_rate, + guardrail_to_apply.guardrail_name, + ) + + endpoint_translation = endpoint_guardrail_translation_mappings[ + CallTypes(call_type) + ]() + + processed_item = ( + await endpoint_translation.process_output_streaming_response( + response=item, + guardrail_to_apply=guardrail_to_apply, + litellm_logging_obj=request_data.get("litellm_logging_obj"), + user_api_key_dict=user_api_key_dict, + ) + ) + + # Add guardrail to applied guardrails header (only once, on first processed chunk) + if chunk_counter == sampling_rate: + add_guardrail_to_applied_guardrails_header( + request_data=request_data, + guardrail_name=guardrail_to_apply.guardrail_name, + ) + + yield processed_item + else: + yield item diff --git a/litellm/proxy/utils.py b/litellm/proxy/utils.py index 3b746b757ec..f0dccfae716 100644 --- a/litellm/proxy/utils.py +++ b/litellm/proxy/utils.py @@ -1560,6 +1560,7 @@ class ProxyLogging: Covers: 1. /chat/completions """ + for callback in litellm.callbacks: _callback: Optional[CustomLogger] = None if isinstance(callback, str): @@ -1574,11 +1575,21 @@ class ProxyLogging: ) or _callback.should_run_guardrail( data=request_data, event_type=GuardrailEventHooks.post_call ): - response = _callback.async_post_call_streaming_iterator_hook( - user_api_key_dict=user_api_key_dict, - response=response, - request_data=request_data, - ) + if "apply_guardrail" in type(callback).__dict__: + request_data["guardrail_to_apply"] = callback + response = ( + unified_guardrail.async_post_call_streaming_iterator_hook( + user_api_key_dict=user_api_key_dict, + request_data=request_data, + response=response, + ) + ) + else: + response = _callback.async_post_call_streaming_iterator_hook( + user_api_key_dict=user_api_key_dict, + response=response, + request_data=request_data, + ) return response def _init_response_taking_too_long_task(self, data: Optional[dict] = None): diff --git a/litellm/types/utils.py b/litellm/types/utils.py index 5a582194147..cf3b1480e6a 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -401,6 +401,328 @@ CallTypesLiteral = Literal[ "responses", ] +# Mapping of API routes to their corresponding call types +API_ROUTE_TO_CALL_TYPES = { + # Chat Completions + "/chat/completions": [CallTypes.acompletion, CallTypes.completion], + "/v1/chat/completions": [CallTypes.acompletion, CallTypes.completion], + "/engines/{model}/chat/completions": [CallTypes.acompletion, CallTypes.completion], + "/openai/deployments/{model}/chat/completions": [ + CallTypes.acompletion, + CallTypes.completion, + ], + # Text Completions + "/completions": [CallTypes.atext_completion, CallTypes.text_completion], + "/v1/completions": [CallTypes.atext_completion, CallTypes.text_completion], + "/engines/{model}/completions": [ + CallTypes.atext_completion, + CallTypes.text_completion, + ], + "/openai/deployments/{model}/completions": [ + CallTypes.atext_completion, + CallTypes.text_completion, + ], + # Embeddings + "/embeddings": [CallTypes.aembedding, CallTypes.embedding], + "/v1/embeddings": [CallTypes.aembedding, CallTypes.embedding], + "/engines/{model}/embeddings": [CallTypes.aembedding, CallTypes.embedding], + "/openai/deployments/{model}/embeddings": [ + CallTypes.aembedding, + CallTypes.embedding, + ], + # Image Generation + "/images/generations": [CallTypes.aimage_generation, CallTypes.image_generation], + "/v1/images/generations": [CallTypes.aimage_generation, CallTypes.image_generation], + "/engines/{model}/images/generations": [ + CallTypes.aimage_generation, + CallTypes.image_generation, + ], + "/openai/deployments/{model}/images/generations": [ + CallTypes.aimage_generation, + CallTypes.image_generation, + ], + # Image Edits + "/images/edits": [CallTypes.aimage_edit, CallTypes.image_edit], + "/v1/images/edits": [CallTypes.aimage_edit, CallTypes.image_edit], + # Audio Transcriptions + "/audio/transcriptions": [CallTypes.atranscription, CallTypes.transcription], + "/v1/audio/transcriptions": [CallTypes.atranscription, CallTypes.transcription], + # Audio Speech + "/audio/speech": [CallTypes.aspeech, CallTypes.speech], + "/v1/audio/speech": [CallTypes.aspeech, CallTypes.speech], + # Moderations + "/moderations": [CallTypes.amoderation, CallTypes.moderation], + "/v1/moderations": [CallTypes.amoderation, CallTypes.moderation], + # Rerank + "/rerank": [CallTypes.arerank, CallTypes.rerank], + "/v1/rerank": [CallTypes.arerank, CallTypes.rerank], + "/v2/rerank": [CallTypes.arerank, CallTypes.rerank], + # Search + "/search": [CallTypes.asearch, CallTypes.search], + "/v1/search": [CallTypes.asearch, CallTypes.search], + # Batches + "/batches": [CallTypes.acreate_batch, CallTypes.create_batch], + "/v1/batches": [CallTypes.acreate_batch, CallTypes.create_batch], + "/batches/{batch_id}": [CallTypes.aretrieve_batch, CallTypes.retrieve_batch], + "/v1/batches/{batch_id}": [CallTypes.aretrieve_batch, CallTypes.retrieve_batch], + # Files + "/files": [ + CallTypes.acreate_file, + CallTypes.create_file, + CallTypes.afile_list, + CallTypes.file_list, + ], + "/v1/files": [ + CallTypes.acreate_file, + CallTypes.create_file, + CallTypes.afile_list, + CallTypes.file_list, + ], + "/files/{file_id}": [ + CallTypes.afile_retrieve, + CallTypes.file_retrieve, + CallTypes.afile_delete, + CallTypes.file_delete, + ], + "/v1/files/{file_id}": [ + CallTypes.afile_retrieve, + CallTypes.file_retrieve, + CallTypes.afile_delete, + CallTypes.file_delete, + ], + "/files/{file_id}/content": [CallTypes.afile_content, CallTypes.file_content], + "/v1/files/{file_id}/content": [CallTypes.afile_content, CallTypes.file_content], + # Assistants + "/assistants": [ + CallTypes.aget_assistants, + CallTypes.get_assistants, + CallTypes.acreate_assistants, + CallTypes.create_assistants, + ], + "/v1/assistants": [ + CallTypes.aget_assistants, + CallTypes.get_assistants, + CallTypes.acreate_assistants, + CallTypes.create_assistants, + ], + "/assistants/{assistant_id}": [ + CallTypes.adelete_assistant, + CallTypes.delete_assistant, + ], + "/v1/assistants/{assistant_id}": [ + CallTypes.adelete_assistant, + CallTypes.delete_assistant, + ], + # Threads + "/threads": [CallTypes.acreate_thread, CallTypes.create_thread], + "/v1/threads": [CallTypes.acreate_thread, CallTypes.create_thread], + "/threads/{thread_id}": [CallTypes.aget_thread, CallTypes.get_thread], + "/v1/threads/{thread_id}": [CallTypes.aget_thread, CallTypes.get_thread], + # Thread Messages + "/threads/{thread_id}/messages": [ + CallTypes.a_add_message, + CallTypes.add_message, + CallTypes.aget_messages, + CallTypes.get_messages, + ], + "/v1/threads/{thread_id}/messages": [ + CallTypes.a_add_message, + CallTypes.add_message, + CallTypes.aget_messages, + CallTypes.get_messages, + ], + # Thread Runs + "/threads/{thread_id}/runs": [ + CallTypes.arun_thread, + CallTypes.run_thread, + CallTypes.arun_thread_stream, + CallTypes.run_thread_stream, + ], + "/v1/threads/{thread_id}/runs": [ + CallTypes.arun_thread, + CallTypes.run_thread, + CallTypes.arun_thread_stream, + CallTypes.run_thread_stream, + ], + # Fine-tuning Jobs + "/fine_tuning/jobs": [ + CallTypes.acreate_fine_tuning_job, + CallTypes.create_fine_tuning_job, + CallTypes.alist_fine_tuning_jobs, + CallTypes.list_fine_tuning_jobs, + ], + "/v1/fine_tuning/jobs": [ + CallTypes.acreate_fine_tuning_job, + CallTypes.create_fine_tuning_job, + CallTypes.alist_fine_tuning_jobs, + CallTypes.list_fine_tuning_jobs, + ], + "/fine_tuning/jobs/{fine_tuning_job_id}": [ + CallTypes.aretrieve_fine_tuning_job, + CallTypes.retrieve_fine_tuning_job, + ], + "/v1/fine_tuning/jobs/{fine_tuning_job_id}": [ + CallTypes.aretrieve_fine_tuning_job, + CallTypes.retrieve_fine_tuning_job, + ], + "/fine_tuning/jobs/{fine_tuning_job_id}/cancel": [ + CallTypes.acancel_fine_tuning_job, + CallTypes.cancel_fine_tuning_job, + ], + "/v1/fine_tuning/jobs/{fine_tuning_job_id}/cancel": [ + CallTypes.acancel_fine_tuning_job, + CallTypes.cancel_fine_tuning_job, + ], + # Video Generation + "/videos": [ + CallTypes.acreate_video, + CallTypes.create_video, + CallTypes.avideo_list, + CallTypes.video_list, + ], + "/v1/videos": [ + CallTypes.acreate_video, + CallTypes.create_video, + CallTypes.avideo_list, + CallTypes.video_list, + ], + "/videos/{video_id}": [ + CallTypes.avideo_retrieve, + CallTypes.video_retrieve, + CallTypes.avideo_delete, + CallTypes.video_delete, + ], + "/v1/videos/{video_id}": [ + CallTypes.avideo_retrieve, + CallTypes.video_retrieve, + CallTypes.avideo_delete, + CallTypes.video_delete, + ], + "/videos/{video_id}/content": [CallTypes.avideo_content, CallTypes.video_content], + "/v1/videos/{video_id}/content": [ + CallTypes.avideo_content, + CallTypes.video_content, + ], + "/videos/{video_id}/remix": [CallTypes.avideo_remix, CallTypes.video_remix], + "/v1/videos/{video_id}/remix": [CallTypes.avideo_remix, CallTypes.video_remix], + # Vector Stores + "/vector_stores": [CallTypes.avector_store_create, CallTypes.vector_store_create], + "/v1/vector_stores": [ + CallTypes.avector_store_create, + CallTypes.vector_store_create, + ], + "/vector_stores/{vector_store_id}/search": [ + CallTypes.avector_store_search, + CallTypes.vector_store_search, + ], + "/v1/vector_stores/{vector_store_id}/search": [ + CallTypes.avector_store_search, + CallTypes.vector_store_search, + ], + "/vector_stores/{vector_store_id}/files": [ + CallTypes.avector_store_file_create, + CallTypes.vector_store_file_create, + CallTypes.avector_store_file_list, + CallTypes.vector_store_file_list, + ], + "/v1/vector_stores/{vector_store_id}/files": [ + CallTypes.avector_store_file_create, + CallTypes.vector_store_file_create, + CallTypes.avector_store_file_list, + CallTypes.vector_store_file_list, + ], + "/vector_stores/{vector_store_id}/files/{file_id}": [ + CallTypes.avector_store_file_retrieve, + CallTypes.vector_store_file_retrieve, + CallTypes.avector_store_file_delete, + CallTypes.vector_store_file_delete, + ], + "/v1/vector_stores/{vector_store_id}/files/{file_id}": [ + CallTypes.avector_store_file_retrieve, + CallTypes.vector_store_file_retrieve, + CallTypes.avector_store_file_delete, + CallTypes.vector_store_file_delete, + ], + "/vector_stores/{vector_store_id}/files/{file_id}/content": [ + CallTypes.avector_store_file_content, + CallTypes.vector_store_file_content, + ], + "/v1/vector_stores/{vector_store_id}/files/{file_id}/content": [ + CallTypes.avector_store_file_content, + CallTypes.vector_store_file_content, + ], + "/vector_stores/{vector_store_id}/files/{file_id}/update": [ + CallTypes.avector_store_file_update, + CallTypes.vector_store_file_update, + ], + "/v1/vector_stores/{vector_store_id}/files/{file_id}/update": [ + CallTypes.avector_store_file_update, + CallTypes.vector_store_file_update, + ], + # Containers + "/containers": [ + CallTypes.acreate_container, + CallTypes.create_container, + CallTypes.alist_containers, + CallTypes.list_containers, + ], + "/v1/containers": [ + CallTypes.acreate_container, + CallTypes.create_container, + CallTypes.alist_containers, + CallTypes.list_containers, + ], + "/containers/{container_id}": [ + CallTypes.aretrieve_container, + CallTypes.retrieve_container, + CallTypes.adelete_container, + CallTypes.delete_container, + ], + "/v1/containers/{container_id}": [ + CallTypes.aretrieve_container, + CallTypes.retrieve_container, + CallTypes.adelete_container, + CallTypes.delete_container, + ], + # Responses API + "/responses": [CallTypes.aresponses, CallTypes.responses], + "/v1/responses": [CallTypes.aresponses, CallTypes.responses], + "/responses/{response_id}": [CallTypes.aresponses, CallTypes.responses], + "/v1/responses/{response_id}": [CallTypes.aresponses, CallTypes.responses], + "/responses/{response_id}/input_items": [CallTypes.alist_input_items], + "/v1/responses/{response_id}/input_items": [CallTypes.alist_input_items], + # Realtime API + "/realtime": [CallTypes.arealtime], + "/v1/realtime": [CallTypes.arealtime], + # Provider-specific routes + "/anthropic/v1/messages": [CallTypes.anthropic_messages], + # Google GenAI routes + "/generate_content": [CallTypes.agenerate_content, CallTypes.generate_content], + "/models/{model}:generateContent": [ + CallTypes.agenerate_content, + CallTypes.generate_content, + ], + "/generate_content_stream": [ + CallTypes.agenerate_content_stream, + CallTypes.generate_content_stream, + ], + "/models/{model}:streamGenerateContent": [ + CallTypes.agenerate_content_stream, + CallTypes.generate_content_stream, + ], + # MCP (Model Context Protocol) + "/mcp/call_tool": [CallTypes.call_mcp_tool], + # Passthrough endpoints + "/llm_passthrough": [ + CallTypes.llm_passthrough_route, + CallTypes.allm_passthrough_route, + ], + "/v1/llm_passthrough": [ + CallTypes.llm_passthrough_route, + CallTypes.allm_passthrough_route, + ], +} + class PassthroughCallTypes(Enum): passthrough_image_generation = "passthrough-image-generation" @@ -1060,7 +1382,10 @@ class Usage(CompletionUsage): # Auto-calculate text_tokens only if provider didn't set it explicitly # Formula: text_tokens = completion_tokens - reasoning_tokens - image_tokens - audio_tokens - if _completion_tokens_details.text_tokens is None and completion_tokens is not None: + if ( + _completion_tokens_details.text_tokens is None + and completion_tokens is not None + ): calculated_text_tokens = completion_tokens - reasoning_tokens # Subtract other modality tokens if present diff --git a/tests/enterprise/litellm_enterprise/proxy/guardrails/test_apply_guardrail_endpoint.py b/tests/enterprise/litellm_enterprise/proxy/guardrails/test_apply_guardrail_endpoint.py index 186056dac90..7ce99abdd15 100644 --- a/tests/enterprise/litellm_enterprise/proxy/guardrails/test_apply_guardrail_endpoint.py +++ b/tests/enterprise/litellm_enterprise/proxy/guardrails/test_apply_guardrail_endpoint.py @@ -1,6 +1,7 @@ """ Test the /guardrails/apply_guardrail endpoint """ + import os import sys from unittest.mock import AsyncMock, Mock, patch @@ -22,37 +23,45 @@ async def test_apply_guardrail_endpoint_returns_correct_response(): from litellm.proxy.guardrails.guardrail_endpoints import apply_guardrail # Mock the guardrail registry - with patch("litellm.proxy.guardrails.guardrail_endpoints.GUARDRAIL_REGISTRY") as mock_registry: + with patch( + "litellm.proxy.guardrails.guardrail_endpoints.GUARDRAIL_REGISTRY" + ) as mock_registry: # Create a mock guardrail mock_guardrail = Mock(spec=CustomGuardrail) - mock_guardrail.apply_guardrail = AsyncMock(return_value="Redacted text: [REDACTED] and [REDACTED]") - + # Apply guardrail now returns a tuple (List[str], Optional[List[str]]) + mock_guardrail.apply_guardrail = AsyncMock( + return_value=(["Redacted text: [REDACTED] and [REDACTED]"], None) + ) + # Configure the registry to return our mock guardrail mock_registry.get_initialized_guardrail_callback.return_value = mock_guardrail - + # Create the request request = ApplyGuardrailRequest( guardrail_name="test-guardrail", text="Test text with PII", language="en", - entities=["EMAIL_ADDRESS", "PERSON"] + entities=["EMAIL_ADDRESS", "PERSON"], ) - + # Create a mock user API key user_api_key_dict = UserAPIKeyAuth(api_key="test-key") - + # Call the endpoint - response = await apply_guardrail(request=request, user_api_key_dict=user_api_key_dict) - + response = await apply_guardrail( + request=request, user_api_key_dict=user_api_key_dict + ) + # Verify the response is of the correct type assert isinstance(response, ApplyGuardrailResponse) assert response.response_text == "Redacted text: [REDACTED] and [REDACTED]" - - # Verify the guardrail was called with correct parameters + + # Verify the guardrail was called with correct parameters (new signature) mock_guardrail.apply_guardrail.assert_called_once_with( - text="Test text with PII", - language="en", - entities=["EMAIL_ADDRESS", "PERSON"] + texts=["Test text with PII"], + request_data={}, + input_type="request", + images=None, ) @@ -63,23 +72,23 @@ async def test_apply_guardrail_endpoint_guardrail_not_found(): from litellm.proxy.guardrails.guardrail_endpoints import apply_guardrail # Mock the guardrail registry to return None - with patch("litellm.proxy.guardrails.guardrail_endpoints.GUARDRAIL_REGISTRY") as mock_registry: + with patch( + "litellm.proxy.guardrails.guardrail_endpoints.GUARDRAIL_REGISTRY" + ) as mock_registry: mock_registry.get_initialized_guardrail_callback.return_value = None - + # Create the request request = ApplyGuardrailRequest( - guardrail_name="non-existent-guardrail", - text="Test text", - language="en" + guardrail_name="non-existent-guardrail", text="Test text", language="en" ) - + # Create a mock user API key user_api_key_dict = UserAPIKeyAuth(api_key="test-key") - + # Verify exception is raised with pytest.raises(ProxyException) as exc_info: await apply_guardrail(request=request, user_api_key_dict=user_api_key_dict) - + assert "non-existent-guardrail" in exc_info.value.message assert "not found" in exc_info.value.message @@ -90,34 +99,41 @@ async def test_apply_guardrail_endpoint_with_presidio_guardrail(): from litellm.proxy.guardrails.guardrail_endpoints import apply_guardrail # Mock the guardrail registry - with patch("litellm.proxy.guardrails.guardrail_endpoints.GUARDRAIL_REGISTRY") as mock_registry: + with patch( + "litellm.proxy.guardrails.guardrail_endpoints.GUARDRAIL_REGISTRY" + ) as mock_registry: # Create a mock guardrail that simulates Presidio behavior mock_guardrail = Mock(spec=CustomGuardrail) - # Simulate masking PII entities + # Simulate masking PII entities - returns tuple (List[str], Optional[List[str]]) mock_guardrail.apply_guardrail = AsyncMock( - return_value="My name is [PERSON] and my email is [EMAIL_ADDRESS]" + return_value=(["My name is [PERSON] and my email is [EMAIL_ADDRESS]"], None) ) - + # Configure the registry to return our mock guardrail mock_registry.get_initialized_guardrail_callback.return_value = mock_guardrail - + # Create the request request = ApplyGuardrailRequest( guardrail_name="pii-detection-guard", text="My name is John Doe and my email is john@example.com", language="en", - entities=["EMAIL_ADDRESS", "PERSON"] + entities=["EMAIL_ADDRESS", "PERSON"], ) - + # Create a mock user API key user_api_key_dict = UserAPIKeyAuth(api_key="test-key") - + # Call the endpoint - response = await apply_guardrail(request=request, user_api_key_dict=user_api_key_dict) - + response = await apply_guardrail( + request=request, user_api_key_dict=user_api_key_dict + ) + # Verify the response is of the correct type assert isinstance(response, ApplyGuardrailResponse) - assert response.response_text == "My name is [PERSON] and my email is [EMAIL_ADDRESS]" + assert ( + response.response_text + == "My name is [PERSON] and my email is [EMAIL_ADDRESS]" + ) assert "john@example.com" not in response.response_text assert "John Doe" not in response.response_text @@ -128,33 +144,37 @@ async def test_apply_guardrail_endpoint_without_optional_params(): from litellm.proxy.guardrails.guardrail_endpoints import apply_guardrail # Mock the guardrail registry - with patch("litellm.proxy.guardrails.guardrail_endpoints.GUARDRAIL_REGISTRY") as mock_registry: + with patch( + "litellm.proxy.guardrails.guardrail_endpoints.GUARDRAIL_REGISTRY" + ) as mock_registry: # Create a mock guardrail mock_guardrail = Mock(spec=CustomGuardrail) - mock_guardrail.apply_guardrail = AsyncMock(return_value="Processed text") - + # Returns tuple (List[str], Optional[List[str]]) + mock_guardrail.apply_guardrail = AsyncMock( + return_value=(["Processed text"], None) + ) + # Configure the registry to return our mock guardrail mock_registry.get_initialized_guardrail_callback.return_value = mock_guardrail - + # Create the request without optional parameters request = ApplyGuardrailRequest( - guardrail_name="test-guardrail", - text="Test text" + guardrail_name="test-guardrail", text="Test text" ) - + # Create a mock user API key user_api_key_dict = UserAPIKeyAuth(api_key="test-key") - + # Call the endpoint - response = await apply_guardrail(request=request, user_api_key_dict=user_api_key_dict) - + response = await apply_guardrail( + request=request, user_api_key_dict=user_api_key_dict + ) + # Verify the response is of the correct type assert isinstance(response, ApplyGuardrailResponse) assert response.response_text == "Processed text" - - # Verify the guardrail was called with None for optional parameters + + # Verify the guardrail was called with new signature mock_guardrail.apply_guardrail.assert_called_once_with( - text="Test text", - language=None, - entities=None + texts=["Test text"], request_data={}, input_type="request", images=None ) diff --git a/tests/enterprise/litellm_enterprise/proxy/guardrails/test_bedrock_apply_guardrail.py b/tests/enterprise/litellm_enterprise/proxy/guardrails/test_bedrock_apply_guardrail.py index e65d01e41e4..203bd05c57c 100644 --- a/tests/enterprise/litellm_enterprise/proxy/guardrails/test_bedrock_apply_guardrail.py +++ b/tests/enterprise/litellm_enterprise/proxy/guardrails/test_bedrock_apply_guardrail.py @@ -1,6 +1,7 @@ """ Test the Bedrock guardrail apply_guardrail functionality """ + import os import sys from unittest.mock import AsyncMock, Mock, patch @@ -23,32 +24,29 @@ async def test_bedrock_apply_guardrail_success(): guardrail = BedrockGuardrail( guardrail_name="test-bedrock-guard", guardrailIdentifier="test-guard-id", - guardrailVersion="DRAFT" + guardrailVersion="DRAFT", ) - + # Mock the make_bedrock_api_request method - with patch.object(guardrail, 'make_bedrock_api_request', new_callable=AsyncMock) as mock_api_request: + with patch.object( + guardrail, "make_bedrock_api_request", new_callable=AsyncMock + ) as mock_api_request: # Mock a successful response from Bedrock mock_response = { "action": "ALLOWED", - "content": [ - { - "text": { - "text": "This is a test message with some content" - } - } - ] + "content": [{"text": {"text": "This is a test message with some content"}}], } mock_api_request.return_value = mock_response - - # Test the apply_guardrail method - result = await guardrail.apply_guardrail( - text="This is a test message with some content", - language="en" + + # Test the apply_guardrail method with new signature + result, _ = await guardrail.apply_guardrail( + texts=["This is a test message with some content"], + request_data={}, + input_type="request", ) - + # Verify the result - assert result == "This is a test message with some content" + assert result == ["This is a test message with some content"] mock_api_request.assert_called_once() @@ -59,25 +57,23 @@ async def test_bedrock_apply_guardrail_blocked(): guardrail = BedrockGuardrail( guardrail_name="test-bedrock-guard", guardrailIdentifier="test-guard-id", - guardrailVersion="DRAFT" + guardrailVersion="DRAFT", ) - + # Mock the make_bedrock_api_request method - with patch.object(guardrail, 'make_bedrock_api_request', new_callable=AsyncMock) as mock_api_request: + with patch.object( + guardrail, "make_bedrock_api_request", new_callable=AsyncMock + ) as mock_api_request: # Mock a blocked response from Bedrock - mock_response = { - "action": "BLOCKED", - "reason": "Content violates policy" - } + mock_response = {"action": "BLOCKED", "reason": "Content violates policy"} mock_api_request.return_value = mock_response - + # Test the apply_guardrail method should raise an exception with pytest.raises(Exception) as exc_info: await guardrail.apply_guardrail( - text="This is blocked content", - language="en" + texts=["This is blocked content"], request_data={}, input_type="request" ) - + assert "Content blocked by Bedrock guardrail" in str(exc_info.value) assert "Content violates policy" in str(exc_info.value) @@ -89,30 +85,29 @@ async def test_bedrock_apply_guardrail_with_masking(): guardrail = BedrockGuardrail( guardrail_name="test-bedrock-guard", guardrailIdentifier="test-guard-id", - guardrailVersion="DRAFT" + guardrailVersion="DRAFT", ) - + # Mock the make_bedrock_api_request method - with patch.object(guardrail, 'make_bedrock_api_request', new_callable=AsyncMock) as mock_api_request: + with patch.object( + guardrail, "make_bedrock_api_request", new_callable=AsyncMock + ) as mock_api_request: # Mock a response with masked content mock_response = { "action": "ALLOWED", - "outputs": [ - { - "text": "This is a test message with [REDACTED] content" - } - ] + "outputs": [{"text": "This is a test message with [REDACTED] content"}], } mock_api_request.return_value = mock_response - - # Test the apply_guardrail method - result = await guardrail.apply_guardrail( - text="This is a test message with sensitive content", - language="en" + + # Test the apply_guardrail method with new signature + result, _ = await guardrail.apply_guardrail( + texts=["This is a test message with sensitive content"], + request_data={}, + input_type="request", ) - + # Verify the result contains the masked content - assert result == "This is a test message with [REDACTED] content" + assert result == ["This is a test message with [REDACTED] content"] mock_api_request.assert_called_once() @@ -123,21 +118,22 @@ async def test_bedrock_apply_guardrail_api_failure(): guardrail = BedrockGuardrail( guardrail_name="test-bedrock-guard", guardrailIdentifier="test-guard-id", - guardrailVersion="DRAFT" + guardrailVersion="DRAFT", ) - + # Mock the make_bedrock_api_request method to raise an exception - with patch.object(guardrail, 'make_bedrock_api_request', new_callable=AsyncMock) as mock_api_request: + with patch.object( + guardrail, "make_bedrock_api_request", new_callable=AsyncMock + ) as mock_api_request: mock_api_request.side_effect = Exception("API connection failed") - + # Test the apply_guardrail method should raise an exception with pytest.raises(Exception) as exc_info: await guardrail.apply_guardrail( - text="This is a test message", - language="en" + texts=["This is a test message"], request_data={}, input_type="request" ) - - assert "Bedrock guardrail failed" in str(exc_info.value) + + # The error message should contain the original exception assert "API connection failed" in str(exc_info.value) @@ -150,44 +146,50 @@ async def test_bedrock_apply_guardrail_endpoint_integration(): guardrail = BedrockGuardrail( guardrail_name="test-bedrock-guard", guardrailIdentifier="test-guard-id", - guardrailVersion="DRAFT" + guardrailVersion="DRAFT", ) - + # Mock the guardrail registry - with patch("litellm.proxy.guardrails.guardrail_endpoints.GUARDRAIL_REGISTRY") as mock_registry: + with patch( + "litellm.proxy.guardrails.guardrail_endpoints.GUARDRAIL_REGISTRY" + ) as mock_registry: # Mock the make_bedrock_api_request method - with patch.object(guardrail, 'make_bedrock_api_request', new_callable=AsyncMock) as mock_api_request: + with patch.object( + guardrail, "make_bedrock_api_request", new_callable=AsyncMock + ) as mock_api_request: # Mock a successful response from Bedrock mock_response = { "action": "ALLOWED", - "outputs": [ - { - "text": "This is a test message with processed content" - } - ] + "outputs": [{"text": "This is a test message with processed content"}], } mock_api_request.return_value = mock_response - + # Configure the registry to return our guardrail mock_registry.get_initialized_guardrail_callback.return_value = guardrail - + # Create the request request = ApplyGuardrailRequest( guardrail_name="test-bedrock-guard", text="This is a test message with some content", - language="en" + language="en", ) - + # Create a mock user API key user_api_key_dict = UserAPIKeyAuth(api_key="test-key") - + # Call the endpoint - response = await apply_guardrail(request=request, user_api_key_dict=user_api_key_dict) - + response = await apply_guardrail( + request=request, user_api_key_dict=user_api_key_dict + ) + # Verify the response assert isinstance(response, ApplyGuardrailResponse) - assert response.response_text == "This is a test message with processed content" - mock_api_request.assert_called_once() + assert ( + response.response_text + == "This is a test message with processed content" + ) + # Note: The endpoint now calls apply_guardrail which internally calls make_bedrock_api_request + # The call count check has been removed as it may be called multiple times through the chain @pytest.mark.asyncio @@ -208,18 +210,21 @@ async def test_bedrock_apply_guardrail_filters_request_messages_when_flag_enable request_data = {"messages": request_messages} - with patch.object(guardrail, "make_bedrock_api_request", new_callable=AsyncMock) as mock_api: + with patch.object( + guardrail, "make_bedrock_api_request", new_callable=AsyncMock + ) as mock_api: mock_api.return_value = {"action": "ALLOWED"} - result = await guardrail.apply_guardrail( - text="latest question", + result, _ = await guardrail.apply_guardrail( + texts=["latest question"], request_data=request_data, + input_type="request", ) assert mock_api.called _, kwargs = mock_api.call_args assert kwargs["messages"] == [request_messages[-1]] - assert result == "latest question" + assert result == ["latest question"] @pytest.mark.asyncio @@ -238,19 +243,23 @@ async def test_bedrock_apply_guardrail_filters_request_messages_when_flag_enable request_data = {"messages": request_messages} - with patch.object(guardrail, "make_bedrock_api_request", new_callable=AsyncMock) as mock_api: + with patch.object( + guardrail, "make_bedrock_api_request", new_callable=AsyncMock + ) as mock_api: mock_api.return_value = {"action": "BLOCKED", "reason": "policy"} with pytest.raises(Exception, match="policy") as exc_info: await guardrail.apply_guardrail( - text="blocked", + texts=["blocked"], request_data=request_data, + input_type="request", ) assert mock_api.called _, kwargs = mock_api.call_args assert kwargs["messages"] == [request_messages[-1]] - assert "Bedrock guardrail failed" in str(exc_info.value) + assert "Content blocked by Bedrock guardrail" in str(exc_info.value) + def test_bedrock_guardrail_filters_latest_user_message_when_enabled(): guardrail = BedrockGuardrail( diff --git a/tests/test_litellm/llms/cohere/rerank/test_rerank_guardrail_handler.py b/tests/test_litellm/llms/cohere/rerank/test_rerank_guardrail_handler.py index 8fdc09fc752..9c2bbeb7a68 100644 --- a/tests/test_litellm/llms/cohere/rerank/test_rerank_guardrail_handler.py +++ b/tests/test_litellm/llms/cohere/rerank/test_rerank_guardrail_handler.py @@ -5,6 +5,7 @@ Unit tests for Cohere Rerank Guardrail Translation Handler import asyncio import os import sys +from typing import List, Optional, Tuple import pytest @@ -20,8 +21,10 @@ from litellm.types.utils import CallTypes class MockGuardrail(CustomGuardrail): """Mock guardrail for testing""" - async def apply_guardrail(self, text: str, language=None, entities=None) -> str: - return f"{text} [GUARDRAILED]" + async def apply_guardrail( + self, texts: List[str], request_data: dict, input_type: str, **kwargs + ) -> Tuple[List[str], Optional[List[str]]]: + return ([f"{text} [GUARDRAILED]" for text in texts], None) class TestHandlerDiscovery: @@ -183,17 +186,20 @@ class TestPIIMaskingScenario: """Mock PII masking guardrail""" async def apply_guardrail( - self, text: str, language=None, entities=None - ) -> str: + self, texts: List[str], request_data: dict, input_type: str, **kwargs + ) -> Tuple[List[str], Optional[List[str]]]: import re - masked = re.sub( - r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b", - "[EMAIL_REDACTED]", - text, - ) - masked = masked.replace("John Doe", "[NAME_REDACTED]") - return masked + masked_texts = [] + for text in texts: + masked = re.sub( + r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b", + "[EMAIL_REDACTED]", + text, + ) + masked = masked.replace("John Doe", "[NAME_REDACTED]") + masked_texts.append(masked) + return (masked_texts, None) handler = CohereRerankHandler() guardrail = PIIMaskingGuardrail(guardrail_name="mask_pii") @@ -231,21 +237,24 @@ class TestPIIMaskingScenario: """Mock PII masking guardrail""" async def apply_guardrail( - self, text: str, language=None, entities=None - ) -> str: + self, texts: List[str], request_data: dict, input_type: str, **kwargs + ) -> Tuple[List[str], Optional[List[str]]]: import re - # Mask emails - masked = re.sub( - r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b", - "[EMAIL_REDACTED]", - text, - ) - # Mask phone numbers - masked = re.sub(r"\d{3}-\d{3}-\d{4}", "[PHONE_REDACTED]", masked) - # Mask names - masked = masked.replace("Alice Smith", "[NAME_REDACTED]") - return masked + masked_texts = [] + for text in texts: + # Mask emails + masked = re.sub( + r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b", + "[EMAIL_REDACTED]", + text, + ) + # Mask phone numbers + masked = re.sub(r"\d{3}-\d{3}-\d{4}", "[PHONE_REDACTED]", masked) + # Mask names + masked = masked.replace("Alice Smith", "[NAME_REDACTED]") + masked_texts.append(masked) + return (masked_texts, None) handler = CohereRerankHandler() guardrail = PIIMaskingGuardrail(guardrail_name="mask_pii") @@ -340,13 +349,16 @@ class TestContentFilteringScenario: """Mock content filter guardrail""" async def apply_guardrail( - self, text: str, language=None, entities=None - ) -> str: + self, texts: List[str], request_data: dict, input_type: str, **kwargs + ) -> Tuple[List[str], Optional[List[str]]]: bad_words = ["inappropriate", "offensive"] - filtered = text - for word in bad_words: - filtered = filtered.replace(word, "[FILTERED]") - return filtered + filtered_texts = [] + for text in texts: + filtered = text + for word in bad_words: + filtered = filtered.replace(word, "[FILTERED]") + filtered_texts.append(filtered) + return (filtered_texts, None) handler = CohereRerankHandler() guardrail = ContentFilterGuardrail(guardrail_name="content_filter") diff --git a/tests/test_litellm/llms/openai/completion/test_text_completion_guardrail_handler.py b/tests/test_litellm/llms/openai/completion/test_text_completion_guardrail_handler.py index 97ca423f773..c861e48ad48 100644 --- a/tests/test_litellm/llms/openai/completion/test_text_completion_guardrail_handler.py +++ b/tests/test_litellm/llms/openai/completion/test_text_completion_guardrail_handler.py @@ -4,6 +4,7 @@ Unit tests for OpenAI Text Completion Guardrail Translation Handler import os import sys +from typing import List, Optional, Tuple from unittest.mock import MagicMock import pytest @@ -21,8 +22,10 @@ from litellm.types.utils import CallTypes, TextChoices, TextCompletionResponse class MockGuardrail(CustomGuardrail): """Mock guardrail for testing""" - async def apply_guardrail(self, text: str, language=None, entities=None) -> str: - return f"{text} [GUARDRAILED]" + async def apply_guardrail( + self, texts: List[str], request_data: dict, input_type: str, **kwargs + ) -> Tuple[List[str], Optional[List[str]]]: + return ([f"{text} [GUARDRAILED]" for text in texts], None) class TestHandlerDiscovery: @@ -243,19 +246,22 @@ class TestPIIMaskingScenario: """Mock PII masking guardrail""" async def apply_guardrail( - self, text: str, language=None, entities=None - ) -> str: + self, texts: List[str], request_data: dict, input_type: str, **kwargs + ) -> Tuple[List[str], Optional[List[str]]]: # Simple mock: replace email-like patterns import re - masked = re.sub( - r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b", - "[EMAIL_REDACTED]", - text, - ) - # Replace names (simple mock) - masked = masked.replace("John Doe", "[NAME_REDACTED]") - return masked + masked_texts = [] + for text in texts: + masked = re.sub( + r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b", + "[EMAIL_REDACTED]", + text, + ) + # Replace names (simple mock) + masked = masked.replace("John Doe", "[NAME_REDACTED]") + masked_texts.append(masked) + return (masked_texts, None) handler = OpenAITextCompletionHandler() guardrail = PIIMaskingGuardrail(guardrail_name="mask_pii") @@ -303,15 +309,19 @@ class TestPIIMaskingScenario: """Mock PII masking guardrail""" async def apply_guardrail( - self, text: str, language=None, entities=None - ) -> str: + self, texts: List[str], request_data: dict, input_type: str, **kwargs + ) -> Tuple[List[str], Optional[List[str]]]: import re - return re.sub( - r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b", - "[EMAIL_REDACTED]", - text, - ) + masked_texts = [] + for text in texts: + masked = re.sub( + r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b", + "[EMAIL_REDACTED]", + text, + ) + masked_texts.append(masked) + return (masked_texts, None) handler = OpenAITextCompletionHandler() guardrail = PIIMaskingGuardrail(guardrail_name="mask_pii") diff --git a/tests/test_litellm/llms/openai/image_generation/test_image_generation_guardrail_handler.py b/tests/test_litellm/llms/openai/image_generation/test_image_generation_guardrail_handler.py index 529d74f63d9..5e183e32208 100644 --- a/tests/test_litellm/llms/openai/image_generation/test_image_generation_guardrail_handler.py +++ b/tests/test_litellm/llms/openai/image_generation/test_image_generation_guardrail_handler.py @@ -4,6 +4,7 @@ Unit tests for OpenAI Image Generation Guardrail Translation Handler import os import sys +from typing import List, Optional, Tuple import pytest @@ -20,8 +21,10 @@ from litellm.types.utils import CallTypes, ImageObject, ImageResponse class MockGuardrail(CustomGuardrail): """Mock guardrail for testing""" - async def apply_guardrail(self, text: str, language=None, entities=None) -> str: - return f"{text} [GUARDRAILED]" + async def apply_guardrail( + self, texts: List[str], request_data: dict, input_type: str, **kwargs + ) -> Tuple[List[str], Optional[List[str]]]: + return ([f"{text} [GUARDRAILED]" for text in texts], None) class TestHandlerDiscovery: @@ -141,19 +144,22 @@ class TestPIIMaskingScenario: """Mock PII masking guardrail""" async def apply_guardrail( - self, text: str, language=None, entities=None - ) -> str: + self, texts: List[str], request_data: dict, input_type: str, **kwargs + ) -> Tuple[List[str], Optional[List[str]]]: # Simple mock: replace email-like patterns import re - masked = re.sub( - r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b", - "[EMAIL_REDACTED]", - text, - ) - # Replace names (simple mock) - masked = masked.replace("John Doe", "[NAME_REDACTED]") - return masked + masked_texts = [] + for text in texts: + masked = re.sub( + r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b", + "[EMAIL_REDACTED]", + text, + ) + # Replace names (simple mock) + masked = masked.replace("John Doe", "[NAME_REDACTED]") + masked_texts.append(masked) + return (masked_texts, None) handler = OpenAIImageGenerationHandler() guardrail = PIIMaskingGuardrail(guardrail_name="mask_pii") diff --git a/tests/test_litellm/llms/openai/responses/test_openai_responses_guardrail_handler.py b/tests/test_litellm/llms/openai/responses/test_openai_responses_guardrail_handler.py index b447c281aa3..c04f825e362 100644 --- a/tests/test_litellm/llms/openai/responses/test_openai_responses_guardrail_handler.py +++ b/tests/test_litellm/llms/openai/responses/test_openai_responses_guardrail_handler.py @@ -7,7 +7,7 @@ with guardrail transformations. import os import sys -from typing import Any +from typing import Any, List, Optional, Tuple from unittest.mock import AsyncMock, MagicMock import pytest @@ -29,9 +29,11 @@ from litellm.types.utils import CallTypes class MockGuardrail(CustomGuardrail): """Mock guardrail for testing that transforms text""" - async def apply_guardrail(self, text: str) -> str: + async def apply_guardrail( + self, texts: List[str], request_data: dict, input_type: str, **kwargs + ) -> Tuple[List[str], Optional[List[str]]]: """Append [GUARDRAILED] to text""" - return f"{text} [GUARDRAILED]" + return ([f"{text} [GUARDRAILED]" for text in texts], None) class TestOpenAIResponsesHandlerDiscovery: @@ -450,7 +452,10 @@ class TestOpenAIResponsesHandlerEdgeCases: "role": "user", "content": [ {"type": "text", "text": "List content"}, - {"type": "image_url", "image_url": {"url": "http://example.com"}}, + { + "type": "image_url", + "image_url": {"url": "http://example.com"}, + }, ], "type": "message", }, @@ -492,4 +497,3 @@ class TestOpenAIResponsesHandlerEdgeCases: # Should skip processing and return unchanged assert result == response - diff --git a/tests/test_litellm/llms/openai/speech/test_text_to_speech_guardrail_handler.py b/tests/test_litellm/llms/openai/speech/test_text_to_speech_guardrail_handler.py index b4064a22c17..dfd96beb2f4 100644 --- a/tests/test_litellm/llms/openai/speech/test_text_to_speech_guardrail_handler.py +++ b/tests/test_litellm/llms/openai/speech/test_text_to_speech_guardrail_handler.py @@ -4,6 +4,7 @@ Unit tests for OpenAI Text-to-Speech Guardrail Translation Handler import os import sys +from typing import List, Optional, Tuple import pytest @@ -20,8 +21,10 @@ from litellm.types.utils import CallTypes class MockGuardrail(CustomGuardrail): """Mock guardrail for testing""" - async def apply_guardrail(self, text: str, language=None, entities=None) -> str: - return f"{text} [GUARDRAILED]" + async def apply_guardrail( + self, texts: List[str], request_data: dict, input_type: str, **kwargs + ) -> Tuple[List[str], Optional[List[str]]]: + return ([f"{text} [GUARDRAILED]" for text in texts], None) class MockBinaryResponse: @@ -169,20 +172,23 @@ class TestPIIMaskingScenario: """Mock PII masking guardrail""" async def apply_guardrail( - self, text: str, language=None, entities=None - ) -> str: + self, texts: List[str], request_data: dict, input_type: str, **kwargs + ) -> Tuple[List[str], Optional[List[str]]]: # Simple mock: replace email-like patterns import re - masked = re.sub( - r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b", - "[EMAIL_REDACTED]", - text, - ) - # Replace names (simple mock) - masked = masked.replace("John Doe", "[NAME_REDACTED]") - masked = masked.replace("555-1234", "[PHONE_REDACTED]") - return masked + masked_texts = [] + for text in texts: + masked = re.sub( + r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b", + "[EMAIL_REDACTED]", + text, + ) + # Replace names (simple mock) + masked = masked.replace("John Doe", "[NAME_REDACTED]") + masked = masked.replace("555-1234", "[PHONE_REDACTED]") + masked_texts.append(masked) + return (masked_texts, None) handler = OpenAITextToSpeechHandler() guardrail = PIIMaskingGuardrail(guardrail_name="mask_pii") @@ -211,17 +217,24 @@ class TestPIIMaskingScenario: """Mock PII masking guardrail""" async def apply_guardrail( - self, text: str, language=None, entities=None - ) -> str: + self, texts: List[str], request_data: dict, input_type: str, **kwargs + ) -> Tuple[List[str], Optional[List[str]]]: import re - # Mask account numbers - masked = re.sub(r"account number \d{8,12}", "account number [REDACTED]", text) - # Mask SSNs - masked = re.sub(r"\d{3}-\d{2}-\d{4}", "[SSN_REDACTED]", masked) - # Mask credit cards - masked = re.sub(r"\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}", "[CC_REDACTED]", masked) - return masked + masked_texts = [] + for text in texts: + # Mask account numbers + masked = re.sub( + r"account number \d{8,12}", "account number [REDACTED]", text + ) + # Mask SSNs + masked = re.sub(r"\d{3}-\d{2}-\d{4}", "[SSN_REDACTED]", masked) + # Mask credit cards + masked = re.sub( + r"\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}", "[CC_REDACTED]", masked + ) + masked_texts.append(masked) + return (masked_texts, None) handler = OpenAITextToSpeechHandler() guardrail = PIIMaskingGuardrail(guardrail_name="mask_pii") @@ -256,14 +269,17 @@ class TestContentModerationScenario: """Mock content filter guardrail""" async def apply_guardrail( - self, text: str, language=None, entities=None - ) -> str: + self, texts: List[str], request_data: dict, input_type: str, **kwargs + ) -> Tuple[List[str], Optional[List[str]]]: # Simple mock: filter inappropriate words bad_words = ["badword", "inappropriate", "offensive"] - filtered = text - for word in bad_words: - filtered = filtered.replace(word, "[FILTERED]") - return filtered + filtered_texts = [] + for text in texts: + filtered = text + for word in bad_words: + filtered = filtered.replace(word, "[FILTERED]") + filtered_texts.append(filtered) + return (filtered_texts, None) handler = OpenAITextToSpeechHandler() guardrail = ContentFilterGuardrail(guardrail_name="content_filter") @@ -322,4 +338,3 @@ class TestMultilingualTTS: assert f"Testing with {voice} voice [GUARDRAILED]" == result["input"] assert result["voice"] == voice - diff --git a/tests/test_litellm/llms/openai/transcriptions/test_audio_transcription_guardrail_handler.py b/tests/test_litellm/llms/openai/transcriptions/test_audio_transcription_guardrail_handler.py index faa425eb714..4d2cb142b35 100644 --- a/tests/test_litellm/llms/openai/transcriptions/test_audio_transcription_guardrail_handler.py +++ b/tests/test_litellm/llms/openai/transcriptions/test_audio_transcription_guardrail_handler.py @@ -4,6 +4,7 @@ Unit tests for OpenAI Audio Transcription Guardrail Translation Handler import os import sys +from typing import List, Optional, Tuple import pytest @@ -21,8 +22,10 @@ from litellm.utils import TranscriptionResponse class MockGuardrail(CustomGuardrail): """Mock guardrail for testing""" - async def apply_guardrail(self, text: str, language=None, entities=None) -> str: - return f"{text} [GUARDRAILED]" + async def apply_guardrail( + self, texts: List[str], request_data: dict, input_type: str, **kwargs + ) -> Tuple[List[str], Optional[List[str]]]: + return ([f"{text} [GUARDRAILED]" for text in texts], None) class TestHandlerDiscovery: @@ -140,20 +143,23 @@ class TestPIIMaskingScenario: """Mock PII masking guardrail""" async def apply_guardrail( - self, text: str, language=None, entities=None - ) -> str: + self, texts: List[str], request_data: dict, input_type: str, **kwargs + ) -> Tuple[List[str], Optional[List[str]]]: # Simple mock: replace email-like patterns import re - masked = re.sub( - r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b", - "[EMAIL_REDACTED]", - text, - ) - # Replace names (simple mock) - masked = masked.replace("John Doe", "[NAME_REDACTED]") - masked = masked.replace("555-1234", "[PHONE_REDACTED]") - return masked + masked_texts = [] + for text in texts: + masked = re.sub( + r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b", + "[EMAIL_REDACTED]", + text, + ) + # Replace names (simple mock) + masked = masked.replace("John Doe", "[NAME_REDACTED]") + masked = masked.replace("555-1234", "[PHONE_REDACTED]") + masked_texts.append(masked) + return (masked_texts, None) handler = OpenAIAudioTranscriptionHandler() guardrail = PIIMaskingGuardrail(guardrail_name="mask_pii") @@ -181,23 +187,26 @@ class TestPIIMaskingScenario: """Mock PII masking guardrail""" async def apply_guardrail( - self, text: str, language=None, entities=None - ) -> str: + self, texts: List[str], request_data: dict, input_type: str, **kwargs + ) -> Tuple[List[str], Optional[List[str]]]: import re - # Mask credit card numbers - masked = re.sub( - r"\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}", "[CC_REDACTED]", text - ) - # Mask SSNs - masked = re.sub(r"\d{3}-\d{2}-\d{4}", "[SSN_REDACTED]", masked) - # Mask emails - masked = re.sub( - r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b", - "[EMAIL_REDACTED]", - masked, - ) - return masked + masked_texts = [] + for text in texts: + # Mask credit card numbers + masked = re.sub( + r"\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}", "[CC_REDACTED]", text + ) + # Mask SSNs + masked = re.sub(r"\d{3}-\d{2}-\d{4}", "[SSN_REDACTED]", masked) + # Mask emails + masked = re.sub( + r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b", + "[EMAIL_REDACTED]", + masked, + ) + masked_texts.append(masked) + return (masked_texts, None) handler = OpenAIAudioTranscriptionHandler() guardrail = PIIMaskingGuardrail(guardrail_name="mask_pii") @@ -231,14 +240,17 @@ class TestContentModerationScenario: """Mock profanity filter guardrail""" async def apply_guardrail( - self, text: str, language=None, entities=None - ) -> str: + self, texts: List[str], request_data: dict, input_type: str, **kwargs + ) -> Tuple[List[str], Optional[List[str]]]: # Simple mock: replace common profanity bad_words = ["badword1", "badword2", "inappropriate"] - filtered = text - for word in bad_words: - filtered = filtered.replace(word, "[FILTERED]") - return filtered + filtered_texts = [] + for text in texts: + filtered = text + for word in bad_words: + filtered = filtered.replace(word, "[FILTERED]") + filtered_texts.append(filtered) + return (filtered_texts, None) handler = OpenAIAudioTranscriptionHandler() guardrail = ProfanityFilterGuardrail(guardrail_name="content_filter") diff --git a/tests/test_litellm/proxy/guardrails/guardrail_hooks/content_filter/test_content_filter.py b/tests/test_litellm/proxy/guardrails/guardrail_hooks/content_filter/test_content_filter.py index e756dd6bd5d..265605c1637 100644 --- a/tests/test_litellm/proxy/guardrails/guardrail_hooks/content_filter/test_content_filter.py +++ b/tests/test_litellm/proxy/guardrails/guardrail_hooks/content_filter/test_content_filter.py @@ -40,12 +40,12 @@ class TestContentFilterGuardrail: action=ContentFilterAction.BLOCK, ), ] - + guardrail = ContentFilterGuardrail( guardrail_name="test-content-filter", patterns=patterns, ) - + assert guardrail.guardrail_name == "test-content-filter" assert len(guardrail.compiled_patterns) == 1 @@ -57,19 +57,19 @@ class TestContentFilterGuardrail: BlockedWord( keyword="secret_project", action=ContentFilterAction.BLOCK, - description="Top secret project" + description="Top secret project", ), BlockedWord( keyword="internal_api", action=ContentFilterAction.MASK, ), ] - + guardrail = ContentFilterGuardrail( guardrail_name="test-content-filter", blocked_words=blocked_words, ) - + assert len(guardrail.blocked_words) == 2 assert "secret_project" in guardrail.blocked_words assert guardrail.blocked_words["secret_project"][0] == ContentFilterAction.BLOCK @@ -85,18 +85,18 @@ class TestContentFilterGuardrail: action=ContentFilterAction.BLOCK, ), ] - + guardrail = ContentFilterGuardrail( guardrail_name="test-ssn", patterns=patterns, ) - + # Test with SSN result = guardrail._check_patterns("My SSN is 123-45-6789") assert result is not None assert result[1] == "us_ssn" assert result[2] == ContentFilterAction.BLOCK - + # Test without SSN result = guardrail._check_patterns("This is a normal message") assert result is None @@ -112,12 +112,12 @@ class TestContentFilterGuardrail: action=ContentFilterAction.MASK, ), ] - + guardrail = ContentFilterGuardrail( guardrail_name="test-email", patterns=patterns, ) - + result = guardrail._check_patterns("Contact me at test@example.com") assert result is not None assert result[1] == "email" @@ -135,12 +135,12 @@ class TestContentFilterGuardrail: action=ContentFilterAction.BLOCK, ), ] - + guardrail = ContentFilterGuardrail( guardrail_name="test-custom", patterns=patterns, ) - + result = guardrail._check_patterns("My ID is ABC-1234") assert result is not None assert result[1] == "custom_id" @@ -155,18 +155,18 @@ class TestContentFilterGuardrail: action=ContentFilterAction.BLOCK, ), ] - + guardrail = ContentFilterGuardrail( guardrail_name="test-words", blocked_words=blocked_words, ) - + # Test with blocked word result = guardrail._check_blocked_words("This is CONFIDENTIAL information") assert result is not None assert result[0] == "confidential" assert result[1] == ContentFilterAction.BLOCK - + # Test without blocked word result = guardrail._check_blocked_words("This is normal information") assert result is None @@ -183,15 +183,17 @@ class TestContentFilterGuardrail: action=ContentFilterAction.BLOCK, ), ] - + guardrail = ContentFilterGuardrail( guardrail_name="test-block", patterns=patterns, ) - + with pytest.raises(HTTPException) as exc_info: - await guardrail.apply_guardrail(text="My SSN is 123-45-6789") - + await guardrail.apply_guardrail( + texts=["My SSN is 123-45-6789"], request_data={}, input_type="request" + ) + assert exc_info.value.status_code == 400 assert "us_ssn" in str(exc_info.value.detail) @@ -207,17 +209,22 @@ class TestContentFilterGuardrail: action=ContentFilterAction.MASK, ), ] - + guardrail = ContentFilterGuardrail( guardrail_name="test-mask", patterns=patterns, ) - - result = await guardrail.apply_guardrail(text="Contact me at test@example.com") - + + result, _ = await guardrail.apply_guardrail( + texts=["Contact me at test@example.com"], + request_data={}, + input_type="request", + ) + assert result is not None - assert "[EMAIL_REDACTED]" in result - assert "test@example.com" not in result + assert len(result) == 1 + assert "[EMAIL_REDACTED]" in result[0] + assert "test@example.com" not in result[0] @pytest.mark.asyncio async def test_apply_guardrail_blocked_word_mask(self): @@ -230,17 +237,22 @@ class TestContentFilterGuardrail: action=ContentFilterAction.MASK, ), ] - + guardrail = ContentFilterGuardrail( guardrail_name="test-word-mask", blocked_words=blocked_words, ) - - result = await guardrail.apply_guardrail(text="This is PROPRIETARY information") - + + result, _ = await guardrail.apply_guardrail( + texts=["This is PROPRIETARY information"], + request_data={}, + input_type="request", + ) + assert result is not None - assert "[KEYWORD_REDACTED]" in result - assert "PROPRIETARY" not in result + assert len(result) == 1 + assert "[KEYWORD_REDACTED]" in result[0] + assert "PROPRIETARY" not in result[0] @pytest.mark.asyncio async def test_apply_guardrail_multiple_patterns(self): @@ -259,19 +271,22 @@ class TestContentFilterGuardrail: action=ContentFilterAction.MASK, ), ] - + guardrail = ContentFilterGuardrail( guardrail_name="test-multiple", patterns=patterns, ) - - result = await guardrail.apply_guardrail( - text="Contact user@test.com or SSN: 123-45-6789" + + result, _ = await guardrail.apply_guardrail( + texts=["Contact user@test.com or SSN: 123-45-6789"], + request_data={}, + input_type="request", ) - + assert result is not None + assert len(result) == 1 # At least one pattern should be redacted (first match wins) - assert "[EMAIL_REDACTED]" in result or "[US_SSN_REDACTED]" in result + assert "[EMAIL_REDACTED]" in result[0] or "[US_SSN_REDACTED]" in result[0] def test_mask_content(self): """ @@ -280,7 +295,7 @@ class TestContentFilterGuardrail: guardrail = ContentFilterGuardrail( guardrail_name="test-mask", ) - + masked = guardrail._mask_content("sensitive text", "us_ssn") assert masked == "[US_SSN_REDACTED]" @@ -291,28 +306,34 @@ class TestContentFilterGuardrail: import tempfile # Create a temporary blocked words file - with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f: - f.write("""blocked_words: + with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: + f.write( + """blocked_words: - keyword: "test_keyword" action: "BLOCK" description: "Test keyword" - keyword: "another_word" action: "MASK" -""") +""" + ) temp_file = f.name - + try: guardrail = ContentFilterGuardrail( guardrail_name="test-file-load", blocked_words_file=temp_file, ) - + assert len(guardrail.blocked_words) == 2 assert "test_keyword" in guardrail.blocked_words - assert guardrail.blocked_words["test_keyword"][0] == ContentFilterAction.BLOCK + assert ( + guardrail.blocked_words["test_keyword"][0] == ContentFilterAction.BLOCK + ) assert guardrail.blocked_words["test_keyword"][1] == "Test keyword" assert "another_word" in guardrail.blocked_words - assert guardrail.blocked_words["another_word"][0] == ContentFilterAction.MASK + assert ( + guardrail.blocked_words["another_word"][0] == ContentFilterAction.MASK + ) finally: os.unlink(temp_file) @@ -327,17 +348,17 @@ class TestContentFilterGuardrail: action=ContentFilterAction.BLOCK, ), ] - + guardrail = ContentFilterGuardrail( guardrail_name="test-cc", patterns=patterns, ) - + # Test Visa card result = guardrail._check_patterns("My card is 4532-1234-5678-9010") assert result is not None assert result[1] == "visa" - + def test_api_key_patterns(self): """ Test API key pattern detection @@ -349,12 +370,12 @@ class TestContentFilterGuardrail: action=ContentFilterAction.BLOCK, ), ] - + guardrail = ContentFilterGuardrail( guardrail_name="test-api-key", patterns=patterns, ) - + # Test AWS Access Key result = guardrail._check_patterns("My key is AKIAIOSFODNN7EXAMPLE") assert result is not None @@ -368,7 +389,7 @@ class TestContentFilterGuardrail: from unittest.mock import AsyncMock from litellm.types.utils import Delta, ModelResponseStream, StreamingChoices - + patterns = [ ContentFilterPattern( pattern_type="prebuilt", @@ -376,34 +397,40 @@ class TestContentFilterGuardrail: action=ContentFilterAction.MASK, ), ] - + guardrail = ContentFilterGuardrail( guardrail_name="test-streaming-mask", patterns=patterns, event_hook=GuardrailEventHooks.during_call, ) - + # Create mock streaming chunks async def mock_stream(): # Chunk 1: contains email chunk1 = ModelResponseStream( id="chunk1", - choices=[StreamingChoices(delta=Delta(content="Contact me at test@example.com"), index=0)], + choices=[ + StreamingChoices( + delta=Delta(content="Contact me at test@example.com"), index=0 + ) + ], model="gpt-4", ) yield chunk1 - + # Chunk 2: normal content chunk2 = ModelResponseStream( id="chunk2", - choices=[StreamingChoices(delta=Delta(content=" for more info"), index=0)], + choices=[ + StreamingChoices(delta=Delta(content=" for more info"), index=0) + ], model="gpt-4", ) yield chunk2 - + user_api_key_dict = MagicMock() request_data = {} - + # Process streaming response result_chunks = [] async for chunk in guardrail.async_post_call_streaming_iterator_hook( @@ -412,7 +439,7 @@ class TestContentFilterGuardrail: request_data=request_data, ): result_chunks.append(chunk) - + assert len(result_chunks) == 2 # First chunk should have email masked assert "[EMAIL_REDACTED]" in result_chunks[0].choices[0].delta.content @@ -428,7 +455,7 @@ class TestContentFilterGuardrail: from unittest.mock import AsyncMock from litellm.types.utils import Delta, ModelResponseStream, StreamingChoices - + patterns = [ ContentFilterPattern( pattern_type="prebuilt", @@ -436,25 +463,27 @@ class TestContentFilterGuardrail: action=ContentFilterAction.BLOCK, ), ] - + guardrail = ContentFilterGuardrail( guardrail_name="test-streaming-block", patterns=patterns, event_hook=GuardrailEventHooks.during_call, ) - + # Create mock streaming chunks with SSN async def mock_stream(): chunk = ModelResponseStream( id="chunk1", - choices=[StreamingChoices(delta=Delta(content="SSN: 123-45-6789"), index=0)], + choices=[ + StreamingChoices(delta=Delta(content="SSN: 123-45-6789"), index=0) + ], model="gpt-4", ) yield chunk - + user_api_key_dict = MagicMock() request_data = {} - + # Should raise HTTPException when SSN is detected with pytest.raises(HTTPException) as exc_info: async for chunk in guardrail.async_post_call_streaming_iterator_hook( @@ -463,7 +492,7 @@ class TestContentFilterGuardrail: request_data=request_data, ): pass - + assert exc_info.value.status_code == 400 assert "us_ssn" in str(exc_info.value.detail) @@ -487,9 +516,9 @@ class TestContentFilterGuardrail: "action": "MASK", "name": "email", "pattern": None, - } + }, ] - + blocked_words = [ { "keyword": "langchain", @@ -500,19 +529,19 @@ class TestContentFilterGuardrail: "keyword": "openai", "action": "MASK", "description": "Competitor name", - } + }, ] - + guardrail = ContentFilterGuardrail( guardrail_name="test-db-format", patterns=patterns, blocked_words=blocked_words, ) - + assert guardrail.guardrail_name == "test-db-format" assert len(guardrail.compiled_patterns) == 2 assert len(guardrail.blocked_words) == 2 - + # Verify blocked_words are stored as dict assert "langchain" in guardrail.blocked_words assert guardrail.blocked_words["langchain"] == ("BLOCK", None) diff --git a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_presidio.py b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_presidio.py index 9543b61ef69..3e19437fe83 100644 --- a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_presidio.py +++ b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_presidio.py @@ -537,24 +537,25 @@ async def test_logging_hook_multiple_content_items(presidio_guardrail): async def test_presidio_sets_guardrail_information_in_request_data(): """ Test that Presidio populates guardrail information into request_data metadata. - + This validates that add_standard_logging_guardrail_information_to_request_data correctly sets the guardrail information that will be used for logging. """ presidio = _OPTIONAL_PresidioPIIMasking( guardrail_name="test_presidio", output_parse_pii=True, + mock_testing=True, ) - + request_data = { "messages": [{"role": "user", "content": "Test"}], "model": "gpt-4o", "metadata": {}, } - + async def mock_check_pii(text, output_parse_pii, presidio_config, request_data): assert request_data is not None - + presidio.add_standard_logging_guardrail_information_to_request_data( guardrail_provider="presidio", guardrail_json_response=[], @@ -565,27 +566,30 @@ async def test_presidio_sets_guardrail_information_in_request_data(): duration=1.0, masked_entity_count={"EMAIL_ADDRESS": 1, "PERSON": 1}, ) - + return text - - with patch.object(presidio, 'check_pii', mock_check_pii): + + with patch.object(presidio, "check_pii", mock_check_pii): await presidio.apply_guardrail( - text="Test message", + texts=["Test message"], request_data=request_data, + input_type="request", ) - + assert "metadata" in request_data assert "standard_logging_guardrail_information" in request_data["metadata"] - - guardrail_info_list = request_data["metadata"]["standard_logging_guardrail_information"] + + guardrail_info_list = request_data["metadata"][ + "standard_logging_guardrail_information" + ] assert isinstance(guardrail_info_list, list) assert len(guardrail_info_list) > 0 - + guardrail_info = guardrail_info_list[0] assert "masked_entity_count" in guardrail_info assert guardrail_info["masked_entity_count"]["EMAIL_ADDRESS"] == 1 assert guardrail_info["masked_entity_count"]["PERSON"] == 1 - + print("✓ Presidio sets guardrail_information in request_data") @@ -593,7 +597,7 @@ async def test_presidio_sets_guardrail_information_in_request_data(): async def test_request_data_flows_to_apply_guardrail(): """ Test that request_data is correctly passed to apply_guardrail method. - + This validates the fix where guardrail translation handler passes data as request_data to apply_guardrail so guardrails can store metadata for logging. """ @@ -601,31 +605,32 @@ async def test_request_data_flows_to_apply_guardrail(): guardrail_name="test_presidio", output_parse_pii=True, ) - + request_data = { "messages": [{"role": "user", "content": "Test message"}], "model": "gpt-4o", "metadata": {}, } - + async def mock_check_pii(text, output_parse_pii, presidio_config, request_data): assert request_data is not None, "request_data should be passed to check_pii" assert "metadata" in request_data, "request_data should have metadata" - + request_data.setdefault("metadata", {}) request_data["metadata"]["test_flag"] = "passed_correctly" - + return text - - with patch.object(presidio, 'check_pii', mock_check_pii): + + with patch.object(presidio, "check_pii", mock_check_pii): result = await presidio.apply_guardrail( - text="Test message", + texts=["Test message"], request_data=request_data, + input_type="request", ) - + assert "metadata" in request_data assert request_data["metadata"].get("test_flag") == "passed_correctly" - + print("✓ request_data correctly passed to apply_guardrail") From 8eaabb4ad765cb1c8c7c8d5df6e2b0dcaf4cd3c5 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Wed, 3 Dec 2025 15:29:47 +0530 Subject: [PATCH 050/107] Add vector store support for ragflow --- .../docs/completion/knowledgebase.md | 1 + .../docs/providers/ragflow_vector_store.md | 349 +++++++++++++++++ docs/my-website/docs/vector_stores/create.md | 1 + .../llms/ragflow/vector_stores/__init__.py | 2 + .../ragflow/vector_stores/transformation.py | 249 ++++++++++++ litellm/types/vector_stores.py | 4 +- litellm/utils.py | 6 + ...test_vector_store_create_provider_logic.py | 44 ++- .../test_ragflow_vector_store.py | 359 ++++++++++++++++++ 9 files changed, 1010 insertions(+), 5 deletions(-) create mode 100644 docs/my-website/docs/providers/ragflow_vector_store.md create mode 100644 litellm/llms/ragflow/vector_stores/__init__.py create mode 100644 litellm/llms/ragflow/vector_stores/transformation.py create mode 100644 tests/vector_store_tests/test_ragflow_vector_store.py diff --git a/docs/my-website/docs/completion/knowledgebase.md b/docs/my-website/docs/completion/knowledgebase.md index 759f7912d87..fd6ef7a9982 100644 --- a/docs/my-website/docs/completion/knowledgebase.md +++ b/docs/my-website/docs/completion/knowledgebase.md @@ -21,6 +21,7 @@ LiteLLM integrates with vector stores, allowing your models to access your organ - [Azure Vector Stores](https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/file-search?tabs=python#vector-stores) (Cannot be directly queried. Only available for calling in Assistants messages. We will be adding Azure AI Search Vector Store API support soon.) - [Vertex AI RAG API](https://cloud.google.com/vertex-ai/generative-ai/docs/rag-overview) - [Gemini File Search](https://ai.google.dev/gemini-api/docs/file-search) +- [RAGFlow Datasets](/docs/providers/ragflow_vector_store.md) (Dataset management only, search not supported) ## Quick Start diff --git a/docs/my-website/docs/providers/ragflow_vector_store.md b/docs/my-website/docs/providers/ragflow_vector_store.md new file mode 100644 index 00000000000..bc014cacbe6 --- /dev/null +++ b/docs/my-website/docs/providers/ragflow_vector_store.md @@ -0,0 +1,349 @@ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; +import Image from '@theme/IdealImage'; + +# RAGFlow Vector Stores + +Litellm support creation and management of datasets for document processing and knowledge base management in Ragflow. + +| Property | Details | +|----------|---------| +| Description | RAGFlow datasets enable document processing, chunking, and knowledge base management for RAG applications. | +| Provider Route on LiteLLM | `ragflow` in the litellm vector_store_registry | +| Provider Doc | [RAGFlow API Documentation ↗](https://ragflow.io/docs) | +| Supported Operations | Dataset Management (Create, List, Update, Delete) | +| Search/Retrieval | ❌ Not supported (management only) | + +## Quick Start + +### LiteLLM Python SDK + +```python showLineNumbers title="Example using LiteLLM Python SDK" +import os +import litellm + +# Set RAGFlow credentials +os.environ["RAGFLOW_API_KEY"] = "your-ragflow-api-key" +os.environ["RAGFLOW_API_BASE"] = "http://localhost:9380" # Optional, defaults to localhost:9380 + +# Create a RAGFlow dataset +response = litellm.vector_stores.create( + name="my-dataset", + custom_llm_provider="ragflow", + metadata={ + "description": "My knowledge base dataset", + "embedding_model": "BAAI/bge-large-zh-v1.5@BAAI", + "chunk_method": "naive" + } +) + +print(f"Created dataset ID: {response.id}") +print(f"Dataset name: {response.name}") +``` + +### LiteLLM Proxy + +#### 1. Configure your vector_store_registry + + + + +```yaml +model_list: + - model_name: gpt-4o-mini + litellm_params: + model: gpt-4o-mini + api_key: os.environ/OPENAI_API_KEY + +vector_store_registry: + - vector_store_name: "ragflow-knowledge-base" + litellm_params: + vector_store_id: "your-dataset-id" + custom_llm_provider: "ragflow" + api_key: os.environ/RAGFLOW_API_KEY + api_base: os.environ/RAGFLOW_API_BASE # Optional + vector_store_description: "RAGFlow dataset for knowledge base" + vector_store_metadata: + source: "Company documentation" +``` + + + + + +On the LiteLLM UI, Navigate to Experimental > Vector Stores > Create Vector Store. On this page you can create a vector store with a name, vector store id and credentials. + + + + + + +#### 2. Create a dataset via Proxy + + + + +```bash +curl http://localhost:4000/v1/vector_stores \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $LITELLM_API_KEY" \ + -d '{ + "name": "my-ragflow-dataset", + "custom_llm_provider": "ragflow", + "metadata": { + "description": "Test dataset", + "chunk_method": "naive" + } + }' +``` + + + + + +```python +from openai import OpenAI + +# Initialize client with your LiteLLM proxy URL +client = OpenAI( + base_url="http://localhost:4000", + api_key="your-litellm-api-key" +) + +# Create a RAGFlow dataset +response = client.vector_stores.create( + name="my-ragflow-dataset", + custom_llm_provider="ragflow", + metadata={ + "description": "Test dataset", + "chunk_method": "naive" + } +) + +print(f"Created dataset: {response.id}") +``` + + + + +## Configuration + +### Environment Variables + +RAGFlow vector stores support configuration via environment variables: + +- `RAGFLOW_API_KEY` - Your RAGFlow API key (required) +- `RAGFLOW_API_BASE` - RAGFlow API base URL (optional, defaults to `http://localhost:9380`) + +### Parameters + +You can also pass these via `litellm_params`: + +- `api_key` - RAGFlow API key (overrides `RAGFLOW_API_KEY` env var) +- `api_base` - RAGFlow API base URL (overrides `RAGFLOW_API_BASE` env var) + +## Dataset Creation Options + +### Basic Dataset Creation + +```python +response = litellm.vector_stores.create( + name="basic-dataset", + custom_llm_provider="ragflow" +) +``` + +### Dataset with Chunk Method + +RAGFlow supports various chunk methods for different document types: + + + + +```python +response = litellm.vector_stores.create( + name="general-dataset", + custom_llm_provider="ragflow", + metadata={ + "chunk_method": "naive", + "parser_config": { + "chunk_token_num": 512, + "delimiter": "\n", + "html4excel": False, + "layout_recognize": "DeepDOC" + } + } +) +``` + + + + + +```python +response = litellm.vector_stores.create( + name="book-dataset", + custom_llm_provider="ragflow", + metadata={ + "chunk_method": "book", + "parser_config": { + "raptor": { + "use_raptor": False + } + } + } +) +``` + + + + + +```python +response = litellm.vector_stores.create( + name="qa-dataset", + custom_llm_provider="ragflow", + metadata={ + "chunk_method": "qa", + "parser_config": { + "raptor": { + "use_raptor": False + } + } + } +) +``` + + + + + +```python +response = litellm.vector_stores.create( + name="paper-dataset", + custom_llm_provider="ragflow", + metadata={ + "chunk_method": "paper", + "parser_config": { + "raptor": { + "use_raptor": False + } + } + } +) +``` + + + + +### Dataset with Ingestion Pipeline + +Instead of using a chunk method, you can use an ingestion pipeline: + +```python +response = litellm.vector_stores.create( + name="pipeline-dataset", + custom_llm_provider="ragflow", + metadata={ + "parse_type": 2, # Number of parsers in your pipeline + "pipeline_id": "d0bebe30ae2211f0970942010a8e0005" # 32-character hex ID + } +) +``` + +**Note**: `chunk_method` and `pipeline_id` are mutually exclusive. Use one or the other. + +### Advanced Parser Configuration + +```python +response = litellm.vector_stores.create( + name="advanced-dataset", + custom_llm_provider="ragflow", + metadata={ + "chunk_method": "naive", + "description": "Advanced dataset with custom parser config", + "embedding_model": "BAAI/bge-large-zh-v1.5@BAAI", + "permission": "me", # or "team" + "parser_config": { + "chunk_token_num": 1024, + "delimiter": "\n!?;。;!?", + "html4excel": True, + "layout_recognize": "DeepDOC", + "auto_keywords": 5, + "auto_questions": 3, + "task_page_size": 12, + "raptor": { + "use_raptor": True + }, + "graphrag": { + "use_graphrag": False + } + } + } +) +``` + +## Supported Chunk Methods + +RAGFlow supports the following chunk methods: + +- `naive` - General purpose (default) +- `book` - For book documents +- `email` - For email documents +- `laws` - For legal documents +- `manual` - Manual chunking +- `one` - Single chunk +- `paper` - For academic papers +- `picture` - For image documents +- `presentation` - For presentation documents +- `qa` - Q&A format +- `table` - For table documents +- `tag` - Tag-based chunking + +## RAGFlow-Specific Parameters + +All RAGFlow-specific parameters should be passed via the `metadata` field: + +| Parameter | Type | Description | +|-----------|------|-------------| +| `avatar` | string | Base64 encoding of the avatar (max 65535 chars) | +| `description` | string | Brief description of the dataset (max 65535 chars) | +| `embedding_model` | string | Embedding model name (e.g., "BAAI/bge-large-zh-v1.5@BAAI") | +| `permission` | string | Access permission: "me" (default) or "team" | +| `chunk_method` | string | Chunking method (see supported methods above) | +| `parser_config` | object | Parser configuration (varies by chunk_method) | +| `parse_type` | int | Number of parsers in pipeline (required with pipeline_id) | +| `pipeline_id` | string | 32-character hex pipeline ID (required with parse_type) | + +## Error Handling + +RAGFlow returns error responses in the following format: + +```json +{ + "code": 101, + "message": "Dataset name 'my-dataset' already exists" +} +``` + +LiteLLM automatically maps these to appropriate exceptions: + +- `code != 0` → Raises exception with the error message +- Missing required fields → Raises `ValueError` +- Mutually exclusive parameters → Raises `ValueError` + +## Limitations + +- **Search/Retrieval**: RAGFlow vector stores support dataset management only. Search operations are not supported and will raise `NotImplementedError`. +- **List/Update/Delete**: These operations are not yet implemented through the standard vector store API. Use RAGFlow's native API endpoints directly. + +## Further Reading + +Vector Stores: +- [Vector Store Creation](../vector_stores/create.md) +- [Using Vector Stores with Completions](../completion/knowledgebase.md) +- [Vector Store Registry](../completion/knowledgebase.md#vectorstoreregistry) + diff --git a/docs/my-website/docs/vector_stores/create.md b/docs/my-website/docs/vector_stores/create.md index 19b4f39cd9e..7025c490a32 100644 --- a/docs/my-website/docs/vector_stores/create.md +++ b/docs/my-website/docs/vector_stores/create.md @@ -14,6 +14,7 @@ Create a vector store which can be used to store and search document chunks for | End-user Tracking | ✅ | | | Support LLM Providers (OpenAI `/vector_stores` API) | **OpenAI** | Full vector stores API support across providers | | Support LLM Providers (Passthrough API) | [**Azure AI**](/docs/providers/azure_ai/azure_ai_vector_stores_passthrough) | Full vector stores API support across providers | +| Support LLM Providers (Dataset Management) | [**RAGFlow**](/docs/providers/ragflow_vector_store.md) | Dataset creation and management (search not supported) | ## Usage diff --git a/litellm/llms/ragflow/vector_stores/__init__.py b/litellm/llms/ragflow/vector_stores/__init__.py new file mode 100644 index 00000000000..3be29310b39 --- /dev/null +++ b/litellm/llms/ragflow/vector_stores/__init__.py @@ -0,0 +1,2 @@ +# RAGFlow vector stores module + diff --git a/litellm/llms/ragflow/vector_stores/transformation.py b/litellm/llms/ragflow/vector_stores/transformation.py new file mode 100644 index 00000000000..b6401a4b8d7 --- /dev/null +++ b/litellm/llms/ragflow/vector_stores/transformation.py @@ -0,0 +1,249 @@ +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union + +import httpx + +from litellm.llms.base_llm.vector_store.transformation import BaseVectorStoreConfig +from litellm.secret_managers.main import get_secret_str +from litellm.types.router import GenericLiteLLMParams +from litellm.types.vector_stores import ( + BaseVectorStoreAuthCredentials, + VectorStoreCreateOptionalRequestParams, + VectorStoreCreateResponse, + VectorStoreFileCounts, + VectorStoreIndexEndpoints, + VectorStoreSearchOptionalRequestParams, + VectorStoreSearchResponse, +) + +if TYPE_CHECKING: + from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj +else: + LiteLLMLoggingObj = Any + + +class RAGFlowVectorStoreConfig(BaseVectorStoreConfig): + """Vector store configuration for RAGFlow datasets.""" + + def get_auth_credentials( + self, litellm_params: dict + ) -> BaseVectorStoreAuthCredentials: + api_key = litellm_params.get("api_key") + if api_key is None: + # Try to get from environment variable + api_key = get_secret_str("RAGFLOW_API_KEY") + if api_key is None: + raise ValueError("api_key is required (set RAGFLOW_API_KEY env var or pass in litellm_params)") + return { + "headers": { + "Authorization": f"Bearer {api_key}", + }, + } + + def get_vector_store_endpoints_by_type(self) -> VectorStoreIndexEndpoints: + """RAGFlow vector stores are management-only, no search support.""" + return { + "read": [], + "write": [], + } + + def validate_environment( + self, headers: dict, litellm_params: Optional[GenericLiteLLMParams] + ) -> dict: + """Validate environment and set headers for RAGFlow API.""" + litellm_params = litellm_params or GenericLiteLLMParams() + api_key = ( + litellm_params.api_key + or get_secret_str("RAGFLOW_API_KEY") + ) + + if api_key is None: + raise ValueError("RAGFLOW_API_KEY is required (set env var or pass in litellm_params)") + + headers.update( + { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + } + ) + return headers + + def get_complete_url( + self, + api_base: Optional[str], + litellm_params: dict, + ) -> str: + """ + Get the complete URL for RAGFlow datasets API. + + Supports: + - RAGFLOW_API_BASE env var + - api_base in litellm_params + - Default: http://localhost:9380 + """ + api_base = ( + api_base + or litellm_params.get("api_base") + or get_secret_str("RAGFLOW_API_BASE") + or "http://localhost:9380" + ) + + # Remove trailing slashes + api_base = api_base.rstrip("/") + + # RAGFlow datasets API endpoint + return f"{api_base}/api/v1/datasets" + + def transform_search_vector_store_request( + self, + vector_store_id: str, + query: Union[str, List[str]], + vector_store_search_optional_params: VectorStoreSearchOptionalRequestParams, + api_base: str, + litellm_logging_obj: LiteLLMLoggingObj, + litellm_params: dict, + ) -> Tuple[str, Dict]: + """RAGFlow vector stores are management-only, search is not supported.""" + raise NotImplementedError( + "RAGFlow vector stores support dataset management only, not search/retrieval" + ) + + def transform_search_vector_store_response( + self, response: httpx.Response, litellm_logging_obj: LiteLLMLoggingObj + ) -> VectorStoreSearchResponse: + """RAGFlow vector stores are management-only, search is not supported.""" + raise NotImplementedError( + "RAGFlow vector stores support dataset management only, not search/retrieval" + ) + + def transform_create_vector_store_request( + self, + vector_store_create_optional_params: VectorStoreCreateOptionalRequestParams, + api_base: str, + ) -> Tuple[str, Dict]: + """ + Transform create request to RAGFlow POST /api/v1/datasets format. + + Maps LiteLLM params to RAGFlow dataset creation parameters. + RAGFlow-specific fields can be passed via metadata. + """ + url = api_base # Already includes /api/v1/datasets from get_complete_url + + # Extract name (required by RAGFlow) + name = vector_store_create_optional_params.get("name") + if not name: + raise ValueError("name is required for RAGFlow dataset creation") + + # Build request body + request_body: Dict[str, Any] = { + "name": name, + } + + # Extract RAGFlow-specific fields from metadata + metadata = vector_store_create_optional_params.get("metadata") + if metadata: + # RAGFlow-specific fields that can be in metadata + ragflow_fields = [ + "avatar", + "description", + "embedding_model", + "permission", + "chunk_method", + "parser_config", + "parse_type", + "pipeline_id", + ] + + for field in ragflow_fields: + if field in metadata: + request_body[field] = metadata[field] + + # Validate: chunk_method and pipeline_id are mutually exclusive + if "chunk_method" in request_body and "pipeline_id" in request_body: + raise ValueError( + "chunk_method and pipeline_id are mutually exclusive. " + "Specify either chunk_method or pipeline_id, not both." + ) + + # If neither chunk_method nor pipeline_id is specified, default to naive + if "chunk_method" not in request_body and "pipeline_id" not in request_body: + request_body["chunk_method"] = "naive" + + return url, request_body + + def transform_create_vector_store_response( + self, response: httpx.Response + ) -> VectorStoreCreateResponse: + """ + Transform RAGFlow response to VectorStoreCreateResponse format. + + RAGFlow response format: + { + "code": 0, + "data": { + "id": "...", + "name": "...", + "create_time": 1745836841611, # milliseconds + ... + } + } + """ + try: + response_json = response.json() + + # Check for RAGFlow error response + if response_json.get("code") != 0: + error_message = response_json.get("message", "Unknown error") + raise self.get_error_class( + error_message=error_message, + status_code=response.status_code, + headers=response.headers, + ) + + data = response_json.get("data", {}) + + # Extract dataset ID + dataset_id = data.get("id") + if not dataset_id: + raise ValueError("RAGFlow response missing dataset id") + + # Extract name + name = data.get("name") + + # Convert create_time from milliseconds to seconds (Unix timestamp) + create_time_ms = data.get("create_time", 0) + created_at = int(create_time_ms / 1000) if create_time_ms else None + + # Build VectorStoreCreateResponse + return VectorStoreCreateResponse( + id=dataset_id, + object="vector_store", + created_at=created_at or 0, + name=name, + bytes=0, # RAGFlow doesn't provide bytes in response + file_counts=VectorStoreFileCounts( + in_progress=0, + completed=0, + failed=0, + cancelled=0, + total=0, + ), + status="completed", + expires_after=None, + expires_at=None, + last_active_at=None, + metadata=None, + ) + except Exception as e: + # If it's already a ValueError we raised, re-raise it + if isinstance(e, ValueError) and "RAGFlow response" in str(e): + raise + # If it's already our error class (has status_code), re-raise + if hasattr(e, "status_code"): + raise + # Otherwise, wrap in our error class + raise self.get_error_class( + error_message=str(e), + status_code=response.status_code, + headers=response.headers, + ) + diff --git a/litellm/types/vector_stores.py b/litellm/types/vector_stores.py index 6ae0b4bd2fd..a4ceb2c9ac7 100644 --- a/litellm/types/vector_stores.py +++ b/litellm/types/vector_stores.py @@ -3,17 +3,15 @@ from datetime import datetime from enum import Enum from typing import Any, Dict, List, Literal, Optional, Tuple, Union -from annotated_types import Ge from pydantic import BaseModel from typing_extensions import TypedDict -from litellm.types.router import CredentialLiteLLMParams, GenericLiteLLMParams - class SupportedVectorStoreIntegrations(str, Enum): """Supported vector store integrations.""" BEDROCK = "bedrock" + RAGFLOW = "ragflow" class LiteLLM_VectorStoreConfig(TypedDict, total=False): diff --git a/litellm/utils.py b/litellm/utils.py index 44d4b5c25fd..b77c0e62e7d 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -7633,6 +7633,12 @@ class ProviderConfigManager: ) return GeminiVectorStoreConfig() + elif litellm.LlmProviders.RAGFLOW == provider: + from litellm.llms.ragflow.vector_stores.transformation import ( + RAGFlowVectorStoreConfig, + ) + + return RAGFlowVectorStoreConfig() return None @staticmethod diff --git a/tests/test_litellm/vector_stores/test_vector_store_create_provider_logic.py b/tests/test_litellm/vector_stores/test_vector_store_create_provider_logic.py index 501a5ff0389..cca20847f12 100644 --- a/tests/test_litellm/vector_stores/test_vector_store_create_provider_logic.py +++ b/tests/test_litellm/vector_stores/test_vector_store_create_provider_logic.py @@ -9,9 +9,12 @@ sys.path.insert( ) # Adds the parent directory to the system path import litellm -from litellm.utils import ProviderConfigManager from litellm.llms.openai.vector_stores.transformation import OpenAIVectorStoreConfig -from litellm.llms.vertex_ai.vector_stores.rag_api.transformation import VertexVectorStoreConfig +from litellm.llms.ragflow.vector_stores.transformation import RAGFlowVectorStoreConfig +from litellm.llms.vertex_ai.vector_stores.rag_api.transformation import ( + VertexVectorStoreConfig, +) +from litellm.utils import ProviderConfigManager def test_vector_store_create_with_simple_provider_name(): @@ -100,3 +103,40 @@ def test_vector_store_create_with_provider_api_type(): print("✅ Test passed: Provider with api_type 'vertex_ai/rag_api' handled correctly") + +def test_vector_store_create_with_ragflow_provider(): + """ + Test that vector store create correctly handles RAGFlow provider. + + This should: + - Return correct RAGFlowVectorStoreConfig + - Support dataset management operations + """ + custom_llm_provider = "ragflow" + + # Simulate the logic from vector_stores/main.py create function + if "/" in custom_llm_provider: + pytest.fail("Should not enter this branch for RAGFlow provider") + else: + api_type = None + custom_llm_provider = custom_llm_provider # Keep as-is + + # Verify api_type is None + assert api_type is None, "api_type should be None for RAGFlow provider" + + # Verify custom_llm_provider is unchanged + assert custom_llm_provider == "ragflow", "custom_llm_provider should remain 'ragflow'" + + # Verify ProviderConfigManager returns correct config + vector_store_provider_config = ProviderConfigManager.get_provider_vector_stores_config( + provider=litellm.LlmProviders(custom_llm_provider), + api_type=api_type, + ) + + assert vector_store_provider_config is not None, "Should return a config for RAGFlow" + assert isinstance( + vector_store_provider_config, RAGFlowVectorStoreConfig + ), "Should return RAGFlowVectorStoreConfig for RAGFlow provider" + + print("✅ Test passed: RAGFlow provider handled correctly") + diff --git a/tests/vector_store_tests/test_ragflow_vector_store.py b/tests/vector_store_tests/test_ragflow_vector_store.py new file mode 100644 index 00000000000..0839bd7153b --- /dev/null +++ b/tests/vector_store_tests/test_ragflow_vector_store.py @@ -0,0 +1,359 @@ +""" +Test RAGFlow Vector Store helper functions and transformation. +""" +import os +import sys +import json +import pytest +from unittest.mock import Mock, patch, MagicMock +import httpx + +sys.path.insert(0, os.path.abspath("../..")) +import litellm + +from tests.vector_store_tests.base_vector_store_test import BaseVectorStoreTest +from litellm.llms.ragflow.vector_stores.transformation import RAGFlowVectorStoreConfig +from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj +from litellm.types.vector_stores import VectorStoreCreateOptionalRequestParams + + +class TestRAGFlowVectorStore(BaseVectorStoreTest): + """ + Test the RAGFlow vector store transformation functionality. + """ + + def get_base_create_vector_store_args(self) -> dict: + """Must return the base create vector store args""" + return { + "custom_llm_provider": "ragflow", + "api_key": os.getenv("RAGFLOW_API_KEY", "test-api-key"), + "api_base": os.getenv("RAGFLOW_API_BASE", "http://localhost:9380") + } + + def get_base_request_args(self): + # RAGFlow doesn't support search, so we'll skip search tests + return { + "vector_store_id": "test-dataset-id", + "custom_llm_provider": "ragflow", + "query": "test query" + } + + def test_get_auth_credentials(self): + """Test that auth credentials are correctly extracted.""" + config = RAGFlowVectorStoreConfig() + + # Test with api_key in params + litellm_params = {"api_key": "test-api-key-123"} + credentials = config.get_auth_credentials(litellm_params) + assert "headers" in credentials + assert credentials["headers"]["Authorization"] == "Bearer test-api-key-123" + + # Test with missing api_key (should raise ValueError) + with pytest.raises(ValueError, match="api_key is required"): + config.get_auth_credentials({}) + + def test_get_complete_url(self): + """Test that complete URL is correctly constructed.""" + config = RAGFlowVectorStoreConfig() + + # Test with api_base in params + litellm_params = {"api_base": "http://custom-host:9999"} + url = config.get_complete_url(api_base=None, litellm_params=litellm_params) + assert url == "http://custom-host:9999/api/v1/datasets" + + # Test with api_base parameter + url = config.get_complete_url(api_base="http://test-host:8888", litellm_params={}) + assert url == "http://test-host:8888/api/v1/datasets" + + # Test with default (no api_base provided) + with patch.dict(os.environ, {}, clear=True): + url = config.get_complete_url(api_base=None, litellm_params={}) + assert url == "http://localhost:9380/api/v1/datasets" + + # Test with trailing slash removal + url = config.get_complete_url(api_base="http://test-host:8888/", litellm_params={}) + assert url == "http://test-host:8888/api/v1/datasets" + + def test_validate_environment(self): + """Test environment validation and header setting.""" + config = RAGFlowVectorStoreConfig() + from litellm.types.router import GenericLiteLLMParams + + # Test with api_key in litellm_params + litellm_params = GenericLiteLLMParams(api_key="test-key") + headers = config.validate_environment({}, litellm_params) + assert headers["Authorization"] == "Bearer test-key" + assert headers["Content-Type"] == "application/json" + + # Test with missing api_key + with pytest.raises(ValueError, match="RAGFLOW_API_KEY"): + config.validate_environment({}, GenericLiteLLMParams()) + + def test_get_vector_store_endpoints_by_type(self): + """Test that endpoints are correctly configured (empty for management only).""" + config = RAGFlowVectorStoreConfig() + endpoints = config.get_vector_store_endpoints_by_type() + assert endpoints["read"] == [] + assert endpoints["write"] == [] + + def test_transform_create_vector_store_request_basic(self): + """Test basic dataset creation request transformation.""" + config = RAGFlowVectorStoreConfig() + + params: VectorStoreCreateOptionalRequestParams = { + "name": "test-dataset" + } + + url, body = config.transform_create_vector_store_request( + params, "http://localhost:9380/api/v1/datasets" + ) + + assert url == "http://localhost:9380/api/v1/datasets" + assert body["name"] == "test-dataset" + assert body["chunk_method"] == "naive" # Default chunk method + + def test_transform_create_vector_store_request_with_metadata(self): + """Test dataset creation with RAGFlow-specific metadata.""" + config = RAGFlowVectorStoreConfig() + + params: VectorStoreCreateOptionalRequestParams = { + "name": "test-dataset-advanced", + "metadata": { + "description": "Test dataset", + "embedding_model": "BAAI/bge-large-zh-v1.5@BAAI", + "permission": "me", + "chunk_method": "naive", + "parser_config": { + "chunk_token_num": 512, + "delimiter": "\n" + } + } + } + + url, body = config.transform_create_vector_store_request( + params, "http://localhost:9380/api/v1/datasets" + ) + + assert body["name"] == "test-dataset-advanced" + assert body["description"] == "Test dataset" + assert body["embedding_model"] == "BAAI/bge-large-zh-v1.5@BAAI" + assert body["permission"] == "me" + assert body["chunk_method"] == "naive" + assert "parser_config" in body + assert body["parser_config"]["chunk_token_num"] == 512 + + def test_transform_create_vector_store_request_missing_name(self): + """Test that missing name raises ValueError.""" + config = RAGFlowVectorStoreConfig() + + params: VectorStoreCreateOptionalRequestParams = {} + + with pytest.raises(ValueError, match="name is required"): + config.transform_create_vector_store_request( + params, "http://localhost:9380/api/v1/datasets" + ) + + def test_transform_create_vector_store_request_mutually_exclusive(self): + """Test that chunk_method and pipeline_id are mutually exclusive.""" + config = RAGFlowVectorStoreConfig() + + params: VectorStoreCreateOptionalRequestParams = { + "name": "test-dataset", + "metadata": { + "chunk_method": "naive", + "pipeline_id": "d0bebe30ae2211f0970942010a8e0005" + } + } + + with pytest.raises(ValueError, match="mutually exclusive"): + config.transform_create_vector_store_request( + params, "http://localhost:9380/api/v1/datasets" + ) + + def test_transform_create_vector_store_request_with_pipeline(self): + """Test dataset creation with ingestion pipeline.""" + config = RAGFlowVectorStoreConfig() + + params: VectorStoreCreateOptionalRequestParams = { + "name": "test-pipeline-dataset", + "metadata": { + "parse_type": 2, + "pipeline_id": "d0bebe30ae2211f0970942010a8e0005" + } + } + + url, body = config.transform_create_vector_store_request( + params, "http://localhost:9380/api/v1/datasets" + ) + + assert body["name"] == "test-pipeline-dataset" + assert body["parse_type"] == 2 + assert body["pipeline_id"] == "d0bebe30ae2211f0970942010a8e0005" + assert "chunk_method" not in body + + def test_transform_create_vector_store_response_success(self): + """Test successful response transformation.""" + config = RAGFlowVectorStoreConfig() + + # Mock RAGFlow response + mock_response = Mock(spec=httpx.Response) + mock_response.status_code = 200 + mock_response.headers = {} + mock_response.json.return_value = { + "code": 0, + "data": { + "id": "3b4de7d4241d11f0a6a79f24fc270c7f", + "name": "test-dataset", + "create_time": 1745836841611, + "chunk_method": "naive", + "embedding_model": "BAAI/bge-large-zh-v1.5@BAAI" + } + } + + response = config.transform_create_vector_store_response(mock_response) + + assert response["id"] == "3b4de7d4241d11f0a6a79f24fc270c7f" + assert response["name"] == "test-dataset" + assert response["object"] == "vector_store" + assert response["status"] == "completed" + assert response["created_at"] == 1745836841 # Converted from milliseconds + assert response["bytes"] == 0 + assert "file_counts" in response + + def test_transform_create_vector_store_response_error(self): + """Test error response transformation.""" + config = RAGFlowVectorStoreConfig() + + # Mock RAGFlow error response + mock_response = Mock(spec=httpx.Response) + mock_response.status_code = 400 + mock_response.headers = {} + mock_response.json.return_value = { + "code": 101, + "message": "Dataset name 'test-dataset' already exists" + } + + with pytest.raises(Exception): # Should raise BaseLLMException + config.transform_create_vector_store_response(mock_response) + + def test_transform_create_vector_store_response_missing_id(self): + """Test response with missing dataset ID.""" + config = RAGFlowVectorStoreConfig() + + mock_response = Mock(spec=httpx.Response) + mock_response.status_code = 200 + mock_response.headers = {} + mock_response.json.return_value = { + "code": 0, + "data": { + "name": "test-dataset" + # Missing "id" + } + } + + with pytest.raises(ValueError, match="missing dataset id"): + config.transform_create_vector_store_response(mock_response) + + def test_transform_search_vector_store_request_not_implemented(self): + """Test that search operations raise NotImplementedError.""" + config = RAGFlowVectorStoreConfig() + logging_obj = MagicMock(spec=LiteLLMLoggingObj) + + with pytest.raises(NotImplementedError, match="management only"): + config.transform_search_vector_store_request( + vector_store_id="test-id", + query="test query", + vector_store_search_optional_params={}, + api_base="http://localhost:9380", + litellm_logging_obj=logging_obj, + litellm_params={} + ) + + def test_transform_search_vector_store_response_not_implemented(self): + """Test that search response transformation raises NotImplementedError.""" + config = RAGFlowVectorStoreConfig() + logging_obj = MagicMock(spec=LiteLLMLoggingObj) + mock_response = Mock(spec=httpx.Response) + + with pytest.raises(NotImplementedError, match="management only"): + config.transform_search_vector_store_response(mock_response, logging_obj) + + def _validate_vector_store_create_response(self, response): + """Override to handle RAGFlow-specific response format.""" + # RAGFlow IDs are hex strings (not OpenAI-style vs_* format) + # So we override the base validation to not check for vs_ prefix + assert isinstance(response, dict), f"Response should be a dict, got {type(response)}" + assert "id" in response, "Missing required field 'id' in create response" + assert "object" in response, "Missing required field 'object' in create response" + assert "created_at" in response, "Missing required field 'created_at' in create response" + + assert response["object"] == "vector_store", \ + f"Expected object to be 'vector_store', got '{response['object']}'" + + assert isinstance(response["id"], str), \ + f"id should be a string, got {type(response['id'])}" + assert len(response["id"]) > 0, "id should not be empty" + # RAGFlow IDs are hex strings, not OpenAI-style vs_* format + + assert isinstance(response["created_at"], int), \ + f"created_at should be an integer, got {type(response['created_at'])}" + assert response["created_at"] > 0, "created_at should be a positive timestamp" + + print(f"✅ RAGFlow create response validation passed: Dataset '{response['id']}' created successfully") + + @pytest.mark.parametrize("sync_mode", [True, False]) + @pytest.mark.asyncio + async def test_basic_create_vector_store(self, sync_mode): + """Override to handle RAGFlow-specific connection errors.""" + litellm._turn_on_debug() + litellm.set_verbose = True + base_request_args = self.get_base_create_vector_store_args() + + # Skip if no API key is set + if not os.getenv("RAGFLOW_API_KEY") and not base_request_args.get("api_key"): + pytest.skip("RAGFLOW_API_KEY not set, skipping integration test") + + # Extract custom_llm_provider from base args if present + create_args = base_request_args + try: + if sync_mode: + response = litellm.vector_stores.create( + name=f"test-ragflow-{int(__import__('time').time())}", + **create_args + ) + else: + response = await litellm.vector_stores.acreate( + name=f"test-ragflow-{int(__import__('time').time())}", + **create_args + ) + except litellm.InternalServerError: + pytest.skip("Skipping test due to litellm.InternalServerError") + except Exception as e: + error_str = str(e).lower() + error_type = type(e).__name__ + + # Check if it's a connection error + if (isinstance(e, (ConnectionError, OSError)) or + "connection" in error_str or + "connect" in error_str or + "APIConnectionError" in error_type): + pytest.skip(f"Skipping test due to connection error (RAGFlow instance may not be running): {e}") + + # If this is an authentication or permission error, skip the test + if "authentication" in error_str or "permission" in error_str or "unauthorized" in error_str: + pytest.skip(f"Skipping test due to authentication/permission error: {e}") + + # Re-raise if it's not a handled error + raise + + print("litellm create response=", json.dumps(response, indent=4, default=str)) + + # Validate response structure + self._validate_vector_store_create_response(response) + + @pytest.mark.parametrize("sync_mode", [True, False]) + @pytest.mark.asyncio + async def test_basic_search_vector_store(self, sync_mode): + """Override search test - RAGFlow doesn't support search.""" + pytest.skip("RAGFlow vector stores support dataset management only, not search") + From dad0b2c1117d3c2624d27dd229f7faa8babfbb41 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Wed, 3 Dec 2025 15:32:42 +0530 Subject: [PATCH 051/107] Fix unused imports --- litellm/llms/ragflow/chat/transformation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/llms/ragflow/chat/transformation.py b/litellm/llms/ragflow/chat/transformation.py index d33a1593be8..58fbfa83c98 100644 --- a/litellm/llms/ragflow/chat/transformation.py +++ b/litellm/llms/ragflow/chat/transformation.py @@ -10,7 +10,7 @@ Model name format: - Agent: ragflow/agent/{agent_id}/{model_name} """ -from typing import Any, List, Optional, Tuple +from typing import List, Optional, Tuple import litellm from litellm.llms.openai.openai import OpenAIConfig From c9c7823f43b072a62ca2cd80bb9fc33f309dd1b1 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Wed, 3 Dec 2025 17:31:20 +0530 Subject: [PATCH 052/107] Fix bedrock models in model map --- ...odel_prices_and_context_window_backup.json | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 0fc97ce7b0a..dbaa60e0f1d 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -9629,6 +9629,21 @@ "supports_prompt_caching": true, "supports_tool_choice": true }, + "deepseek/deepseek-v3.2": { + "input_cost_per_token": 2.8e-07, + "input_cost_per_token_cache_hit": 2.8e-08, + "litellm_provider": "deepseek", + "max_input_tokens": 163840, + "max_output_tokens": 163840, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 4e-07, + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, "deepseek.v3-v1:0": { "input_cost_per_token": 5.8e-07, "litellm_provider": "bedrock_converse", @@ -20637,6 +20652,21 @@ "supports_reasoning": true, "supports_tool_choice": true }, + "openrouter/deepseek/deepseek-v3.2": { + "input_cost_per_token": 2.8e-07, + "input_cost_per_token_cache_hit": 2.8e-08, + "litellm_provider": "openrouter", + "max_input_tokens": 163840, + "max_output_tokens": 163840, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 4e-07, + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, "openrouter/deepseek/deepseek-v3.2-exp": { "input_cost_per_token": 2e-07, "input_cost_per_token_cache_hit": 2e-08, @@ -23892,6 +23922,32 @@ "supports_vision": true, "tool_use_system_prompt_tokens": 159 }, + "global.anthropic.claude-opus-4-5-20251101-v1:0": { + "cache_creation_input_token_cost": 6.25e-06, + "cache_read_input_token_cost": 5e-07, + "input_cost_per_token": 5e-06, + "litellm_provider": "bedrock_converse", + "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 + }, "us.anthropic.claude-sonnet-4-20250514-v1:0": { "cache_creation_input_token_cost": 3.75e-06, "cache_read_input_token_cost": 3e-07, From be5dd234bfe46f28bdfced4333f4df7562236c64 Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Wed, 3 Dec 2025 08:01:26 -0800 Subject: [PATCH 053/107] docs: fix list --- docs/my-website/docs/providers/bedrock.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/my-website/docs/providers/bedrock.md b/docs/my-website/docs/providers/bedrock.md index a9ac85a7571..7fc7758b15c 100644 --- a/docs/my-website/docs/providers/bedrock.md +++ b/docs/my-website/docs/providers/bedrock.md @@ -43,6 +43,8 @@ export AWS_BEARER_TOKEN_BEDROCK="your-api-key" Option 2: use the api_key parameter to pass in API key for completion, embedding, image_generation API calls. + + ```python response = completion( model="bedrock/anthropic.claude-3-sonnet-20240229-v1:0", @@ -50,7 +52,17 @@ response = completion( api_key="your-api-key" ) ``` - + + +```yaml +model_list: + - model_name: bedrock-claude-3-sonnet + litellm_params: + model: bedrock/anthropic.claude-3-sonnet-20240229-v1:0 + api_key: os.environ/AWS_BEARER_TOKEN_BEDROCK +``` + + ## Usage From 5e791464afb6eb2b6606d20cf23bdb87dd02e10d Mon Sep 17 00:00:00 2001 From: Cesar Garcia <128240629+Chesars@users.noreply.github.com> Date: Wed, 3 Dec 2025 14:07:02 -0300 Subject: [PATCH 054/107] docs: add Microsoft Agent Lightning to projects (#17422) Add Agent Lightning, Microsoft's open-source framework for training AI agents with RL, APO, and SFT. Uses LiteLLM Proxy for LLM routing and trace collection. --- docs/my-website/docs/projects/Agent Lightning.md | 10 ++++++++++ docs/my-website/sidebars.js | 1 + 2 files changed, 11 insertions(+) create mode 100644 docs/my-website/docs/projects/Agent Lightning.md diff --git a/docs/my-website/docs/projects/Agent Lightning.md b/docs/my-website/docs/projects/Agent Lightning.md new file mode 100644 index 00000000000..28e5546e398 --- /dev/null +++ b/docs/my-website/docs/projects/Agent Lightning.md @@ -0,0 +1,10 @@ + +# Agent Lightning + +[Agent Lightning](https://github.com/microsoft/agent-lightning) is Microsoft's open-source framework for training and optimizing AI agents with Reinforcement Learning, Automatic Prompt Optimization, and Supervised Fine-tuning — with almost zero code changes. + +It works with any agent framework including LangChain, OpenAI Agents SDK, AutoGen, and CrewAI. Agent Lightning uses LiteLLM Proxy under the hood to route LLM requests and collect traces that power its training algorithms. + +- [GitHub](https://github.com/microsoft/agent-lightning) +- [Docs](https://microsoft.github.io/agent-lightning/) +- [arXiv Paper](https://arxiv.org/abs/2508.03680) diff --git a/docs/my-website/sidebars.js b/docs/my-website/sidebars.js index e2f682f0a50..4a0f592c422 100644 --- a/docs/my-website/sidebars.js +++ b/docs/my-website/sidebars.js @@ -826,6 +826,7 @@ const sidebars = { "projects/mini-swe-agent", "projects/openai-agents", "projects/Google ADK", + "projects/Agent Lightning", "projects/Harbor", "projects/Docq.AI", "projects/PDL", From 0a7602bb7cc4c21b5cbe2c0283889004e25018ea Mon Sep 17 00:00:00 2001 From: Alexsander Hamir Date: Wed, 3 Dec 2025 10:43:27 -0800 Subject: [PATCH 055/107] fix: prevent memory leak in aiohttp connection pooling (#17388) * fix: prevent memory leak in aiohttp connection pooling Add connection limits to aiohttp TCPConnector to prevent unbounded connection growth that causes memory leaks. Without these limits, aiohttp's _wrap_create_connection can accumulate connections indefinitely in long-running processes. Changes: - Set default limit of 300 total connections and 50 per host - Apply limits to shared proxy session initialization - Apply limits to HTTP handler transport creation - Configurable via AIOHTTP_CONNECTOR_LIMIT and AIOHTTP_CONNECTOR_LIMIT_PER_HOST environment variables - Set to 0 for unlimited (not recommended for production) This fix covers: - All standard LLM provider API calls (OpenAI, Anthropic, etc.) - Proxy server shared session - Most guardrail HTTP calls Impact: Prevents memory exhaustion in high-traffic deployments and long-running proxy servers that make thousands of API calls. Testing: Verified connection limits are applied correctly and existing functionality remains unchanged. --- litellm/constants.py | 6 ++++-- litellm/llms/custom_httpx/http_handler.py | 20 +++++++++++------ litellm/proxy/proxy_server.py | 26 ++++++++++++++--------- 3 files changed, 33 insertions(+), 19 deletions(-) diff --git a/litellm/constants.py b/litellm/constants.py index 05ccfb5993f..5ab56bcaa2f 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -100,8 +100,10 @@ RUNWAYML_POLLING_TIMEOUT = int( ########## Networking constants ############################################################## _DEFAULT_TTL_FOR_HTTPX_CLIENTS = 3600 # 1 hour, re-use the same httpx client for 1 hour -# Aiohttp connection pooling constants -AIOHTTP_CONNECTOR_LIMIT = int(os.getenv("AIOHTTP_CONNECTOR_LIMIT", 0)) +# Aiohttp connection pooling - prevents memory leaks from unbounded connection growth +# Set to 0 for unlimited (not recommended for production) +AIOHTTP_CONNECTOR_LIMIT = int(os.getenv("AIOHTTP_CONNECTOR_LIMIT", 300)) +AIOHTTP_CONNECTOR_LIMIT_PER_HOST = int(os.getenv("AIOHTTP_CONNECTOR_LIMIT_PER_HOST", 50)) AIOHTTP_KEEPALIVE_TIMEOUT = int(os.getenv("AIOHTTP_KEEPALIVE_TIMEOUT", 120)) AIOHTTP_TTL_DNS_CACHE = int(os.getenv("AIOHTTP_TTL_DNS_CACHE", 300)) # enable_cleanup_closed is only needed for Python versions with the SSL leak bug diff --git a/litellm/llms/custom_httpx/http_handler.py b/litellm/llms/custom_httpx/http_handler.py index b06e8463abd..d9a9d4f9dc1 100644 --- a/litellm/llms/custom_httpx/http_handler.py +++ b/litellm/llms/custom_httpx/http_handler.py @@ -16,6 +16,7 @@ from litellm._logging import verbose_logger from litellm.constants import ( _DEFAULT_TTL_FOR_HTTPX_CLIENTS, AIOHTTP_CONNECTOR_LIMIT, + AIOHTTP_CONNECTOR_LIMIT_PER_HOST, AIOHTTP_KEEPALIVE_TIMEOUT, AIOHTTP_NEEDS_CLEANUP_CLOSED, AIOHTTP_TTL_DNS_CACHE, @@ -793,15 +794,20 @@ class AsyncHTTPHandler: verbose_logger.debug( "NEW SESSION: Creating new ClientSession (no shared session provided)" ) + transport_connector_kwargs = { + "keepalive_timeout": AIOHTTP_KEEPALIVE_TIMEOUT, + "ttl_dns_cache": AIOHTTP_TTL_DNS_CACHE, + "enable_cleanup_closed": True, + **connector_kwargs, + } + if AIOHTTP_CONNECTOR_LIMIT > 0: + transport_connector_kwargs["limit"] = AIOHTTP_CONNECTOR_LIMIT + if AIOHTTP_CONNECTOR_LIMIT_PER_HOST > 0: + transport_connector_kwargs["limit_per_host"] = AIOHTTP_CONNECTOR_LIMIT_PER_HOST + return LiteLLMAiohttpTransport( client=lambda: ClientSession( - connector=TCPConnector( - limit=AIOHTTP_CONNECTOR_LIMIT, - keepalive_timeout=AIOHTTP_KEEPALIVE_TIMEOUT, - ttl_dns_cache=AIOHTTP_TTL_DNS_CACHE, - enable_cleanup_closed=AIOHTTP_NEEDS_CLEANUP_CLOSED, - **connector_kwargs, - ), + connector=TCPConnector(**transport_connector_kwargs), trust_env=trust_env, ), ) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index a1e01caddc1..f2c7c361c45 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -31,6 +31,7 @@ from typing import ( from litellm._uuid import uuid from litellm.constants import ( AIOHTTP_CONNECTOR_LIMIT, + AIOHTTP_CONNECTOR_LIMIT_PER_HOST, AIOHTTP_KEEPALIVE_TIMEOUT, AIOHTTP_NEEDS_CLEANUP_CLOSED, AIOHTTP_TTL_DNS_CACHE, @@ -627,21 +628,26 @@ async def proxy_shutdown_event(): async def _initialize_shared_aiohttp_session(): - """Initialize shared aiohttp session for connection reuse.""" + """Initialize shared aiohttp session for connection reuse with connection limits.""" try: from aiohttp import ClientSession, TCPConnector - # Create connector with connection pooling settings optimized for long-lived connections - connector = TCPConnector( - limit=AIOHTTP_CONNECTOR_LIMIT, - keepalive_timeout=AIOHTTP_KEEPALIVE_TIMEOUT, - ttl_dns_cache=AIOHTTP_TTL_DNS_CACHE, - enable_cleanup_closed=AIOHTTP_NEEDS_CLEANUP_CLOSED, - ) - + connector_kwargs = { + "keepalive_timeout": AIOHTTP_KEEPALIVE_TIMEOUT, + "ttl_dns_cache": AIOHTTP_TTL_DNS_CACHE, + "enable_cleanup_closed": True, + } + if AIOHTTP_CONNECTOR_LIMIT > 0: + connector_kwargs["limit"] = AIOHTTP_CONNECTOR_LIMIT + if AIOHTTP_CONNECTOR_LIMIT_PER_HOST > 0: + connector_kwargs["limit_per_host"] = AIOHTTP_CONNECTOR_LIMIT_PER_HOST + + connector = TCPConnector(**connector_kwargs) session = ClientSession(connector=connector) + verbose_proxy_logger.info( - f"SESSION REUSE: Created shared aiohttp session for connection pooling (ID: {id(session)})" + f"SESSION REUSE: Created shared aiohttp session for connection pooling (ID: {id(session)}, " + f"limit={AIOHTTP_CONNECTOR_LIMIT}, limit_per_host={AIOHTTP_CONNECTOR_LIMIT_PER_HOST})" ) return session except Exception as e: From 5ecebe2a578190fb8ecf09d1f4611749bc76ea16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Gar=C3=A9?= <90070734+FelipeRodriguesGare@users.noreply.github.com> Date: Wed, 3 Dec 2025 16:00:18 -0300 Subject: [PATCH 056/107] adding status parameter as optinal for FileObject (#17431) --- litellm/types/llms/openai.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/types/llms/openai.py b/litellm/types/llms/openai.py index 61d58e4c86d..20b0cb4b2c3 100644 --- a/litellm/types/llms/openai.py +++ b/litellm/types/llms/openai.py @@ -303,7 +303,7 @@ class OpenAIFileObject(BaseModel): `fine-tune`, `fine-tune-results`, `vision`, and `user_data`. """ - status: Literal["uploaded", "processed", "error"] + status: Optional[Literal["uploaded", "processed", "error"]] """Deprecated. The current status of the file, which can be either `uploaded`, `processed`, or From e6620fcdad88a9076bc49ebd6bb830e756ff49a7 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Wed, 3 Dec 2025 11:01:10 -0800 Subject: [PATCH 057/107] Ruff checks --- litellm/proxy/auth/login_utils.py | 43 ++++++++++++------- .../proxy/auth/test_login_utils.py | 38 +++++++++++++++- 2 files changed, 65 insertions(+), 16 deletions(-) diff --git a/litellm/proxy/auth/login_utils.py b/litellm/proxy/auth/login_utils.py index 94f2f698e04..6ef983b221c 100644 --- a/litellm/proxy/auth/login_utils.py +++ b/litellm/proxy/auth/login_utils.py @@ -34,6 +34,33 @@ from litellm.secret_managers.main import get_secret_bool from litellm.types.proxy.ui_sso import ReturnedUITokenObject +def get_ui_credentials(master_key: Optional[str]) -> tuple[str, str]: + """ + Get UI username and password from environment variables or master key. + + Args: + master_key: Master key for the proxy (used as fallback for password) + + Returns: + tuple[str, str]: A tuple containing (ui_username, ui_password) + + Raises: + ProxyException: If neither UI_PASSWORD nor master_key is available + """ + ui_username = os.getenv("UI_USERNAME", "admin") + ui_password = os.getenv("UI_PASSWORD", None) + if ui_password is None: + ui_password = str(master_key) if master_key is not None else None + if ui_password is None: + raise ProxyException( + message="set Proxy master key to use UI. https://docs.litellm.ai/docs/proxy/virtual_keys. If set, use `--detailed_debug` to debug issue.", + type=ProxyErrorTypes.auth_error, + param="UI_PASSWORD", + code=500, + ) + return ui_username, ui_password + + class LoginResult: """Result object containing authentication data from login.""" @@ -85,17 +112,7 @@ async def authenticate_user( code=500, ) - ui_username = os.getenv("UI_USERNAME", "admin") - ui_password = os.getenv("UI_PASSWORD", None) - if ui_password is None: - ui_password = str(master_key) if master_key is not None else None - if ui_password is None: - raise ProxyException( - message="set Proxy master key to use UI. https://docs.litellm.ai/docs/proxy/virtual_keys. If set, use `--detailed_debug` to debug issue.", - type=ProxyErrorTypes.auth_error, - param="UI_PASSWORD", - code=500, - ) + ui_username, ui_password = get_ui_credentials(master_key) # Check if we can find the `username` in the db. On the UI, users can enter username=their email _user_row: Optional[LiteLLM_UserTable] = None @@ -116,10 +133,6 @@ async def authenticate_user( ), ) - disabled_non_admin_personal_key_creation = ( - get_disabled_non_admin_personal_key_creation() - ) - """ To login to Admin UI, we support the following - Login with UI_USERNAME and UI_PASSWORD diff --git a/tests/test_litellm/proxy/auth/test_login_utils.py b/tests/test_litellm/proxy/auth/test_login_utils.py index abb7b58c292..201461dc8b5 100644 --- a/tests/test_litellm/proxy/auth/test_login_utils.py +++ b/tests/test_litellm/proxy/auth/test_login_utils.py @@ -18,7 +18,43 @@ from litellm.proxy._types import ( ProxyException, hash_token, ) -from litellm.proxy.auth.login_utils import LoginResult, authenticate_user +from litellm.proxy.auth.login_utils import ( + LoginResult, + authenticate_user, + get_ui_credentials, +) + + +def test_get_ui_credentials_prefers_explicit_password(): + """The configured UI password should be returned when available.""" + with patch.dict( + os.environ, + {"UI_USERNAME": "test-admin", "UI_PASSWORD": "secure-pass"}, + clear=True, + ): + username, password = get_ui_credentials(master_key="sk-123") + + assert username == "test-admin" + assert password == "secure-pass" + + +def test_get_ui_credentials_can_use_master_key(): + """Master key should be used as password when UI_PASSWORD is missing.""" + with patch.dict(os.environ, {"UI_USERNAME": "fallback-admin"}, clear=True): + username, password = get_ui_credentials(master_key="fallback-key") + + assert username == "fallback-admin" + assert password == "fallback-key" + + +def test_get_ui_credentials_requires_password(): + """Missing UI password and master key results in error.""" + with patch.dict(os.environ, {}, clear=True): + with pytest.raises(ProxyException) as exc_info: + get_ui_credentials(master_key=None) + + assert exc_info.value.type == ProxyErrorTypes.auth_error + assert exc_info.value.code == "500" @pytest.mark.asyncio From 82a8134d7a1a26e74d720a130e7d41709e0b0852 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Gar=C3=A9?= <90070734+FelipeRodriguesGare@users.noreply.github.com> Date: Wed, 3 Dec 2025 16:24:32 -0300 Subject: [PATCH 058/107] fixing optional parameter default value (#17434) --- litellm/types/llms/openai.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/types/llms/openai.py b/litellm/types/llms/openai.py index 20b0cb4b2c3..9fb97d47d17 100644 --- a/litellm/types/llms/openai.py +++ b/litellm/types/llms/openai.py @@ -303,7 +303,7 @@ class OpenAIFileObject(BaseModel): `fine-tune`, `fine-tune-results`, `vision`, and `user_data`. """ - status: Optional[Literal["uploaded", "processed", "error"]] + status: Optional[Literal["uploaded", "processed", "error"]] = None """Deprecated. The current status of the file, which can be either `uploaded`, `processed`, or From 56328e653590bc114a0585a1378e38e1fccfad9b Mon Sep 17 00:00:00 2001 From: Alexsander Hamir Date: Wed, 3 Dec 2025 11:40:16 -0800 Subject: [PATCH 059/107] =?UTF-8?q?[Refactor#2]=20litellm/init=20=E2=80=93?= =?UTF-8?q?=20Lazy-load=20utils=20to=20reduce=20memory=20+=20import=20time?= =?UTF-8?q?=20(#17171)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: lazy load utils.py imports Lazy-load most functions and response types from utils.py to avoid loading tiktoken and other heavy dependencies at import time. This significantly reduces memory usage when importing completion from litellm. --- litellm/__init__.py | 147 +++------- litellm/_lazy_imports.py | 259 ++++++++++++++++++ litellm/images/main.py | 4 +- litellm/llms/azure/azure.py | 2 +- litellm/llms/azure_ai/embed/handler.py | 4 +- .../amazon_nova_transformation.py | 3 +- litellm/llms/bedrock/common_utils.py | 19 ++ .../amazon_nova_canvas_transformation.py | 3 +- .../image/amazon_stability1_transformation.py | 3 +- .../image/amazon_stability3_transformation.py | 3 +- .../image/amazon_titan_transformation.py | 2 +- litellm/llms/ovhcloud/chat/transformation.py | 4 +- litellm/llms/together_ai/chat.py | 3 +- litellm/llms/vertex_ai/common_utils.py | 3 +- .../batch_embed_content_handler.py | 2 +- .../batch_embed_content_transformation.py | 2 +- .../image_generation_handler.py | 2 +- .../embedding_handler.py | 4 +- .../vertex_embeddings/embedding_handler.py | 4 +- litellm/main.py | 11 +- 20 files changed, 354 insertions(+), 130 deletions(-) create mode 100644 litellm/_lazy_imports.py diff --git a/litellm/__init__.py b/litellm/__init__.py index 595a0132099..3d40a9b15cf 100644 --- a/litellm/__init__.py +++ b/litellm/__init__.py @@ -1056,57 +1056,10 @@ from .timeout import timeout from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider from litellm.litellm_core_utils.core_helpers import remove_index_from_tool_calls from litellm.litellm_core_utils.token_counter import get_modified_max_tokens -from .utils import ( - client, - exception_type, - get_optional_params, - get_response_string, - token_counter, - create_pretrained_tokenizer, - create_tokenizer, - supports_function_calling, - supports_web_search, - supports_url_context, - supports_response_schema, - supports_parallel_function_calling, - supports_vision, - supports_audio_input, - supports_audio_output, - supports_system_messages, - supports_reasoning, - get_litellm_params, - acreate, - get_max_tokens, - get_model_info, - register_prompt_template, - validate_environment, - check_valid_key, - register_model, - encode, - decode, - _calculate_retry_after, - _should_retry, - get_supported_openai_params, - get_api_base, - get_first_chars_messages, - ModelResponse, - ModelResponseStream, - EmbeddingResponse, - ImageResponse, - TranscriptionResponse, - TextCompletionResponse, - get_provider_fields, - ModelResponseListIterator, - get_valid_models, -) - -ALL_LITELLM_RESPONSE_TYPES = [ - ModelResponse, - EmbeddingResponse, - ImageResponse, - TranscriptionResponse, - TextCompletionResponse, -] +# client must be imported immediately as it's used as a decorator at function definition time +from .utils import client +# Note: Most other utils imports are lazy-loaded via __getattr__ to avoid loading utils.py +# (which imports tiktoken) at import time from .llms.bytez.chat.transformation import BytezChatConfig from .llms.custom_llm import CustomLLM @@ -1538,56 +1491,6 @@ def set_global_gitlab_config(config: Dict[str, Any]) -> None: # Lazy loading system for heavy modules to reduce initial import time and memory usage -def _lazy_import_cost_calculator(name: str) -> Any: - """Lazy import for cost_calculator functions.""" - from .cost_calculator import ( - completion_cost as _completion_cost, - cost_per_token as _cost_per_token, - response_cost_calculator as _response_cost_calculator, - ) - - _cost_functions = { - "completion_cost": _completion_cost, - "cost_per_token": _cost_per_token, - "response_cost_calculator": _response_cost_calculator, - } - - func = _cost_functions[name] - globals()[name] = func - return func - - -def _lazy_import_litellm_logging(name: str) -> Any: - """Lazy import for litellm_logging module.""" - try: - from litellm.litellm_core_utils.litellm_logging import ( - Logging as _Logging, - modify_integration as _modify_integration, - ) - - _logging_objects = { - "Logging": _Logging, - "modify_integration": _modify_integration, - } - - obj = _logging_objects[name] - globals()[name] = obj - return obj - except Exception as e: - raise AttributeError( - f"module {__name__!r} has no attribute {name!r}. " - f"Lazy import failed: {e}" - ) from e - - -_LAZY_LOAD_REGISTRY: Dict[str, Callable[[str], Any]] = { - "completion_cost": _lazy_import_cost_calculator, - "cost_per_token": _lazy_import_cost_calculator, - "response_cost_calculator": _lazy_import_cost_calculator, - "Logging": _lazy_import_litellm_logging, - "modify_integration": _lazy_import_litellm_logging, -} - if TYPE_CHECKING: cost_per_token: Callable[..., Tuple[float, float]] @@ -1598,7 +1501,45 @@ if TYPE_CHECKING: def __getattr__(name: str) -> Any: """Lazy import handler for cost_calculator and litellm_logging functions.""" - if name in _LAZY_LOAD_REGISTRY: - return _LAZY_LOAD_REGISTRY[name](name) + # Lazy load cost_calculator functions + _cost_calculator_names = ( + "completion_cost", + "cost_per_token", + "response_cost_calculator", + ) + if name in _cost_calculator_names: + from ._lazy_imports import _lazy_import_cost_calculator + return _lazy_import_cost_calculator(name) + + # Lazy load litellm_logging functions + _litellm_logging_names = ( + "Logging", + "modify_integration", + ) + if name in _litellm_logging_names: + from ._lazy_imports import _lazy_import_litellm_logging + return _lazy_import_litellm_logging(name) + + # Lazy load utils functions + _utils_names = ( + "exception_type", "get_optional_params", "get_response_string", "token_counter", + "create_pretrained_tokenizer", "create_tokenizer", "supports_function_calling", + "supports_web_search", "supports_url_context", "supports_response_schema", + "supports_parallel_function_calling", "supports_vision", "supports_audio_input", + "supports_audio_output", "supports_system_messages", "supports_reasoning", + "get_litellm_params", "acreate", "get_max_tokens", "get_model_info", + "register_prompt_template", "validate_environment", "check_valid_key", + "register_model", "encode", "decode", "_calculate_retry_after", "_should_retry", + "get_supported_openai_params", "get_api_base", "get_first_chars_messages", + "ModelResponse", "ModelResponseStream", "EmbeddingResponse", "ImageResponse", + "TranscriptionResponse", "TextCompletionResponse", "get_provider_fields", + "ModelResponseListIterator", "get_valid_models", + ) + if name in _utils_names: + from ._lazy_imports import _lazy_import_utils + return _lazy_import_utils(name) raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + +# ALL_LITELLM_RESPONSE_TYPES is lazy-loaded via __getattr__ to avoid loading utils at import time diff --git a/litellm/_lazy_imports.py b/litellm/_lazy_imports.py new file mode 100644 index 00000000000..91b16864de1 --- /dev/null +++ b/litellm/_lazy_imports.py @@ -0,0 +1,259 @@ +from typing import Any +import sys + +def _get_litellm_globals() -> dict: + """Helper to get the globals dictionary of the litellm module.""" + return sys.modules["litellm"].__dict__ + +# Lazy import for utils module - imports only the requested item by name. +# Note: PLR0915 (too many statements) is suppressed because the many if statements +# are intentional - each attribute is imported individually only when requested, +# ensuring true lazy imports rather than importing the entire utils module. +def _lazy_import_utils(name: str) -> Any: # noqa: PLR0915 + """Lazy import for utils module - imports only the requested item by name.""" + _globals = _get_litellm_globals() + if name == "exception_type": + from .utils import exception_type as _exception_type + _globals["exception_type"] = _exception_type + return _exception_type + + if name == "get_optional_params": + from .utils import get_optional_params as _get_optional_params + _globals["get_optional_params"] = _get_optional_params + return _get_optional_params + + if name == "get_response_string": + from .utils import get_response_string as _get_response_string + _globals["get_response_string"] = _get_response_string + return _get_response_string + + if name == "token_counter": + from .utils import token_counter as _token_counter + _globals["token_counter"] = _token_counter + return _token_counter + + if name == "create_pretrained_tokenizer": + from .utils import create_pretrained_tokenizer as _create_pretrained_tokenizer + _globals["create_pretrained_tokenizer"] = _create_pretrained_tokenizer + return _create_pretrained_tokenizer + + if name == "create_tokenizer": + from .utils import create_tokenizer as _create_tokenizer + _globals["create_tokenizer"] = _create_tokenizer + return _create_tokenizer + + if name == "supports_function_calling": + from .utils import supports_function_calling as _supports_function_calling + _globals["supports_function_calling"] = _supports_function_calling + return _supports_function_calling + + if name == "supports_web_search": + from .utils import supports_web_search as _supports_web_search + _globals["supports_web_search"] = _supports_web_search + return _supports_web_search + + if name == "supports_url_context": + from .utils import supports_url_context as _supports_url_context + _globals["supports_url_context"] = _supports_url_context + return _supports_url_context + + if name == "supports_response_schema": + from .utils import supports_response_schema as _supports_response_schema + _globals["supports_response_schema"] = _supports_response_schema + return _supports_response_schema + + if name == "supports_parallel_function_calling": + from .utils import supports_parallel_function_calling as _supports_parallel_function_calling + _globals["supports_parallel_function_calling"] = _supports_parallel_function_calling + return _supports_parallel_function_calling + + if name == "supports_vision": + from .utils import supports_vision as _supports_vision + _globals["supports_vision"] = _supports_vision + return _supports_vision + + if name == "supports_audio_input": + from .utils import supports_audio_input as _supports_audio_input + _globals["supports_audio_input"] = _supports_audio_input + return _supports_audio_input + + if name == "supports_audio_output": + from .utils import supports_audio_output as _supports_audio_output + _globals["supports_audio_output"] = _supports_audio_output + return _supports_audio_output + + if name == "supports_system_messages": + from .utils import supports_system_messages as _supports_system_messages + _globals["supports_system_messages"] = _supports_system_messages + return _supports_system_messages + + if name == "supports_reasoning": + from .utils import supports_reasoning as _supports_reasoning + _globals["supports_reasoning"] = _supports_reasoning + return _supports_reasoning + + if name == "get_litellm_params": + from .utils import get_litellm_params as _get_litellm_params + _globals["get_litellm_params"] = _get_litellm_params + return _get_litellm_params + + if name == "acreate": + from .utils import acreate as _acreate + _globals["acreate"] = _acreate + return _acreate + + if name == "get_max_tokens": + from .utils import get_max_tokens as _get_max_tokens + _globals["get_max_tokens"] = _get_max_tokens + return _get_max_tokens + + if name == "get_model_info": + from .utils import get_model_info as _get_model_info + _globals["get_model_info"] = _get_model_info + return _get_model_info + + if name == "register_prompt_template": + from .utils import register_prompt_template as _register_prompt_template + _globals["register_prompt_template"] = _register_prompt_template + return _register_prompt_template + + if name == "validate_environment": + from .utils import validate_environment as _validate_environment + _globals["validate_environment"] = _validate_environment + return _validate_environment + + if name == "check_valid_key": + from .utils import check_valid_key as _check_valid_key + _globals["check_valid_key"] = _check_valid_key + return _check_valid_key + + if name == "register_model": + from .utils import register_model as _register_model + _globals["register_model"] = _register_model + return _register_model + + if name == "encode": + from .utils import encode as _encode + _globals["encode"] = _encode + return _encode + + if name == "decode": + from .utils import decode as _decode + _globals["decode"] = _decode + return _decode + + if name == "_calculate_retry_after": + from .utils import _calculate_retry_after as __calculate_retry_after + _globals["_calculate_retry_after"] = __calculate_retry_after + return __calculate_retry_after + + if name == "_should_retry": + from .utils import _should_retry as __should_retry + _globals["_should_retry"] = __should_retry + return __should_retry + + if name == "get_supported_openai_params": + from .utils import get_supported_openai_params as _get_supported_openai_params + _globals["get_supported_openai_params"] = _get_supported_openai_params + return _get_supported_openai_params + + if name == "get_api_base": + from .utils import get_api_base as _get_api_base + _globals["get_api_base"] = _get_api_base + return _get_api_base + + if name == "get_first_chars_messages": + from .utils import get_first_chars_messages as _get_first_chars_messages + _globals["get_first_chars_messages"] = _get_first_chars_messages + return _get_first_chars_messages + + if name == "ModelResponse": + from .utils import ModelResponse as _ModelResponse + _globals["ModelResponse"] = _ModelResponse + return _ModelResponse + + if name == "ModelResponseStream": + from .utils import ModelResponseStream as _ModelResponseStream + _globals["ModelResponseStream"] = _ModelResponseStream + return _ModelResponseStream + + if name == "EmbeddingResponse": + from .utils import EmbeddingResponse as _EmbeddingResponse + _globals["EmbeddingResponse"] = _EmbeddingResponse + return _EmbeddingResponse + + if name == "ImageResponse": + from .utils import ImageResponse as _ImageResponse + _globals["ImageResponse"] = _ImageResponse + return _ImageResponse + + if name == "TranscriptionResponse": + from .utils import TranscriptionResponse as _TranscriptionResponse + _globals["TranscriptionResponse"] = _TranscriptionResponse + return _TranscriptionResponse + + if name == "TextCompletionResponse": + from .utils import TextCompletionResponse as _TextCompletionResponse + _globals["TextCompletionResponse"] = _TextCompletionResponse + return _TextCompletionResponse + + if name == "get_provider_fields": + from .utils import get_provider_fields as _get_provider_fields + _globals["get_provider_fields"] = _get_provider_fields + return _get_provider_fields + + if name == "ModelResponseListIterator": + from .utils import ModelResponseListIterator as _ModelResponseListIterator + _globals["ModelResponseListIterator"] = _ModelResponseListIterator + return _ModelResponseListIterator + + if name == "get_valid_models": + from .utils import get_valid_models as _get_valid_models + _globals["get_valid_models"] = _get_valid_models + return _get_valid_models + + raise AttributeError(f"Utils lazy import: unknown attribute {name!r}") + + +def _lazy_import_cost_calculator(name: str) -> Any: + """Lazy import for cost_calculator functions.""" + _globals = _get_litellm_globals() + from .cost_calculator import ( + completion_cost as _completion_cost, + cost_per_token as _cost_per_token, + response_cost_calculator as _response_cost_calculator, + ) + + _cost_functions = { + "completion_cost": _completion_cost, + "cost_per_token": _cost_per_token, + "response_cost_calculator": _response_cost_calculator, + } + + func = _cost_functions[name] + _globals[name] = func + return func + + +def _lazy_import_litellm_logging(name: str) -> Any: + """Lazy import for litellm_logging module.""" + _globals = _get_litellm_globals() + try: + from litellm.litellm_core_utils.litellm_logging import ( + Logging as _Logging, + modify_integration as _modify_integration, + ) + + _logging_objects = { + "Logging": _Logging, + "modify_integration": _modify_integration, + } + + obj = _logging_objects[name] + _globals[name] = obj + return obj + except Exception as e: + raise AttributeError( + f"module 'litellm' has no attribute {name!r}. " + f"Lazy import failed: {e}" + ) from e \ No newline at end of file diff --git a/litellm/images/main.py b/litellm/images/main.py index eacd4778299..770b16c1ed2 100644 --- a/litellm/images/main.py +++ b/litellm/images/main.py @@ -6,7 +6,9 @@ from typing import Any, Coroutine, Dict, List, Literal, Optional, Union, cast, o import httpx import litellm -from litellm import client, exception_type, get_litellm_params +from litellm.utils import exception_type, get_litellm_params +# client is imported from litellm as it's a decorator +from litellm import client from litellm.constants import DEFAULT_IMAGE_ENDPOINT_MODEL from litellm.constants import request_timeout as DEFAULT_REQUEST_TIMEOUT from litellm.exceptions import LiteLLMUnknownProvider diff --git a/litellm/llms/azure/azure.py b/litellm/llms/azure/azure.py index e7aa93ac882..994afa26e9c 100644 --- a/litellm/llms/azure/azure.py +++ b/litellm/llms/azure/azure.py @@ -1020,7 +1020,7 @@ class AzureChatCompletion(BaseAzureLLM, BaseLLM): headers: dict, client=None, timeout=None, - ) -> litellm.ImageResponse: + ) -> ImageResponse: response: Optional[dict] = None try: diff --git a/litellm/llms/azure_ai/embed/handler.py b/litellm/llms/azure_ai/embed/handler.py index 13b8cc4cf29..67733d1ccb5 100644 --- a/litellm/llms/azure_ai/embed/handler.py +++ b/litellm/llms/azure_ai/embed/handler.py @@ -58,7 +58,7 @@ class AzureAIEmbedding(OpenAIChatCompletion): data: ImageEmbeddingRequest, timeout: float, logging_obj, - model_response: litellm.EmbeddingResponse, + model_response: EmbeddingResponse, optional_params: dict, api_key: Optional[str], api_base: Optional[str], @@ -138,7 +138,7 @@ class AzureAIEmbedding(OpenAIChatCompletion): input: List, timeout: float, logging_obj, - model_response: litellm.EmbeddingResponse, + model_response: EmbeddingResponse, optional_params: dict, api_key: Optional[str] = None, api_base: Optional[str] = None, diff --git a/litellm/llms/bedrock/chat/invoke_transformations/amazon_nova_transformation.py b/litellm/llms/bedrock/chat/invoke_transformations/amazon_nova_transformation.py index a81d55f0ad2..3506c8f1cc0 100644 --- a/litellm/llms/bedrock/chat/invoke_transformations/amazon_nova_transformation.py +++ b/litellm/llms/bedrock/chat/invoke_transformations/amazon_nova_transformation.py @@ -10,7 +10,6 @@ from typing import Any, List, Optional import httpx -import litellm from litellm.litellm_core_utils.litellm_logging import Logging from litellm.types.llms.bedrock import BedrockInvokeNovaRequest from litellm.types.llms.openai import AllMessageValues @@ -80,7 +79,7 @@ class AmazonInvokeNovaConfig(AmazonInvokeConfig, AmazonConverseConfig): encoding: Any, api_key: Optional[str] = None, json_mode: Optional[bool] = None, - ) -> litellm.ModelResponse: + ) -> ModelResponse: return AmazonConverseConfig.transform_response( self, model, diff --git a/litellm/llms/bedrock/common_utils.py b/litellm/llms/bedrock/common_utils.py index ad115bc7e92..03ca7a5ad29 100644 --- a/litellm/llms/bedrock/common_utils.py +++ b/litellm/llms/bedrock/common_utils.py @@ -27,6 +27,25 @@ class BedrockError(BaseLLMException): pass +# Lazy import cache to avoid circular imports and performance impact +_get_model_info = None + + +def get_cached_model_info(): + """ + Lazy import and cache get_model_info to avoid circular imports. + + This function is used by bedrock transformation classes that need get_model_info + but cannot import it at module level due to circular import issues. + The function is cached after first use to avoid performance impact. + """ + global _get_model_info + if _get_model_info is None: + from litellm import get_model_info + _get_model_info = get_model_info + return _get_model_info + + class AmazonBedrockGlobalConfig: def __init__(self): pass diff --git a/litellm/llms/bedrock/image/amazon_nova_canvas_transformation.py b/litellm/llms/bedrock/image/amazon_nova_canvas_transformation.py index f2b94b617c0..18366999583 100644 --- a/litellm/llms/bedrock/image/amazon_nova_canvas_transformation.py +++ b/litellm/llms/bedrock/image/amazon_nova_canvas_transformation.py @@ -3,7 +3,6 @@ from typing import Any, Dict, List, Optional from openai.types.image import Image -from litellm import get_model_info from litellm.types.llms.bedrock import ( AmazonNovaCanvasColorGuidedGenerationParams, AmazonNovaCanvasColorGuidedRequest, @@ -15,6 +14,7 @@ from litellm.types.llms.bedrock import ( AmazonNovaCanvasTextToImageRequest, AmazonNovaCanvasTextToImageResponse, ) +from litellm.llms.bedrock.common_utils import get_cached_model_info from litellm.types.utils import ImageResponse @@ -207,6 +207,7 @@ class AmazonNovaCanvasConfig: size: Optional[str] = None, optional_params: Optional[dict] = None, ) -> float: + get_model_info = get_cached_model_info() model_info = get_model_info( model=model, custom_llm_provider="bedrock", diff --git a/litellm/llms/bedrock/image/amazon_stability1_transformation.py b/litellm/llms/bedrock/image/amazon_stability1_transformation.py index 63af32f3f56..07f82cec232 100644 --- a/litellm/llms/bedrock/image/amazon_stability1_transformation.py +++ b/litellm/llms/bedrock/image/amazon_stability1_transformation.py @@ -5,7 +5,7 @@ from typing import List, Optional from openai.types.image import Image -from litellm import get_model_info +from litellm.llms.bedrock.common_utils import get_cached_model_info from litellm.types.utils import ImageResponse @@ -151,6 +151,7 @@ class AmazonStabilityConfig: size = size or "1024-x-1024" model = f"{size}/{steps}/{model}" + get_model_info = get_cached_model_info() model_info = get_model_info( model=model, custom_llm_provider="bedrock", diff --git a/litellm/llms/bedrock/image/amazon_stability3_transformation.py b/litellm/llms/bedrock/image/amazon_stability3_transformation.py index 445a2fe1100..160d0af8e80 100644 --- a/litellm/llms/bedrock/image/amazon_stability3_transformation.py +++ b/litellm/llms/bedrock/image/amazon_stability3_transformation.py @@ -3,12 +3,12 @@ from typing import List, Optional from openai.types.image import Image -from litellm import get_model_info from litellm.llms.bedrock.common_utils import BedrockError from litellm.types.llms.bedrock import ( AmazonStability3TextToImageRequest, AmazonStability3TextToImageResponse, ) +from litellm.llms.bedrock.common_utils import get_cached_model_info from litellm.types.utils import ImageResponse @@ -115,6 +115,7 @@ class AmazonStability3Config: size: Optional[str] = None, optional_params: Optional[dict] = None, ) -> float: + get_model_info = get_cached_model_info() model_info = get_model_info( model=model, custom_llm_provider="bedrock", diff --git a/litellm/llms/bedrock/image/amazon_titan_transformation.py b/litellm/llms/bedrock/image/amazon_titan_transformation.py index bed9ad0c300..65411cabdcf 100644 --- a/litellm/llms/bedrock/image/amazon_titan_transformation.py +++ b/litellm/llms/bedrock/image/amazon_titan_transformation.py @@ -7,7 +7,7 @@ from typing import List, Optional from openai.types.image import Image -from litellm import get_model_info +from litellm.utils import get_model_info from litellm.types.llms.bedrock import ( AmazonNovaCanvasImageGenerationConfig, AmazonTitanImageGenerationRequestBody, diff --git a/litellm/llms/ovhcloud/chat/transformation.py b/litellm/llms/ovhcloud/chat/transformation.py index 6bdc28620ff..e9dc5be3eed 100644 --- a/litellm/llms/ovhcloud/chat/transformation.py +++ b/litellm/llms/ovhcloud/chat/transformation.py @@ -7,7 +7,9 @@ More information on our website: https://endpoints.ai.cloud.ovh.net from typing import Optional, Union, List import httpx -from litellm import ModelResponseStream, OpenAIGPTConfig, get_model_info, verbose_logger +from litellm.utils import ModelResponseStream, get_model_info +from litellm.llms.openai.chat.gpt_transformation import OpenAIGPTConfig +from litellm._logging import verbose_logger from litellm.llms.ovhcloud.utils import OVHCloudException from litellm.llms.base_llm.base_model_iterator import BaseModelResponseIterator from litellm.llms.base_llm.chat.transformation import BaseLLMException diff --git a/litellm/llms/together_ai/chat.py b/litellm/llms/together_ai/chat.py index 06d33f69750..e8a784d2779 100644 --- a/litellm/llms/together_ai/chat.py +++ b/litellm/llms/together_ai/chat.py @@ -8,7 +8,8 @@ Docs: https://docs.together.ai/reference/completions-1 from typing import Optional -from litellm import get_model_info, verbose_logger +from litellm.utils import get_model_info +from litellm._logging import verbose_logger from ..openai.chat.gpt_transformation import OpenAIGPTConfig diff --git a/litellm/llms/vertex_ai/common_utils.py b/litellm/llms/vertex_ai/common_utils.py index dc6a3170afe..a3ea8afe40d 100644 --- a/litellm/llms/vertex_ai/common_utils.py +++ b/litellm/llms/vertex_ai/common_utils.py @@ -5,7 +5,8 @@ from typing import Any, Dict, List, Literal, Optional, Set, Tuple, Union, get_ty import httpx import litellm -from litellm import supports_response_schema, supports_system_messages, verbose_logger +from litellm.utils import supports_response_schema, supports_system_messages +from litellm._logging import verbose_logger from litellm.constants import DEFAULT_MAX_RECURSE_DEPTH from litellm.litellm_core_utils.prompt_templates.common_utils import unpack_defs from litellm.llms.base_llm.base_utils import BaseLLMModelInfo, BaseTokenCounter diff --git a/litellm/llms/vertex_ai/gemini_embeddings/batch_embed_content_handler.py b/litellm/llms/vertex_ai/gemini_embeddings/batch_embed_content_handler.py index af9af71fef4..859bb0a6984 100644 --- a/litellm/llms/vertex_ai/gemini_embeddings/batch_embed_content_handler.py +++ b/litellm/llms/vertex_ai/gemini_embeddings/batch_embed_content_handler.py @@ -8,7 +8,7 @@ from typing import Any, Literal, Optional, Union import httpx import litellm -from litellm import EmbeddingResponse +from litellm.types.utils import EmbeddingResponse from litellm.llms.custom_httpx.http_handler import ( AsyncHTTPHandler, HTTPHandler, diff --git a/litellm/llms/vertex_ai/gemini_embeddings/batch_embed_content_transformation.py b/litellm/llms/vertex_ai/gemini_embeddings/batch_embed_content_transformation.py index 2c0f5dad228..455ec1d18f5 100644 --- a/litellm/llms/vertex_ai/gemini_embeddings/batch_embed_content_transformation.py +++ b/litellm/llms/vertex_ai/gemini_embeddings/batch_embed_content_transformation.py @@ -6,7 +6,7 @@ Why separate file? Make it easy to see how transformation works from typing import List -from litellm import EmbeddingResponse +from litellm.types.utils import EmbeddingResponse from litellm.types.llms.openai import EmbeddingInput from litellm.types.llms.vertex_ai import ( ContentType, diff --git a/litellm/llms/vertex_ai/image_generation/image_generation_handler.py b/litellm/llms/vertex_ai/image_generation/image_generation_handler.py index 04be4de8e32..e14cfe3be0b 100644 --- a/litellm/llms/vertex_ai/image_generation/image_generation_handler.py +++ b/litellm/llms/vertex_ai/image_generation/image_generation_handler.py @@ -176,7 +176,7 @@ class VertexImageGeneration(VertexLLM): vertex_project: Optional[str], vertex_location: Optional[str], vertex_credentials: Optional[VERTEX_CREDENTIALS_TYPES], - model_response: litellm.ImageResponse, + model_response: ImageResponse, logging_obj: Any, model: str = "imagegeneration", # vertex ai uses imagegeneration as the default model client: Optional[AsyncHTTPHandler] = None, diff --git a/litellm/llms/vertex_ai/multimodal_embeddings/embedding_handler.py b/litellm/llms/vertex_ai/multimodal_embeddings/embedding_handler.py index 582d7a4c569..d0ffc7be0a6 100644 --- a/litellm/llms/vertex_ai/multimodal_embeddings/embedding_handler.py +++ b/litellm/llms/vertex_ai/multimodal_embeddings/embedding_handler.py @@ -147,13 +147,13 @@ class VertexMultimodalEmbedding(VertexLLM): optional_params: dict, litellm_params: dict, data: dict, - model_response: litellm.EmbeddingResponse, + model_response: EmbeddingResponse, timeout: Optional[Union[float, httpx.Timeout]], logging_obj: LiteLLMLoggingObj, headers={}, client: Optional[AsyncHTTPHandler] = None, api_key: Optional[str] = None, - ) -> litellm.EmbeddingResponse: + ) -> EmbeddingResponse: if client is None: _params = {} if timeout is not None: diff --git a/litellm/llms/vertex_ai/vertex_embeddings/embedding_handler.py b/litellm/llms/vertex_ai/vertex_embeddings/embedding_handler.py index a170e6cc7f2..aaa6a0bb95f 100644 --- a/litellm/llms/vertex_ai/vertex_embeddings/embedding_handler.py +++ b/litellm/llms/vertex_ai/vertex_embeddings/embedding_handler.py @@ -137,7 +137,7 @@ class VertexEmbedding(VertexBase): self, model: str, input: Union[list, str], - model_response: litellm.EmbeddingResponse, + model_response: EmbeddingResponse, logging_obj: LiteLLMLoggingObject, optional_params: dict, custom_llm_provider: Literal[ @@ -152,7 +152,7 @@ class VertexEmbedding(VertexBase): gemini_api_key: Optional[str] = None, extra_headers: Optional[dict] = None, encoding=None, - ) -> litellm.EmbeddingResponse: + ) -> EmbeddingResponse: """ Async embedding implementation """ diff --git a/litellm/main.py b/litellm/main.py index 4f3abab52b7..0e19699309d 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -52,13 +52,10 @@ from pydantic import BaseModel from typing_extensions import overload import litellm -from litellm import ( # type: ignore - client, - exception_type, - get_litellm_params, - get_optional_params, -) - +# client must be imported from litellm as it's a decorator used at function definition time +from litellm import client +# Other utils are imported directly to avoid circular imports +from litellm.utils import exception_type, get_litellm_params, get_optional_params # Logging is imported lazily when needed to avoid loading litellm_logging at import time if TYPE_CHECKING: from litellm.litellm_core_utils.litellm_logging import Logging From 9783f0ff6c4edf302eb2845c9a6c0c4fdbf5958b Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Wed, 3 Dec 2025 12:14:48 -0800 Subject: [PATCH 060/107] Remove second scrollbar when sidebar is expanded + tooltip z index --- ui/litellm-dashboard/src/app/page.tsx | 2 +- ui/litellm-dashboard/src/components/model_dashboard/table.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ui/litellm-dashboard/src/app/page.tsx b/ui/litellm-dashboard/src/app/page.tsx index 422d140e07c..a71d7dd87d3 100644 --- a/ui/litellm-dashboard/src/app/page.tsx +++ b/ui/litellm-dashboard/src/app/page.tsx @@ -328,7 +328,7 @@ export default function CreateKeyPage() { sidebarCollapsed={sidebarCollapsed} onToggleSidebar={toggleSidebar} /> -
    + )} + + {/* Agent Selector - shown ONLY for A2A Agents endpoint */} + {endpointType === EndpointType.A2A_AGENTS && ( +
    + + Select Agent + + + {agentInfo.length === 0 && ( + + No agents found. Create agents via /v1/agents endpoint. + + )} +
    + )}
    @@ -1440,7 +1550,8 @@ const ChatUI: React.FC = ({ )} {message.role === "assistant" && - (message.timeToFirstToken || message.totalLatency || message.usage) && ( + (message.timeToFirstToken || message.totalLatency || message.usage) && + !message.a2aMetadata && ( = ({ toolName={message.toolName} /> )} + + {/* A2A Metrics - show for A2A agent responses */} + {message.role === "assistant" && message.a2aMetadata && ( + + )}
    @@ -1685,13 +1805,15 @@ const ChatUI: React.FC = ({ endpointType === EndpointType.RESPONSES || endpointType === EndpointType.ANTHROPIC_MESSAGES ? "Type your message... (Shift+Enter for new line)" - : endpointType === EndpointType.IMAGE_EDITS - ? "Describe how you want to edit the image..." - : endpointType === EndpointType.SPEECH - ? "Enter text to convert to speech..." - : endpointType === EndpointType.TRANSCRIPTION - ? "Optional: Add context or prompt for transcription..." - : "Describe the image you want to generate..." + : endpointType === EndpointType.A2A_AGENTS + ? "Send a message to the A2A agent..." + : endpointType === EndpointType.IMAGE_EDITS + ? "Describe how you want to edit the image..." + : endpointType === EndpointType.SPEECH + ? "Enter text to convert to speech..." + : endpointType === EndpointType.TRANSCRIPTION + ? "Optional: Add context or prompt for transcription..." + : "Describe the image you want to generate..." } disabled={isLoading} className="flex-1" diff --git a/ui/litellm-dashboard/src/components/playground/chat_ui/chatConstants.ts b/ui/litellm-dashboard/src/components/playground/chat_ui/chatConstants.ts index a64c0264f4c..e9b44836455 100644 --- a/ui/litellm-dashboard/src/components/playground/chat_ui/chatConstants.ts +++ b/ui/litellm-dashboard/src/components/playground/chat_ui/chatConstants.ts @@ -42,4 +42,5 @@ export const ENDPOINT_OPTIONS = [ { value: EndpointType.EMBEDDINGS, label: "/v1/embeddings" }, { value: EndpointType.SPEECH, label: "/v1/audio/speech" }, { value: EndpointType.TRANSCRIPTION, label: "/v1/audio/transcriptions" }, + { value: EndpointType.A2A_AGENTS, label: "/v1/a2a/message/send" }, ]; diff --git a/ui/litellm-dashboard/src/components/playground/chat_ui/mode_endpoint_mapping.tsx b/ui/litellm-dashboard/src/components/playground/chat_ui/mode_endpoint_mapping.tsx index 479d3cb8940..8ad0ae04a6d 100644 --- a/ui/litellm-dashboard/src/components/playground/chat_ui/mode_endpoint_mapping.tsx +++ b/ui/litellm-dashboard/src/components/playground/chat_ui/mode_endpoint_mapping.tsx @@ -25,6 +25,7 @@ export enum EndpointType { EMBEDDINGS = "embeddings", SPEECH = "speech", TRANSCRIPTION = "transcription", + A2A_AGENTS = "a2a_agents", // add additional endpoint types if required } diff --git a/ui/litellm-dashboard/src/components/playground/chat_ui/types.ts b/ui/litellm-dashboard/src/components/playground/chat_ui/types.ts index 15c90221fc0..4e43e6ec77d 100644 --- a/ui/litellm-dashboard/src/components/playground/chat_ui/types.ts +++ b/ui/litellm-dashboard/src/components/playground/chat_ui/types.ts @@ -70,12 +70,24 @@ export interface VectorStoreSearchResponse { data: VectorStoreSearchResult[]; } +export interface A2ATaskMetadata { + taskId?: string; + contextId?: string; + status?: { + state?: string; + timestamp?: string; + message?: string; + }; + metadata?: Record; +} + export interface MessageType { role: string; content: string | MultimodalContent[]; model?: string; isImage?: boolean; isAudio?: boolean; + isEmbeddings?: boolean; reasoningContent?: string; timeToFirstToken?: number; totalLatency?: number; @@ -93,6 +105,7 @@ export interface MessageType { detail: string; }; searchResults?: VectorStoreSearchResponse[]; + a2aMetadata?: A2ATaskMetadata; } export interface MultimodalContent { diff --git a/ui/litellm-dashboard/src/components/playground/llm_calls/a2a_send_message.tsx b/ui/litellm-dashboard/src/components/playground/llm_calls/a2a_send_message.tsx new file mode 100644 index 00000000000..7d799a837a7 --- /dev/null +++ b/ui/litellm-dashboard/src/components/playground/llm_calls/a2a_send_message.tsx @@ -0,0 +1,413 @@ +// a2a_send_message.tsx +// A2A Protocol (JSON-RPC 2.0) implementation for sending messages to agents + +import { v4 as uuidv4 } from "uuid"; +import { getProxyBaseUrl } from "../../networking"; +import { A2ATaskMetadata } from "../chat_ui/types"; + +interface A2AMessagePart { + kind: "text"; + text: string; +} + +interface A2AMessage { + kind: "message"; + messageId: string; + role: "user" | "agent"; + parts: A2AMessagePart[]; +} + +interface A2AJsonRpcRequest { + jsonrpc: "2.0"; + id: string; + method: string; + params: { + message: A2AMessage; + }; +} + +interface A2AJsonRpcResponse { + jsonrpc: "2.0"; + id: string; + result?: { + kind?: string; + parts?: A2AMessagePart[]; + id?: string; + contextId?: string; + status?: { + state?: string; + timestamp?: string; + message?: { + parts?: A2AMessagePart[]; + }; + }; + metadata?: Record; + artifacts?: Array<{ + artifactId?: string; + name?: string; + parts?: A2AMessagePart[]; + }>; + [key: string]: any; + }; + error?: { + code: number; + message: string; + }; +} + +/** + * Extracts A2A task metadata from the response result. + */ +const extractA2AMetadata = (result: A2AJsonRpcResponse["result"]): A2ATaskMetadata | undefined => { + if (!result) return undefined; + + const metadata: A2ATaskMetadata = {}; + + // Extract task ID + if (result.id) { + metadata.taskId = result.id; + } + + // Extract context/session ID + if (result.contextId) { + metadata.contextId = result.contextId; + } + + // Extract status + if (result.status) { + metadata.status = { + state: result.status.state, + timestamp: result.status.timestamp, + }; + + // Extract status message text if present + if (result.status.message?.parts) { + const statusText = result.status.message.parts + .filter((p: any) => p.kind === "text" && p.text) + .map((p: any) => p.text) + .join(" "); + if (statusText) { + metadata.status.message = statusText; + } + } + } + + // Extract custom metadata + if (result.metadata && typeof result.metadata === "object") { + metadata.metadata = result.metadata; + } + + return Object.keys(metadata).length > 0 ? metadata : undefined; +}; + +/** + * Sends a message to an A2A agent using the JSON-RPC 2.0 protocol. + * Uses the non-streaming message/send method. + */ +export const makeA2ASendMessageRequest = async ( + agentId: string, + message: string, + onTextUpdate: (chunk: string, model?: string) => void, + accessToken: string, + signal?: AbortSignal, + onTimingData?: (timeToFirstToken: number) => void, + onTotalLatency?: (totalLatency: number) => void, + onA2AMetadata?: (metadata: A2ATaskMetadata) => void, +): Promise => { + const proxyBaseUrl = getProxyBaseUrl(); + const url = proxyBaseUrl + ? `${proxyBaseUrl}/a2a/${agentId}/message/send` + : `/a2a/${agentId}/message/send`; + + const requestId = uuidv4(); + const messageId = uuidv4().replace(/-/g, ""); + + const jsonRpcRequest: A2AJsonRpcRequest = { + jsonrpc: "2.0", + id: requestId, + method: "message/send", + params: { + message: { + kind: "message", + messageId: messageId, + role: "user", + parts: [{ kind: "text", text: message }], + }, + }, + }; + + const startTime = performance.now(); + + try { + const response = await fetch(url, { + method: "POST", + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(jsonRpcRequest), + signal, + }); + + const timeToFirstToken = performance.now() - startTime; + if (onTimingData) { + onTimingData(timeToFirstToken); + } + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error?.message || errorData.detail || `HTTP ${response.status}`); + } + + const jsonRpcResponse: A2AJsonRpcResponse = await response.json(); + + const totalLatency = performance.now() - startTime; + if (onTotalLatency) { + onTotalLatency(totalLatency); + } + + if (jsonRpcResponse.error) { + throw new Error(jsonRpcResponse.error.message); + } + + // Extract text and metadata from response + const result = jsonRpcResponse.result; + if (result) { + let responseText = ""; + + // Extract and send A2A metadata + const a2aMetadata = extractA2AMetadata(result); + if (a2aMetadata && onA2AMetadata) { + onA2AMetadata(a2aMetadata); + } + + // A2A Task response format with artifacts array + // Extract text from artifacts[*].parts[*] where kind === "text" + if (result.artifacts && Array.isArray(result.artifacts)) { + for (const artifact of result.artifacts) { + if (artifact.parts && Array.isArray(artifact.parts)) { + for (const part of artifact.parts) { + if (part.kind === "text" && part.text) { + responseText += part.text; + } + } + } + } + } + // Fallback: direct parts array (simpler response format) + else if (result.parts && Array.isArray(result.parts)) { + for (const part of result.parts) { + if (part.kind === "text" && part.text) { + responseText += part.text; + } + } + } + // Fallback: status.message.parts format + else if (result.status?.message?.parts) { + for (const part of result.status.message.parts) { + if (part.kind === "text" && part.text) { + responseText += part.text; + } + } + } + + if (responseText) { + onTextUpdate(responseText, `a2a_agent/${agentId}`); + } else { + // Fallback: show raw result if we couldn't parse it + console.warn("Could not extract text from A2A response, showing raw JSON:", result); + onTextUpdate(JSON.stringify(result, null, 2), `a2a_agent/${agentId}`); + } + } + } catch (error) { + if (signal?.aborted) { + console.log("A2A request was cancelled"); + return; + } + console.error("A2A send message error:", error); + throw error; + } +}; + +/** + * Sends a streaming message to an A2A agent using the JSON-RPC 2.0 protocol. + * Uses the message/stream method with NDJSON responses. + */ +export const makeA2AStreamMessageRequest = async ( + agentId: string, + message: string, + onTextUpdate: (chunk: string, model?: string) => void, + accessToken: string, + signal?: AbortSignal, + onTimingData?: (timeToFirstToken: number) => void, + onTotalLatency?: (totalLatency: number) => void, + onA2AMetadata?: (metadata: A2ATaskMetadata) => void, +): Promise => { + const proxyBaseUrl = getProxyBaseUrl(); + const url = proxyBaseUrl + ? `${proxyBaseUrl}/a2a/${agentId}` + : `/a2a/${agentId}`; + + const requestId = uuidv4(); + const messageId = uuidv4().replace(/-/g, ""); + + const jsonRpcRequest: A2AJsonRpcRequest = { + jsonrpc: "2.0", + id: requestId, + method: "message/stream", + params: { + message: { + kind: "message", + messageId: messageId, + role: "user", + parts: [{ kind: "text", text: message }], + }, + }, + }; + + const startTime = performance.now(); + let firstChunkReceived = false; + let latestMetadata: A2ATaskMetadata | undefined; + let accumulatedText = ""; + + try { + const response = await fetch(url, { + method: "POST", + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(jsonRpcRequest), + signal, + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error?.message || errorData.detail || `HTTP ${response.status}`); + } + + const reader = response.body?.getReader(); + if (!reader) { + throw new Error("No response body"); + } + + const decoder = new TextDecoder(); + let buffer = ""; + let done = false; + + while (!done) { + const readResult = await reader.read(); + done = readResult.done; + const value = readResult.value; + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split("\n"); + buffer = lines.pop() || ""; + + for (const line of lines) { + if (!line.trim()) continue; + + try { + const chunk = JSON.parse(line); + + if (!firstChunkReceived) { + firstChunkReceived = true; + const timeToFirstToken = performance.now() - startTime; + if (onTimingData) { + onTimingData(timeToFirstToken); + } + } + + // Handle streaming chunks - extract text from various A2A formats + const result = chunk.result; + if (result) { + // Extract metadata from each chunk (keep latest) + const chunkMetadata = extractA2AMetadata(result); + if (chunkMetadata) { + latestMetadata = { ...latestMetadata, ...chunkMetadata }; + } + + const chunkKind = result.kind; + + // Handle artifact-update chunks (streaming response content) + // Note: streaming uses "artifact" (singular), not "artifacts" (plural) + if (chunkKind === "artifact-update" && result.artifact) { + const artifact = result.artifact; + if (artifact.parts && Array.isArray(artifact.parts)) { + for (const part of artifact.parts) { + if (part.kind === "text" && part.text) { + // Accumulate actual response content + accumulatedText += part.text; + onTextUpdate(accumulatedText, `a2a_agent/${agentId}`); + } + } + } + } + // Handle non-streaming Task response format with artifacts array (plural) + else if (result.artifacts && Array.isArray(result.artifacts)) { + for (const artifact of result.artifacts) { + if (artifact.parts && Array.isArray(artifact.parts)) { + for (const part of artifact.parts) { + if (part.kind === "text" && part.text) { + accumulatedText += part.text; + onTextUpdate(accumulatedText, `a2a_agent/${agentId}`); + } + } + } + } + } + // Handle status-update chunks (progress messages like "Processing request...") + // Only show these temporarily if we haven't received actual content yet + else if (chunkKind === "status-update" && result.status?.message?.parts) { + // Skip status messages once we have real content + if (!accumulatedText) { + for (const part of result.status.message.parts) { + if (part.kind === "text" && part.text) { + // Show as temporary status - will be replaced when real content arrives + onTextUpdate(part.text, `a2a_agent/${agentId}`); + } + } + } + } + // Direct parts array (fallback) + else if (result.parts && Array.isArray(result.parts)) { + for (const part of result.parts) { + if (part.kind === "text" && part.text) { + accumulatedText += part.text; + onTextUpdate(accumulatedText, `a2a_agent/${agentId}`); + } + } + } + } + + if (chunk.error) { + throw new Error(chunk.error.message); + } + } catch (parseError) { + // Only warn if it's not a JSON parse error on an empty/partial line + if (line.trim().length > 0) { + console.warn("Failed to parse A2A streaming chunk:", line, parseError); + } + } + } + } + + const totalLatency = performance.now() - startTime; + if (onTotalLatency) { + onTotalLatency(totalLatency); + } + + // Send final metadata after streaming completes + if (latestMetadata && onA2AMetadata) { + onA2AMetadata(latestMetadata); + } + } catch (error) { + if (signal?.aborted) { + console.log("A2A streaming request was cancelled"); + return; + } + console.error("A2A stream message error:", error); + throw error; + } +}; diff --git a/ui/litellm-dashboard/src/components/playground/llm_calls/fetch_agents.tsx b/ui/litellm-dashboard/src/components/playground/llm_calls/fetch_agents.tsx new file mode 100644 index 00000000000..054e2216b63 --- /dev/null +++ b/ui/litellm-dashboard/src/components/playground/llm_calls/fetch_agents.tsx @@ -0,0 +1,52 @@ +// fetch_agents.tsx + +import { getProxyBaseUrl } from "../../networking"; + +export interface Agent { + agent_id: string; + agent_name: string; + description?: string; + agent_card_params?: { + name?: string; + description?: string; + url?: string; + }; +} + +/** + * Fetches available A2A agents from /v1/agents endpoint. + */ +export const fetchAvailableAgents = async (accessToken: string): Promise => { + try { + const proxyBaseUrl = getProxyBaseUrl(); + const url = proxyBaseUrl ? `${proxyBaseUrl}/v1/agents` : `/v1/agents`; + + const response = await fetch(url, { + method: "GET", + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.detail || "Failed to fetch agents"); + } + + const agents: Agent[] = await response.json(); + console.log("Fetched agents:", agents); + + // Sort agents alphabetically by name + agents.sort((a, b) => { + const nameA = a.agent_name || a.agent_id; + const nameB = b.agent_name || b.agent_id; + return nameA.localeCompare(nameB); + }); + + return agents; + } catch (error) { + console.error("Error fetching agents:", error); + throw error; + } +}; From b96657a0830458f4ad3788dae4a4fe8f148e832f Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 3 Dec 2025 18:57:57 -0800 Subject: [PATCH 075/107] [Feat] UI - Agent Gateway - allow testing agents on UI (#17455) * init litellm A2a client * simpler a2a client interface * test a2a * move a2a invoking tests * test fix * ensure a2a send message is tracked n logs * rename tags * add streaming handlng * add a2a invocation * add a2a invocation i cost calc * test_a2a_logging_payload * update invoke_agent_a2a * test_invoke_agent_a2a_adds_litellm_data * add A2a agent * fix endpoints on A2a * UI allow testing a2a endpoints From e7f066ea7dfffd55a2b9aa69d1f1ec86c0b9526b Mon Sep 17 00:00:00 2001 From: Ishaan Jaffer Date: Wed, 3 Dec 2025 19:32:30 -0800 Subject: [PATCH 076/107] test_hosted_vllm_full_workflow --- tests/batches_tests/test_hosted_vllm_batches_and_files.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/batches_tests/test_hosted_vllm_batches_and_files.py b/tests/batches_tests/test_hosted_vllm_batches_and_files.py index 432dc81bad2..aa4d847a45d 100644 --- a/tests/batches_tests/test_hosted_vllm_batches_and_files.py +++ b/tests/batches_tests/test_hosted_vllm_batches_and_files.py @@ -26,6 +26,7 @@ SERVER_URL = "https://exampleopenaiendpoint-production-0ee2.up.railway.app/v1" @pytest.mark.asyncio() +@pytest.mark.skip(reason="Local only test") async def test_hosted_vllm_full_workflow(): """ Test the complete workflow: create file -> create batch -> retrieve batch -> retrieve file. From 92cca5d515d984f497ed74d5005ee0ace50fdc37 Mon Sep 17 00:00:00 2001 From: Ishaan Jaffer Date: Wed, 3 Dec 2025 19:32:40 -0800 Subject: [PATCH 077/107] test_cyberark_write_and_read_secret --- tests/litellm_utils_tests/test_cyberark.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/tests/litellm_utils_tests/test_cyberark.py b/tests/litellm_utils_tests/test_cyberark.py index 24c84687705..b94e5949534 100644 --- a/tests/litellm_utils_tests/test_cyberark.py +++ b/tests/litellm_utils_tests/test_cyberark.py @@ -53,13 +53,14 @@ async def test_cyberark_write_and_read_secret(): secret_value = f"test-value-{uuid.uuid4()}" # Mock sync httpx client (for auth, ensure variable exists, sync read) + # The _get_httpx_client returns an HTTPHandler with a .client property mock_sync_client = MagicMock() - # Auth response - mock_sync_client.post.return_value = create_mock_response( + # Auth response - note: the actual client is accessed via .client property + mock_sync_client.client.post.return_value = create_mock_response( status_code=200, text="mock-token" ) # Sync read response - mock_sync_client.get.return_value = create_mock_response( + mock_sync_client.client.get.return_value = create_mock_response( status_code=200, text=secret_value ) @@ -123,9 +124,10 @@ async def test_cyberark_rotate_secret(): current_value = {"value": initial_key_value} # Mock sync httpx client (for auth, ensure variable exists, sync reads) + # The _get_httpx_client returns an HTTPHandler with a .client property mock_sync_client = MagicMock() - # Auth response - mock_sync_client.post.return_value = create_mock_response( + # Auth response - note: the actual client is accessed via .client property + mock_sync_client.client.post.return_value = create_mock_response( status_code=200, text="mock-token" ) @@ -133,7 +135,7 @@ async def test_cyberark_rotate_secret(): def get_mock_sync_read_response(*args, **kwargs): return create_mock_response(status_code=200, text=current_value["value"]) - mock_sync_client.get.side_effect = get_mock_sync_read_response + mock_sync_client.client.get.side_effect = get_mock_sync_read_response # Mock async httpx client (for async writes and reads) mock_async_client = AsyncMock() @@ -228,9 +230,10 @@ async def test_cyberark_rotate_secret_with_new_alias(): secrets_store = {} # Mock sync httpx client (for auth, ensure variable exists, sync reads) + # The _get_httpx_client returns an HTTPHandler with a .client property mock_sync_client = MagicMock() - # Auth response - mock_sync_client.post.return_value = create_mock_response( + # Auth response - note: the actual client is accessed via .client property + mock_sync_client.client.post.return_value = create_mock_response( status_code=200, text="mock-token" ) @@ -243,7 +246,7 @@ async def test_cyberark_rotate_secret_with_new_alias(): return create_mock_response(status_code=200, text=secret_val) return create_mock_response(status_code=404, text="Not found") - mock_sync_client.get.side_effect = get_mock_sync_read + mock_sync_client.client.get.side_effect = get_mock_sync_read # Mock async httpx client (for async writes and reads) mock_async_client = AsyncMock() From 8a824b7c17c6e30d751a0b6fe31b2637391e8b19 Mon Sep 17 00:00:00 2001 From: Ishaan Jaffer Date: Wed, 3 Dec 2025 19:35:55 -0800 Subject: [PATCH 078/107] fix mypy linting --- litellm/{a2a => a2a_protocol}/__init__.py | 8 ++++---- litellm/{a2a => a2a_protocol}/client.py | 10 +++++----- litellm/{a2a => a2a_protocol}/cost_calculator.py | 0 litellm/{a2a => a2a_protocol}/main.py | 4 ++-- litellm/cost_calculator.py | 2 +- litellm/proxy/agent_endpoints/a2a_endpoints.py | 2 +- tests/agent_tests/test_a2a.py | 4 ++-- .../proxy/agent_endpoints/test_a2a_endpoints.py | 4 ++-- 8 files changed, 17 insertions(+), 17 deletions(-) rename litellm/{a2a => a2a_protocol}/__init__.py (88%) rename litellm/{a2a => a2a_protocol}/client.py (90%) rename litellm/{a2a => a2a_protocol}/cost_calculator.py (100%) rename litellm/{a2a => a2a_protocol}/main.py (98%) diff --git a/litellm/a2a/__init__.py b/litellm/a2a_protocol/__init__.py similarity index 88% rename from litellm/a2a/__init__.py rename to litellm/a2a_protocol/__init__.py index a2e9770cd20..d8d349bb98a 100644 --- a/litellm/a2a/__init__.py +++ b/litellm/a2a_protocol/__init__.py @@ -8,7 +8,7 @@ This module provides a thin wrapper around the official `a2a` SDK that: Example usage (standalone functions with @client decorator): ```python - from litellm.a2a import asend_message + from litellm.a2a_protocol import asend_message from a2a.types import SendMessageRequest, MessageSendParams from uuid import uuid4 @@ -31,15 +31,15 @@ Example usage (standalone functions with @client decorator): Example usage (class-based): ```python - from litellm.a2a import A2AClient + from litellm.a2a_protocol import A2AClient client = A2AClient(base_url="http://localhost:10001") response = await client.send_message(request) ``` """ -from litellm.a2a.client import A2AClient -from litellm.a2a.main import ( +from litellm.a2a_protocol.client import A2AClient +from litellm.a2a_protocol.main import ( aget_agent_card, asend_message, asend_message_streaming, diff --git a/litellm/a2a/client.py b/litellm/a2a_protocol/client.py similarity index 90% rename from litellm/a2a/client.py rename to litellm/a2a_protocol/client.py index 0c3b8fff2ba..31f7c3b6a90 100644 --- a/litellm/a2a/client.py +++ b/litellm/a2a_protocol/client.py @@ -26,7 +26,7 @@ class A2AClient: Example: ```python - from litellm.a2a import A2AClient + from litellm.a2a_protocol import A2AClient from a2a.types import SendMessageRequest, MessageSendParams from uuid import uuid4 @@ -68,7 +68,7 @@ class A2AClient: async def _get_client(self) -> "A2AClientType": """Get or create the underlying A2A client.""" if self._a2a_client is None: - from litellm.a2a.main import create_a2a_client + from litellm.a2a_protocol.main import create_a2a_client self._a2a_client = await create_a2a_client( base_url=self.base_url, @@ -79,7 +79,7 @@ class A2AClient: async def get_agent_card(self) -> "AgentCard": """Fetch the agent card from the server.""" - from litellm.a2a.main import aget_agent_card + from litellm.a2a_protocol.main import aget_agent_card return await aget_agent_card( base_url=self.base_url, @@ -91,7 +91,7 @@ class A2AClient: self, request: "SendMessageRequest" ) -> LiteLLMSendMessageResponse: """Send a message to the A2A agent.""" - from litellm.a2a.main import asend_message + from litellm.a2a_protocol.main import asend_message a2a_client = await self._get_client() return await asend_message(a2a_client=a2a_client, request=request) @@ -100,7 +100,7 @@ class A2AClient: self, request: "SendStreamingMessageRequest" ) -> AsyncIterator["SendStreamingMessageResponse"]: """Send a streaming message to the A2A agent.""" - from litellm.a2a.main import asend_message_streaming + from litellm.a2a_protocol.main import asend_message_streaming a2a_client = await self._get_client() async for chunk in asend_message_streaming(a2a_client=a2a_client, request=request): diff --git a/litellm/a2a/cost_calculator.py b/litellm/a2a_protocol/cost_calculator.py similarity index 100% rename from litellm/a2a/cost_calculator.py rename to litellm/a2a_protocol/cost_calculator.py diff --git a/litellm/a2a/main.py b/litellm/a2a_protocol/main.py similarity index 98% rename from litellm/a2a/main.py rename to litellm/a2a_protocol/main.py index 7827581ceac..5cb238904f0 100644 --- a/litellm/a2a/main.py +++ b/litellm/a2a_protocol/main.py @@ -91,7 +91,7 @@ async def asend_message( Example: ```python - from litellm.a2a import asend_message, create_a2a_client + from litellm.a2a_protocol import asend_message, create_a2a_client from a2a.types import SendMessageRequest, MessageSendParams from uuid import uuid4 @@ -205,7 +205,7 @@ async def create_a2a_client( Example: ```python - from litellm.a2a import create_a2a_client, asend_message + from litellm.a2a_protocol import create_a2a_client, asend_message # Create client once client = await create_a2a_client(base_url="http://localhost:10001") diff --git a/litellm/cost_calculator.py b/litellm/cost_calculator.py index 943eb069565..ea7caf93ed0 100644 --- a/litellm/cost_calculator.py +++ b/litellm/cost_calculator.py @@ -939,7 +939,7 @@ def completion_cost( # noqa: PLR0915 CallTypes.asend_message.value, CallTypes.send_message.value, ): - from litellm.a2a.cost_calculator import A2ACostCalculator + from litellm.a2a_protocol.cost_calculator import A2ACostCalculator return A2ACostCalculator.calculate_a2a_cost( litellm_logging_obj=litellm_logging_obj diff --git a/litellm/proxy/agent_endpoints/a2a_endpoints.py b/litellm/proxy/agent_endpoints/a2a_endpoints.py index 69c1c5938b6..32af1232f6b 100644 --- a/litellm/proxy/agent_endpoints/a2a_endpoints.py +++ b/litellm/proxy/agent_endpoints/a2a_endpoints.py @@ -138,7 +138,7 @@ async def invoke_agent_a2a( - message/send: Send a message and get a response - message/stream: Send a message and stream the response """ - from litellm.a2a import asend_message, create_a2a_client + from litellm.a2a_protocol import asend_message, create_a2a_client from litellm.proxy.litellm_pre_call_utils import add_litellm_data_to_request from litellm.proxy.proxy_server import ( general_settings, diff --git a/tests/agent_tests/test_a2a.py b/tests/agent_tests/test_a2a.py index 675b3b98e4d..eeab2680564 100644 --- a/tests/agent_tests/test_a2a.py +++ b/tests/agent_tests/test_a2a.py @@ -32,7 +32,7 @@ async def test_asend_message_with_client_decorator(): This tests the LiteLLM logging integration. """ litellm._turn_on_debug() - from litellm.a2a import asend_message, create_a2a_client + from litellm.a2a_protocol import asend_message, create_a2a_client # Create the A2A client first a2a_client = await create_a2a_client(base_url="http://localhost:10001") @@ -97,7 +97,7 @@ async def test_a2a_logging_payload(): test_logger = TestA2ALogger() litellm.callbacks = [test_logger] - from litellm.a2a import asend_message, create_a2a_client + from litellm.a2a_protocol import asend_message, create_a2a_client # Create the A2A client first a2a_client = await create_a2a_client(base_url="http://localhost:10001") diff --git a/tests/test_litellm/proxy/agent_endpoints/test_a2a_endpoints.py b/tests/test_litellm/proxy/agent_endpoints/test_a2a_endpoints.py index c9b8917b20c..c7257073b33 100644 --- a/tests/test_litellm/proxy/agent_endpoints/test_a2a_endpoints.py +++ b/tests/test_litellm/proxy/agent_endpoints/test_a2a_endpoints.py @@ -75,10 +75,10 @@ async def test_invoke_agent_a2a_adds_litellm_data(): "litellm.proxy.litellm_pre_call_utils.add_litellm_data_to_request", side_effect=mock_add_litellm_data, ) as mock_add_data, patch( - "litellm.a2a.create_a2a_client", + "litellm.a2a_protocol.create_a2a_client", new_callable=AsyncMock, ), patch( - "litellm.a2a.asend_message", + "litellm.a2a_protocol.asend_message", new_callable=AsyncMock, return_value=mock_response, ), patch( From 607d517c013a087f8b38d0ea01bf9b1435db056f Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 3 Dec 2025 19:44:24 -0800 Subject: [PATCH 079/107] Revert "Bump mcp from 1.10.1 to 1.23.0 in /.circleci (#17363)" (#17457) This reverts commit 462d423d86ce424b887f599d2b6427099936831e. --- .circleci/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/requirements.txt b/.circleci/requirements.txt index 6f1043f59a5..2294c84813c 100644 --- a/.circleci/requirements.txt +++ b/.circleci/requirements.txt @@ -13,7 +13,7 @@ google-cloud-aiplatform==1.43.0 google-cloud-iam==2.19.1 fastapi-sso==0.16.0 uvloop==0.21.0 -mcp==1.23.0 # for MCP server +mcp==1.10.1 # for MCP server semantic_router==0.1.10 # for auto-routing with litellm fastuuid==0.12.0 responses==0.25.7 # for proxy client tests \ No newline at end of file From a711b63b062ab7b907ccd7fd976a4c6eccd5f4ca Mon Sep 17 00:00:00 2001 From: Xander Song Date: Wed, 3 Dec 2025 22:50:20 -0500 Subject: [PATCH 080/107] fix: clean arize-phoenix traces (#16611) * fix * clean * clean * clean * clean * clean * lint --- litellm/integrations/arize/arize_phoenix.py | 57 +++++++++++++++++-- litellm/integrations/opentelemetry.py | 9 +-- litellm/litellm_core_utils/litellm_logging.py | 8 +-- 3 files changed, 58 insertions(+), 16 deletions(-) diff --git a/litellm/integrations/arize/arize_phoenix.py b/litellm/integrations/arize/arize_phoenix.py index ab70dd9d0e2..4a6e0cec8ca 100644 --- a/litellm/integrations/arize/arize_phoenix.py +++ b/litellm/integrations/arize/arize_phoenix.py @@ -1,18 +1,20 @@ import os -from typing import TYPE_CHECKING, Any, Union +from typing import TYPE_CHECKING, Any, Optional, Union +from datetime import datetime from litellm._logging import verbose_logger from litellm.integrations.arize import _utils from litellm.integrations.arize._utils import ArizeOTELAttributes from litellm.types.integrations.arize_phoenix import ArizePhoenixConfig +from litellm.types.services import ServiceLoggerPayload +from litellm.integrations.opentelemetry import OpenTelemetry if TYPE_CHECKING: from opentelemetry.trace import Span as _Span + from litellm.integrations.opentelemetry import OpenTelemetryConfig as _OpenTelemetryConfig from litellm.types.integrations.arize import Protocol as _Protocol - from .opentelemetry import OpenTelemetryConfig as _OpenTelemetryConfig - Protocol = _Protocol OpenTelemetryConfig = _OpenTelemetryConfig Span = Union[_Span, Any] @@ -25,7 +27,11 @@ else: ARIZE_HOSTED_PHOENIX_ENDPOINT = "https://otlp.arize.com/v1/traces" -class ArizePhoenixLogger: +class ArizePhoenixLogger(OpenTelemetry): + def set_attributes(self, span: Span, kwargs, response_obj: Optional[Any]): + ArizePhoenixLogger.set_arize_phoenix_attributes(span, kwargs, response_obj) + return + @staticmethod def set_arize_phoenix_attributes(span: Span, kwargs, response_obj): _utils.set_attributes(span, kwargs, response_obj, ArizeOTELAttributes) @@ -97,3 +103,46 @@ class ArizePhoenixLogger: endpoint=endpoint, project_name=project_name, ) + + async def async_service_success_hook( + self, + payload: ServiceLoggerPayload, + parent_otel_span: Optional[Span] = None, + start_time: Optional[Union[datetime, float]] = None, + end_time: Optional[Union[datetime, float]] = None, + event_metadata: Optional[dict] = None, + ): + pass # suppress additional spans + + async def async_service_failure_hook( + self, + payload: ServiceLoggerPayload, + error: Optional[str] = "", + parent_otel_span: Optional[Span] = None, + start_time: Optional[Union[datetime, float]] = None, + end_time: Optional[Union[float, datetime]] = None, + event_metadata: Optional[dict] = None, + ): + pass # suppress additional spans + + def create_litellm_proxy_request_started_span( + self, + start_time: datetime, + headers: dict, + ): + pass # suppress additional spans + + async def async_health_check(self): + + config = self.get_arize_phoenix_config() + + if not config.otlp_auth_headers: + return { + "status": "unhealthy", + "error_message": "PHOENIX_API_KEY environment variable not set", + } + + return { + "status": "healthy", + "message": "Arize-Phoenix credentials are configured properly", + } \ No newline at end of file diff --git a/litellm/integrations/opentelemetry.py b/litellm/integrations/opentelemetry.py index 90bf19b21fe..23bffc6c7e5 100644 --- a/litellm/integrations/opentelemetry.py +++ b/litellm/integrations/opentelemetry.py @@ -1065,14 +1065,7 @@ class OpenTelemetry(CustomLogger): self, span: Span, kwargs, response_obj: Optional[Any] ): try: - if self.callback_name == "arize_phoenix": - from litellm.integrations.arize.arize_phoenix import ArizePhoenixLogger - - ArizePhoenixLogger.set_arize_phoenix_attributes( - span, kwargs, response_obj - ) - return - elif self.callback_name == "langtrace": + if self.callback_name == "langtrace": from litellm.integrations.langtrace import LangtraceAttributes LangtraceAttributes().set_langtrace_attributes( diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index 4fd2c988797..6440e5f6dee 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -3619,15 +3619,15 @@ def _init_custom_logger_compatible_class( # noqa: PLR0915 for callback in _in_memory_loggers: if ( - isinstance(callback, OpenTelemetry) + isinstance(callback, ArizePhoenixLogger) and callback.callback_name == "arize_phoenix" ): return callback # type: ignore - _otel_logger = OpenTelemetry( + _arize_phoenix_otel_logger = ArizePhoenixLogger( config=otel_config, callback_name="arize_phoenix" ) - _in_memory_loggers.append(_otel_logger) - return _otel_logger # type: ignore + _in_memory_loggers.append(_arize_phoenix_otel_logger) + return _arize_phoenix_otel_logger # type: ignore elif logging_integration == "otel": from litellm.integrations.opentelemetry import OpenTelemetry From 3de84b3f8b2e6d001827da05bae088114b24eae1 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Wed, 3 Dec 2025 20:34:41 -0800 Subject: [PATCH 081/107] e2e tests --- litellm/proxy/proxy_server.py | 2 +- .../proxy_admin_ui_tests/e2e_ui_tests/login_to_ui.spec.ts | 8 ++++++-- .../e2e_ui_tests/search_users.spec.ts | 4 ++-- .../e2e_ui_tests/view_internal_user.spec.ts | 8 ++++---- .../e2e_ui_tests/view_user_info.spec.ts | 1 + ui/litellm-dashboard/src/app/page.tsx | 2 +- 6 files changed, 15 insertions(+), 10 deletions(-) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 40436e1716b..ebe2a90f37f 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -555,7 +555,7 @@ else: global_max_parallel_request_retry_timeout_env ) -ui_link = f"{server_root_path}/ui/login" +ui_link = f"{server_root_path}/ui" fallback_login_link = f"{server_root_path}/fallback/login" model_hub_link = f"{server_root_path}/ui/model_hub_table" ui_message = ( diff --git a/tests/proxy_admin_ui_tests/e2e_ui_tests/login_to_ui.spec.ts b/tests/proxy_admin_ui_tests/e2e_ui_tests/login_to_ui.spec.ts index fc4d4653246..793ae53b39a 100644 --- a/tests/proxy_admin_ui_tests/e2e_ui_tests/login_to_ui.spec.ts +++ b/tests/proxy_admin_ui_tests/e2e_ui_tests/login_to_ui.spec.ts @@ -10,7 +10,9 @@ import { test, expect } from "@playwright/test"; test("admin login test", async ({ page }) => { // Go to the specified URL - await page.goto("http://localhost:4000/ui/login"); + await page.goto("http://localhost:4000/ui"); + + page.screenshot({ path: "login_before.png" }); // Enter "admin" in the username input field await page.fill('input[name="username"]', "admin"); @@ -18,8 +20,10 @@ test("admin login test", async ({ page }) => { // Enter "gm" in the password input field await page.fill('input[name="password"]', "gm"); + page.screenshot({ path: "login_after_inputs.png" }); + // Optionally, you can add an assertion to verify the login button is enabled - const loginButton = page.locator('button[type="submit"]'); + const loginButton = page.getByRole("button", { name: "Login" }); await expect(loginButton).toBeEnabled(); // Optionally, you can click the login button to submit the form diff --git a/tests/proxy_admin_ui_tests/e2e_ui_tests/search_users.spec.ts b/tests/proxy_admin_ui_tests/e2e_ui_tests/search_users.spec.ts index 7b9da6a27db..a384248a5f7 100644 --- a/tests/proxy_admin_ui_tests/e2e_ui_tests/search_users.spec.ts +++ b/tests/proxy_admin_ui_tests/e2e_ui_tests/search_users.spec.ts @@ -31,7 +31,7 @@ test("user search test", async ({ page }) => { await page.fill('input[name="password"]', "gm"); console.log("Filled login credentials"); - const loginButton = page.locator('input[type="submit"]'); + const loginButton = page.getByRole("button", { name: "Login" }); await expect(loginButton).toBeEnabled(); await loginButton.click(); console.log("Clicked login button"); @@ -138,7 +138,7 @@ test("user filter test", async ({ page }) => { await page.fill('input[name="password"]', "gm"); console.log("Filled login credentials"); - const loginButton = page.locator('input[type="submit"]'); + const loginButton = page.getByRole("button", { name: "Login" }); await expect(loginButton).toBeEnabled(); await loginButton.click(); console.log("Clicked login button"); diff --git a/tests/proxy_admin_ui_tests/e2e_ui_tests/view_internal_user.spec.ts b/tests/proxy_admin_ui_tests/e2e_ui_tests/view_internal_user.spec.ts index e578ab57e3f..dd6f812df4a 100644 --- a/tests/proxy_admin_ui_tests/e2e_ui_tests/view_internal_user.spec.ts +++ b/tests/proxy_admin_ui_tests/e2e_ui_tests/view_internal_user.spec.ts @@ -15,7 +15,7 @@ test("view internal user page", async ({ page }) => { await page.fill('input[name="password"]', "gm"); // Click the login button - const loginButton = page.locator('input[type="submit"]'); + const loginButton = page.getByRole("button", { name: "Login" }); await expect(loginButton).toBeEnabled(); await loginButton.click(); @@ -35,9 +35,9 @@ test("view internal user page", async ({ page }) => { const rowCount = await page.locator("tbody tr").count(); expect(rowCount).toBeGreaterThan(0); - // Verify table headers are present (including Virtual Keys column) - const virtualKeysHeader = page.locator("th", { hasText: "Virtual Keys" }); - await expect(virtualKeysHeader).toBeVisible(); + const userIdHeader = page.locator("th", { hasText: "User ID" }); + page.screenshot({ path: "user_id_header.png" }); + await expect(userIdHeader).toBeVisible(); // test pagination // Wait for pagination controls to be visible 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 01eadc9ad1a..742dbd14d44 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 @@ -14,6 +14,7 @@ test.describe("User Info View", () => { // Wait for loading state to disappear await page.waitForSelector('text="🚅 Loading users..."', { state: "hidden", + timeout: 10000, }); // Wait for users table to load await page.waitForSelector("table"); diff --git a/ui/litellm-dashboard/src/app/page.tsx b/ui/litellm-dashboard/src/app/page.tsx index 68060ae6968..20f5480c970 100644 --- a/ui/litellm-dashboard/src/app/page.tsx +++ b/ui/litellm-dashboard/src/app/page.tsx @@ -186,7 +186,7 @@ export default function CreateKeyPage() { useEffect(() => { if (redirectToLogin) { // Replace instead of assigning to avoid back-button loops - const dest = (proxyBaseUrl || "") + "/sso/key/generate"; + const dest = (proxyBaseUrl || "") + "/ui/login"; window.location.replace(dest); } }, [redirectToLogin]); From 2072dcefa87c0dbd1b747873fbe4cd2b034844e6 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Thu, 4 Dec 2025 10:20:45 +0530 Subject: [PATCH 082/107] Map output_tokens_details of responses API to completion_tokens_details --- .../llms/azure/chat/gpt_5_transformation.py | 1 - litellm/responses/utils.py | 19 +++++++++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/litellm/llms/azure/chat/gpt_5_transformation.py b/litellm/llms/azure/chat/gpt_5_transformation.py index 2d0ce8b5bce..87f81d117f0 100644 --- a/litellm/llms/azure/chat/gpt_5_transformation.py +++ b/litellm/llms/azure/chat/gpt_5_transformation.py @@ -60,7 +60,6 @@ class AzureOpenAIGPT5Config(AzureOpenAIConfig, OpenAIGPT5Config): message=( "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" diff --git a/litellm/responses/utils.py b/litellm/responses/utils.py index 198182cf118..ad99609e905 100644 --- a/litellm/responses/utils.py +++ b/litellm/responses/utils.py @@ -24,7 +24,12 @@ from litellm.types.llms.openai import ( ResponseText, ) from litellm.types.responses.main import DecodedResponseId -from litellm.types.utils import PromptTokensDetails, SpecialEnums, Usage +from litellm.types.utils import ( + CompletionTokensDetailsWrapper, + PromptTokensDetails, + SpecialEnums, + Usage, +) class ResponsesAPIRequestUtils: @@ -366,10 +371,11 @@ class ResponsesAPIRequestUtils: Headers from tools.headers in request body should be passed to MCP server. """ from starlette.datastructures import Headers + from litellm.proxy._experimental.mcp_server.auth.user_api_key_auth_mcp import ( MCPRequestHandler, ) - + # Extract headers from secret_fields which contains the original request headers raw_headers_from_request: Optional[Dict[str, str]] = None if secret_fields and isinstance(secret_fields, dict): @@ -445,11 +451,20 @@ class ResponseAPILoggingUtils: cached_tokens=response_api_usage.input_tokens_details.cached_tokens, audio_tokens=response_api_usage.input_tokens_details.audio_tokens, ) + completion_tokens_details: Optional[CompletionTokensDetailsWrapper] = None + if response_api_usage.output_tokens_details: + completion_tokens_details = CompletionTokensDetailsWrapper( + reasoning_tokens=getattr( + response_api_usage.output_tokens_details, "reasoning_tokens", None + ) + ) + chat_usage = Usage( prompt_tokens=prompt_tokens, completion_tokens=completion_tokens, total_tokens=prompt_tokens + completion_tokens, prompt_tokens_details=prompt_tokens_details, + completion_tokens_details=completion_tokens_details, ) # Preserve cost attribute if it exists on ResponseAPIUsage From be0530a6b331dc32f2ca58c46fccea5f60f598c4 Mon Sep 17 00:00:00 2001 From: Krish Dholakia Date: Wed, 3 Dec 2025 20:54:56 -0800 Subject: [PATCH 083/107] fix(unified_guardrail.py): correctly map a v1/messages call to the anthropic unified guardrail (#17424) * fix(unified_guardrail.py): correctly map a v1/messages call to the anthropic unified guardrail * fix: add more rigorous call type checks * fix(anthropic_endpoints/endpoints.py): initialize logging object at the beginning of endpoint ensures call id + trace id are emitted to guardrail api * feat(anthropic/chat/guardrail_translation): support streaming guardrails sample on every 5 chunks * fix(openai/chat/guardrail_translation): support openai streaming guardrails * fix: initial commit fixing output guardrails for responses api * feat(openai/responses/guardrail_translation): handler.py - fix output checks on responses api * fix(openai/responses/guardrail_translation/handler.py): ensure responses api guardrails work on streaming * test: update tests * test: update tests * test: update tests * fix(bedrock_guardrails.py): fix post call streaming iterator logic * fix: fix return * fix(bedrock_guardrails.py): fix --- .../mock_bedrock_guardrail_server.py | 2 + litellm/cost_calculator.py | 37 +- .../chat/guardrail_translation/handler.py | 110 ++ litellm/llms/anthropic/chat/handler.py | 8 +- .../guardrail_translation/base_translation.py | 6 +- .../chat/guardrail_translation/handler.py | 188 ++- .../guardrail_translation/handler.py | 53 +- litellm/proxy/_new_secret_config.yaml | 20 +- .../proxy/anthropic_endpoints/endpoints.py | 221 +--- litellm/proxy/common_request_processing.py | 50 +- .../guardrail_hooks/bedrock_guardrails.py | 34 +- .../unified_guardrail/unified_guardrail.py | 52 +- litellm/proxy/route_llm_request.py | 20 +- litellm/proxy/utils.py | 4 + litellm/router.py | 33 +- litellm/types/utils.py | 1 + .../test_bedrock_guardrails.py | 1095 +++++++++-------- ...test_openai_responses_guardrail_handler.py | 159 ++- .../content_filter/test_content_filter.py | 15 +- 19 files changed, 1211 insertions(+), 897 deletions(-) diff --git a/cookbook/mock_guardrail_server/mock_bedrock_guardrail_server.py b/cookbook/mock_guardrail_server/mock_bedrock_guardrail_server.py index f75a53e8798..c9f7f13f6aa 100644 --- a/cookbook/mock_guardrail_server/mock_bedrock_guardrail_server.py +++ b/cookbook/mock_guardrail_server/mock_bedrock_guardrail_server.py @@ -429,6 +429,8 @@ class LitellmBasicGuardrailRequest(BaseModel): request_data: Dict[str, Any] = Field(default_factory=dict) additional_provider_specific_params: Dict[str, Any] = Field(default_factory=dict) input_type: Literal["request", "response"] + litellm_call_id: Optional[str] = None + litellm_trace_id: Optional[str] = None class LitellmBasicGuardrailResponse(BaseModel): diff --git a/litellm/cost_calculator.py b/litellm/cost_calculator.py index ea7caf93ed0..57eab6d29a3 100644 --- a/litellm/cost_calculator.py +++ b/litellm/cost_calculator.py @@ -95,6 +95,7 @@ from litellm.utils import ( EmbeddingResponse, ImageResponse, ModelResponse, + ModelResponseStream, ProviderConfigManager, TextCompletionResponse, TranscriptionResponse, @@ -654,7 +655,9 @@ def _infer_call_type( if completion_response is None: return None - if isinstance(completion_response, ModelResponse): + if isinstance(completion_response, ModelResponse) or isinstance( + completion_response, ModelResponseStream + ): return "completion" elif isinstance(completion_response, EmbeddingResponse): return "embedding" @@ -1057,29 +1060,33 @@ def completion_cost( # noqa: PLR0915 number_of_queries = len(query) elif query is not None: number_of_queries = 1 - + search_model = model or "" if custom_llm_provider and "/" not in search_model: # If model is like "tavily-search", construct "tavily/search" for cost lookup search_model = f"{custom_llm_provider}/search" - - prompt_cost, completion_cost_result = search_provider_cost_per_query( - model=search_model, - custom_llm_provider=custom_llm_provider, - number_of_queries=number_of_queries, - optional_params=optional_params, + + prompt_cost, completion_cost_result = ( + search_provider_cost_per_query( + model=search_model, + custom_llm_provider=custom_llm_provider, + number_of_queries=number_of_queries, + optional_params=optional_params, + ) ) - + # Return the total cost (prompt_cost + completion_cost, but for search it's just prompt_cost) _final_cost = prompt_cost + completion_cost_result - + # Apply discount original_cost = _final_cost - _final_cost, discount_percent, discount_amount = _apply_cost_discount( - base_cost=_final_cost, - custom_llm_provider=custom_llm_provider, + _final_cost, discount_percent, discount_amount = ( + _apply_cost_discount( + base_cost=_final_cost, + custom_llm_provider=custom_llm_provider, + ) ) - + # Store cost breakdown in logging object if available _store_cost_breakdown_in_logging_obj( litellm_logging_obj=litellm_logging_obj, @@ -1091,7 +1098,7 @@ def completion_cost( # noqa: PLR0915 discount_percent=discount_percent, discount_amount=discount_amount, ) - + return _final_cost elif call_type == CallTypes.arealtime.value and isinstance( completion_response, LiteLLMRealtimeStreamLoggingObject diff --git a/litellm/llms/anthropic/chat/guardrail_translation/handler.py b/litellm/llms/anthropic/chat/guardrail_translation/handler.py index 383a4941238..e248e409de4 100644 --- a/litellm/llms/anthropic/chat/guardrail_translation/handler.py +++ b/litellm/llms/anthropic/chat/guardrail_translation/handler.py @@ -12,6 +12,7 @@ Pattern Overview: 4. Apply guardrail responses back to the original structure """ +import json from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, cast from litellm._logging import verbose_proxy_logger @@ -246,6 +247,115 @@ class AnthropicMessagesHandler(BaseTranslation): return response + async def process_output_streaming_response( + self, + responses_so_far: List[Any], + guardrail_to_apply: "CustomGuardrail", + litellm_logging_obj: Optional[Any] = None, + user_api_key_dict: Optional[Any] = None, + ) -> List[Any]: + """ + Process output streaming response by applying guardrails to text content. + + Get the string so far, check the apply guardrail to the string so far, and return the list of responses so far. + """ + string_so_far = self.get_streaming_string_so_far(responses_so_far) + _, _ = ( + await guardrail_to_apply.apply_guardrail( # allow rejecting the response, if invalid + texts=[string_so_far], + request_data={}, + input_type="response", + logging_obj=litellm_logging_obj, + images=None, + ) + ) + return responses_so_far + + def get_streaming_string_so_far(self, responses_so_far: List[Any]) -> str: + """ + Parse streaming responses and extract accumulated text content. + + Handles two formats: + 1. Raw bytes in SSE (Server-Sent Events) format from Anthropic API + 2. Parsed dict objects (for backwards compatibility) + + SSE format example: + b'event: content_block_delta\\ndata: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" curious"}}\\n\\n' + + Dict format example: + { + "type": "content_block_delta", + "index": 0, + "delta": { + "type": "text_delta", + "text": " curious" + } + } + """ + text_so_far = "" + for response in responses_so_far: + # Handle raw bytes in SSE format + if isinstance(response, bytes): + text_so_far += self._extract_text_from_sse(response) + # Handle already-parsed dict format + elif isinstance(response, dict): + delta = response.get("delta") if response.get("delta") else None + if delta and delta.get("type") == "text_delta": + text = delta.get("text", "") + if text: + text_so_far += text + return text_so_far + + def _extract_text_from_sse(self, sse_bytes: bytes) -> str: + """ + Extract text content from Server-Sent Events (SSE) format. + + Args: + sse_bytes: Raw bytes in SSE format + + Returns: + Accumulated text from all content_block_delta events + """ + text = "" + try: + # Decode bytes to string + sse_string = sse_bytes.decode("utf-8") + + # Split by double newline to get individual events + events = sse_string.split("\n\n") + + for event in events: + if not event.strip(): + continue + + # Parse event lines + lines = event.strip().split("\n") + event_type = None + data_line = None + + for line in lines: + if line.startswith("event:"): + event_type = line[6:].strip() + elif line.startswith("data:"): + data_line = line[5:].strip() + + # Only process content_block_delta events + if event_type == "content_block_delta" and data_line: + try: + data = json.loads(data_line) + delta = data.get("delta", {}) + if delta.get("type") == "text_delta": + text += delta.get("text", "") + except json.JSONDecodeError: + verbose_proxy_logger.warning( + f"Failed to parse JSON from SSE data: {data_line}" + ) + + except Exception as e: + verbose_proxy_logger.error(f"Error extracting text from SSE: {e}") + + return text + def _has_text_content(self, response: "AnthropicMessagesResponse") -> bool: """ Check if response has any text content to process. diff --git a/litellm/llms/anthropic/chat/handler.py b/litellm/llms/anthropic/chat/handler.py index b363b747de5..36156d56a59 100644 --- a/litellm/llms/anthropic/chat/handler.py +++ b/litellm/llms/anthropic/chat/handler.py @@ -436,9 +436,7 @@ class AnthropicChatCompletion(BaseLLM): else: if client is None or not isinstance(client, HTTPHandler): - client = _get_httpx_client( - params={"timeout": timeout} - ) + client = _get_httpx_client(params={"timeout": timeout}) else: client = client @@ -528,9 +526,7 @@ class ModelResponseIterator: usage_object=cast(dict, anthropic_usage_chunk), reasoning_content=None ) - def _content_block_delta_helper( - self, chunk: dict - ) -> Tuple[ + def _content_block_delta_helper(self, chunk: dict) -> Tuple[ str, Optional[ChatCompletionToolCallChunk], List[Union[ChatCompletionThinkingBlock, ChatCompletionRedactedThinkingBlock]], diff --git a/litellm/llms/base_llm/guardrail_translation/base_translation.py b/litellm/llms/base_llm/guardrail_translation/base_translation.py index c1ea3311bd8..7106c207bd6 100644 --- a/litellm/llms/base_llm/guardrail_translation/base_translation.py +++ b/litellm/llms/base_llm/guardrail_translation/base_translation.py @@ -1,5 +1,5 @@ from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Any, Dict, Optional +from typing import TYPE_CHECKING, Any, Dict, List, Optional if TYPE_CHECKING: from litellm.integrations.custom_guardrail import CustomGuardrail @@ -87,7 +87,7 @@ class BaseTranslation(ABC): async def process_output_streaming_response( self, - response: Any, + responses_so_far: List[Any], guardrail_to_apply: "CustomGuardrail", litellm_logging_obj: Optional["LiteLLMLoggingObj"] = None, user_api_key_dict: Optional["UserAPIKeyAuth"] = None, @@ -97,4 +97,4 @@ class BaseTranslation(ABC): Optional to override in subclasses. """ - return response + return responses_so_far diff --git a/litellm/llms/openai/chat/guardrail_translation/handler.py b/litellm/llms/openai/chat/guardrail_translation/handler.py index 29fb12a6f74..2b381e28bc0 100644 --- a/litellm/llms/openai/chat/guardrail_translation/handler.py +++ b/litellm/llms/openai/chat/guardrail_translation/handler.py @@ -243,48 +243,122 @@ class OpenAIChatCompletionsHandler(BaseTranslation): async def process_output_streaming_response( self, - response: "ModelResponseStream", + responses_so_far: List["ModelResponseStream"], guardrail_to_apply: "CustomGuardrail", litellm_logging_obj: Optional[Any] = None, user_api_key_dict: Optional[Any] = None, - ) -> Any: + ) -> List["ModelResponseStream"]: """ - Process output streaming response by applying guardrails to text content. + Process output streaming responses by applying guardrails to text content. Args: - response: LiteLLM ModelResponseStream object + responses_so_far: List of LiteLLM ModelResponseStream objects guardrail_to_apply: The guardrail instance to apply litellm_logging_obj: Optional logging object user_api_key_dict: User API key metadata to pass to guardrails Returns: - Modified response with guardrail applied to content + Modified list of responses with guardrail applied to content Response Format Support: - String content: choice.message.content = "text here" - List content: choice.message.content = [{"type": "text", "text": "text here"}, ...] """ - # Step 0: Check if response has any text content to process - if not self._has_text_content(response): - return response + # Step 0: Check if any response has text content to process + has_any_text_content = False + for response in responses_so_far: + if self._has_text_content(response): + has_any_text_content = True + break + if not has_any_text_content: + verbose_proxy_logger.warning( + "OpenAI Chat Completions: No text content in streaming responses, skipping guardrail" + ) + return responses_so_far + + # Step 1: Combine all streaming chunks into complete text per choice + # For streaming, we need to concatenate all delta.content across all chunks + # Key: (choice_idx, content_idx), Value: combined text + combined_texts: Dict[Tuple[int, Optional[int]], str] = {} + + for response_idx, response in enumerate(responses_so_far): + for choice_idx, choice in enumerate(response.choices): + if isinstance(choice, litellm.StreamingChoices): + content = choice.delta.content + elif isinstance(choice, litellm.Choices): + content = choice.message.content + else: + continue + + if content is None: + continue + + if isinstance(content, str): + # String content - accumulate for this choice + key = (choice_idx, None) + if key not in combined_texts: + combined_texts[key] = "" + combined_texts[key] += content + + elif isinstance(content, list): + # List content - accumulate for each content item + for content_idx, content_item in enumerate(content): + text_str = content_item.get("text") + if text_str: + key = (choice_idx, content_idx) + if key not in combined_texts: + combined_texts[key] = "" + combined_texts[key] += text_str + + # Step 2: Create lists for guardrail processing texts_to_check: List[str] = [] images_to_check: List[str] = [] task_mappings: List[Tuple[int, Optional[int]]] = [] - # Track (choice_index, content_index) for each text + # Track (choice_index, content_index) for each combined text - # Step 1: Extract all text content and images from response choices - for choice_idx, choice in enumerate(response.choices): + for (choice_idx, content_idx), combined_text in combined_texts.items(): + texts_to_check.append(combined_text) + task_mappings.append((choice_idx, content_idx)) - self._extract_output_text_and_images( - choice=choice, - choice_idx=choice_idx, - texts_to_check=texts_to_check, - images_to_check=images_to_check, + # Step 3: Apply guardrail to all combined texts in batch + if texts_to_check: + # Create a request_data dict with response info and user API key metadata + request_data: dict = {"responses": responses_so_far} + + # Add user API key metadata with prefixed keys + user_metadata = self.transform_user_api_key_dict_to_metadata( + user_api_key_dict + ) + if user_metadata: + request_data["litellm_metadata"] = user_metadata + + guardrailed_texts, guardrailed_images = ( + await guardrail_to_apply.apply_guardrail( + texts=texts_to_check, + request_data=request_data, + input_type="response", + images=images_to_check if images_to_check else None, + logging_obj=litellm_logging_obj, + ) + ) + + # Step 4: Apply guardrailed text back to all streaming chunks + # For each choice, replace the combined text across all chunks + await self._apply_guardrail_responses_to_output_streaming( + responses=responses_so_far, + guardrailed_texts=guardrailed_texts, task_mappings=task_mappings, ) + verbose_proxy_logger.debug( + "OpenAI Chat Completions: Processed output streaming responses: %s", + responses_so_far, + ) + + return responses_so_far + def _has_text_content( self, response: Union["ModelResponse", "ModelResponseStream"] ) -> bool: @@ -304,10 +378,8 @@ class OpenAIChatCompletionsHandler(BaseTranslation): return True elif isinstance(response, ModelResponseStream): for choice in response.choices: - if isinstance(choice, litellm.Choices): - if choice.message.content and isinstance( - choice.message.content, str - ): + if isinstance(choice, litellm.StreamingChoices): + if choice.delta.content and isinstance(choice.delta.content, str): return True return False @@ -394,3 +466,79 @@ class OpenAIChatCompletionsHandler(BaseTranslation): ][ "text" ] = guardrail_response + + async def _apply_guardrail_responses_to_output_streaming( + self, + responses: List["ModelResponseStream"], + guardrailed_texts: List[str], + task_mappings: List[Tuple[int, Optional[int]]], + ) -> None: + """ + Apply guardrail responses back to output streaming responses. + + For streaming responses, the guardrailed text (which is the combined text from all chunks) + is placed in the first chunk, and subsequent chunks are cleared. + + Args: + responses: List of ModelResponseStream objects to modify + guardrailed_texts: List of guardrailed text responses (combined from all chunks) + task_mappings: List of tuples (choice_idx, content_idx) + + Override this method to customize how responses are applied to streaming responses. + """ + # Build a mapping of what guardrailed text to use for each (choice_idx, content_idx) + guardrail_map: Dict[Tuple[int, Optional[int]], str] = {} + for task_idx, guardrail_response in enumerate(guardrailed_texts): + mapping = task_mappings[task_idx] + choice_idx = cast(int, mapping[0]) + content_idx_optional = cast(Optional[int], mapping[1]) + guardrail_map[(choice_idx, content_idx_optional)] = guardrail_response + + # Track which choices we've already set the guardrailed text for + # Key: (choice_idx, content_idx), Value: boolean (True if already set) + already_set: Dict[Tuple[int, Optional[int]], bool] = {} + + # Iterate through all responses and update content + for response_idx, response in enumerate(responses): + for choice_idx_in_response, choice in enumerate(response.choices): + if isinstance(choice, litellm.StreamingChoices): + content = choice.delta.content + elif isinstance(choice, litellm.Choices): + content = choice.message.content + else: + continue + + if content is None: + continue + + if isinstance(content, str): + # String content + key = (choice_idx_in_response, None) + if key in guardrail_map: + if key not in already_set: + # First chunk - set the complete guardrailed text + if isinstance(choice, litellm.StreamingChoices): + choice.delta.content = guardrail_map[key] + elif isinstance(choice, litellm.Choices): + choice.message.content = guardrail_map[key] + already_set[key] = True + else: + # Subsequent chunks - clear the content + if isinstance(choice, litellm.StreamingChoices): + choice.delta.content = "" + elif isinstance(choice, litellm.Choices): + choice.message.content = "" + + elif isinstance(content, list): + # List content - handle each content item + for content_idx, content_item in enumerate(content): + if "text" in content_item: + key = (choice_idx_in_response, content_idx) + if key in guardrail_map: + if key not in already_set: + # First chunk - set the complete guardrailed text + content_item["text"] = guardrail_map[key] + already_set[key] = True + else: + # Subsequent chunks - clear the text + content_item["text"] = "" diff --git a/litellm/llms/openai/responses/guardrail_translation/handler.py b/litellm/llms/openai/responses/guardrail_translation/handler.py index 667a72a426a..7ef6576159e 100644 --- a/litellm/llms/openai/responses/guardrail_translation/handler.py +++ b/litellm/llms/openai/responses/guardrail_translation/handler.py @@ -30,6 +30,8 @@ Output: response.output is List[GenericResponseOutputItem] where each has: from typing import TYPE_CHECKING, Any, List, Optional, Tuple, Union, cast +from openai import BaseModel + from litellm._logging import verbose_proxy_logger from litellm.llms.base_llm.guardrail_translation.base_translation import BaseTranslation from litellm.types.responses.main import GenericResponseOutputItem, OutputText @@ -275,6 +277,32 @@ class OpenAIResponsesHandler(BaseTranslation): return response + async def process_output_streaming_response( + self, + responses_so_far: List[Any], + guardrail_to_apply: "CustomGuardrail", + litellm_logging_obj: Optional[Any] = None, + user_api_key_dict: Optional[Any] = None, + ) -> List[Any]: + """ + Process output streaming response by applying guardrails to text content. + """ + string_so_far = self.get_streaming_string_so_far(responses_so_far) + guardrailed_text, _ = await guardrail_to_apply.apply_guardrail( + texts=[string_so_far], + request_data={}, + input_type="response", + logging_obj=litellm_logging_obj, + images=None, + ) + return responses_so_far + + def get_streaming_string_so_far(self, responses_so_far: List[Any]) -> str: + """ + Get the string so far from the responses so far. + """ + return "".join([response.get("text", "") for response in responses_so_far]) + def _has_text_content(self, response: "ResponsesAPIResponse") -> bool: """ Check if response has any text content to process. @@ -285,6 +313,17 @@ class OpenAIResponsesHandler(BaseTranslation): return False for output_item in response.output: + if isinstance(output_item, BaseModel): + try: + generic_response_output_item = ( + GenericResponseOutputItem.model_validate( + output_item.model_dump() + ) + ) + if generic_response_output_item.content: + output_item = generic_response_output_item + except Exception: + continue if isinstance(output_item, (GenericResponseOutputItem, dict)): content = ( output_item.content @@ -296,9 +335,11 @@ class OpenAIResponsesHandler(BaseTranslation): # Check if it's an OutputText with text if isinstance(content_item, OutputText): if content_item.text: + return True elif isinstance(content_item, dict): if content_item.get("text"): + return True return False @@ -316,8 +357,16 @@ class OpenAIResponsesHandler(BaseTranslation): Override this method to customize text/image extraction logic. """ # Handle both GenericResponseOutputItem and dict - if isinstance(output_item, GenericResponseOutputItem): - content = output_item.content + content: Optional[Union[List[OutputText], List[dict]]] = None + if isinstance(output_item, BaseModel): + try: + generic_response_output_item = GenericResponseOutputItem.model_validate( + output_item.model_dump() + ) + if generic_response_output_item.content: + content = generic_response_output_item.content + except Exception: + return elif isinstance(output_item, dict): content = output_item.get("content", []) else: diff --git a/litellm/proxy/_new_secret_config.yaml b/litellm/proxy/_new_secret_config.yaml index d2bbe8ee6e3..f1916e99ff9 100644 --- a/litellm/proxy/_new_secret_config.yaml +++ b/litellm/proxy/_new_secret_config.yaml @@ -3,23 +3,19 @@ model_list: litellm_params: model: openai/gpt-3.5-turbo api_key: os.environ/OPENAI_API_KEY + - model_name: claude-sonnet-4-5-20250929 + litellm_params: + model: anthropic/claude-sonnet-4-5-20250929 + - model_name: gpt-4.1-mini + litellm_params: + model: openai/gpt-4.1-mini + guardrails: - # - guardrail_name: model-armor-shield - # litellm_params: - # guardrail: model_armor - # mode: "post_call" # Run on both input and output - # template_id: "test-prompt-template" # Required: Your Model Armor template ID - # project_id: "test-vector-store-db" # Your GCP project ID - # location: "us" # GCP location (default: us-central1) - # mask_request_content: true # Enable request content masking - # mask_response_content: true # Enable response content masking - # fail_on_error: true # Fail request if Model Armor errors (default: true) - # default_on: true # Run by default for all requests - guardrail_name: generic-guardrail litellm_params: guardrail: generic_guardrail_api - mode: [pre_call, post_call] + mode: ["pre_call", "post_call", "during_call"] headers: Authorization: Bearer mock-bedrock-token-12345 api_base: http://localhost:8080 diff --git a/litellm/proxy/anthropic_endpoints/endpoints.py b/litellm/proxy/anthropic_endpoints/endpoints.py index abea9e6fee1..3317560904d 100644 --- a/litellm/proxy/anthropic_endpoints/endpoints.py +++ b/litellm/proxy/anthropic_endpoints/endpoints.py @@ -2,20 +2,14 @@ Unified /v1/messages endpoint - (Anthropic Spec) """ -import asyncio -from fastapi import APIRouter, Depends, HTTPException, Request, Response, status +from fastapi import APIRouter, Depends, HTTPException, Request, Response -import litellm from litellm._logging import verbose_proxy_logger from litellm.proxy._types import * from litellm.proxy.auth.user_api_key_auth import user_api_key_auth -from litellm.proxy.common_request_processing import ( - ProxyBaseLLMRequestProcessing, - create_streaming_response, -) +from litellm.proxy.common_request_processing import ProxyBaseLLMRequestProcessing from litellm.proxy.common_utils.http_parsing_utils import _read_request_body -from litellm.proxy.litellm_pre_call_utils import add_litellm_data_to_request from litellm.types.utils import TokenCountResponse router = APIRouter() @@ -49,169 +43,28 @@ async def anthropic_response( # noqa: PLR0915 version, ) - request_data = await _read_request_body(request=request) - data: dict = {**request_data} + data = await _read_request_body(request=request) + base_llm_response_processor = ProxyBaseLLMRequestProcessing(data=data) try: - data["model"] = ( - general_settings.get("completion_model", None) # server default - or user_model # model name passed via cli args - or data.get("model", None) # default passed in http request - ) - if user_model: - data["model"] = user_model - - data = await add_litellm_data_to_request( - data=data, # type: ignore + result = await base_llm_response_processor.base_process_llm_request( request=request, - general_settings=general_settings, + fastapi_response=fastapi_response, user_api_key_dict=user_api_key_dict, - version=version, + route_type="anthropic_messages", + proxy_logging_obj=proxy_logging_obj, + llm_router=llm_router, + general_settings=general_settings, proxy_config=proxy_config, + select_data_generator=None, + model=None, + user_model=user_model, + user_temperature=user_temperature, + user_request_timeout=user_request_timeout, + user_max_tokens=user_max_tokens, + user_api_base=user_api_base, + version=version, ) - - # override with user settings, these are params passed via cli - if user_temperature: - data["temperature"] = user_temperature - if user_request_timeout: - data["request_timeout"] = user_request_timeout - if user_max_tokens: - data["max_tokens"] = user_max_tokens - if user_api_base: - data["api_base"] = user_api_base - - ### MODEL ALIAS MAPPING ### - # check if model name in model alias map - # get the actual model name - if data["model"] in litellm.model_alias_map: - data["model"] = litellm.model_alias_map[data["model"]] - - ### CALL HOOKS ### - modify incoming data before calling the model - data = await proxy_logging_obj.pre_call_hook( # type: ignore - user_api_key_dict=user_api_key_dict, data=data, call_type=CallTypes.anthropic_messages.value - ) - - tasks = [] - tasks.append( - proxy_logging_obj.during_call_hook( - data=data, - user_api_key_dict=user_api_key_dict, - call_type=ProxyBaseLLMRequestProcessing._get_pre_call_type( - route_type="anthropic_messages" # type: ignore - ), - ) - ) - - ### ROUTE THE REQUESTs ### - router_model_names = llm_router.model_names if llm_router is not None else [] - - # skip router if user passed their key - if ( - llm_router is not None and data["model"] in router_model_names - ): # model in router model list - llm_coro = llm_router.aanthropic_messages(**data) - elif ( - llm_router is not None - and llm_router.model_group_alias is not None - and data["model"] in llm_router.model_group_alias - ): # model set in model_group_alias - llm_coro = llm_router.aanthropic_messages(**data) - elif ( - llm_router is not None and data["model"] in llm_router.deployment_names - ): # model in router deployments, calling a specific deployment on the router - llm_coro = llm_router.aanthropic_messages(**data, specific_deployment=True) - elif ( - llm_router is not None and llm_router.has_model_id(data["model"]) - ): # model in router model list - llm_coro = llm_router.aanthropic_messages(**data) - elif ( - llm_router is not None - and data["model"] not in router_model_names - and ( - llm_router.default_deployment is not None - or len(llm_router.pattern_router.patterns) > 0 - ) - ): # model in router deployments, calling a specific deployment on the router - llm_coro = llm_router.aanthropic_messages(**data) - elif user_model is not None: # `litellm --model ` - llm_coro = litellm.anthropic_messages(**data) - else: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail={ - "error": "completion: Invalid model name passed in model=" - + data.get("model", "") - }, - ) - - tasks.append(llm_coro) - - # wait for call to end - llm_responses = asyncio.gather( - *tasks - ) # run the moderation check in parallel to the actual llm api call - - responses = await llm_responses - - response = responses[1] - - # Extract model_id from request metadata (set by router during routing) - litellm_metadata = data.get("litellm_metadata", {}) or {} - model_info = litellm_metadata.get("model_info", {}) or {} - model_id = model_info.get("id", "") or "" - - # Get other metadata from hidden_params - hidden_params = getattr(response, "_hidden_params", {}) or {} - cache_key = hidden_params.get("cache_key", None) or "" - api_base = hidden_params.get("api_base", None) or "" - response_cost = hidden_params.get("response_cost", None) or "" - - ### ALERTING ### - asyncio.create_task( - proxy_logging_obj.update_request_status( - litellm_call_id=data.get("litellm_call_id", ""), status="success" - ) - ) - - verbose_proxy_logger.debug("final response: %s", response) - - fastapi_response.headers.update( - ProxyBaseLLMRequestProcessing.get_custom_headers( - user_api_key_dict=user_api_key_dict, - model_id=model_id, - cache_key=cache_key, - api_base=api_base, - version=version, - response_cost=response_cost, - request_data=data, - hidden_params=hidden_params, - ) - ) - - if ( - "stream" in data and data["stream"] is True - ): # use generate_responses to stream responses - selected_data_generator = ( - ProxyBaseLLMRequestProcessing.async_sse_data_generator( - response=response, - user_api_key_dict=user_api_key_dict, - request_data=data, - proxy_logging_obj=proxy_logging_obj, - ) - ) - - return await create_streaming_response( - generator=selected_data_generator, - media_type="text/event-stream", - headers=dict(fastapi_response.headers), - ) - - ### CALL HOOKS ### - modify outgoing data - response = await proxy_logging_obj.post_call_success_hook( - data=data, user_api_key_dict=user_api_key_dict, response=response # type: ignore - ) - - verbose_proxy_logger.debug("\nResponse from Litellm:\n{}".format(response)) - return response + return result except Exception as e: await proxy_logging_obj.post_call_failure_hook( user_api_key_dict=user_api_key_dict, original_exception=e, request_data=data @@ -280,35 +133,30 @@ async def count_tokens( Returns: {"input_tokens": } """ from litellm.proxy.proxy_server import token_counter as internal_token_counter - + try: request_data = await _read_request_body(request=request) data: dict = {**request_data} - + # Extract required fields model_name = data.get("model") messages = data.get("messages", []) - + if not model_name: raise HTTPException( - status_code=400, - detail={"error": "model parameter is required"} + status_code=400, detail={"error": "model parameter is required"} ) - + if not messages: raise HTTPException( - status_code=400, - detail={"error": "messages parameter is required"} + status_code=400, detail={"error": "messages parameter is required"} ) - + # Create TokenCountRequest for the internal endpoint from litellm.proxy._types import TokenCountRequest - - token_request = TokenCountRequest( - model=model_name, - messages=messages - ) - + + token_request = TokenCountRequest(model=model_name, messages=messages) + # Call the internal token counter function with direct request flag set to False token_response = await internal_token_counter( request=token_request, @@ -319,17 +167,18 @@ async def count_tokens( _token_response_dict = token_response.model_dump() elif isinstance(token_response, dict): _token_response_dict = token_response - + # Convert the internal response to Anthropic API format return {"input_tokens": _token_response_dict.get("total_tokens", 0)} - + except HTTPException: raise except Exception as e: verbose_proxy_logger.exception( - "litellm.proxy.anthropic_endpoints.count_tokens(): Exception occurred - {}".format(str(e)) + "litellm.proxy.anthropic_endpoints.count_tokens(): Exception occurred - {}".format( + str(e) + ) ) raise HTTPException( - status_code=500, - detail={"error": f"Internal server error: {str(e)}"} + status_code=500, detail={"error": f"Internal server error: {str(e)}"} ) diff --git a/litellm/proxy/common_request_processing.py b/litellm/proxy/common_request_processing.py index 1c6c9b97173..0d2ffc70f29 100644 --- a/litellm/proxy/common_request_processing.py +++ b/litellm/proxy/common_request_processing.py @@ -337,6 +337,7 @@ class ProxyBaseLLMRequestProcessing: "alist_skills", "aget_skill", "adelete_skill", + "anthropic_messages", ], version: Optional[str] = None, user_model: Optional[str] = None, @@ -461,11 +462,12 @@ class ProxyBaseLLMRequestProcessing: "alist_skills", "aget_skill", "adelete_skill", + "anthropic_messages", ], proxy_logging_obj: ProxyLogging, general_settings: dict, proxy_config: ProxyConfig, - select_data_generator: Callable, + select_data_generator: Optional[Callable] = None, llm_router: Optional[Router] = None, model: Optional[str] = None, user_model: Optional[str] = None, @@ -605,7 +607,21 @@ class ProxyBaseLLMRequestProcessing: status_code=response.status_code, headers=custom_headers, ) - else: + elif route_type == "anthropic_messages": + selected_data_generator = ( + ProxyBaseLLMRequestProcessing.async_sse_data_generator( + response=response, + user_api_key_dict=user_api_key_dict, + request_data=self.data, + proxy_logging_obj=proxy_logging_obj, + ) + ) + return await create_streaming_response( + generator=selected_data_generator, + media_type="text/event-stream", + headers=custom_headers, + ) + elif select_data_generator: selected_data_generator = select_data_generator( response=response, user_api_key_dict=user_api_key_dict, @@ -804,21 +820,30 @@ 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}, ) - 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)) + ( + 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}", @@ -1110,7 +1135,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. @@ -1120,10 +1147,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/litellm/proxy/guardrails/guardrail_hooks/bedrock_guardrails.py b/litellm/proxy/guardrails/guardrail_hooks/bedrock_guardrails.py index bd1e805361c..3932e1220b4 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/bedrock_guardrails.py +++ b/litellm/proxy/guardrails/guardrail_hooks/bedrock_guardrails.py @@ -729,9 +729,9 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM): ######################################################### ########## 1. Make the Bedrock API request ########## ######################################################### - bedrock_guardrail_response: Optional[ - Union[BedrockGuardrailResponse, str] - ] = None + bedrock_guardrail_response: Optional[Union[BedrockGuardrailResponse, str]] = ( + None + ) try: bedrock_guardrail_response = await self.make_bedrock_api_request( source="INPUT", messages=filtered_messages, request_data=data @@ -801,9 +801,9 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM): ######################################################### ########## 1. Make the Bedrock API request ########## ######################################################### - bedrock_guardrail_response: Optional[ - Union[BedrockGuardrailResponse, str] - ] = None + bedrock_guardrail_response: Optional[Union[BedrockGuardrailResponse, str]] = ( + None + ) try: bedrock_guardrail_response = await self.make_bedrock_api_request( source="INPUT", messages=filtered_messages, request_data=data @@ -1276,10 +1276,9 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM): masked_texts = [] - for text in texts: - mock_messages: List[AllMessageValues] = [ - ChatCompletionUserMessage(role="user", content=text) - ] + mock_messages: List[AllMessageValues] = [ + ChatCompletionUserMessage(role="user", content=text) for text in texts + ] request_messages = mock_messages filter_result = self._prepare_guardrail_messages_for_role( messages=request_messages @@ -1292,19 +1291,12 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM): request_data=request_data, ) - bedrock_response = await self.make_bedrock_api_request( - source="INPUT", - messages=mock_messages, - request_data=request_data, - ) - if bedrock_response.get("action") == "BLOCKED": raise Exception( f"Content blocked by Bedrock guardrail: {bedrock_response.get('reason', 'Unknown reason')}" ) # Apply any masking that was applied by the guardrail - masked_text = text output_list = bedrock_response.get("output") if output_list: # If the guardrail returned modified content, use that @@ -1312,7 +1304,7 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM): text_content = output_item.get("text") if text_content: masked_text = str(text_content) - break + masked_texts.append(masked_text) else: outputs_list = bedrock_response.get("outputs") if outputs_list: @@ -1321,15 +1313,13 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM): text_content = output_item.get("text") if text_content: masked_text = str(text_content) - break - - masked_texts.append(masked_text) + masked_texts.append(masked_text) verbose_proxy_logger.debug( "Bedrock Guardrail: Successfully applied guardrail" ) - return masked_texts, images + return masked_texts or texts, images except Exception as e: verbose_proxy_logger.error( diff --git a/litellm/proxy/guardrails/guardrail_hooks/unified_guardrail/unified_guardrail.py b/litellm/proxy/guardrails/guardrail_hooks/unified_guardrail/unified_guardrail.py index 0f05696af42..d234dffc511 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/unified_guardrail/unified_guardrail.py +++ b/litellm/proxy/guardrails/guardrail_hooks/unified_guardrail/unified_guardrail.py @@ -6,7 +6,7 @@ Unified Guardrail, leveraging LiteLLM's /applyGuardrail endpoint 3. Implements a way to call /applyGuardrail endpoint for `/chat/completions` + `/v1/messages` requests on async_post_call_streaming_iterator_hook """ -from typing import Any, AsyncGenerator, Union +from typing import Any, AsyncGenerator, List, Optional, Union from litellm._logging import verbose_proxy_logger from litellm.caching.caching import DualCache @@ -126,16 +126,21 @@ class UnifiedLLMGuardrails(CustomLogger): ) is not True ): - return verbose_proxy_logger.debug( "async_post_call_success_hook response: %s", response ) - call_type = _infer_call_type(call_type=None, completion_response=response) + call_type: Optional[CallTypesLiteral] = None + if user_api_key_dict.request_route is not None: + call_types = get_call_types_for_route(user_api_key_dict.request_route) + if call_types is not None: + call_type = call_types[0] if call_type is None: + call_type = _infer_call_type(call_type=None, completion_response=response) + if call_type is None: return response if endpoint_guardrail_translation_mappings is None: @@ -183,9 +188,6 @@ class UnifiedLLMGuardrails(CustomLogger): """ global endpoint_guardrail_translation_mappings - from litellm.proxy.common_utils.callback_utils import ( - add_guardrail_to_applied_guardrails_header, - ) guardrail_to_apply: CustomGuardrail = request_data.pop( "guardrail_to_apply", None @@ -234,9 +236,11 @@ class UnifiedLLMGuardrails(CustomLogger): # Infer call type from first chunk call_type = None chunk_counter = 0 + responses_so_far: List[Any] = [] async for item in response: chunk_counter += 1 + responses_so_far.append(item) # Infer call type from first chunk if not already done if call_type is None and user_api_key_dict.request_route is not None: @@ -244,19 +248,22 @@ class UnifiedLLMGuardrails(CustomLogger): if call_types is not None: call_type = call_types[0] - # If call type not supported, just pass through all chunks - if ( - call_type is None - or CallTypes(call_type) - not in endpoint_guardrail_translation_mappings - ): - yield item - async for remaining_item in response: - yield remaining_item - return + if call_type is None: + call_type = _infer_call_type(call_type=None, completion_response=item) + + # If call type not supported, just pass through all chunks + if ( + call_type is None + or CallTypes(call_type) not in endpoint_guardrail_translation_mappings + ): + yield item + async for remaining_item in response: + yield remaining_item + return # Process chunk based on sampling rate if chunk_counter % sampling_rate == 0: + verbose_proxy_logger.debug( "Processing streaming chunk %s (sampling_rate=%s) with guardrail %s", chunk_counter, @@ -268,22 +275,17 @@ class UnifiedLLMGuardrails(CustomLogger): CallTypes(call_type) ]() - processed_item = ( + processed_items = ( await endpoint_translation.process_output_streaming_response( - response=item, + responses_so_far=responses_so_far, guardrail_to_apply=guardrail_to_apply, litellm_logging_obj=request_data.get("litellm_logging_obj"), user_api_key_dict=user_api_key_dict, ) ) - # Add guardrail to applied guardrails header (only once, on first processed chunk) - if chunk_counter == sampling_rate: - add_guardrail_to_applied_guardrails_header( - request_data=request_data, - guardrail_name=guardrail_to_apply.guardrail_name, - ) + last_item = processed_items[-1] - yield processed_item + yield last_item else: yield item diff --git a/litellm/proxy/route_llm_request.py b/litellm/proxy/route_llm_request.py index 79e316d4702..221aa16f912 100644 --- a/litellm/proxy/route_llm_request.py +++ b/litellm/proxy/route_llm_request.py @@ -75,12 +75,13 @@ def add_shared_session_to_data(data: dict) -> None: """ Add shared aiohttp session for connection reuse (prevents cold starts). Silently continues without session reuse if import fails or session is unavailable. - + Args: data: Dictionary to add the shared session to """ try: from litellm.proxy.proxy_server import shared_aiohttp_session + if shared_aiohttp_session is not None and not shared_aiohttp_session.closed: data["shared_session"] = shared_aiohttp_session except Exception: @@ -136,13 +137,14 @@ async def route_request( "aget_skill", "adelete_skill", "aingest", + "anthropic_messages", ], ): """ Common helper to route the request """ add_shared_session_to_data(data) - + team_id = get_team_id_from_data(data) router_model_names = llm_router.model_names if llm_router is not None else [] @@ -177,7 +179,12 @@ async def route_request( return llm_router.abatch_completion(models=models, **data) elif llm_router is not None: # Skip model-based routing for container operations - if route_type in ["acreate_container", "alist_containers", "aretrieve_container", "adelete_container"]: + if route_type in [ + "acreate_container", + "alist_containers", + "aretrieve_container", + "adelete_container", + ]: return getattr(llm_router, f"{route_type}")(**data) if route_type in [ "avideo_list", @@ -196,7 +203,7 @@ async def route_request( ] and (data.get("model") is None or data.get("model") == ""): # These endpoints don't need a model, use custom_llm_provider directly return getattr(litellm, f"{route_type}")(**data) - + team_model_name = ( llm_router.map_team_model(data["model"], team_id) if team_id is not None @@ -206,9 +213,8 @@ async def route_request( data["model"] = team_model_name return getattr(llm_router, f"{route_type}")(**data) - elif ( - data["model"] in router_model_names - or llm_router.has_model_id(data["model"]) + elif data["model"] in router_model_names or llm_router.has_model_id( + data["model"] ): return getattr(llm_router, f"{route_type}")(**data) diff --git a/litellm/proxy/utils.py b/litellm/proxy/utils.py index f0dccfae716..28a7ef001d5 100644 --- a/litellm/proxy/utils.py +++ b/litellm/proxy/utils.py @@ -1417,6 +1417,7 @@ class ProxyLogging: 3. /image/generation 4. /files """ + from litellm.types.guardrails import GuardrailEventHooks guardrail_callbacks: List[CustomGuardrail] = [] @@ -1451,6 +1452,7 @@ class ProxyLogging: continue guardrail_response: Optional[Any] = None + if "apply_guardrail" in type(callback).__dict__: data["guardrail_to_apply"] = callback guardrail_response = ( @@ -1562,6 +1564,7 @@ class ProxyLogging: """ for callback in litellm.callbacks: + _callback: Optional[CustomLogger] = None if isinstance(callback, str): _callback = litellm.litellm_core_utils.litellm_logging.get_custom_logger_compatible_class( @@ -1575,6 +1578,7 @@ class ProxyLogging: ) or _callback.should_run_guardrail( data=request_data, event_type=GuardrailEventHooks.post_call ): + if "apply_guardrail" in type(callback).__dict__: request_data["guardrail_to_apply"] = callback response = ( diff --git a/litellm/router.py b/litellm/router.py index a52e0260bad..9488f341cbf 100644 --- a/litellm/router.py +++ b/litellm/router.py @@ -153,11 +153,7 @@ from litellm.types.utils import ( ) from litellm.types.utils import ModelInfo from litellm.types.utils import ModelInfo as ModelMapInfo -from litellm.types.utils import ( - ModelResponseStream, - StandardLoggingPayload, - Usage, -) +from litellm.types.utils import ModelResponseStream, StandardLoggingPayload, Usage from litellm.utils import ( CustomStreamWrapper, EmbeddingResponse, @@ -779,6 +775,9 @@ class Router: self.aanthropic_messages = self.factory_function( litellm.anthropic_messages, call_type="anthropic_messages" ) + self.anthropic_messages = self.factory_function( + litellm.anthropic_messages, call_type="anthropic_messages" + ) self.agenerate_content = self.factory_function( litellm.agenerate_content, call_type="agenerate_content" ) @@ -886,6 +885,7 @@ class Router: from litellm.vector_store_files.main import ( update as vector_store_file_update_fn, ) + self.avector_store_file_create = self.factory_function( avector_store_file_create_fn, call_type="avector_store_file_create" ) @@ -3865,6 +3865,7 @@ class Router: "retrieve_container", "delete_container", ): + def sync_wrapper( custom_llm_provider: Optional[str] = None, client: Optional[Any] = None, @@ -5944,36 +5945,38 @@ class Router: """ Get API credentials and provider info from a model name in model_list. Useful for passthrough endpoints (files, batches, etc.) that need credentials. - + This method tries to find a deployment by model_id first, and if not found, it tries to find by model_group_name (model_name). - + Args: model_id: Model ID or model name from model_list (e.g., "gpt-4o-litellm") - + Returns: Dictionary containing api_key, api_base, custom_llm_provider, etc. Returns None if model not found. - + Example: credentials = router.get_deployment_credentials_with_provider("gpt-4o-litellm") # Returns: {"api_key": "sk-...", "custom_llm_provider": "openai", ...} """ # Try to get deployment by model_id first deployment = self.get_deployment(model_id=model_id) - + # If not found, try by model_group_name if deployment is None: - deployment = self.get_deployment_by_model_group_name(model_group_name=model_id) - + deployment = self.get_deployment_by_model_group_name( + model_group_name=model_id + ) + if deployment is None: return None - + # Get basic credentials credentials = CredentialLiteLLMParams( **deployment.litellm_params.model_dump(exclude_none=True) ).model_dump(exclude_none=True) - + # Add custom_llm_provider if deployment.litellm_params.custom_llm_provider: credentials["custom_llm_provider"] = ( @@ -5986,7 +5989,7 @@ class Router: )[0] else: credentials["custom_llm_provider"] = "openai" # default - + return credentials @overload diff --git a/litellm/types/utils.py b/litellm/types/utils.py index cb4d5c4d47f..3d92e78d494 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -731,6 +731,7 @@ API_ROUTE_TO_CALL_TYPES = { CallTypes.llm_passthrough_route, CallTypes.allm_passthrough_route, ], + "/v1/messages": [CallTypes.anthropic_messages], } diff --git a/tests/guardrails_tests/test_bedrock_guardrails.py b/tests/guardrails_tests/test_bedrock_guardrails.py index b0e192c00a7..1795ff23605 100644 --- a/tests/guardrails_tests/test_bedrock_guardrails.py +++ b/tests/guardrails_tests/test_bedrock_guardrails.py @@ -2,6 +2,7 @@ import sys import os import io, asyncio import pytest + sys.path.insert(0, os.path.abspath("../..")) import litellm from litellm.proxy.guardrails.guardrail_hooks.bedrock_guardrails import BedrockGuardrail @@ -9,11 +10,12 @@ from litellm.proxy._types import UserAPIKeyAuth from litellm.caching import DualCache from unittest.mock import MagicMock, AsyncMock, patch + @pytest.mark.asyncio async def test_bedrock_guardrails_pii_masking(): # Create proper mock objects mock_user_api_key_dict = UserAPIKeyAuth() - + guardrail = BedrockGuardrail( guardrailIdentifier="wf0hkdb5x07f", guardrailVersion="DRAFT", @@ -25,14 +27,17 @@ async def test_bedrock_guardrails_pii_masking(): {"role": "user", "content": "Hello, my phone number is +1 412 555 1212"}, {"role": "assistant", "content": "Hello, how can I help you today?"}, {"role": "user", "content": "I need to cancel my order"}, - {"role": "user", "content": "ok, my credit card number is 1234-5678-9012-3456"}, + { + "role": "user", + "content": "ok, my credit card number is 1234-5678-9012-3456", + }, ], } response = await guardrail.async_moderation_hook( data=request_data, user_api_key_dict=mock_user_api_key_dict, - call_type="completion" + call_type="completion", ) print("response after moderation hook", response) @@ -40,14 +45,17 @@ async def test_bedrock_guardrails_pii_masking(): assert response["messages"][0]["content"] == "Hello, my phone number is {PHONE}" assert response["messages"][1]["content"] == "Hello, how can I help you today?" assert response["messages"][2]["content"] == "I need to cancel my order" - assert response["messages"][3]["content"] == "ok, my credit card number is {CREDIT_DEBIT_CARD_NUMBER}" + assert ( + response["messages"][3]["content"] + == "ok, my credit card number is {CREDIT_DEBIT_CARD_NUMBER}" + ) @pytest.mark.asyncio async def test_bedrock_guardrails_pii_masking_content_list(): # Create proper mock objects mock_user_api_key_dict = UserAPIKeyAuth() - + guardrail = BedrockGuardrail( guardrailIdentifier="wf0hkdb5x07f", guardrailVersion="DRAFT", @@ -56,34 +64,41 @@ async def test_bedrock_guardrails_pii_masking_content_list(): request_data = { "model": "gpt-4o", "messages": [ - {"role": "user", "content": [ - {"type": "text", "text": "Hello, my phone number is +1 412 555 1212"}, - {"type": "text", "text": "what time is it?"}, - ]}, - {"role": "assistant", "content": "Hello, how can I help you today?"}, { "role": "user", - "content": "who is the president of the united states?" - } + "content": [ + { + "type": "text", + "text": "Hello, my phone number is +1 412 555 1212", + }, + {"type": "text", "text": "what time is it?"}, + ], + }, + {"role": "assistant", "content": "Hello, how can I help you today?"}, + {"role": "user", "content": "who is the president of the united states?"}, ], } response = await guardrail.async_moderation_hook( data=request_data, user_api_key_dict=mock_user_api_key_dict, - call_type="completion" + call_type="completion", ) print(response) - + if response: # Only assert if response is not None # Verify that the list content is properly masked assert isinstance(response["messages"][0]["content"], list) - assert response["messages"][0]["content"][0]["text"] == "Hello, my phone number is {PHONE}" + assert ( + response["messages"][0]["content"][0]["text"] + == "Hello, my phone number is {PHONE}" + ) assert response["messages"][0]["content"][1]["text"] == "what time is it?" assert response["messages"][1]["content"] == "Hello, how can I help you today?" - assert response["messages"][2]["content"] == "who is the president of the united states?" - - + assert ( + response["messages"][2]["content"] + == "who is the president of the united states?" + ) @pytest.mark.asyncio @@ -92,10 +107,10 @@ async def test_bedrock_guardrails_block_messages_api(): Test that guardrails block messages API requests containing 'coffee' and raise the expected exception. """ from fastapi import HTTPException - + # Create proper mock objects mock_user_api_key_dict = UserAPIKeyAuth() - + guardrail = BedrockGuardrail( guardrailIdentifier="ff6ujrregl1q", guardrailVersion="DRAFT", @@ -104,14 +119,17 @@ async def test_bedrock_guardrails_block_messages_api(): request_data = { "model": "claude-3-5-sonnet-20240620", "messages": [ - {"role": "user", "content": [ - {"type": "text", "text": "Hello, my phone number is +1 412 555 1212"}, - {"type": "text", "text": "what time is it?"}, - ]}, { "role": "user", - "content": "tell me about coffee" - } + "content": [ + { + "type": "text", + "text": "Hello, my phone number is +1 412 555 1212", + }, + {"type": "text", "text": "what time is it?"}, + ], + }, + {"role": "user", "content": "tell me about coffee"}, ], } @@ -122,13 +140,17 @@ async def test_bedrock_guardrails_block_messages_api(): call_type="anthropic_messages", cache=MagicMock(spec=DualCache), ) - + exception = exc_info.value assert exception.status_code == 400 detail = exception.detail assert isinstance(detail, dict) assert detail["error"] == "Violated guardrail policy" - assert detail["bedrock_guardrail_response"] == "Sorry, the model cannot answer this question. coffee guardrail applied " + assert ( + detail["bedrock_guardrail_response"] + == "Sorry, the model cannot answer this question. coffee guardrail applied " + ) + @pytest.mark.asyncio async def test_bedrock_guardrails_block_responses_api(): @@ -136,10 +158,10 @@ async def test_bedrock_guardrails_block_responses_api(): Test that guardrails block responses API requests containing 'coffee' and raise the expected exception. """ from fastapi import HTTPException - + # Create proper mock objects mock_user_api_key_dict = UserAPIKeyAuth() - + guardrail = BedrockGuardrail( guardrailIdentifier="ff6ujrregl1q", guardrailVersion="DRAFT", @@ -158,14 +180,16 @@ async def test_bedrock_guardrails_block_responses_api(): call_type="responses", cache=MagicMock(spec=DualCache), ) - + exception = exc_info.value assert exception.status_code == 400 detail = exception.detail assert isinstance(detail, dict) assert detail["error"] == "Violated guardrail policy" - assert detail["bedrock_guardrail_response"] == "Sorry, the model cannot answer this question. coffee guardrail applied " - + assert ( + detail["bedrock_guardrail_response"] + == "Sorry, the model cannot answer this question. coffee guardrail applied " + ) @pytest.mark.asyncio @@ -182,7 +206,7 @@ async def test_bedrock_guardrails_with_streaming(): user_api_key_cache=mock_user_api_key_cache, premium_user=True, ) - + guardrail = BedrockGuardrail( guardrailIdentifier="ff6ujrregl1q", guardrailVersion="DRAFT", @@ -194,14 +218,9 @@ async def test_bedrock_guardrails_with_streaming(): request_data = { "model": "gpt-4o", - "messages": [ - { - "role": "user", - "content": "Hi I like coffee" - } - ], + "messages": [{"role": "user", "content": "Hi I like coffee"}], "stream": True, - "metadata": {"guardrails": ["bedrock-post-guard"]} + "metadata": {"guardrails": ["bedrock-post-guard"]}, } response = await litellm.acompletion( @@ -213,7 +232,7 @@ async def test_bedrock_guardrails_with_streaming(): response=response, request_data=request_data, ) - + async for chunk in response: print(chunk) @@ -231,7 +250,7 @@ async def test_bedrock_guardrails_with_streaming_no_violation(): user_api_key_cache=mock_user_api_key_cache, premium_user=True, ) - + guardrail = BedrockGuardrail( guardrailIdentifier="ff6ujrregl1q", guardrailVersion="DRAFT", @@ -241,17 +260,11 @@ async def test_bedrock_guardrails_with_streaming_no_violation(): litellm.callbacks.append(guardrail) - request_data = { "model": "gpt-4o", - "messages": [ - { - "role": "user", - "content": "hi" - } - ], + "messages": [{"role": "user", "content": "hi"}], "stream": True, - "metadata": {"guardrails": ["bedrock-post-guard"]} + "metadata": {"guardrails": ["bedrock-post-guard"]}, } response = await litellm.acompletion( @@ -263,11 +276,10 @@ async def test_bedrock_guardrails_with_streaming_no_violation(): response=response, request_data=request_data, ) - - + async for chunk in response: print(chunk) - + @pytest.mark.asyncio async def test_bedrock_guardrails_streaming_request_body_mock(): @@ -277,7 +289,7 @@ async def test_bedrock_guardrails_streaming_request_body_mock(): from litellm.proxy._types import UserAPIKeyAuth from litellm.caching import DualCache from litellm.types.guardrails import GuardrailEventHooks - + # Create mock objects mock_user_api_key_dict = UserAPIKeyAuth() mock_cache = MagicMock(spec=DualCache) @@ -297,79 +309,68 @@ async def test_bedrock_guardrails_streaming_request_body_mock(): litellm.Choices( index=0, message=litellm.Message( - role="assistant", - content="The capital of Spain is Madrid." + role="assistant", content="The capital of Spain is Madrid." ), - finish_reason="stop" + finish_reason="stop", ) ], created=1234567890, model="gpt-4o", - object="chat.completion" + object="chat.completion", ) # Mock Bedrock API response mock_bedrock_response = MagicMock() mock_bedrock_response.status_code = 200 - mock_bedrock_response.json.return_value = { - "action": "NONE", - "outputs": [] - } + mock_bedrock_response.json.return_value = {"action": "NONE", "outputs": []} # Patch the async_handler.post method to capture the request body - with patch.object(guardrail, 'async_handler') as mock_async_handler: + with patch.object(guardrail, "async_handler") as mock_async_handler: mock_async_handler.post = AsyncMock(return_value=mock_bedrock_response) - + # Test data - simulating request data and assembled response request_data = { "model": "gpt-4o", - "messages": [ - { - "role": "user", - "content": "what's the capital of spain?" - } - ], + "messages": [{"role": "user", "content": "what's the capital of spain?"}], "stream": True, - "metadata": {"guardrails": ["bedrock-post-guard"]} + "metadata": {"guardrails": ["bedrock-post-guard"]}, } # Call the method that should make the Bedrock API request await guardrail.make_bedrock_api_request( - source="OUTPUT", - response=mock_response, - request_data=request_data + source="OUTPUT", response=mock_response, request_data=request_data ) # Verify the API call was made mock_async_handler.post.assert_called_once() - + # Get the request data that was passed call_args = mock_async_handler.post.call_args - + # The data should be in the 'data' parameter of the prepared request # We need to parse the JSON from the prepared request body - prepared_request_body = call_args.kwargs.get('data') - + prepared_request_body = call_args.kwargs.get("data") + # Parse the JSON body if isinstance(prepared_request_body, bytes): - actual_body = json.loads(prepared_request_body.decode('utf-8')) + actual_body = json.loads(prepared_request_body.decode("utf-8")) else: actual_body = json.loads(prepared_request_body) - + # Expected body based on the convert_to_bedrock_format method behavior expected_body = { - 'source': 'OUTPUT', - 'content': [ - {'text': {'text': 'The capital of Spain is Madrid.'}} - ] + "source": "OUTPUT", + "content": [{"text": {"text": "The capital of Spain is Madrid."}}], } - + print("Actual Bedrock request body:", json.dumps(actual_body, indent=2)) print("Expected Bedrock request body:", json.dumps(expected_body, indent=2)) - + # Assert the request body matches exactly - assert actual_body == expected_body, f"Request body mismatch. Expected: {expected_body}, Got: {actual_body}" - + assert ( + actual_body == expected_body + ), f"Request body mismatch. Expected: {expected_body}, Got: {actual_body}" + @pytest.mark.asyncio async def test_bedrock_guardrail_aws_param_persistence(): @@ -387,23 +388,31 @@ async def test_bedrock_guardrail_aws_param_persistence(): guardrail_name="bedrock-post-guard", ) - with patch.object(guardrail, "get_credentials", wraps=guardrail.get_credentials) as mock_get_creds: + with patch.object( + guardrail, "get_credentials", wraps=guardrail.get_credentials + ) as mock_get_creds: for i in range(3): request_data = { "model": "gpt-4o", - "messages": [ - {"role": "user", "content": f"request {i}"} - ], + "messages": [{"role": "user", "content": f"request {i}"}], "stream": False, - "metadata": {"guardrails": ["bedrock-post-guard"]} + "metadata": {"guardrails": ["bedrock-post-guard"]}, } - with patch.object(guardrail.async_handler, "post", new_callable=AsyncMock) as mock_post: + with patch.object( + guardrail.async_handler, "post", new_callable=AsyncMock + ) as mock_post: # Configure the mock response properly mock_response = AsyncMock() mock_response.status_code = 200 - mock_response.json = MagicMock(return_value={"action": "NONE", "outputs": []}) + mock_response.json = MagicMock( + return_value={"action": "NONE", "outputs": []} + ) mock_post.return_value = mock_response - await guardrail.make_bedrock_api_request(source="INPUT", messages=request_data.get("messages"), request_data=request_data) + await guardrail.make_bedrock_api_request( + source="INPUT", + messages=request_data.get("messages"), + request_data=request_data, + ) assert mock_get_creds.call_count == 3 for call in mock_get_creds.call_args_list: @@ -413,114 +422,124 @@ async def test_bedrock_guardrail_aws_param_persistence(): assert kwargs["aws_secret_access_key"] == "test-secret-key" assert kwargs["aws_region_name"] == "us-east-1" + @pytest.mark.asyncio async def test_bedrock_guardrail_blocked_vs_anonymized_actions(): """Test that BLOCKED actions raise exceptions but ANONYMIZED actions do not""" from unittest.mock import MagicMock - from litellm.proxy.guardrails.guardrail_hooks.bedrock_guardrails import BedrockGuardrail - from litellm.types.proxy.guardrails.guardrail_hooks.bedrock_guardrails import BedrockGuardrailResponse - - guardrail = BedrockGuardrail( - guardrailIdentifier="test-guardrail", - guardrailVersion="DRAFT" + from litellm.proxy.guardrails.guardrail_hooks.bedrock_guardrails import ( + BedrockGuardrail, ) - + from litellm.types.proxy.guardrails.guardrail_hooks.bedrock_guardrails import ( + BedrockGuardrailResponse, + ) + + guardrail = BedrockGuardrail( + guardrailIdentifier="test-guardrail", guardrailVersion="DRAFT" + ) + # Test 1: ANONYMIZED action should NOT raise exception anonymized_response: BedrockGuardrailResponse = { "action": "GUARDRAIL_INTERVENED", - "outputs": [{ - "text": "Hello, my phone number is {PHONE}" - }], - "assessments": [{ - "sensitiveInformationPolicy": { - "piiEntities": [{ - "type": "PHONE", - "match": "+1 412 555 1212", - "action": "ANONYMIZED" - }] + "outputs": [{"text": "Hello, my phone number is {PHONE}"}], + "assessments": [ + { + "sensitiveInformationPolicy": { + "piiEntities": [ + { + "type": "PHONE", + "match": "+1 412 555 1212", + "action": "ANONYMIZED", + } + ] + } } - }] + ], } - - should_raise = guardrail._should_raise_guardrail_blocked_exception(anonymized_response) + + should_raise = guardrail._should_raise_guardrail_blocked_exception( + anonymized_response + ) assert should_raise is False, "ANONYMIZED actions should not raise exceptions" - + # Test 2: BLOCKED action should raise exception blocked_response: BedrockGuardrailResponse = { - "action": "GUARDRAIL_INTERVENED", - "outputs": [{ - "text": "I can't provide that information." - }], - "assessments": [{ - "topicPolicy": { - "topics": [{ - "name": "Sensitive Topic", - "type": "DENY", - "action": "BLOCKED" - }] + "action": "GUARDRAIL_INTERVENED", + "outputs": [{"text": "I can't provide that information."}], + "assessments": [ + { + "topicPolicy": { + "topics": [ + {"name": "Sensitive Topic", "type": "DENY", "action": "BLOCKED"} + ] + } } - }] + ], } - + should_raise = guardrail._should_raise_guardrail_blocked_exception(blocked_response) assert should_raise is True, "BLOCKED actions should raise exceptions" - + # Test 3: Mixed actions - should raise if ANY action is BLOCKED mixed_response: BedrockGuardrailResponse = { "action": "GUARDRAIL_INTERVENED", - "outputs": [{ - "text": "I can't provide that information." - }], - "assessments": [{ - "sensitiveInformationPolicy": { - "piiEntities": [{ - "type": "PHONE", - "match": "+1 412 555 1212", - "action": "ANONYMIZED" - }] - }, - "topicPolicy": { - "topics": [{ - "name": "Blocked Topic", - "type": "DENY", - "action": "BLOCKED" - }] + "outputs": [{"text": "I can't provide that information."}], + "assessments": [ + { + "sensitiveInformationPolicy": { + "piiEntities": [ + { + "type": "PHONE", + "match": "+1 412 555 1212", + "action": "ANONYMIZED", + } + ] + }, + "topicPolicy": { + "topics": [ + {"name": "Blocked Topic", "type": "DENY", "action": "BLOCKED"} + ] + }, } - }] + ], } - + should_raise = guardrail._should_raise_guardrail_blocked_exception(mixed_response) - assert should_raise is True, "Mixed actions with any BLOCKED should raise exceptions" - + assert ( + should_raise is True + ), "Mixed actions with any BLOCKED should raise exceptions" + # Test 4: NONE action should not raise exception none_response: BedrockGuardrailResponse = { "action": "NONE", "outputs": [], - "assessments": [] + "assessments": [], } - + should_raise = guardrail._should_raise_guardrail_blocked_exception(none_response) assert should_raise is False, "NONE actions should not raise exceptions" - + # Test 5: Test other policy types with BLOCKED actions content_blocked_response: BedrockGuardrailResponse = { "action": "GUARDRAIL_INTERVENED", - "outputs": [{ - "text": "I can't provide that information." - }], - "assessments": [{ - "contentPolicy": { - "filters": [{ - "type": "VIOLENCE", - "confidence": "HIGH", - "action": "BLOCKED" - }] + "outputs": [{"text": "I can't provide that information."}], + "assessments": [ + { + "contentPolicy": { + "filters": [ + {"type": "VIOLENCE", "confidence": "HIGH", "action": "BLOCKED"} + ] + } } - }] + ], } - - should_raise = guardrail._should_raise_guardrail_blocked_exception(content_blocked_response) - assert should_raise is True, "Content policy BLOCKED actions should raise exceptions" + + should_raise = guardrail._should_raise_guardrail_blocked_exception( + content_blocked_response + ) + assert ( + should_raise is True + ), "Content policy BLOCKED actions should raise exceptions" @pytest.mark.asyncio @@ -529,10 +548,10 @@ async def test_bedrock_guardrail_masking_with_anonymized_response(): from unittest.mock import AsyncMock, MagicMock, patch from litellm.proxy._types import UserAPIKeyAuth from litellm.caching import DualCache - + # Create proper mock objects mock_user_api_key_dict = UserAPIKeyAuth() - + guardrail = BedrockGuardrail( guardrailIdentifier="test-guardrail", guardrailVersion="DRAFT", @@ -544,18 +563,20 @@ async def test_bedrock_guardrail_masking_with_anonymized_response(): mock_bedrock_response.status_code = 200 mock_bedrock_response.json.return_value = { "action": "GUARDRAIL_INTERVENED", - "outputs": [{ - "text": "Hello, my phone number is {PHONE}" - }], - "assessments": [{ - "sensitiveInformationPolicy": { - "piiEntities": [{ - "type": "PHONE", - "match": "+1 412 555 1212", - "action": "ANONYMIZED" - }] + "outputs": [{"text": "Hello, my phone number is {PHONE}"}], + "assessments": [ + { + "sensitiveInformationPolicy": { + "piiEntities": [ + { + "type": "PHONE", + "match": "+1 412 555 1212", + "action": "ANONYMIZED", + } + ] + } } - }] + ], } request_data = { @@ -566,21 +587,28 @@ async def test_bedrock_guardrail_masking_with_anonymized_response(): } # Patch the async_handler.post method - with patch.object(guardrail.async_handler, 'post', new_callable=AsyncMock) as mock_post: + with patch.object( + guardrail.async_handler, "post", new_callable=AsyncMock + ) as mock_post: mock_post.return_value = mock_bedrock_response - + # This should NOT raise an exception since action is ANONYMIZED try: response = await guardrail.async_moderation_hook( data=request_data, user_api_key_dict=mock_user_api_key_dict, - call_type="completion" + call_type="completion", ) # Should succeed and return data with masked content assert response is not None - assert response["messages"][0]["content"] == "Hello, my phone number is {PHONE}" + assert ( + response["messages"][0]["content"] + == "Hello, my phone number is {PHONE}" + ) except Exception as e: - pytest.fail(f"Should not raise exception for ANONYMIZED actions, but got: {e}") + pytest.fail( + f"Should not raise exception for ANONYMIZED actions, but got: {e}" + ) @pytest.mark.asyncio @@ -588,10 +616,10 @@ async def test_bedrock_guardrail_uses_masked_output_without_masking_flags(): """Test that masked output from guardrails is used even when masking flags are not enabled""" from unittest.mock import AsyncMock, MagicMock, patch from litellm.proxy._types import UserAPIKeyAuth - + # Create proper mock objects mock_user_api_key_dict = UserAPIKeyAuth() - + # Create guardrail WITHOUT masking flags enabled guardrail = BedrockGuardrail( guardrailIdentifier="test-guardrail", @@ -604,48 +632,56 @@ async def test_bedrock_guardrail_uses_masked_output_without_masking_flags(): mock_bedrock_response.status_code = 200 mock_bedrock_response.json.return_value = { "action": "GUARDRAIL_INTERVENED", - "outputs": [{ - "text": "Hello, my phone number is {PHONE} and email is {EMAIL}" - }], - "assessments": [{ - "sensitiveInformationPolicy": { - "piiEntities": [ - { - "type": "PHONE", - "match": "+1 412 555 1212", - "action": "ANONYMIZED" - }, - { - "type": "EMAIL", - "match": "user@example.com", - "action": "ANONYMIZED" - } - ] + "outputs": [{"text": "Hello, my phone number is {PHONE} and email is {EMAIL}"}], + "assessments": [ + { + "sensitiveInformationPolicy": { + "piiEntities": [ + { + "type": "PHONE", + "match": "+1 412 555 1212", + "action": "ANONYMIZED", + }, + { + "type": "EMAIL", + "match": "user@example.com", + "action": "ANONYMIZED", + }, + ] + } } - }] + ], } request_data = { "model": "gpt-4o", "messages": [ - {"role": "user", "content": "Hello, my phone number is +1 412 555 1212 and email is user@example.com"}, + { + "role": "user", + "content": "Hello, my phone number is +1 412 555 1212 and email is user@example.com", + }, ], } # Patch the async_handler.post method - with patch.object(guardrail.async_handler, 'post', new_callable=AsyncMock) as mock_post: + with patch.object( + guardrail.async_handler, "post", new_callable=AsyncMock + ) as mock_post: mock_post.return_value = mock_bedrock_response - + # This should use the masked output even without masking flags response = await guardrail.async_moderation_hook( data=request_data, user_api_key_dict=mock_user_api_key_dict, - call_type="completion" + call_type="completion", ) - + # Should use the masked content from guardrail output assert response is not None - assert response["messages"][0]["content"] == "Hello, my phone number is {PHONE} and email is {EMAIL}" + assert ( + response["messages"][0]["content"] + == "Hello, my phone number is {PHONE} and email is {EMAIL}" + ) print("✅ Masked output was applied even without masking flags enabled") @@ -654,10 +690,10 @@ async def test_bedrock_guardrail_response_pii_masking_non_streaming(): """Test that PII masking is applied to response content in non-streaming scenarios""" from unittest.mock import AsyncMock, MagicMock, patch from litellm.proxy._types import UserAPIKeyAuth - + # Create proper mock objects mock_user_api_key_dict = UserAPIKeyAuth() - + # Create guardrail with response masking enabled guardrail = BedrockGuardrail( guardrailIdentifier="test-guardrail", @@ -669,25 +705,29 @@ async def test_bedrock_guardrail_response_pii_masking_non_streaming(): mock_bedrock_response.status_code = 200 mock_bedrock_response.json.return_value = { "action": "GUARDRAIL_INTERVENED", - "outputs": [{ - "text": "My credit card number is {CREDIT_DEBIT_CARD_NUMBER} and my phone is {PHONE}" - }], - "assessments": [{ - "sensitiveInformationPolicy": { - "piiEntities": [ - { - "type": "CREDIT_DEBIT_CARD_NUMBER", - "match": "1234-5678-9012-3456", - "action": "ANONYMIZED" - }, - { - "type": "PHONE", - "match": "+1 412 555 1212", - "action": "ANONYMIZED" - } - ] + "outputs": [ + { + "text": "My credit card number is {CREDIT_DEBIT_CARD_NUMBER} and my phone is {PHONE}" } - }] + ], + "assessments": [ + { + "sensitiveInformationPolicy": { + "piiEntities": [ + { + "type": "CREDIT_DEBIT_CARD_NUMBER", + "match": "1234-5678-9012-3456", + "action": "ANONYMIZED", + }, + { + "type": "PHONE", + "match": "+1 412 555 1212", + "action": "ANONYMIZED", + }, + ] + } + } + ], } # Create a mock response that contains PII @@ -697,15 +737,15 @@ async def test_bedrock_guardrail_response_pii_masking_non_streaming(): litellm.Choices( index=0, message=litellm.Message( - role="assistant", - content="My credit card number is 1234-5678-9012-3456 and my phone is +1 412 555 1212" + role="assistant", + content="My credit card number is 1234-5678-9012-3456 and my phone is +1 412 555 1212", ), - finish_reason="stop" + finish_reason="stop", ) ], created=1234567890, model="gpt-4o", - object="chat.completion" + object="chat.completion", ) request_data = { @@ -716,18 +756,23 @@ async def test_bedrock_guardrail_response_pii_masking_non_streaming(): } # Patch the async_handler.post method - with patch.object(guardrail.async_handler, 'post', new_callable=AsyncMock) as mock_post: + with patch.object( + guardrail.async_handler, "post", new_callable=AsyncMock + ) as mock_post: mock_post.return_value = mock_bedrock_response - + # Call the post-call success hook await guardrail.async_post_call_success_hook( data=request_data, user_api_key_dict=mock_user_api_key_dict, - response=mock_response + response=mock_response, ) - + # Verify that the response content was masked - assert mock_response.choices[0].message.content == "My credit card number is {CREDIT_DEBIT_CARD_NUMBER} and my phone is {PHONE}" + assert ( + mock_response.choices[0].message.content + == "My credit card number is {CREDIT_DEBIT_CARD_NUMBER} and my phone is {PHONE}" + ) print("✓ Non-streaming response PII masking test passed") @@ -737,10 +782,10 @@ async def test_bedrock_guardrail_response_pii_masking_streaming(): from unittest.mock import AsyncMock, MagicMock, patch from litellm.proxy._types import UserAPIKeyAuth from litellm.types.utils import ModelResponseStream - + # Create proper mock objects mock_user_api_key_dict = UserAPIKeyAuth() - + # Create guardrail with response masking enabled guardrail = BedrockGuardrail( guardrailIdentifier="test-guardrail", @@ -752,25 +797,25 @@ async def test_bedrock_guardrail_response_pii_masking_streaming(): mock_bedrock_response.status_code = 200 mock_bedrock_response.json.return_value = { "action": "GUARDRAIL_INTERVENED", - "outputs": [{ - "text": "Sure! My email is {EMAIL} and SSN is {US_SSN}" - }], - "assessments": [{ - "sensitiveInformationPolicy": { - "piiEntities": [ - { - "type": "EMAIL", - "match": "john@example.com", - "action": "ANONYMIZED" - }, - { - "type": "US_SSN", - "match": "123-45-6789", - "action": "ANONYMIZED" - } - ] + "outputs": [{"text": "Sure! My email is {EMAIL} and SSN is {US_SSN}"}], + "assessments": [ + { + "sensitiveInformationPolicy": { + "piiEntities": [ + { + "type": "EMAIL", + "match": "john@example.com", + "action": "ANONYMIZED", + }, + { + "type": "US_SSN", + "match": "123-45-6789", + "action": "ANONYMIZED", + }, + ] + } } - }] + ], } # Create mock streaming chunks @@ -782,25 +827,27 @@ async def test_bedrock_guardrail_response_pii_masking_streaming(): litellm.utils.StreamingChoices( index=0, delta=litellm.utils.Delta(content="Sure! My email is "), - finish_reason=None + finish_reason=None, ) ], created=1234567890, model="gpt-4o", - object="chat.completion.chunk" + object="chat.completion.chunk", ), ModelResponseStream( id="test-id", choices=[ litellm.utils.StreamingChoices( index=0, - delta=litellm.utils.Delta(content="john@example.com and SSN is "), - finish_reason=None + delta=litellm.utils.Delta( + content="john@example.com and SSN is " + ), + finish_reason=None, ) ], created=1234567890, model="gpt-4o", - object="chat.completion.chunk" + object="chat.completion.chunk", ), ModelResponseStream( id="test-id", @@ -808,13 +855,13 @@ async def test_bedrock_guardrail_response_pii_masking_streaming(): litellm.utils.StreamingChoices( index=0, delta=litellm.utils.Delta(content="123-45-6789"), - finish_reason="stop" + finish_reason="stop", ) ], created=1234567890, model="gpt-4o", - object="chat.completion.chunk" - ) + object="chat.completion.chunk", + ), ] for chunk in chunks: yield chunk @@ -828,32 +875,37 @@ async def test_bedrock_guardrail_response_pii_masking_streaming(): } # Patch the async_handler.post method - with patch.object(guardrail.async_handler, 'post', new_callable=AsyncMock) as mock_post: + with patch.object( + guardrail.async_handler, "post", new_callable=AsyncMock + ) as mock_post: mock_post.return_value = mock_bedrock_response - + # Call the streaming hook masked_stream = guardrail.async_post_call_streaming_iterator_hook( user_api_key_dict=mock_user_api_key_dict, response=mock_streaming_response(), - request_data=request_data + request_data=request_data, ) - + # Collect all chunks from the masked stream masked_chunks = [] async for chunk in masked_stream: masked_chunks.append(chunk) - + # Verify that we got chunks back assert len(masked_chunks) > 0 - + # Reconstruct the full response from chunks to verify masking full_content = "" for chunk in masked_chunks: - if hasattr(chunk, 'choices') and chunk.choices: - if hasattr(chunk.choices[0], 'delta') and chunk.choices[0].delta: - if hasattr(chunk.choices[0].delta, 'content') and chunk.choices[0].delta.content: + if hasattr(chunk, "choices") and chunk.choices: + if hasattr(chunk.choices[0], "delta") and chunk.choices[0].delta: + if ( + hasattr(chunk.choices[0].delta, "content") + and chunk.choices[0].delta.content + ): full_content += chunk.choices[0].delta.content - + # Verify that the reconstructed content contains the masked PII assert "Sure! My email is {EMAIL} and SSN is {US_SSN}" == full_content print("✓ Streaming response PII masking test passed") @@ -862,64 +914,70 @@ async def test_bedrock_guardrail_response_pii_masking_streaming(): @pytest.mark.asyncio async def test_convert_to_bedrock_format_input_source(): """Test convert_to_bedrock_format with INPUT source and mock messages""" - from litellm.proxy.guardrails.guardrail_hooks.bedrock_guardrails import BedrockGuardrail - from litellm.types.proxy.guardrails.guardrail_hooks.bedrock_guardrails import BedrockRequest + from litellm.proxy.guardrails.guardrail_hooks.bedrock_guardrails import ( + BedrockGuardrail, + ) + from litellm.types.proxy.guardrails.guardrail_hooks.bedrock_guardrails import ( + BedrockRequest, + ) from unittest.mock import patch - + # Create the guardrail instance guardrail = BedrockGuardrail( - guardrailIdentifier="test-guardrail", - guardrailVersion="DRAFT" + guardrailIdentifier="test-guardrail", guardrailVersion="DRAFT" ) - + # Mock messages mock_messages = [ {"role": "user", "content": "Hello, how are you?"}, {"role": "assistant", "content": "I'm doing well, thank you!"}, - {"role": "user", "content": [ - {"type": "text", "text": "What's the weather like?"}, - {"type": "text", "text": "Is it sunny today?"} - ]} + { + "role": "user", + "content": [ + {"type": "text", "text": "What's the weather like?"}, + {"type": "text", "text": "Is it sunny today?"}, + ], + }, ] - + # Call the method - result = guardrail.convert_to_bedrock_format( - source="INPUT", - messages=mock_messages - ) - + result = guardrail.convert_to_bedrock_format(source="INPUT", messages=mock_messages) + # Verify the result structure assert isinstance(result, dict) assert result.get("source") == "INPUT" assert "content" in result assert isinstance(result.get("content"), list) - + # Verify content items expected_content_items = [ {"text": {"text": "Hello, how are you?"}}, {"text": {"text": "I'm doing well, thank you!"}}, {"text": {"text": "What's the weather like?"}}, - {"text": {"text": "Is it sunny today?"}} + {"text": {"text": "Is it sunny today?"}}, ] - + assert result.get("content") == expected_content_items print("✅ INPUT source test passed - result:", result) -@pytest.mark.asyncio +@pytest.mark.asyncio async def test_convert_to_bedrock_format_output_source(): """Test convert_to_bedrock_format with OUTPUT source and mock ModelResponse""" - from litellm.proxy.guardrails.guardrail_hooks.bedrock_guardrails import BedrockGuardrail - from litellm.types.proxy.guardrails.guardrail_hooks.bedrock_guardrails import BedrockRequest + from litellm.proxy.guardrails.guardrail_hooks.bedrock_guardrails import ( + BedrockGuardrail, + ) + from litellm.types.proxy.guardrails.guardrail_hooks.bedrock_guardrails import ( + BedrockRequest, + ) import litellm from unittest.mock import patch - - # Create the guardrail instance + + # Create the guardrail instance guardrail = BedrockGuardrail( - guardrailIdentifier="test-guardrail", - guardrailVersion="DRAFT" + guardrailIdentifier="test-guardrail", guardrailVersion="DRAFT" ) - + # Mock ModelResponse mock_response = litellm.ModelResponse( id="test-response-id", @@ -927,43 +985,40 @@ async def test_convert_to_bedrock_format_output_source(): litellm.Choices( index=0, message=litellm.Message( - role="assistant", - content="This is a test response from the model." + role="assistant", content="This is a test response from the model." ), - finish_reason="stop" + finish_reason="stop", ), litellm.Choices( - index=1, + index=1, message=litellm.Message( - role="assistant", - content="This is a second choice response." + role="assistant", content="This is a second choice response." ), - finish_reason="stop" - ) + finish_reason="stop", + ), ], created=1234567890, model="gpt-4o", - object="chat.completion" + object="chat.completion", ) - + # Call the method result = guardrail.convert_to_bedrock_format( - source="OUTPUT", - response=mock_response + source="OUTPUT", response=mock_response ) - + # Verify the result structure assert isinstance(result, dict) assert result.get("source") == "OUTPUT" assert "content" in result assert isinstance(result.get("content"), list) - + # Verify content items - should contain both choice contents expected_content_items = [ {"text": {"text": "This is a test response from the model."}}, - {"text": {"text": "This is a second choice response."}} + {"text": {"text": "This is a second choice response."}}, ] - + assert result.get("content") == expected_content_items print("✅ OUTPUT source test passed - result:", result) @@ -975,16 +1030,15 @@ async def test_convert_to_bedrock_format_post_call_streaming_hook(): from litellm.proxy._types import UserAPIKeyAuth from litellm.types.utils import ModelResponseStream import litellm - + # Create proper mock objects mock_user_api_key_dict = UserAPIKeyAuth() - + # Create guardrail instance guardrail = BedrockGuardrail( - guardrailIdentifier="test-guardrail", - guardrailVersion="DRAFT" + guardrailIdentifier="test-guardrail", guardrailVersion="DRAFT" ) - + # Mock streaming chunks that contain PII async def mock_streaming_response(): chunks = [ @@ -994,12 +1048,12 @@ async def test_convert_to_bedrock_format_post_call_streaming_hook(): litellm.utils.StreamingChoices( index=0, delta=litellm.utils.Delta(content="My email is "), - finish_reason=None + finish_reason=None, ) ], created=1234567890, model="gpt-4o", - object="chat.completion.chunk" + object="chat.completion.chunk", ), ModelResponseStream( id="test-id", @@ -1007,99 +1061,121 @@ async def test_convert_to_bedrock_format_post_call_streaming_hook(): litellm.utils.StreamingChoices( index=0, delta=litellm.utils.Delta(content="john@example.com"), - finish_reason="stop" + finish_reason="stop", ) ], created=1234567890, model="gpt-4o", - object="chat.completion.chunk" - ) + object="chat.completion.chunk", + ), ] for chunk in chunks: yield chunk - + # Mock Bedrock API response with PII masking mock_bedrock_response = MagicMock() mock_bedrock_response.status_code = 200 mock_bedrock_response.json.return_value = { "action": "GUARDRAIL_INTERVENED", - "outputs": [{ - "text": "My email is {EMAIL}" - }], - "assessments": [{ - "sensitiveInformationPolicy": { - "piiEntities": [{ - "type": "EMAIL", - "match": "john@example.com", - "action": "ANONYMIZED" - }] + "outputs": [{"text": "My email is {EMAIL}"}], + "assessments": [ + { + "sensitiveInformationPolicy": { + "piiEntities": [ + { + "type": "EMAIL", + "match": "john@example.com", + "action": "ANONYMIZED", + } + ] + } } - }] + ], } - + request_data = { "model": "gpt-4o", - "messages": [ - {"role": "user", "content": "What's your email?"} - ], - "stream": True + "messages": [{"role": "user", "content": "What's your email?"}], + "stream": True, } - + # Track which bedrock API calls were made bedrock_calls = [] - + # Mock the make_bedrock_api_request method to track calls - async def mock_make_bedrock_api_request(source, messages=None, response=None, request_data=None): - bedrock_calls.append({ - "source": source, - "messages": messages, - "response": response, - "request_data": request_data - }) + async def mock_make_bedrock_api_request( + source, messages=None, response=None, request_data=None + ): + bedrock_calls.append( + { + "source": source, + "messages": messages, + "response": response, + "request_data": request_data, + } + ) # Return the mock bedrock response - from litellm.types.proxy.guardrails.guardrail_hooks.bedrock_guardrails import BedrockGuardrailResponse + from litellm.types.proxy.guardrails.guardrail_hooks.bedrock_guardrails import ( + BedrockGuardrailResponse, + ) + return BedrockGuardrailResponse(**mock_bedrock_response.json()) - + # Patch the bedrock API request method - with patch.object(guardrail, 'make_bedrock_api_request', side_effect=mock_make_bedrock_api_request): - + with patch.object( + guardrail, "make_bedrock_api_request", side_effect=mock_make_bedrock_api_request + ): + # Call the streaming hook result_generator = guardrail.async_post_call_streaming_iterator_hook( user_api_key_dict=mock_user_api_key_dict, response=mock_streaming_response(), - request_data=request_data + request_data=request_data, ) - + # Collect all chunks from the result result_chunks = [] async for chunk in result_generator: result_chunks.append(chunk) - + # Verify bedrock API calls were made - assert len(bedrock_calls) == 2, f"Expected 2 bedrock calls (INPUT and OUTPUT), got {len(bedrock_calls)}" - + assert ( + len(bedrock_calls) == 2 + ), f"Expected 2 bedrock calls (INPUT and OUTPUT), got {len(bedrock_calls)}" + # Find the OUTPUT call output_calls = [call for call in bedrock_calls if call["source"] == "OUTPUT"] - assert len(output_calls) == 1, f"Expected 1 OUTPUT call, got {len(output_calls)}" - + assert ( + len(output_calls) == 1 + ), f"Expected 1 OUTPUT call, got {len(output_calls)}" + output_call = output_calls[0] assert output_call["source"] == "OUTPUT" assert output_call["response"] is not None assert output_call["messages"] is None # OUTPUT calls don't need messages - + # Verify that the response content was masked # The streaming chunks should now contain the masked content full_content = "" for chunk in result_chunks: - if hasattr(chunk, 'choices') and chunk.choices: - if hasattr(chunk.choices[0], 'delta') and chunk.choices[0].delta.content: + if hasattr(chunk, "choices") and chunk.choices: + if ( + hasattr(chunk.choices[0], "delta") + and chunk.choices[0].delta.content + ): full_content += chunk.choices[0].delta.content - + # The content should be masked (contains {EMAIL} instead of john@example.com) - assert "{EMAIL}" in full_content, f"Expected masked content with {{EMAIL}}, got: {full_content}" - assert "john@example.com" not in full_content, f"Original email should be masked, got: {full_content}" - - print("✅ Post-call streaming hook test passed - OUTPUT source used for masking") + assert ( + "{EMAIL}" in full_content + ), f"Expected masked content with {{EMAIL}}, got: {full_content}" + assert ( + "john@example.com" not in full_content + ), f"Original email should be masked, got: {full_content}" + + print( + "✅ Post-call streaming hook test passed - OUTPUT source used for masking" + ) print(f"✅ Bedrock calls made: {[call['source'] for call in bedrock_calls]}") print(f"✅ Final masked content: {full_content}") @@ -1110,13 +1186,12 @@ async def test_bedrock_guardrail_blocked_action_shows_output_text(): from unittest.mock import AsyncMock, MagicMock, patch from litellm.proxy._types import UserAPIKeyAuth from fastapi import HTTPException - + # Create proper mock objects mock_user_api_key_dict = UserAPIKeyAuth() - + guardrail = BedrockGuardrail( - guardrailIdentifier="test-guardrail", - guardrailVersion="DRAFT" + guardrailIdentifier="test-guardrail", guardrailVersion="DRAFT" ) # Mock the Bedrock API response with BLOCKED action and output text @@ -1124,20 +1199,16 @@ async def test_bedrock_guardrail_blocked_action_shows_output_text(): mock_bedrock_response.status_code = 200 mock_bedrock_response.json.return_value = { "action": "GUARDRAIL_INTERVENED", - "outputs": [ + "outputs": [{"text": "this violates litellm corporate guardrail policy"}], + "assessments": [ { - "text": "this violates litellm corporate guardrail policy" + "topicPolicy": { + "topics": [ + {"name": "Sensitive Topic", "type": "DENY", "action": "BLOCKED"} + ] + } } ], - "assessments": [{ - "topicPolicy": { - "topics": [{ - "name": "Sensitive Topic", - "type": "DENY", - "action": "BLOCKED" - }] - } - }] } request_data = { @@ -1148,32 +1219,36 @@ async def test_bedrock_guardrail_blocked_action_shows_output_text(): } # Patch the async_handler.post method - with patch.object(guardrail.async_handler, 'post', new_callable=AsyncMock) as mock_post: + with patch.object( + guardrail.async_handler, "post", new_callable=AsyncMock + ) as mock_post: mock_post.return_value = mock_bedrock_response - + # This should raise HTTPException due to BLOCKED action with pytest.raises(HTTPException) as exc_info: await guardrail.async_moderation_hook( data=request_data, user_api_key_dict=mock_user_api_key_dict, - call_type="completion" + call_type="completion", ) - + # Verify the exception details exception = exc_info.value assert exception.status_code == 400 assert "detail" in exception.__dict__ - + # Check that the detail contains the expected structure detail = exception.detail assert isinstance(detail, dict) assert detail["error"] == "Violated guardrail policy" - + # Verify that the output text from both outputs is included expected_output_text = "this violates litellm corporate guardrail policy" assert detail["bedrock_guardrail_response"] == expected_output_text - - print("✅ BLOCKED action HTTPException test passed - output text properly included") + + print( + "✅ BLOCKED action HTTPException test passed - output text properly included" + ) @pytest.mark.asyncio @@ -1182,13 +1257,12 @@ async def test_bedrock_guardrail_blocked_action_empty_outputs(): from unittest.mock import AsyncMock, MagicMock, patch from litellm.proxy._types import UserAPIKeyAuth from fastapi import HTTPException - + # Create proper mock objects mock_user_api_key_dict = UserAPIKeyAuth() - + guardrail = BedrockGuardrail( - guardrailIdentifier="test-guardrail", - guardrailVersion="DRAFT" + guardrailIdentifier="test-guardrail", guardrailVersion="DRAFT" ) # Mock the Bedrock API response with BLOCKED action but no outputs @@ -1197,15 +1271,15 @@ async def test_bedrock_guardrail_blocked_action_empty_outputs(): mock_bedrock_response.json.return_value = { "action": "GUARDRAIL_INTERVENED", "outputs": [], # Empty outputs - "assessments": [{ - "contentPolicy": { - "filters": [{ - "type": "VIOLENCE", - "confidence": "HIGH", - "action": "BLOCKED" - }] + "assessments": [ + { + "contentPolicy": { + "filters": [ + {"type": "VIOLENCE", "confidence": "HIGH", "action": "BLOCKED"} + ] + } } - }] + ], } request_data = { @@ -1216,27 +1290,29 @@ async def test_bedrock_guardrail_blocked_action_empty_outputs(): } # Patch the async_handler.post method - with patch.object(guardrail.async_handler, 'post', new_callable=AsyncMock) as mock_post: + with patch.object( + guardrail.async_handler, "post", new_callable=AsyncMock + ) as mock_post: mock_post.return_value = mock_bedrock_response - + # This should raise HTTPException due to BLOCKED action with pytest.raises(HTTPException) as exc_info: await guardrail.async_moderation_hook( data=request_data, user_api_key_dict=mock_user_api_key_dict, - call_type="completion" + call_type="completion", ) - + # Verify the exception details exception = exc_info.value assert exception.status_code == 400 - + # Check that the detail contains the expected structure with empty output text detail = exception.detail assert isinstance(detail, dict) assert detail["error"] == "Violated guardrail policy" assert detail["bedrock_guardrail_response"] == "" # Empty string for no outputs - + print("✅ BLOCKED action with empty outputs test passed") @@ -1246,15 +1322,15 @@ async def test_bedrock_guardrail_disable_exception_on_block_non_streaming(): from unittest.mock import AsyncMock, MagicMock, patch from litellm.proxy._types import UserAPIKeyAuth from fastapi import HTTPException - + # Create proper mock objects mock_user_api_key_dict = UserAPIKeyAuth() - + # Test 1: disable_exception_on_block=False (default) - should raise exception guardrail_default = BedrockGuardrail( guardrailIdentifier="test-guardrail", guardrailVersion="DRAFT", - disable_exception_on_block=False + disable_exception_on_block=False, ) # Mock the Bedrock API response with BLOCKED action @@ -1262,18 +1338,16 @@ async def test_bedrock_guardrail_disable_exception_on_block_non_streaming(): mock_bedrock_response.status_code = 200 mock_bedrock_response.json.return_value = { "action": "GUARDRAIL_INTERVENED", - "outputs": [{ - "text": "I can't provide that information." - }], - "assessments": [{ - "topicPolicy": { - "topics": [{ - "name": "Sensitive Topic", - "type": "DENY", - "action": "BLOCKED" - }] + "outputs": [{"text": "I can't provide that information."}], + "assessments": [ + { + "topicPolicy": { + "topics": [ + {"name": "Sensitive Topic", "type": "DENY", "action": "BLOCKED"} + ] + } } - }] + ], } request_data = { @@ -1284,17 +1358,19 @@ async def test_bedrock_guardrail_disable_exception_on_block_non_streaming(): } # Patch the async_handler.post method - with patch.object(guardrail_default.async_handler, 'post', new_callable=AsyncMock) as mock_post: + with patch.object( + guardrail_default.async_handler, "post", new_callable=AsyncMock + ) as mock_post: mock_post.return_value = mock_bedrock_response - + # Should raise HTTPException when disable_exception_on_block=False with pytest.raises(HTTPException) as exc_info: await guardrail_default.async_moderation_hook( data=request_data, user_api_key_dict=mock_user_api_key_dict, - call_type="completion" + call_type="completion", ) - + # Verify the exception details exception = exc_info.value assert exception.status_code == 400 @@ -1304,24 +1380,28 @@ async def test_bedrock_guardrail_disable_exception_on_block_non_streaming(): guardrail_disabled = BedrockGuardrail( guardrailIdentifier="test-guardrail", guardrailVersion="DRAFT", - disable_exception_on_block=True + disable_exception_on_block=True, ) - with patch.object(guardrail_disabled.async_handler, 'post', new_callable=AsyncMock) as mock_post: + with patch.object( + guardrail_disabled.async_handler, "post", new_callable=AsyncMock + ) as mock_post: mock_post.return_value = mock_bedrock_response - + # Should NOT raise exception when disable_exception_on_block=True try: response = await guardrail_disabled.async_moderation_hook( data=request_data, user_api_key_dict=mock_user_api_key_dict, - call_type="completion" + call_type="completion", ) # Should succeed and return data (even though content was blocked) assert response is not None print("✅ No exception raised when disable_exception_on_block=True") except Exception as e: - pytest.fail(f"Should not raise exception when disable_exception_on_block=True, but got: {e}") + pytest.fail( + f"Should not raise exception when disable_exception_on_block=True, but got: {e}" + ) @pytest.mark.asyncio @@ -1332,10 +1412,10 @@ async def test_bedrock_guardrail_disable_exception_on_block_streaming(): from litellm.types.utils import ModelResponseStream from fastapi import HTTPException import litellm - + # Create proper mock objects mock_user_api_key_dict = UserAPIKeyAuth() - + # Mock streaming chunks that would normally trigger a block async def mock_streaming_response(): chunks = [ @@ -1344,13 +1424,15 @@ async def test_bedrock_guardrail_disable_exception_on_block_streaming(): choices=[ litellm.utils.StreamingChoices( index=0, - delta=litellm.utils.Delta(content="Here's how to make explosives: "), - finish_reason=None + delta=litellm.utils.Delta( + content="Here's how to make explosives: " + ), + finish_reason=None, ) ], created=1234567890, model="gpt-4o", - object="chat.completion.chunk" + object="chat.completion.chunk", ), ModelResponseStream( id="test-id", @@ -1358,62 +1440,62 @@ async def test_bedrock_guardrail_disable_exception_on_block_streaming(): litellm.utils.StreamingChoices( index=0, delta=litellm.utils.Delta(content="step 1, step 2..."), - finish_reason="stop" + finish_reason="stop", ) ], created=1234567890, model="gpt-4o", - object="chat.completion.chunk" - ) + object="chat.completion.chunk", + ), ] for chunk in chunks: yield chunk - + # Mock Bedrock API response with BLOCKED action mock_bedrock_response = MagicMock() mock_bedrock_response.status_code = 200 mock_bedrock_response.json.return_value = { "action": "GUARDRAIL_INTERVENED", - "outputs": [{ - "text": "I can't provide that information." - }], - "assessments": [{ - "contentPolicy": { - "filters": [{ - "type": "VIOLENCE", - "confidence": "HIGH", - "action": "BLOCKED" - }] + "outputs": [{"text": "I can't provide that information."}], + "assessments": [ + { + "contentPolicy": { + "filters": [ + {"type": "VIOLENCE", "confidence": "HIGH", "action": "BLOCKED"} + ] + } } - }] + ], } - + request_data = { "model": "gpt-4o", - "messages": [ - {"role": "user", "content": "Tell me how to make explosives"} - ], - "stream": True + "messages": [{"role": "user", "content": "Tell me how to make explosives"}], + "stream": True, } # Test 1: disable_exception_on_block=False (default) - should raise exception guardrail_default = BedrockGuardrail( guardrailIdentifier="test-guardrail", guardrailVersion="DRAFT", - disable_exception_on_block=False + disable_exception_on_block=False, ) - with patch.object(guardrail_default.async_handler, 'post', new_callable=AsyncMock) as mock_post: + with patch.object( + guardrail_default.async_handler, "post", new_callable=AsyncMock + ) as mock_post: mock_post.return_value = mock_bedrock_response - + # Should raise exception during streaming processing with pytest.raises(HTTPException): - result_generator = guardrail_default.async_post_call_streaming_iterator_hook( - user_api_key_dict=mock_user_api_key_dict, - response=mock_streaming_response(), - request_data=request_data + result_generator = ( + guardrail_default.async_post_call_streaming_iterator_hook( + user_api_key_dict=mock_user_api_key_dict, + response=mock_streaming_response(), + request_data=request_data, + ) ) - + # Try to consume the generator - should raise exception async for chunk in result_generator: pass @@ -1422,31 +1504,40 @@ async def test_bedrock_guardrail_disable_exception_on_block_streaming(): guardrail_disabled = BedrockGuardrail( guardrailIdentifier="test-guardrail", guardrailVersion="DRAFT", - disable_exception_on_block=True + disable_exception_on_block=True, ) - with patch.object(guardrail_disabled.async_handler, 'post', new_callable=AsyncMock) as mock_post: + with patch.object( + guardrail_disabled.async_handler, "post", new_callable=AsyncMock + ) as mock_post: mock_post.return_value = mock_bedrock_response - + # Should NOT raise exception when disable_exception_on_block=True try: - result_generator = guardrail_disabled.async_post_call_streaming_iterator_hook( - user_api_key_dict=mock_user_api_key_dict, - response=mock_streaming_response(), - request_data=request_data + result_generator = ( + guardrail_disabled.async_post_call_streaming_iterator_hook( + user_api_key_dict=mock_user_api_key_dict, + response=mock_streaming_response(), + request_data=request_data, + ) ) - + # Consume the generator - should succeed without exceptions result_chunks = [] async for chunk in result_generator: result_chunks.append(chunk) - + # Should have received chunks back even though content was blocked assert len(result_chunks) > 0 - print("✅ Streaming completed without exception when disable_exception_on_block=True") - + print( + "✅ Streaming completed without exception when disable_exception_on_block=True" + ) + except Exception as e: - pytest.fail(f"Should not raise exception when disable_exception_on_block=True in streaming, but got: {e}") + pytest.fail( + f"Should not raise exception when disable_exception_on_block=True in streaming, but got: {e}" + ) + @pytest.mark.asyncio async def test_bedrock_guardrail_post_call_success_hook_no_output_text(): @@ -1455,16 +1546,15 @@ async def test_bedrock_guardrail_post_call_success_hook_no_output_text(): from litellm.proxy._types import UserAPIKeyAuth from litellm.types.utils import ModelResponseStream import litellm - + # Create proper mock objects mock_user_api_key_dict = UserAPIKeyAuth() - + # Create guardrail instance guardrail = BedrockGuardrail( - guardrailIdentifier="test-guardrail", - guardrailVersion="DRAFT" + guardrailIdentifier="test-guardrail", guardrailVersion="DRAFT" ) - + # Create a ModelResponse with tool calls (no text content) # This simulates a response where the LLM is making a tool call mock_response = litellm.ModelResponse( @@ -1479,34 +1569,33 @@ async def test_bedrock_guardrail_post_call_success_hook_no_output_text(): litellm.utils.ChatCompletionMessageToolCall( id="tooluse_kZJMlvQmRJ6eAyJE5GIl7Q", function=litellm.utils.Function( - name="top_song", - arguments='{"sign": "WZPZ"}' + name="top_song", arguments='{"sign": "WZPZ"}' ), - type="function" + type="function", ) - ] + ], ), - finish_reason="tool_calls" + finish_reason="tool_calls", ) ], created=1234567890, model="gpt-4o", - object="chat.completion" + object="chat.completion", ) - + data = { "model": "gpt-4o", "messages": [ {"role": "user", "content": "Hello"}, ], - } + } mock_user_api_key_dict = UserAPIKeyAuth() result = await guardrail.async_post_call_success_hook( data=data, - response=mock_response, + response=mock_response, user_api_key_dict=mock_user_api_key_dict, ) # If no error is raised and result is None, then the test passes assert result is None - print("✅ No output text in response test passed") \ No newline at end of file + print("✅ No output text in response test passed") diff --git a/tests/test_litellm/llms/openai/responses/test_openai_responses_guardrail_handler.py b/tests/test_litellm/llms/openai/responses/test_openai_responses_guardrail_handler.py index c04f825e362..cc76be08178 100644 --- a/tests/test_litellm/llms/openai/responses/test_openai_responses_guardrail_handler.py +++ b/tests/test_litellm/llms/openai/responses/test_openai_responses_guardrail_handler.py @@ -7,7 +7,7 @@ with guardrail transformations. import os import sys -from typing import Any, List, Optional, Tuple +from typing import Any, List, Literal, Optional, Tuple from unittest.mock import AsyncMock, MagicMock import pytest @@ -16,6 +16,8 @@ sys.path.insert( 0, os.path.abspath("../../../../../..") ) # Adds the parent directory to the system path +from fastapi import HTTPException + from litellm.integrations.custom_guardrail import CustomGuardrail from litellm.llms import get_guardrail_translation_mapping from litellm.llms.openai.responses.guardrail_translation.handler import ( @@ -27,12 +29,27 @@ from litellm.types.utils import CallTypes class MockGuardrail(CustomGuardrail): - """Mock guardrail for testing that transforms text""" + """Mock guardrail for testing that transforms text for requests and blocks responses""" async def apply_guardrail( - self, texts: List[str], request_data: dict, input_type: str, **kwargs + self, + texts: List[str], + request_data: dict, + input_type: Literal["request", "response"], + logging_obj: Optional[Any] = None, + images: Optional[List[str]] = None, ) -> Tuple[List[str], Optional[List[str]]]: - """Append [GUARDRAILED] to text""" + """ + For requests: Append [GUARDRAILED] to text + For responses: Block by raising HTTPException (masking responses is no longer supported) + """ + if input_type == "response": + # Responses should be blocked, not masked + raise HTTPException( + status_code=400, + detail={"error": "Response blocked by guardrail", "texts": texts}, + ) + # For requests, we can still mask/transform return ([f"{text} [GUARDRAILED]" for text in texts], None) @@ -169,11 +186,15 @@ class TestOpenAIResponsesHandlerOutputProcessing: @pytest.mark.asyncio async def test_process_output_response_simple(self): - """Test processing simple output response""" + """Test processing simple output response - should block, not mask + + After unified_guardrail.py changes, responses can only be blocked/rejected, not masked. + This test verifies that the guardrail properly blocks responses. + """ handler = OpenAIResponsesHandler() guardrail = MockGuardrail(guardrail_name="test") - # Create a mock response + # Create a mock response with dict format (works with current handler) response = ResponsesAPIResponse( id="resp_123", created_at=1234567890, @@ -181,30 +202,36 @@ class TestOpenAIResponsesHandlerOutputProcessing: object="response", status="completed", output=[ - GenericResponseOutputItem( - type="message", - id="msg_123", - status="completed", - role="assistant", - content=[ - OutputText( - type="output_text", text="Hello user", annotations=None - ), + { + "type": "message", + "id": "msg_123", + "status": "completed", + "role": "assistant", + "content": [ + {"type": "output_text", "text": "Hello user"}, ], - ) + } ], ) - result = await handler.process_output_response(response, guardrail) + # Response should be blocked, not masked + with pytest.raises(HTTPException) as exc_info: + await handler.process_output_response(response, guardrail) - assert result.output[0].content[0].text == "Hello user [GUARDRAILED]" + assert exc_info.value.status_code == 400 + assert "Response blocked by guardrail" in str(exc_info.value.detail) @pytest.mark.asyncio async def test_process_output_response_multiple_items(self): - """Test processing output response with multiple output items""" + """Test processing output response with multiple output items - should block, not mask + + After unified_guardrail.py changes, responses can only be blocked/rejected, not masked. + This test verifies that the guardrail properly blocks responses with multiple items. + """ handler = OpenAIResponsesHandler() guardrail = MockGuardrail(guardrail_name="test") + # Use dict format (works with current handler) response = ResponsesAPIResponse( id="resp_123", created_at=1234567890, @@ -212,46 +239,45 @@ class TestOpenAIResponsesHandlerOutputProcessing: object="response", status="completed", output=[ - GenericResponseOutputItem( - type="message", - id="msg_123", - status="completed", - role="assistant", - content=[ - OutputText( - type="output_text", - text="First message", - annotations=None, - ), + { + "type": "message", + "id": "msg_123", + "status": "completed", + "role": "assistant", + "content": [ + {"type": "output_text", "text": "First message"}, ], - ), - GenericResponseOutputItem( - type="message", - id="msg_124", - status="completed", - role="assistant", - content=[ - OutputText( - type="output_text", - text="Second message", - annotations=None, - ), + }, + { + "type": "message", + "id": "msg_124", + "status": "completed", + "role": "assistant", + "content": [ + {"type": "output_text", "text": "Second message"}, ], - ), + }, ], ) - result = await handler.process_output_response(response, guardrail) + # Response should be blocked, not masked + with pytest.raises(HTTPException) as exc_info: + await handler.process_output_response(response, guardrail) - assert result.output[0].content[0].text == "First message [GUARDRAILED]" - assert result.output[1].content[0].text == "Second message [GUARDRAILED]" + assert exc_info.value.status_code == 400 + assert "Response blocked by guardrail" in str(exc_info.value.detail) @pytest.mark.asyncio async def test_process_output_response_multiple_content_items(self): - """Test processing output response with multiple content items in one output""" + """Test processing output response with multiple content items - should block, not mask + + After unified_guardrail.py changes, responses can only be blocked/rejected, not masked. + This test verifies that the guardrail properly blocks responses with multiple content items. + """ handler = OpenAIResponsesHandler() guardrail = MockGuardrail(guardrail_name="test") + # Use dict format (works with current handler) response = ResponsesAPIResponse( id="resp_123", created_at=1234567890, @@ -259,27 +285,33 @@ class TestOpenAIResponsesHandlerOutputProcessing: object="response", status="completed", output=[ - GenericResponseOutputItem( - type="message", - id="msg_123", - status="completed", - role="assistant", - content=[ - OutputText(type="output_text", text="Part 1", annotations=None), - OutputText(type="output_text", text="Part 2", annotations=None), + { + "type": "message", + "id": "msg_123", + "status": "completed", + "role": "assistant", + "content": [ + {"type": "output_text", "text": "Part 1"}, + {"type": "output_text", "text": "Part 2"}, ], - ) + } ], ) - result = await handler.process_output_response(response, guardrail) + # Response should be blocked, not masked + with pytest.raises(HTTPException) as exc_info: + await handler.process_output_response(response, guardrail) - assert result.output[0].content[0].text == "Part 1 [GUARDRAILED]" - assert result.output[0].content[1].text == "Part 2 [GUARDRAILED]" + assert exc_info.value.status_code == 400 + assert "Response blocked by guardrail" in str(exc_info.value.detail) @pytest.mark.asyncio async def test_process_output_response_with_dict_format(self): - """Test processing output response where content items are dicts instead of OutputText objects""" + """Test processing output response with dict format - should block, not mask + + After unified_guardrail.py changes, responses can only be blocked/rejected, not masked. + This test verifies blocking works even when content items are dicts instead of OutputText objects. + """ handler = OpenAIResponsesHandler() guardrail = MockGuardrail(guardrail_name="test") @@ -303,9 +335,12 @@ class TestOpenAIResponsesHandlerOutputProcessing: ], ) - result = await handler.process_output_response(response, guardrail) + # Response should be blocked, not masked + with pytest.raises(HTTPException) as exc_info: + await handler.process_output_response(response, guardrail) - assert result.output[0]["content"][0]["text"] == "Hello from dict [GUARDRAILED]" + assert exc_info.value.status_code == 400 + assert "Response blocked by guardrail" in str(exc_info.value.detail) @pytest.mark.asyncio async def test_process_output_response_no_text_content(self): diff --git a/tests/test_litellm/proxy/guardrails/guardrail_hooks/content_filter/test_content_filter.py b/tests/test_litellm/proxy/guardrails/guardrail_hooks/content_filter/test_content_filter.py index 265605c1637..2b192dad5d5 100644 --- a/tests/test_litellm/proxy/guardrails/guardrail_hooks/content_filter/test_content_filter.py +++ b/tests/test_litellm/proxy/guardrails/guardrail_hooks/content_filter/test_content_filter.py @@ -381,10 +381,17 @@ class TestContentFilterGuardrail: assert result is not None assert result[1] == "aws_access_key" + @pytest.mark.skip( + reason="Masking in streaming responses is no longer supported after unified_guardrail.py changes. Only blocking/rejecting is supported for responses." + ) @pytest.mark.asyncio async def test_streaming_hook_mask(self): """ Test streaming hook with MASK action + + Note: After changes to unified_guardrail.py, masking responses to users + is no longer supported. This test is skipped as the feature is deprecated. + Only BLOCK actions (test_streaming_hook_block) are supported for streaming responses. """ from unittest.mock import AsyncMock @@ -431,7 +438,7 @@ class TestContentFilterGuardrail: user_api_key_dict = MagicMock() request_data = {} - # Process streaming response + # Process streaming response - no masking expected result_chunks = [] async for chunk in guardrail.async_post_call_streaming_iterator_hook( user_api_key_dict=user_api_key_dict, @@ -440,12 +447,8 @@ class TestContentFilterGuardrail: ): result_chunks.append(chunk) + # Chunks should pass through unchanged since masking is no longer supported assert len(result_chunks) == 2 - # First chunk should have email masked - assert "[EMAIL_REDACTED]" in result_chunks[0].choices[0].delta.content - assert "test@example.com" not in result_chunks[0].choices[0].delta.content - # Second chunk should be unchanged - assert result_chunks[1].choices[0].delta.content == " for more info" @pytest.mark.asyncio async def test_streaming_hook_block(self): From a4341ccf833c19f716e00a8fedc66fb257d5bcd6 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Wed, 3 Dec 2025 21:00:49 -0800 Subject: [PATCH 084/107] ci/cd changes for debugging --- .circleci/config.yml | 7 ++++++- .../e2e_ui_tests/login_to_ui.spec.ts | 4 ++-- .../require_auth_for_dashboard.spec.ts | 14 ++++++++++---- tests/proxy_admin_ui_tests/utils/login.ts | 2 +- 4 files changed, 19 insertions(+), 8 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index a518628afb9..0adfd5be529 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3496,8 +3496,13 @@ jobs: command: | npx playwright test e2e_ui_tests/ --reporter=html --output=test-results no_output_timeout: 120m - - store_test_results: + - store_artifacts: path: test-results + destination: playwright-results + + - store_artifacts: + path: playwright-report + destination: playwright-report test_nonroot_image: machine: diff --git a/tests/proxy_admin_ui_tests/e2e_ui_tests/login_to_ui.spec.ts b/tests/proxy_admin_ui_tests/e2e_ui_tests/login_to_ui.spec.ts index 793ae53b39a..b9c16299cc0 100644 --- a/tests/proxy_admin_ui_tests/e2e_ui_tests/login_to_ui.spec.ts +++ b/tests/proxy_admin_ui_tests/e2e_ui_tests/login_to_ui.spec.ts @@ -12,7 +12,7 @@ test("admin login test", async ({ page }) => { // Go to the specified URL await page.goto("http://localhost:4000/ui"); - page.screenshot({ path: "login_before.png" }); + await page.screenshot({ path: "test-results/login_before.png" }); // Enter "admin" in the username input field await page.fill('input[name="username"]', "admin"); @@ -20,7 +20,7 @@ test("admin login test", async ({ page }) => { // Enter "gm" in the password input field await page.fill('input[name="password"]', "gm"); - page.screenshot({ path: "login_after_inputs.png" }); + page.screenshot({ path: "test-results/login_after_inputs.png" }); // Optionally, you can add an assertion to verify the login button is enabled const loginButton = page.getByRole("button", { name: "Login" }); diff --git a/tests/proxy_admin_ui_tests/e2e_ui_tests/require_auth_for_dashboard.spec.ts b/tests/proxy_admin_ui_tests/e2e_ui_tests/require_auth_for_dashboard.spec.ts index 18bc91c0977..2b198d6ab7e 100644 --- a/tests/proxy_admin_ui_tests/e2e_ui_tests/require_auth_for_dashboard.spec.ts +++ b/tests/proxy_admin_ui_tests/e2e_ui_tests/require_auth_for_dashboard.spec.ts @@ -2,15 +2,19 @@ import { test, expect } from "@playwright/test"; test.describe("Authentication Checks", () => { - test("should redirect unauthenticated user from a protected page", async ({ page }) => { + test("should redirect unauthenticated user from a protected page", async ({ + page, + }) => { test.setTimeout(30000); page.on("console", (msg) => console.log("PAGE LOG:", msg.text())); const protectedPageUrl = "http://localhost:4000/ui?page=llm-playground"; - const expectedRedirectUrl = "http://localhost:4000/sso/key/generate"; + const expectedRedirectUrl = "http://localhost:4000/ui/login"; - console.log(`Attempting to navigate to protected page: ${protectedPageUrl}`); + console.log( + `Attempting to navigate to protected page: ${protectedPageUrl}` + ); await page.goto(protectedPageUrl); @@ -20,7 +24,9 @@ test.describe("Authentication Checks", () => { await page.waitForURL(expectedRedirectUrl, { timeout: 10000 }); console.log(`Waited for URL. Current URL is now: ${page.url()}`); } catch (error) { - console.error(`Timeout waiting for URL: ${expectedRedirectUrl}. Current URL: ${page.url()}`); + console.error( + `Timeout waiting for URL: ${expectedRedirectUrl}. Current URL: ${page.url()}` + ); await page.screenshot({ path: "redirect-fail-screenshot.png" }); throw error; } diff --git a/tests/proxy_admin_ui_tests/utils/login.ts b/tests/proxy_admin_ui_tests/utils/login.ts index e8755089976..716efe58c76 100644 --- a/tests/proxy_admin_ui_tests/utils/login.ts +++ b/tests/proxy_admin_ui_tests/utils/login.ts @@ -13,7 +13,7 @@ export async function loginToUI(page: Page) { await page.fill('input[name="password"]', "gm"); console.log("Filled login credentials"); - const loginButton = page.locator('input[type="submit"]'); + const loginButton = page.getByRole("button", { name: "Login" }); await expect(loginButton).toBeEnabled(); await loginButton.click(); console.log("Clicked login button"); From 32013f63a0e03f6fc198cba40dea7cf9ee48b117 Mon Sep 17 00:00:00 2001 From: Krish Dholakia Date: Wed, 3 Dec 2025 21:20:39 -0800 Subject: [PATCH 085/107] Guardrail API - support tool call checks on OpenAI `/chat/completions`, OpenAI `/responses`, Anthropic `/v1/messages` (#17459) * fix(unified_guardrail.py): correctly map a v1/messages call to the anthropic unified guardrail * fix: add more rigorous call type checks * fix(anthropic_endpoints/endpoints.py): initialize logging object at the beginning of endpoint ensures call id + trace id are emitted to guardrail api * feat(anthropic/chat/guardrail_translation): support streaming guardrails sample on every 5 chunks * fix(openai/chat/guardrail_translation): support openai streaming guardrails * fix: initial commit fixing output guardrails for responses api * feat(openai/responses/guardrail_translation): handler.py - fix output checks on responses api * fix(openai/responses/guardrail_translation/handler.py): ensure responses api guardrails work on streaming * test: update tests * test: update tests * fix: support multiple kinds of input to the guardrail api * feat(guardrail_translation/handler.py): support extracting tool calls from openai chat completions for guardrail api's * feat(generic_guardrail_api.py): support extracting + returning modified tool calls on generic_guardrails_api allows guardrail api to analyze tool call being sent to provider - to run any analysis on it * fix(guardrails.py): support anthropic /v1/messages tool calls * feat(responses_api/): extract tool calls for guardrail processing * docs(generic_guardrail_api.md): document tools param support * docs: generic_guardrail_api.md improve documentation --- .../mock_bedrock_guardrail_server.py | 1 + .../adding_provider/generic_guardrail_api.md | 87 +++ litellm/integrations/custom_guardrail.py | 17 +- .../chat/guardrail_translation/handler.py | 93 +++- .../rerank/guardrail_translation/handler.py | 5 +- .../chat/guardrail_translation/handler.py | 357 ++++++++---- .../guardrail_translation/handler.py | 15 +- .../guardrail_translation/handler.py | 5 +- .../guardrail_translation/handler.py | 96 +++- .../speech/guardrail_translation/handler.py | 5 +- .../guardrail_translation/handler.py | 5 +- .../guardrail_translation/handler.py | 8 +- .../proxy/guardrails/guardrail_endpoints.py | 10 +- .../guardrail_hooks/bedrock_guardrails.py | 20 +- .../guardrail_hooks/enkryptai/enkryptai.py | 18 +- .../generic_guardrail_api.py | 44 +- .../litellm_content_filter/content_filter.py | 28 +- .../guardrails/guardrail_hooks/presidio.py | 12 +- .../unified_guardrail/unified_guardrail.py | 1 + .../zscaler_ai_guard/zscaler_ai_guard.py | 17 +- litellm/types/guardrails.py | 7 + .../guardrail_hooks/generic_guardrail_api.py | 7 + .../test_bedrock_apply_guardrail.py | 25 +- .../test_litellm/llms/openai/chat/__init__.py | 0 .../chat/guardrail_translation/__init__.py | 0 .../guardrail_translation/test_handler.py | 518 ++++++++++++++++++ .../content_filter/test_content_filter.py | 19 +- .../test_generic_guardrail_api.py | 85 ++- .../guardrail_hooks/test_presidio.py | 4 +- 29 files changed, 1234 insertions(+), 275 deletions(-) create mode 100644 tests/test_litellm/llms/openai/chat/__init__.py create mode 100644 tests/test_litellm/llms/openai/chat/guardrail_translation/__init__.py create mode 100644 tests/test_litellm/llms/openai/chat/guardrail_translation/test_handler.py diff --git a/cookbook/mock_guardrail_server/mock_bedrock_guardrail_server.py b/cookbook/mock_guardrail_server/mock_bedrock_guardrail_server.py index c9f7f13f6aa..fd53ece6604 100644 --- a/cookbook/mock_guardrail_server/mock_bedrock_guardrail_server.py +++ b/cookbook/mock_guardrail_server/mock_bedrock_guardrail_server.py @@ -426,6 +426,7 @@ This is a beta API. Please help us improve it. class LitellmBasicGuardrailRequest(BaseModel): texts: List[str] images: Optional[List[str]] = None + tools: Optional[List[dict]] = None request_data: Dict[str, Any] = Field(default_factory=dict) additional_provider_specific_params: Dict[str, Any] = Field(default_factory=dict) input_type: Literal["request", "response"] diff --git a/docs/my-website/docs/adding_provider/generic_guardrail_api.md b/docs/my-website/docs/adding_provider/generic_guardrail_api.md index 9392deeb5db..f8c07b25f9d 100644 --- a/docs/my-website/docs/adding_provider/generic_guardrail_api.md +++ b/docs/my-website/docs/adding_provider/generic_guardrail_api.md @@ -21,6 +21,20 @@ The **Generic Guardrail API** lets you integrate with LiteLLM **instantly** by i 5. **Custom Parameters** - Pass provider-specific params via config 6. **Full Control** - You own and maintain your guardrail API +## Supported Endpoints + +The Generic Guardrail API works with the following LiteLLM endpoints: + +- `/v1/chat/completions` - OpenAI Chat Completions +- `/v1/completions` - OpenAI Text Completions +- `/v1/responses` - OpenAI Responses API +- `/v1/images/generations` - OpenAI Image Generation +- `/v1/audio/transcriptions` - OpenAI Audio Transcriptions +- `/v1/audio/speech` - OpenAI Text-to-Speech +- `/v1/messages` - Anthropic Messages +- `/v1/rerank` - Cohere Rerank +- Pass-through endpoints + ## How It Works 1. LiteLLM extracts text and images from any request (chat messages, embeddings, image prompts, etc.) @@ -40,6 +54,21 @@ Implement `POST /beta/litellm_basic_guardrail_api` { "texts": ["extracted text from the request"], // array of text strings "images": ["base64_encoded_image_data"], // optional array of images + "tools": [ // optional array of tools (OpenAI ChatCompletionToolParam format) + { + "type": "function", + "function": { + "name": "get_weather", + "description": "Get the current weather", + "parameters": { + "type": "object", + "properties": { + "location": {"type": "string"} + } + } + } + } + ], "request_data": { "user_api_key_hash": "hash of the litellm virtual key used", "user_api_key_alias": "alias of the litellm virtual key used", @@ -75,6 +104,49 @@ Implement `POST /beta/litellm_basic_guardrail_api` - `NONE` - Request proceeds unchanged - `GUARDRAIL_INTERVENED` - Request proceeds with modified texts/images (provide `texts` and/or `images` fields) +## Parameters + +### `tools` Parameter + +The `tools` parameter provides information about available function/tool definitions in the request. + +**Format:** OpenAI `ChatCompletionToolParam` format (see [OpenAI API reference](https://platform.openai.com/docs/api-reference/chat/create#chat-create-tools)) + +**Example:** +```json +{ + "type": "function", + "function": { + "name": "get_weather", + "description": "Get the current weather in a location", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "City and state, e.g. San Francisco, CA" + }, + "unit": { + "type": "string", + "enum": ["celsius", "fahrenheit"] + } + }, + "required": ["location"] + } + } +} +``` + +**Limitations:** +- **Input only:** Tools are only passed for `input_type="request"` (pre-call guardrails). Output/response guardrails do not currently receive tool information. +- **Supported endpoints:** The `tools` parameter is supported on: `/v1/chat/completions`, `/v1/responses`, and `/v1/messages`. Other endpoints do not have tool support. + +**Use cases:** +- Enforce tool permission policies (e.g., only allow certain users/teams to access specific tools) +- Validate tool schemas before sending to LLM +- Log tool usage for audit purposes +- Block sensitive tools based on user context + ## LiteLLM Configuration Add to `config.yaml`: @@ -138,6 +210,7 @@ app = FastAPI() class GuardrailRequest(BaseModel): texts: List[str] images: Optional[List[str]] = None + tools: Optional[List[Dict[str, Any]]] = None # OpenAI ChatCompletionToolParam format request_data: Dict[str, Any] input_type: str # "request" or "response" litellm_call_id: Optional[str] = None @@ -153,6 +226,8 @@ class GuardrailResponse(BaseModel): @app.post("/beta/litellm_basic_guardrail_api") async def apply_guardrail(request: GuardrailRequest): # Your guardrail logic here + + # Example: Check text content for text in request.texts: if "badword" in text.lower(): return GuardrailResponse( @@ -160,6 +235,18 @@ async def apply_guardrail(request: GuardrailRequest): blocked_reason="Content contains prohibited terms" ) + # Example: Check tools (if present in request) + if request.tools: + for tool in request.tools: + if tool.get("type") == "function": + function_name = tool.get("function", {}).get("name", "") + # Block sensitive tools + if function_name in ["delete_data", "access_admin_panel"]: + return GuardrailResponse( + action="BLOCKED", + blocked_reason=f"Tool '{function_name}' is not allowed" + ) + return GuardrailResponse(action="NONE") ``` diff --git a/litellm/integrations/custom_guardrail.py b/litellm/integrations/custom_guardrail.py index 507a754a7e0..782f9460044 100644 --- a/litellm/integrations/custom_guardrail.py +++ b/litellm/integrations/custom_guardrail.py @@ -17,6 +17,7 @@ from litellm.caching import DualCache from litellm.integrations.custom_logger import CustomLogger from litellm.types.guardrails import ( DynamicGuardrailParams, + GenericGuardrailAPIInputs, GuardrailEventHooks, LitellmParams, Mode, @@ -449,20 +450,22 @@ class CustomGuardrail(CustomLogger): async def apply_guardrail( self, - texts: List[str], + inputs: GenericGuardrailAPIInputs, request_data: dict, input_type: Literal["request", "response"], logging_obj: Optional["LiteLLMLoggingObj"] = None, - images: Optional[List[str]] = None, - ) -> Tuple[List[str], Optional[List[str]]]: + ) -> GenericGuardrailAPIInputs: """ - Apply your guardrail logic to the given text + Apply your guardrail logic to the given inputs Args: - texts: The texts to apply the guardrail to - images: The images to apply the guardrail to + inputs: Dictionary containing: + - texts: List of texts to apply the guardrail to + - images: Optional list of images to apply the guardrail to + - tool_calls: Optional list of tool calls to apply the guardrail to request_data: The request data dictionary - containing user api key metadata (e.g. user_id, team_id, etc.) input_type: The type of input to apply the guardrail to - "request" or "response" + logging_obj: Optional logging object for tracking the guardrail execution Any of the custom guardrails can override this method to provide custom guardrail logic @@ -473,7 +476,7 @@ class CustomGuardrail(CustomLogger): - If the guardrail raises an exception """ - return texts, images + return inputs def _process_response( self, diff --git a/litellm/llms/anthropic/chat/guardrail_translation/handler.py b/litellm/llms/anthropic/chat/guardrail_translation/handler.py index e248e409de4..9b6511f151c 100644 --- a/litellm/llms/anthropic/chat/guardrail_translation/handler.py +++ b/litellm/llms/anthropic/chat/guardrail_translation/handler.py @@ -16,7 +16,13 @@ import json from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, cast from litellm._logging import verbose_proxy_logger +from litellm.llms.anthropic.experimental_pass_through.adapters.transformation import ( + LiteLLMAnthropicMessagesAdapter, +) from litellm.llms.base_llm.guardrail_translation.base_translation import BaseTranslation +from litellm.types.guardrails import GenericGuardrailAPIInputs +from litellm.types.llms.anthropic import AllAnthropicToolsValues +from litellm.types.llms.openai import ChatCompletionToolParam if TYPE_CHECKING: from litellm.integrations.custom_guardrail import CustomGuardrail @@ -37,6 +43,10 @@ class AnthropicMessagesHandler(BaseTranslation): Methods can be overridden to customize behavior for different message formats. """ + def __init__(self): + super().__init__() + self.adapter = LiteLLMAnthropicMessagesAdapter() + async def process_input_messages( self, data: dict, @@ -47,11 +57,13 @@ class AnthropicMessagesHandler(BaseTranslation): Process input messages by applying guardrails to text content. """ messages = data.get("messages") + tools = data.get("tools", None) if messages is None: return data texts_to_check: List[str] = [] images_to_check: List[str] = [] + tools_to_check: List[ChatCompletionToolParam] = [] task_mappings: List[Tuple[int, Optional[int]]] = [] # Track (message_index, content_index) for each text # content_index is None for string content, int for list content @@ -66,18 +78,28 @@ class AnthropicMessagesHandler(BaseTranslation): task_mappings=task_mappings, ) + if tools is not None: + self._extract_input_tools( + tools=tools, + tools_to_check=tools_to_check, + ) + # Step 2: Apply guardrail to all texts in batch if texts_to_check: - guardrailed_texts, guardrailed_images = ( - await guardrail_to_apply.apply_guardrail( - texts=texts_to_check, - request_data=data, - input_type="request", - images=images_to_check if images_to_check else None, - logging_obj=litellm_logging_obj, - ) + inputs = GenericGuardrailAPIInputs(texts=texts_to_check) + if images_to_check: + inputs["images"] = images_to_check + if tools_to_check: + inputs["tools"] = tools_to_check + guardrailed_inputs = await guardrail_to_apply.apply_guardrail( + inputs=inputs, + request_data=data, + input_type="request", + logging_obj=litellm_logging_obj, ) + guardrailed_texts = guardrailed_inputs.get("texts", []) + # Step 3: Map guardrail responses back to original message structure await self._apply_guardrail_responses_to_input( messages=messages, @@ -105,15 +127,17 @@ class AnthropicMessagesHandler(BaseTranslation): Override this method to customize text/image extraction logic. """ content = message.get("content", None) - if content is None: + tools = message.get("tools", None) + if content is None and tools is None: return - if isinstance(content, str): + ## CHECK FOR TEXT + IMAGES + if content is not None and isinstance(content, str): # Simple string content texts_to_check.append(content) task_mappings.append((msg_idx, None)) - elif isinstance(content, list): + elif content is not None and isinstance(content, list): # List content (e.g., multimodal with text and images) for content_idx, content_item in enumerate(content): # Extract text @@ -131,6 +155,22 @@ class AnthropicMessagesHandler(BaseTranslation): if data: images_to_check.append(data) + def _extract_input_tools( + self, + tools: List[Dict[str, Any]], + tools_to_check: List[ChatCompletionToolParam], + ) -> None: + """ + Extract tools from a message. + """ + ## CHECK FOR TOOLS + if tools is not None and isinstance(tools, list): + # TRANSFORM ANTHROPIC TOOLS TO OPENAI TOOLS + openai_tools = self.adapter.translate_anthropic_tools_to_openai( + tools=cast(List[AllAnthropicToolsValues], tools) + ) + tools_to_check.extend(openai_tools) + async def _apply_guardrail_responses_to_input( self, messages: List[Dict[str, Any]], @@ -224,16 +264,18 @@ class AnthropicMessagesHandler(BaseTranslation): if user_metadata: request_data["litellm_metadata"] = user_metadata - guardrailed_texts, guardrailed_images = ( - await guardrail_to_apply.apply_guardrail( - texts=texts_to_check, - request_data=request_data, - input_type="response", - images=images_to_check if images_to_check else None, - logging_obj=litellm_logging_obj, - ) + inputs = GenericGuardrailAPIInputs(texts=texts_to_check) + if images_to_check: + inputs["images"] = images_to_check + guardrailed_inputs = await guardrail_to_apply.apply_guardrail( + inputs=inputs, + request_data=request_data, + input_type="response", + logging_obj=litellm_logging_obj, ) + guardrailed_texts = guardrailed_inputs.get("texts", []) + # Step 3: Map guardrail responses back to original response structure await self._apply_guardrail_responses_to_output( response=response, @@ -260,14 +302,11 @@ class AnthropicMessagesHandler(BaseTranslation): Get the string so far, check the apply guardrail to the string so far, and return the list of responses so far. """ string_so_far = self.get_streaming_string_so_far(responses_so_far) - _, _ = ( - await guardrail_to_apply.apply_guardrail( # allow rejecting the response, if invalid - texts=[string_so_far], - request_data={}, - input_type="response", - logging_obj=litellm_logging_obj, - images=None, - ) + guardrailed_inputs = await guardrail_to_apply.apply_guardrail( # allow rejecting the response, if invalid + inputs={"texts": [string_so_far]}, + request_data={}, + input_type="response", + logging_obj=litellm_logging_obj, ) return responses_so_far diff --git a/litellm/llms/cohere/rerank/guardrail_translation/handler.py b/litellm/llms/cohere/rerank/guardrail_translation/handler.py index e042295ab0b..6893a5991c3 100644 --- a/litellm/llms/cohere/rerank/guardrail_translation/handler.py +++ b/litellm/llms/cohere/rerank/guardrail_translation/handler.py @@ -49,12 +49,13 @@ class CohereRerankHandler(BaseTranslation): # Process query only query = data.get("query") if query is not None and isinstance(query, str): - guardrailed_texts, _ = await guardrail_to_apply.apply_guardrail( - texts=[query], + guardrailed_inputs = await guardrail_to_apply.apply_guardrail( + inputs={"texts": [query]}, request_data=data, input_type="request", logging_obj=litellm_logging_obj, ) + guardrailed_texts = guardrailed_inputs.get("texts", []) data["query"] = guardrailed_texts[0] if guardrailed_texts else query verbose_proxy_logger.debug( diff --git a/litellm/llms/openai/chat/guardrail_translation/handler.py b/litellm/llms/openai/chat/guardrail_translation/handler.py index 2b381e28bc0..76aa6f730c2 100644 --- a/litellm/llms/openai/chat/guardrail_translation/handler.py +++ b/litellm/llms/openai/chat/guardrail_translation/handler.py @@ -19,6 +19,8 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union, cast import litellm from litellm._logging import verbose_proxy_logger from litellm.llms.base_llm.guardrail_translation.base_translation import BaseTranslation +from litellm.types.guardrails import GenericGuardrailAPIInputs +from litellm.types.llms.openai import ChatCompletionToolParam from litellm.types.utils import Choices, StreamingChoices if TYPE_CHECKING: @@ -52,38 +54,60 @@ class OpenAIChatCompletionsHandler(BaseTranslation): texts_to_check: List[str] = [] images_to_check: List[str] = [] - task_mappings: List[Tuple[int, Optional[int]]] = [] - # Track (message_index, content_index) for each text + tool_calls_to_check: List[ChatCompletionToolParam] = [] + text_task_mappings: List[Tuple[int, Optional[int]]] = [] + tool_call_task_mappings: List[Tuple[int, int]] = [] + # text_task_mappings: Track (message_index, content_index) for each text # content_index is None for string content, int for list content + # tool_call_task_mappings: Track (message_index, tool_call_index) for each tool call - # Step 1: Extract all text content and images + # Step 1: Extract all text content, images, and tool calls for msg_idx, message in enumerate(messages): - self._extract_input_text_and_images( + self._extract_inputs( message=message, msg_idx=msg_idx, texts_to_check=texts_to_check, images_to_check=images_to_check, - task_mappings=task_mappings, + tool_calls_to_check=tool_calls_to_check, + text_task_mappings=text_task_mappings, + tool_call_task_mappings=tool_call_task_mappings, ) - # Step 2: Apply guardrail to all texts in batch - if texts_to_check: - guardrailed_texts, guardrailed_images = ( - await guardrail_to_apply.apply_guardrail( - texts=texts_to_check, - request_data=data, - input_type="request", - images=images_to_check if images_to_check else None, - logging_obj=litellm_logging_obj, - ) + # Step 2: Apply guardrail to all texts and tool calls in batch + if texts_to_check or tool_calls_to_check: + inputs = GenericGuardrailAPIInputs(texts=texts_to_check) + if images_to_check: + inputs["images"] = images_to_check + if tool_calls_to_check: + inputs["tool_calls"] = tool_calls_to_check # type: ignore + + guardrailed_inputs = await guardrail_to_apply.apply_guardrail( + inputs=inputs, + request_data=data, + input_type="request", + logging_obj=litellm_logging_obj, ) + guardrailed_texts = guardrailed_inputs.get("texts", []) + guardrailed_tool_calls = guardrailed_inputs.get("tools", []) + # Step 3: Map guardrail responses back to original message structure - await self._apply_guardrail_responses_to_input( - messages=messages, - responses=guardrailed_texts, - task_mappings=task_mappings, - ) + if guardrailed_texts and texts_to_check: + await self._apply_guardrail_responses_to_input_texts( + messages=messages, + responses=guardrailed_texts, + task_mappings=text_task_mappings, + ) + + # Step 4: Apply guardrailed tool calls back to messages + if guardrailed_tool_calls: + # Note: The guardrail may modify tool_calls_to_check in place + # or we may need to handle returned tool calls differently + await self._apply_guardrail_responses_to_input_tool_calls( + messages=messages, + tool_calls=guardrailed_tool_calls, # type: ignore + task_mappings=tool_call_task_mappings, + ) verbose_proxy_logger.debug( "OpenAI Chat Completions: Processed input messages: %s", messages @@ -91,61 +115,71 @@ class OpenAIChatCompletionsHandler(BaseTranslation): return data - def _extract_input_text_and_images( + def _extract_inputs( self, message: Dict[str, Any], msg_idx: int, texts_to_check: List[str], images_to_check: List[str], - task_mappings: List[Tuple[int, Optional[int]]], + tool_calls_to_check: List[ChatCompletionToolParam], + text_task_mappings: List[Tuple[int, Optional[int]]], + tool_call_task_mappings: List[Tuple[int, int]], ) -> None: """ - Extract text content and images from a message. + Extract text content, images, and tool calls from a message. - Override this method to customize text/image extraction logic. + Override this method to customize text/image/tool call extraction logic. """ content = message.get("content", None) - if content is None: - return + if content is not None: + if isinstance(content, str): + # Simple string content + texts_to_check.append(content) + text_task_mappings.append((msg_idx, None)) - if isinstance(content, str): - # Simple string content - texts_to_check.append(content) - task_mappings.append((msg_idx, None)) + elif isinstance(content, list): + # List content (e.g., multimodal with text and images) + for content_idx, content_item in enumerate(content): + # Extract text + text_str = content_item.get("text", None) + if text_str is not None: + texts_to_check.append(text_str) + text_task_mappings.append((msg_idx, int(content_idx))) - elif isinstance(content, list): - # List content (e.g., multimodal with text and images) - for content_idx, content_item in enumerate(content): - # Extract text - text_str = content_item.get("text", None) - if text_str is not None: - texts_to_check.append(text_str) - task_mappings.append((msg_idx, int(content_idx))) + # Extract images (image_url) + if content_item.get("type") == "image_url": + image_url = content_item.get("image_url", {}) + if isinstance(image_url, dict): + url = image_url.get("url") + if url: + images_to_check.append(url) - # Extract images (image_url) - if content_item.get("type") == "image_url": - image_url = content_item.get("image_url", {}) - if isinstance(image_url, dict): - url = image_url.get("url") - if url: - images_to_check.append(url) + # Extract tool calls (typically in assistant messages) + tool_calls = message.get("tools", None) + if tool_calls is not None and isinstance(tool_calls, list): + for tool_call_idx, tool_call in enumerate(tool_calls): + if isinstance(tool_call, dict): + # Add the full tool call object to the list + tool_calls_to_check.append(ChatCompletionToolParam(**tool_call)) + tool_call_task_mappings.append((msg_idx, int(tool_call_idx))) - async def _apply_guardrail_responses_to_input( + async def _apply_guardrail_responses_to_input_texts( self, messages: List[Dict[str, Any]], responses: List[str], task_mappings: List[Tuple[int, Optional[int]]], ) -> None: """ - Apply guardrail responses back to input messages. + Apply guardrail responses back to input message text content. - Override this method to customize how responses are applied. + Override this method to customize how text responses are applied. """ for task_idx, guardrail_response in enumerate(responses): mapping = task_mappings[task_idx] msg_idx = cast(int, mapping[0]) content_idx_optional = cast(Optional[int], mapping[1]) + # Handle content content = messages[msg_idx].get("content", None) if content is None: continue @@ -160,6 +194,31 @@ class OpenAIChatCompletionsHandler(BaseTranslation): "text" ] = guardrail_response + async def _apply_guardrail_responses_to_input_tool_calls( + self, + messages: List[Dict[str, Any]], + tool_calls: List[Dict[str, Any]], + task_mappings: List[Tuple[int, int]], + ) -> None: + """ + Apply guardrailed tool calls back to input messages. + + The guardrail may have modified the tool_calls list in place, + so we apply the modified tool calls back to the original messages. + + Override this method to customize how tool call responses are applied. + """ + for task_idx, (msg_idx, tool_call_idx) in enumerate(task_mappings): + if task_idx < len(tool_calls): + guardrailed_tool_call = tool_calls[task_idx] + message_tool_calls = messages[msg_idx].get("tool_calls", None) + if message_tool_calls is not None and isinstance( + message_tool_calls, list + ): + if tool_call_idx < len(message_tool_calls): + # Replace the tool call with the guardrailed version + message_tool_calls[tool_call_idx] = guardrailed_tool_call + async def process_output_response( self, response: "ModelResponse", @@ -193,21 +252,27 @@ class OpenAIChatCompletionsHandler(BaseTranslation): texts_to_check: List[str] = [] images_to_check: List[str] = [] - task_mappings: List[Tuple[int, Optional[int]]] = [] - # Track (choice_index, content_index) for each text + tool_calls_to_check: List[Dict[str, Any]] = [] + text_task_mappings: List[Tuple[int, Optional[int]]] = [] + tool_call_task_mappings: List[Tuple[int, int]] = [] + # text_task_mappings: Track (choice_index, content_index) for each text + # content_index is None for string content, int for list content + # tool_call_task_mappings: Track (choice_index, tool_call_index) for each tool call - # Step 1: Extract all text content and images from response choices + # Step 1: Extract all text content, images, and tool calls from response choices for choice_idx, choice in enumerate(response.choices): self._extract_output_text_and_images( choice=choice, choice_idx=choice_idx, texts_to_check=texts_to_check, images_to_check=images_to_check, - task_mappings=task_mappings, + tool_calls_to_check=tool_calls_to_check, + text_task_mappings=text_task_mappings, + tool_call_task_mappings=tool_call_task_mappings, ) - # Step 2: Apply guardrail to all texts in batch - if texts_to_check: + # Step 2: Apply guardrail to all texts and tool calls in batch + if texts_to_check or tool_calls_to_check: # Create a request_data dict with response info and user API key metadata request_data: dict = {"response": response} @@ -218,22 +283,36 @@ class OpenAIChatCompletionsHandler(BaseTranslation): if user_metadata: request_data["litellm_metadata"] = user_metadata - guardrailed_texts, guardrailed_images = ( - await guardrail_to_apply.apply_guardrail( - texts=texts_to_check, - request_data=request_data, - input_type="response", - images=images_to_check if images_to_check else None, - logging_obj=litellm_logging_obj, - ) + inputs = GenericGuardrailAPIInputs(texts=texts_to_check) + if images_to_check: + inputs["images"] = images_to_check + if tool_calls_to_check: + inputs["tool_calls"] = tool_calls_to_check # type: ignore + + guardrailed_inputs = await guardrail_to_apply.apply_guardrail( + inputs=inputs, + request_data=request_data, + input_type="response", + logging_obj=litellm_logging_obj, ) + guardrailed_texts = guardrailed_inputs.get("texts", []) + # Step 3: Map guardrail responses back to original response structure - await self._apply_guardrail_responses_to_output( - response=response, - responses=guardrailed_texts, - task_mappings=task_mappings, - ) + if guardrailed_texts and texts_to_check: + await self._apply_guardrail_responses_to_output_texts( + response=response, + responses=guardrailed_texts, + task_mappings=text_task_mappings, + ) + + # Step 4: Apply guardrailed tool calls back to response + if tool_calls_to_check: + await self._apply_guardrail_responses_to_output_tool_calls( + response=response, + tool_calls=tool_calls_to_check, + task_mappings=tool_call_task_mappings, + ) verbose_proxy_logger.debug( "OpenAI Chat Completions: Processed output response: %s", response @@ -334,16 +413,18 @@ class OpenAIChatCompletionsHandler(BaseTranslation): if user_metadata: request_data["litellm_metadata"] = user_metadata - guardrailed_texts, guardrailed_images = ( - await guardrail_to_apply.apply_guardrail( - texts=texts_to_check, - request_data=request_data, - input_type="response", - images=images_to_check if images_to_check else None, - logging_obj=litellm_logging_obj, - ) + inputs = GenericGuardrailAPIInputs(texts=texts_to_check) + if images_to_check: + inputs["images"] = images_to_check + guardrailed_inputs = await guardrail_to_apply.apply_guardrail( + inputs=inputs, + request_data=request_data, + input_type="response", + logging_obj=litellm_logging_obj, ) + guardrailed_texts = guardrailed_inputs.get("texts", []) + # Step 4: Apply guardrailed text back to all streaming chunks # For each choice, replace the combined text across all chunks await self._apply_guardrail_responses_to_output_streaming( @@ -363,7 +444,7 @@ class OpenAIChatCompletionsHandler(BaseTranslation): self, response: Union["ModelResponse", "ModelResponseStream"] ) -> bool: """ - Check if response has any text content to process. + Check if response has any text content or tool calls to process. Override this method to customize text content detection. """ @@ -372,15 +453,29 @@ class OpenAIChatCompletionsHandler(BaseTranslation): if isinstance(response, ModelResponse): for choice in response.choices: if isinstance(choice, litellm.Choices): + # Check for text content if choice.message.content and isinstance( choice.message.content, str ): return True + # Check for tool calls + if choice.message.tool_calls and isinstance( + choice.message.tool_calls, list + ): + if len(choice.message.tool_calls) > 0: + return True elif isinstance(response, ModelResponseStream): for choice in response.choices: if isinstance(choice, litellm.StreamingChoices): + # Check for text content if choice.delta.content and isinstance(choice.delta.content, str): return True + # Check for tool calls + if choice.delta.tool_calls and isinstance( + choice.delta.tool_calls, list + ): + if len(choice.delta.tool_calls) > 0: + return True return False def _extract_output_text_and_images( @@ -389,23 +484,28 @@ class OpenAIChatCompletionsHandler(BaseTranslation): choice_idx: int, texts_to_check: List[str], images_to_check: List[str], - task_mappings: List[Tuple[int, Optional[int]]], + tool_calls_to_check: List[Dict[str, Any]], + text_task_mappings: List[Tuple[int, Optional[int]]], + tool_call_task_mappings: List[Tuple[int, int]], ) -> None: """ - Extract text content and images from a response choice. + Extract text content, images, and tool calls from a response choice. - Override this method to customize text/image extraction logic. + Override this method to customize text/image/tool call extraction logic. """ verbose_proxy_logger.debug( "OpenAI Chat Completions: Processing choice: %s", choice ) - # Determine content source based on choice type + # Determine content source and tool calls based on choice type content = None + tool_calls = None if isinstance(choice, litellm.Choices): content = choice.message.content + tool_calls = choice.message.tool_calls elif isinstance(choice, litellm.StreamingChoices): content = choice.delta.content + tool_calls = choice.delta.tool_calls else: # Unknown choice type, skip processing return @@ -414,7 +514,7 @@ class OpenAIChatCompletionsHandler(BaseTranslation): if content and isinstance(content, str): # Simple string content texts_to_check.append(content) - task_mappings.append((choice_idx, None)) + text_task_mappings.append((choice_idx, None)) elif content and isinstance(content, list): # List content (e.g., multimodal response) @@ -423,7 +523,7 @@ class OpenAIChatCompletionsHandler(BaseTranslation): content_text = content_item.get("text") if content_text: texts_to_check.append(content_text) - task_mappings.append((choice_idx, int(content_idx))) + text_task_mappings.append((choice_idx, int(content_idx))) # Extract images if content_item.get("type") == "image_url": @@ -433,39 +533,108 @@ class OpenAIChatCompletionsHandler(BaseTranslation): if url: images_to_check.append(url) - async def _apply_guardrail_responses_to_output( + # Process tool calls if they exist + if tool_calls is not None and isinstance(tool_calls, list): + for tool_call_idx, tool_call in enumerate(tool_calls): + # Convert tool call to dict format for guardrail processing + tool_call_dict = self._convert_tool_call_to_dict(tool_call) + if tool_call_dict: + tool_calls_to_check.append(tool_call_dict) + tool_call_task_mappings.append((choice_idx, int(tool_call_idx))) + + def _convert_tool_call_to_dict( + self, tool_call: Union[Dict[str, Any], Any] + ) -> Optional[Dict[str, Any]]: + """ + Convert a tool call object to dictionary format. + + Tool calls can be either dict or object depending on the type. + """ + if isinstance(tool_call, dict): + return tool_call + elif hasattr(tool_call, "id") and hasattr(tool_call, "function"): + # Convert object to dict + function = tool_call.function + function_dict = {} + if hasattr(function, "name"): + function_dict["name"] = function.name + if hasattr(function, "arguments"): + function_dict["arguments"] = function.arguments + + tool_call_dict = { + "id": tool_call.id if hasattr(tool_call, "id") else None, + "type": tool_call.type if hasattr(tool_call, "type") else "function", + "function": function_dict, + } + return tool_call_dict + return None + + async def _apply_guardrail_responses_to_output_texts( self, response: "ModelResponse", responses: List[str], task_mappings: List[Tuple[int, Optional[int]]], ) -> None: """ - Apply guardrail responses back to output response. + Apply guardrail text responses back to output response. - Override this method to customize how responses are applied. + Override this method to customize how text responses are applied. """ for task_idx, guardrail_response in enumerate(responses): mapping = task_mappings[task_idx] choice_idx = cast(int, mapping[0]) content_idx_optional = cast(Optional[int], mapping[1]) - content = cast(Choices, response.choices[choice_idx]).message.content + choice = cast(Choices, response.choices[choice_idx]) + + # Handle content + content = choice.message.content if content is None: continue if isinstance(content, str) and content_idx_optional is None: # Replace string content with guardrail response - cast(Choices, response.choices[choice_idx]).message.content = ( - guardrail_response - ) + choice.message.content = guardrail_response elif isinstance(content, list) and content_idx_optional is not None: # Replace specific text item in list content - cast(Choices, response.choices[choice_idx]).message.content[ # type: ignore - content_idx_optional - ][ - "text" - ] = guardrail_response + choice.message.content[content_idx_optional]["text"] = guardrail_response # type: ignore + + async def _apply_guardrail_responses_to_output_tool_calls( + self, + response: "ModelResponse", + tool_calls: List[Dict[str, Any]], + task_mappings: List[Tuple[int, int]], + ) -> None: + """ + Apply guardrailed tool calls back to output response. + + The guardrail may have modified the tool_calls list in place, + so we apply the modified tool calls back to the original response. + + Override this method to customize how tool call responses are applied. + """ + for task_idx, (choice_idx, tool_call_idx) in enumerate(task_mappings): + if task_idx < len(tool_calls): + guardrailed_tool_call = tool_calls[task_idx] + choice = cast(Choices, response.choices[choice_idx]) + choice_tool_calls = choice.message.tool_calls + + if choice_tool_calls is not None and isinstance( + choice_tool_calls, list + ): + if tool_call_idx < len(choice_tool_calls): + # Update the tool call with guardrailed version + existing_tool_call = choice_tool_calls[tool_call_idx] + # Update object attributes (output responses always have typed objects) + if "function" in guardrailed_tool_call: + func_dict = guardrailed_tool_call["function"] + if "arguments" in func_dict: + existing_tool_call.function.arguments = func_dict[ + "arguments" + ] + if "name" in func_dict: + existing_tool_call.function.name = func_dict["name"] async def _apply_guardrail_responses_to_output_streaming( self, diff --git a/litellm/llms/openai/completion/guardrail_translation/handler.py b/litellm/llms/openai/completion/guardrail_translation/handler.py index f8b733567d3..73d08cfead4 100644 --- a/litellm/llms/openai/completion/guardrail_translation/handler.py +++ b/litellm/llms/openai/completion/guardrail_translation/handler.py @@ -53,12 +53,13 @@ class OpenAITextCompletionHandler(BaseTranslation): if isinstance(prompt, str): # Single string prompt - guardrailed_texts, _ = await guardrail_to_apply.apply_guardrail( - texts=[prompt], + guardrailed_inputs = await guardrail_to_apply.apply_guardrail( + inputs={"texts": [prompt]}, request_data=data, input_type="request", logging_obj=litellm_logging_obj, ) + guardrailed_texts = guardrailed_inputs.get("texts", []) data["prompt"] = guardrailed_texts[0] if guardrailed_texts else prompt verbose_proxy_logger.debug( @@ -79,12 +80,13 @@ class OpenAITextCompletionHandler(BaseTranslation): text_indices.append(idx) if texts_to_check: - guardrailed_texts, _ = await guardrail_to_apply.apply_guardrail( - texts=texts_to_check, + guardrailed_inputs = await guardrail_to_apply.apply_guardrail( + inputs={"texts": texts_to_check}, request_data=data, input_type="request", logging_obj=litellm_logging_obj, ) + guardrailed_texts = guardrailed_inputs.get("texts", []) # Replace guardrailed texts back for guardrail_idx, prompt_idx in enumerate(text_indices): @@ -152,12 +154,13 @@ class OpenAITextCompletionHandler(BaseTranslation): if user_metadata: request_data["litellm_metadata"] = user_metadata - guardrailed_texts, _ = await guardrail_to_apply.apply_guardrail( - texts=texts_to_check, + guardrailed_inputs = await guardrail_to_apply.apply_guardrail( + inputs={"texts": texts_to_check}, request_data=request_data, input_type="response", logging_obj=litellm_logging_obj, ) + guardrailed_texts = guardrailed_inputs.get("texts", []) # Apply guardrailed texts back to choices for guardrail_idx, choice_idx in enumerate(choice_indices): diff --git a/litellm/llms/openai/image_generation/guardrail_translation/handler.py b/litellm/llms/openai/image_generation/guardrail_translation/handler.py index 53ee994c48f..842a64b1878 100644 --- a/litellm/llms/openai/image_generation/guardrail_translation/handler.py +++ b/litellm/llms/openai/image_generation/guardrail_translation/handler.py @@ -52,12 +52,13 @@ class OpenAIImageGenerationHandler(BaseTranslation): # Apply guardrail to the prompt if isinstance(prompt, str): - guardrailed_texts, _ = await guardrail_to_apply.apply_guardrail( - texts=[prompt], + guardrailed_inputs = await guardrail_to_apply.apply_guardrail( + inputs={"texts": [prompt]}, request_data=data, input_type="request", logging_obj=litellm_logging_obj, ) + guardrailed_texts = guardrailed_inputs.get("texts", []) data["prompt"] = guardrailed_texts[0] if guardrailed_texts else prompt verbose_proxy_logger.debug( diff --git a/litellm/llms/openai/responses/guardrail_translation/handler.py b/litellm/llms/openai/responses/guardrail_translation/handler.py index 7ef6576159e..9377eb4e193 100644 --- a/litellm/llms/openai/responses/guardrail_translation/handler.py +++ b/litellm/llms/openai/responses/guardrail_translation/handler.py @@ -28,12 +28,19 @@ Output: response.output is List[GenericResponseOutputItem] where each has: - text: str """ -from typing import TYPE_CHECKING, Any, List, Optional, Tuple, Union, cast +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union, cast + +from openai import BaseModel from openai import BaseModel from litellm._logging import verbose_proxy_logger from litellm.llms.base_llm.guardrail_translation.base_translation import BaseTranslation +from litellm.responses.litellm_completion_transformation.transformation import ( + LiteLLMCompletionResponsesConfig, +) +from litellm.types.guardrails import GenericGuardrailAPIInputs +from litellm.types.llms.openai import ChatCompletionToolParam from litellm.types.responses.main import GenericResponseOutputItem, OutputText if TYPE_CHECKING: @@ -65,17 +72,28 @@ class OpenAIResponsesHandler(BaseTranslation): Handles both string input and list of message objects. """ input_data: Optional[Union[str, "ResponseInputParam"]] = data.get("input") + tools_to_check: List[ChatCompletionToolParam] = [] if input_data is None: return data # Handle simple string input if isinstance(input_data, str): - guardrailed_texts, _ = await guardrail_to_apply.apply_guardrail( - texts=[input_data], + inputs = GenericGuardrailAPIInputs(texts=[input_data]) + + # Extract and transform tools if present + + if "tools" in data and data["tools"]: + self._extract_and_transform_tools(data["tools"], tools_to_check) + if tools_to_check: + inputs["tools"] = tools_to_check + + guardrailed_inputs = await guardrail_to_apply.apply_guardrail( + inputs=inputs, request_data=data, input_type="request", logging_obj=litellm_logging_obj, ) + guardrailed_texts = guardrailed_inputs.get("texts", []) data["input"] = guardrailed_texts[0] if guardrailed_texts else input_data verbose_proxy_logger.debug("OpenAI Responses API: Processed string input") return data @@ -90,7 +108,7 @@ class OpenAIResponsesHandler(BaseTranslation): # Track (message_index, content_index) for each text # content_index is None for string content, int for list content - # Step 1: Extract all text content and images + # Step 1: Extract all text content, images, and tools for msg_idx, message in enumerate(input_data): self._extract_input_text_and_images( message=message, @@ -100,18 +118,26 @@ class OpenAIResponsesHandler(BaseTranslation): task_mappings=task_mappings, ) + # Extract and transform tools if present + if "tools" in data and data["tools"]: + self._extract_and_transform_tools(data["tools"], tools_to_check) + # Step 2: Apply guardrail to all texts in batch if texts_to_check: - guardrailed_texts, guardrailed_images = ( - await guardrail_to_apply.apply_guardrail( - texts=texts_to_check, - request_data=data, - input_type="request", - images=images_to_check if images_to_check else None, - logging_obj=litellm_logging_obj, - ) + inputs = GenericGuardrailAPIInputs(texts=texts_to_check) + if images_to_check: + inputs["images"] = images_to_check + if tools_to_check: + inputs["tools"] = tools_to_check + guardrailed_inputs = await guardrail_to_apply.apply_guardrail( + inputs=inputs, + request_data=data, + input_type="request", + logging_obj=litellm_logging_obj, ) + guardrailed_texts = guardrailed_inputs.get("texts", []) + # Step 3: Map guardrail responses back to original input structure await self._apply_guardrail_responses_to_input( messages=input_data, @@ -125,6 +151,29 @@ class OpenAIResponsesHandler(BaseTranslation): return data + def _extract_and_transform_tools( + self, + tools: List[Dict[str, Any]], + tools_to_check: List[ChatCompletionToolParam], + ) -> None: + """ + Extract and transform tools from Responses API format to Chat Completion format. + + Uses the LiteLLM transformation function to convert Responses API tools + to Chat Completion tools that can be passed to guardrails. + """ + if tools is not None and isinstance(tools, list): + # Transform Responses API tools to Chat Completion tools + ( + transformed_tools, + _, + ) = LiteLLMCompletionResponsesConfig.transform_responses_api_tools_to_chat_completion_tools( + tools # type: ignore + ) + tools_to_check.extend( + cast(List[ChatCompletionToolParam], transformed_tools) + ) + def _extract_input_text_and_images( self, message: Any, # Can be Dict[str, Any] or ResponseInputParam @@ -254,16 +303,18 @@ class OpenAIResponsesHandler(BaseTranslation): if user_metadata: request_data["litellm_metadata"] = user_metadata - guardrailed_texts, guardrailed_images = ( - await guardrail_to_apply.apply_guardrail( - texts=texts_to_check, - request_data=request_data, - input_type="response", - images=images_to_check if images_to_check else None, - logging_obj=litellm_logging_obj, - ) + inputs = GenericGuardrailAPIInputs(texts=texts_to_check) + if images_to_check: + inputs["images"] = images_to_check + guardrailed_inputs = await guardrail_to_apply.apply_guardrail( + inputs=inputs, + request_data=request_data, + input_type="response", + logging_obj=litellm_logging_obj, ) + guardrailed_texts = guardrailed_inputs.get("texts", []) + # Step 3: Map guardrail responses back to original response structure await self._apply_guardrail_responses_to_output( response=response, @@ -288,12 +339,11 @@ class OpenAIResponsesHandler(BaseTranslation): Process output streaming response by applying guardrails to text content. """ string_so_far = self.get_streaming_string_so_far(responses_so_far) - guardrailed_text, _ = await guardrail_to_apply.apply_guardrail( - texts=[string_so_far], + guardrailed_inputs = await guardrail_to_apply.apply_guardrail( + inputs={"texts": [string_so_far]}, request_data={}, input_type="response", logging_obj=litellm_logging_obj, - images=None, ) return responses_so_far diff --git a/litellm/llms/openai/speech/guardrail_translation/handler.py b/litellm/llms/openai/speech/guardrail_translation/handler.py index 71edd4f2801..4c2f71477be 100644 --- a/litellm/llms/openai/speech/guardrail_translation/handler.py +++ b/litellm/llms/openai/speech/guardrail_translation/handler.py @@ -50,12 +50,13 @@ class OpenAITextToSpeechHandler(BaseTranslation): return data if isinstance(input_text, str): - guardrailed_texts, _ = await guardrail_to_apply.apply_guardrail( - texts=[input_text], + guardrailed_inputs = await guardrail_to_apply.apply_guardrail( + inputs={"texts": [input_text]}, request_data=data, input_type="request", logging_obj=litellm_logging_obj, ) + guardrailed_texts = guardrailed_inputs.get("texts", []) data["input"] = guardrailed_texts[0] if guardrailed_texts else input_text verbose_proxy_logger.debug( diff --git a/litellm/llms/openai/transcriptions/guardrail_translation/handler.py b/litellm/llms/openai/transcriptions/guardrail_translation/handler.py index 18678a9878b..ac416f42c81 100644 --- a/litellm/llms/openai/transcriptions/guardrail_translation/handler.py +++ b/litellm/llms/openai/transcriptions/guardrail_translation/handler.py @@ -88,12 +88,13 @@ class OpenAIAudioTranscriptionHandler(BaseTranslation): if user_metadata: request_data["litellm_metadata"] = user_metadata - guardrailed_texts, _ = await guardrail_to_apply.apply_guardrail( - texts=[original_text], + guardrailed_inputs = await guardrail_to_apply.apply_guardrail( + inputs={"texts": [original_text]}, request_data=request_data, input_type="response", logging_obj=litellm_logging_obj, ) + guardrailed_texts = guardrailed_inputs.get("texts", []) response.text = guardrailed_texts[0] if guardrailed_texts else original_text verbose_proxy_logger.debug( diff --git a/litellm/llms/pass_through/guardrail_translation/handler.py b/litellm/llms/pass_through/guardrail_translation/handler.py index ae96335cec3..0702086a578 100644 --- a/litellm/llms/pass_through/guardrail_translation/handler.py +++ b/litellm/llms/pass_through/guardrail_translation/handler.py @@ -118,8 +118,8 @@ class PassThroughEndpointHandler(BaseTranslation): return data # Apply guardrail (pass-through doesn't modify the text, just checks it) - await guardrail_to_apply.apply_guardrail( - texts=[text_to_check], + guardrailed_inputs = await guardrail_to_apply.apply_guardrail( + inputs={"texts": [text_to_check]}, request_data=data, input_type="request", logging_obj=litellm_logging_obj, @@ -178,8 +178,8 @@ class PassThroughEndpointHandler(BaseTranslation): request_data["litellm_metadata"] = user_metadata # Apply guardrail (pass-through doesn't modify the text, just checks it) - await guardrail_to_apply.apply_guardrail( - texts=[text_to_check], + guardrailed_inputs = await guardrail_to_apply.apply_guardrail( + inputs={"texts": [text_to_check]}, request_data=request_data, input_type="response", logging_obj=litellm_logging_obj, diff --git a/litellm/proxy/guardrails/guardrail_endpoints.py b/litellm/proxy/guardrails/guardrail_endpoints.py index 395fdb249d3..bb78383ce44 100644 --- a/litellm/proxy/guardrails/guardrail_endpoints.py +++ b/litellm/proxy/guardrails/guardrail_endpoints.py @@ -1214,13 +1214,15 @@ async def apply_guardrail( detail=f"Guardrail '{request.guardrail_name}' not found. Please ensure the guardrail is configured in your LiteLLM proxy.", ) - response_text = await active_guardrail.apply_guardrail( - texts=[request.text], + guardrailed_inputs = await active_guardrail.apply_guardrail( + inputs={"texts": [request.text]}, request_data={}, input_type="request", - images=None, ) + response_text = guardrailed_inputs.get("texts", []) - return ApplyGuardrailResponse(response_text=response_text[0][0]) + return ApplyGuardrailResponse( + response_text=response_text[0] if response_text else request.text + ) except Exception as e: raise handle_exception_on_proxy(e) diff --git a/litellm/proxy/guardrails/guardrail_hooks/bedrock_guardrails.py b/litellm/proxy/guardrails/guardrail_hooks/bedrock_guardrails.py index 3932e1220b4..e0fb3192401 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/bedrock_guardrails.py +++ b/litellm/proxy/guardrails/guardrail_hooks/bedrock_guardrails.py @@ -42,7 +42,7 @@ from litellm.llms.custom_httpx.http_handler import ( ) from litellm.proxy._types import UserAPIKeyAuth from litellm.secret_managers.main import get_secret_str -from litellm.types.guardrails import GuardrailEventHooks +from litellm.types.guardrails import GenericGuardrailAPIInputs, GuardrailEventHooks from litellm.types.llms.openai import AllMessageValues, ChatCompletionUserMessage from litellm.types.proxy.guardrails.guardrail_hooks.bedrock_guardrails import ( BedrockContentItem, @@ -54,6 +54,7 @@ from litellm.types.proxy.guardrails.guardrail_hooks.bedrock_guardrails import ( if TYPE_CHECKING: from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj + from litellm.types.utils import ( CallTypes, CallTypesLiteral, @@ -1245,12 +1246,11 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM): async def apply_guardrail( self, - texts: List[str], + inputs: "GenericGuardrailAPIInputs", request_data: dict, input_type: Literal["request", "response"], logging_obj: Optional["LiteLLMLoggingObj"] = None, - images: Optional[List[str]] = None, - ) -> Tuple[List[str], Optional[List[str]]]: + ) -> "GenericGuardrailAPIInputs": """ Apply Bedrock guardrail to a batch of texts for testing purposes. @@ -1258,17 +1258,18 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM): It creates mock messages to test the guardrail functionality. Args: - texts: List of texts to analyze + inputs: Dictionary containing texts and optional images request_data: Request data dictionary for logging metadata input_type: Whether this is a "request" or "response" - images: Optional list of images (not processed separately) + logging_obj: Optional logging object Returns: - Tuple of (processed_texts, images) - texts may be masked, images unchanged + GenericGuardrailAPIInputs - processed_texts may be masked, images unchanged Raises: Exception: If content is blocked by Bedrock guardrail """ + texts = inputs.get("texts", []) try: verbose_proxy_logger.debug( f"Bedrock Guardrail: Applying guardrail to {len(texts)} text(s)" @@ -1279,6 +1280,7 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM): mock_messages: List[AllMessageValues] = [ ChatCompletionUserMessage(role="user", content=text) for text in texts ] + request_messages = mock_messages filter_result = self._prepare_guardrail_messages_for_role( messages=request_messages @@ -1297,6 +1299,7 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM): ) # Apply any masking that was applied by the guardrail + output_list = bedrock_response.get("output") if output_list: # If the guardrail returned modified content, use that @@ -1319,7 +1322,8 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM): "Bedrock Guardrail: Successfully applied guardrail" ) - return masked_texts or texts, images + inputs["texts"] = masked_texts + return inputs except Exception as e: verbose_proxy_logger.error( diff --git a/litellm/proxy/guardrails/guardrail_hooks/enkryptai/enkryptai.py b/litellm/proxy/guardrails/guardrail_hooks/enkryptai/enkryptai.py index 49401ca7921..493d432eebb 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/enkryptai/enkryptai.py +++ b/litellm/proxy/guardrails/guardrail_hooks/enkryptai/enkryptai.py @@ -15,7 +15,6 @@ from typing import ( List, Literal, Optional, - Tuple, Union, ) @@ -30,7 +29,7 @@ from litellm.llms.custom_httpx.http_handler import ( httpxSpecialProvider, ) from litellm.proxy._types import UserAPIKeyAuth -from litellm.types.guardrails import GuardrailEventHooks +from litellm.types.guardrails import GenericGuardrailAPIInputs, GuardrailEventHooks from litellm.types.proxy.guardrails.guardrail_hooks.enkryptai import ( EnkryptAIProcessedResult, EnkryptAIResponse, @@ -481,27 +480,28 @@ class EnkryptAIGuardrails(CustomGuardrail): async def apply_guardrail( self, - texts: List[str], + inputs: "GenericGuardrailAPIInputs", request_data: dict, input_type: Literal["request", "response"], logging_obj: Optional["LiteLLMLoggingObj"] = None, - images: Optional[List[str]] = None, - ) -> Tuple[List[str], Optional[List[str]]]: + ) -> "GenericGuardrailAPIInputs": """ Apply EnkryptAI guardrail to a batch of texts. Args: - texts: List of texts to check for attacks + inputs: Dictionary containing texts and optional images request_data: Request data dictionary containing metadata input_type: Whether this is a "request" or "response" - images: Optional list of images (not used by EnkryptAI) + logging_obj: Optional logging object Returns: - Tuple of (texts, images) - texts unchanged if passed, images unchanged + GenericGuardrailAPIInputs - texts unchanged if passed, images unchanged Raises: ValueError: If any attacks are detected """ + texts = inputs.get("texts", []) + # Check each text for attacks for text in texts: result = await self._call_enkryptai_guardrails( @@ -517,7 +517,7 @@ class EnkryptAIGuardrails(CustomGuardrail): error_message = self._create_error_message(processed_result) raise ValueError(error_message) - return texts, images + return inputs async def async_post_call_streaming_iterator_hook( self, 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 index f806e8b4802..33912b8fcd9 100644 --- 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 @@ -14,7 +14,7 @@ from litellm.llms.custom_httpx.http_handler import ( get_async_httpx_client, httpxSpecialProvider, ) -from litellm.types.guardrails import GuardrailEventHooks +from litellm.types.guardrails import GenericGuardrailAPIInputs, GuardrailEventHooks from litellm.types.proxy.guardrails.guardrail_hooks.generic_guardrail_api import ( GenericGuardrailAPIMetadata, GenericGuardrailAPIRequest, @@ -144,22 +144,24 @@ class GenericGuardrailAPI(CustomGuardrail): async def apply_guardrail( self, - texts: List[str], + inputs: GenericGuardrailAPIInputs, request_data: dict, input_type: Literal["request", "response"], logging_obj: Optional["LiteLLMLoggingObj"] = None, - images: Optional[List[str]] = None, - ) -> Tuple[List[str], Optional[List[str]]]: + ) -> GenericGuardrailAPIInputs: """ - Apply the Generic Guardrail API to the given text. + Apply the Generic Guardrail API to the given inputs. This is the main method that gets called by the framework. Args: - texts: List of texts to check + inputs: Dictionary containing: + - texts: List of texts to check + - images: Optional list of images to check + - tool_calls: Optional list of tool calls to check request_data: Request data dictionary containing user_api_key_dict and other metadata input_type: Whether this is a "request" or "response" guardrail - images: Optional list of images to check + logging_obj: Optional logging object for tracking the guardrail execution Returns: Tuple of (processed texts, processed images) @@ -169,6 +171,11 @@ class GenericGuardrailAPI(CustomGuardrail): """ verbose_proxy_logger.debug("Generic Guardrail API: Applying guardrail to text") + # Extract texts and images from inputs + texts = inputs.get("texts", []) + images = inputs.get("images") + tools = inputs.get("tools") + # Use provided request_data or create an empty dict if request_data is None: request_data = {} @@ -193,6 +200,7 @@ class GenericGuardrailAPI(CustomGuardrail): texts=texts, request_data=user_metadata, images=images, + tools=tools, additional_provider_specific_params=additional_params, input_type=input_type, ) @@ -230,17 +238,19 @@ class GenericGuardrailAPI(CustomGuardrail): ) raise Exception(f"Content blocked by guardrail: {error_message}") - elif guardrail_response.action == "GUARDRAIL_INTERVENED": - # Content was modified by the guardrail - if guardrail_response.texts: - verbose_proxy_logger.debug("Generic Guardrail API modified text") - return guardrail_response.texts, guardrail_response.images - # Action is NONE or no modifications needed - return ( - guardrail_response.texts or texts, - guardrail_response.images or images, - ) + return_inputs = GenericGuardrailAPIInputs(texts=texts) + if guardrail_response.texts: + return_inputs["texts"] = guardrail_response.texts + if guardrail_response.images: + return_inputs["images"] = guardrail_response.images + elif images: + return_inputs["images"] = images + if guardrail_response.tools: + return_inputs["tools"] = guardrail_response.tools + elif tools: + return_inputs["tools"] = tools + return return_inputs except Exception as e: # Check if it's already an exception we raised diff --git a/litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/content_filter.py b/litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/content_filter.py index 776b9d36d5e..6756b188847 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/content_filter.py +++ b/litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/content_filter.py @@ -27,6 +27,7 @@ from litellm.integrations.custom_guardrail import CustomGuardrail if TYPE_CHECKING: from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj + from litellm.types.guardrails import GenericGuardrailAPIInputs from litellm.proxy._types import UserAPIKeyAuth from litellm.types.guardrails import ( BlockedWord, @@ -304,12 +305,11 @@ class ContentFilterGuardrail(CustomGuardrail): async def apply_guardrail( self, - texts: List[str], + inputs: "GenericGuardrailAPIInputs", request_data: dict, input_type: Literal["request", "response"], logging_obj: Optional["LiteLLMLoggingObj"] = None, - images: Optional[List[str]] = None, - ) -> Tuple[List[str], Optional[List[str]]]: + ) -> "GenericGuardrailAPIInputs": """ Apply content filtering guardrail to a batch of texts. @@ -317,17 +317,19 @@ class ContentFilterGuardrail(CustomGuardrail): either blocking the request or masking the sensitive content. Args: - texts: List of texts to apply the guardrail to + inputs: Dictionary containing texts and optional images request_data: Request data dictionary for logging metadata input_type: Whether this is a "request" or "response" - images: Optional list of images (not processed) + logging_obj: Optional logging object Returns: - Tuple of (processed_texts, images) - texts may be masked, images unchanged + GenericGuardrailAPIInputs - processed_texts may be masked, images unchanged Raises: HTTPException: If sensitive content is detected and action is BLOCK """ + texts = inputs.get("texts", []) + verbose_proxy_logger.debug( f"ContentFilterGuardrail: Applying guardrail to {len(texts)} text(s)" ) @@ -386,7 +388,8 @@ class ContentFilterGuardrail(CustomGuardrail): verbose_proxy_logger.debug( "ContentFilterGuardrail: Guardrail applied successfully" ) - return processed_texts, images + inputs["texts"] = processed_texts + return inputs async def async_post_call_streaming_iterator_hook( self, @@ -423,12 +426,17 @@ class ContentFilterGuardrail(CustomGuardrail): if isinstance(choice.delta.content, str): # Check the chunk content using apply_guardrail try: - processed_content = await self.apply_guardrail( - texts=[choice.delta.content], + guardrailed_inputs = await self.apply_guardrail( + inputs={"texts": [choice.delta.content]}, input_type="response", - images=None, request_data=request_data, ) + processed_texts = guardrailed_inputs.get("texts", []) + processed_content = ( + processed_texts[0] + if processed_texts + else choice.delta.content + ) if processed_content != choice.delta.content: choice.delta.content = processed_content verbose_proxy_logger.debug( diff --git a/litellm/proxy/guardrails/guardrail_hooks/presidio.py b/litellm/proxy/guardrails/guardrail_hooks/presidio.py index 6b19685f07e..d183b688edd 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/presidio.py +++ b/litellm/proxy/guardrails/guardrail_hooks/presidio.py @@ -29,9 +29,11 @@ import aiohttp import litellm # noqa: E401 from litellm import get_secret from litellm._logging import verbose_proxy_logger +from litellm.types.guardrails import GenericGuardrailAPIInputs if TYPE_CHECKING: from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj + from litellm._uuid import uuid from litellm.caching.caching import DualCache from litellm.exceptions import BlockedPiiEntityError @@ -713,17 +715,18 @@ class _OPTIONAL_PresidioPIIMasking(CustomGuardrail): async def apply_guardrail( self, - texts: List[str], + inputs: "GenericGuardrailAPIInputs", request_data: dict, input_type: Literal["request", "response"], logging_obj: Optional["LiteLLMLoggingObj"] = None, - images: Optional[List[str]] = None, - ) -> Tuple[List[str], Optional[List[str]]]: + ) -> "GenericGuardrailAPIInputs": """ UI will call this function to check: 1. If the connection to the guardrail is working 2. When Testing the guardrail with some text, this function will be called with the input text and returns a text after applying the guardrail """ + texts = inputs.get("texts", []) + new_texts = [] for text in texts: modified_text = await self.check_pii( @@ -733,7 +736,8 @@ class _OPTIONAL_PresidioPIIMasking(CustomGuardrail): request_data=request_data or {}, ) new_texts.append(modified_text) - return new_texts, images + inputs["texts"] = new_texts + return inputs def update_in_memory_litellm_params(self, litellm_params: LitellmParams) -> None: """ diff --git a/litellm/proxy/guardrails/guardrail_hooks/unified_guardrail/unified_guardrail.py b/litellm/proxy/guardrails/guardrail_hooks/unified_guardrail/unified_guardrail.py index d234dffc511..c16cb89b785 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/unified_guardrail/unified_guardrail.py +++ b/litellm/proxy/guardrails/guardrail_hooks/unified_guardrail/unified_guardrail.py @@ -193,6 +193,7 @@ class UnifiedLLMGuardrails(CustomLogger): "guardrail_to_apply", None ) + # Get sampling rate from guardrail config or optional_params, default to 5 sampling_rate = 5 if guardrail_to_apply is not None: diff --git a/litellm/proxy/guardrails/guardrail_hooks/zscaler_ai_guard/zscaler_ai_guard.py b/litellm/proxy/guardrails/guardrail_hooks/zscaler_ai_guard/zscaler_ai_guard.py index 562f0d40272..bebb87f8d21 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/zscaler_ai_guard/zscaler_ai_guard.py +++ b/litellm/proxy/guardrails/guardrail_hooks/zscaler_ai_guard/zscaler_ai_guard.py @@ -4,7 +4,7 @@ # # +-------------------------------------------------------------+ import os -from typing import TYPE_CHECKING, List, Literal, Optional, Tuple +from typing import TYPE_CHECKING, Literal, Optional from fastapi import HTTPException @@ -14,6 +14,7 @@ from litellm.llms.custom_httpx.http_handler import ( get_async_httpx_client, httpxSpecialProvider, ) +from litellm.types.guardrails import GenericGuardrailAPIInputs if TYPE_CHECKING: from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj @@ -71,27 +72,27 @@ class ZscalerAIGuard(CustomGuardrail): async def apply_guardrail( self, - texts: List[str], + inputs: "GenericGuardrailAPIInputs", request_data: dict, input_type: Literal["request", "response"], logging_obj: Optional["LiteLLMLoggingObj"] = None, - images: Optional[List[str]] = None, - ) -> Tuple[List[str], Optional[List[str]]]: + ) -> "GenericGuardrailAPIInputs": """ Apply Zscaler AI Guard guardrail to batch of texts. Args: - texts: List of texts to check + inputs: Dictionary containing texts and optional images request_data: Request data dictionary containing metadata input_type: Whether this is a "request" or "response" - images: Optional list of images (not used by Zscaler) + logging_obj: Optional logging object Returns: - Tuple of (processed_texts, images) - texts unchanged if passed, images unchanged + GenericGuardrailAPIInputs - texts unchanged if passed, images unchanged Raises: Exception: If content is blocked by Zscaler AI Guard """ + texts = inputs.get("texts", []) try: verbose_proxy_logger.debug(f"ZscalerAIGuard: Checking {len(texts)} text(s)") @@ -143,7 +144,7 @@ class ZscalerAIGuard(CustomGuardrail): raise e verbose_proxy_logger.debug("ZscalerAIGuard: Successfully applied guardrail.") - return texts, images + return inputs def extract_blocking_info(self, response): """ diff --git a/litellm/types/guardrails.py b/litellm/types/guardrails.py index 2c0b10ec67d..ea3a6cc4ede 100644 --- a/litellm/types/guardrails.py +++ b/litellm/types/guardrails.py @@ -5,6 +5,7 @@ from typing import Any, Dict, List, Literal, Optional, Union from pydantic import BaseModel, ConfigDict, Field from typing_extensions import Required, TypedDict +from litellm.types.llms.openai import ChatCompletionToolParam from litellm.types.proxy.guardrails.guardrail_hooks.enkryptai import ( EnkryptAIGuardrailConfigs, ) @@ -738,3 +739,9 @@ class PatchGuardrailRequest(BaseModel): guardrail_name: Optional[str] = None litellm_params: Optional[BaseLitellmParams] = None guardrail_info: Optional[Dict[str, Any]] = None + + +class GenericGuardrailAPIInputs(TypedDict, total=False): + texts: List[str] + images: List[str] + tools: List[ChatCompletionToolParam] diff --git a/litellm/types/proxy/guardrails/guardrail_hooks/generic_guardrail_api.py b/litellm/types/proxy/guardrails/guardrail_hooks/generic_guardrail_api.py index d2acc3e3403..66823270e87 100644 --- a/litellm/types/proxy/guardrails/guardrail_hooks/generic_guardrail_api.py +++ b/litellm/types/proxy/guardrails/guardrail_hooks/generic_guardrail_api.py @@ -3,6 +3,7 @@ from typing import Any, Dict, List, Literal, Optional from pydantic import BaseModel, Field from typing_extensions import TypedDict +from litellm.types.llms.openai import ChatCompletionToolParam from litellm.types.proxy.guardrails.guardrail_hooks.base import GuardrailConfigModel @@ -59,6 +60,7 @@ class GenericGuardrailAPIRequest: litellm_trace_id: Optional[str], additional_provider_specific_params: Optional[Dict[str, Any]] = None, images: Optional[List[str]] = None, + tools: Optional[List[ChatCompletionToolParam]] = None, ): self.texts = texts self.request_data = request_data @@ -69,12 +71,14 @@ class GenericGuardrailAPIRequest: self.input_type = input_type self.litellm_call_id = litellm_call_id self.litellm_trace_id = litellm_trace_id + self.tools = tools def to_dict(self) -> dict: return { "texts": self.texts, "request_data": self.request_data, "images": self.images, + "tools": self.tools, "additional_provider_specific_params": self.additional_provider_specific_params, "input_type": self.input_type, "litellm_call_id": self.litellm_call_id, @@ -87,6 +91,7 @@ class GenericGuardrailAPIResponse: texts: Optional[List[str]] images: Optional[List[str]] + tools: Optional[List[ChatCompletionToolParam]] action: str blocked_reason: Optional[str] @@ -96,11 +101,13 @@ class GenericGuardrailAPIResponse: texts: Optional[List[str]] = None, blocked_reason: Optional[str] = None, images: Optional[List[str]] = None, + tools: Optional[List[ChatCompletionToolParam]] = None, ): self.action = action self.blocked_reason = blocked_reason self.texts = texts self.images = images + self.tools = tools @classmethod def from_dict(cls, data: dict) -> "GenericGuardrailAPIResponse": diff --git a/tests/enterprise/litellm_enterprise/proxy/guardrails/test_bedrock_apply_guardrail.py b/tests/enterprise/litellm_enterprise/proxy/guardrails/test_bedrock_apply_guardrail.py index 203bd05c57c..8d98f56cd3a 100644 --- a/tests/enterprise/litellm_enterprise/proxy/guardrails/test_bedrock_apply_guardrail.py +++ b/tests/enterprise/litellm_enterprise/proxy/guardrails/test_bedrock_apply_guardrail.py @@ -39,11 +39,12 @@ async def test_bedrock_apply_guardrail_success(): mock_api_request.return_value = mock_response # Test the apply_guardrail method with new signature - result, _ = await guardrail.apply_guardrail( - texts=["This is a test message with some content"], + guardrailed_inputs = await guardrail.apply_guardrail( + inputs={"texts": ["This is a test message with some content"]}, request_data={}, input_type="request", ) + result = guardrailed_inputs.get("texts", []) # Verify the result assert result == ["This is a test message with some content"] @@ -71,7 +72,9 @@ async def test_bedrock_apply_guardrail_blocked(): # Test the apply_guardrail method should raise an exception with pytest.raises(Exception) as exc_info: await guardrail.apply_guardrail( - texts=["This is blocked content"], request_data={}, input_type="request" + inputs={"texts": ["This is blocked content"]}, + request_data={}, + input_type="request", ) assert "Content blocked by Bedrock guardrail" in str(exc_info.value) @@ -100,11 +103,12 @@ async def test_bedrock_apply_guardrail_with_masking(): mock_api_request.return_value = mock_response # Test the apply_guardrail method with new signature - result, _ = await guardrail.apply_guardrail( - texts=["This is a test message with sensitive content"], + guardrailed_inputs = await guardrail.apply_guardrail( + inputs={"texts": ["This is a test message with sensitive content"]}, request_data={}, input_type="request", ) + result = guardrailed_inputs.get("texts", []) # Verify the result contains the masked content assert result == ["This is a test message with [REDACTED] content"] @@ -130,7 +134,9 @@ async def test_bedrock_apply_guardrail_api_failure(): # Test the apply_guardrail method should raise an exception with pytest.raises(Exception) as exc_info: await guardrail.apply_guardrail( - texts=["This is a test message"], request_data={}, input_type="request" + inputs={"texts": ["This is a test message"]}, + request_data={}, + input_type="request", ) # The error message should contain the original exception @@ -215,11 +221,12 @@ async def test_bedrock_apply_guardrail_filters_request_messages_when_flag_enable ) as mock_api: mock_api.return_value = {"action": "ALLOWED"} - result, _ = await guardrail.apply_guardrail( - texts=["latest question"], + guardrailed_inputs = await guardrail.apply_guardrail( + inputs={"texts": ["latest question"]}, request_data=request_data, input_type="request", ) + result = guardrailed_inputs.get("texts", []) assert mock_api.called _, kwargs = mock_api.call_args @@ -250,7 +257,7 @@ async def test_bedrock_apply_guardrail_filters_request_messages_when_flag_enable with pytest.raises(Exception, match="policy") as exc_info: await guardrail.apply_guardrail( - texts=["blocked"], + inputs={"texts": ["blocked"]}, request_data=request_data, input_type="request", ) diff --git a/tests/test_litellm/llms/openai/chat/__init__.py b/tests/test_litellm/llms/openai/chat/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/test_litellm/llms/openai/chat/guardrail_translation/__init__.py b/tests/test_litellm/llms/openai/chat/guardrail_translation/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/test_litellm/llms/openai/chat/guardrail_translation/test_handler.py b/tests/test_litellm/llms/openai/chat/guardrail_translation/test_handler.py new file mode 100644 index 00000000000..09f3f8dcf0f --- /dev/null +++ b/tests/test_litellm/llms/openai/chat/guardrail_translation/test_handler.py @@ -0,0 +1,518 @@ +""" +Unit tests for OpenAI Chat Completions Guardrail Translation Handler + +Tests the handler's ability to process input/output for Chat Completions API +with guardrail transformations, including tool calls. +""" + +import json +import os +import sys +from typing import Any, List, Literal, Optional, Tuple +from unittest.mock import AsyncMock, MagicMock + +import pytest + +sys.path.insert( + 0, os.path.abspath("../../../../../../..") +) # Adds the parent directory to the system path + +from litellm.integrations.custom_guardrail import CustomGuardrail +from litellm.llms.openai.chat.guardrail_translation.handler import ( + OpenAIChatCompletionsHandler, +) +from litellm.types.guardrails import GenericGuardrailAPIInputs +from litellm.types.utils import ( + ChatCompletionMessageToolCall, + Choices, + Function, + Message, + ModelResponse, +) + + +class MockGuardrail(CustomGuardrail): + """Mock guardrail for testing that transforms text and tool calls""" + + def __init__(self, guardrail_name: str = "test"): + super().__init__(guardrail_name=guardrail_name) + self.last_inputs = None + self.last_request_data = None + self.tool_calls_modified = False + + async def apply_guardrail( + self, + inputs: GenericGuardrailAPIInputs, + request_data: dict, + input_type: Literal["request", "response"], + logging_obj: Optional[Any] = None, + ) -> Tuple[List[str], Optional[List[str]]]: + """Mock apply_guardrail that uppercases text and modifies tool calls""" + self.last_inputs = inputs + self.last_request_data = request_data + + # Return modified texts (uppercase for testing) + texts = inputs.get("texts", []) + modified_texts = [text.upper() for text in texts] + + # Modify tool calls in place if present + tool_calls = inputs.get("tool_calls", []) + if tool_calls: + self.tool_calls_modified = True + for tool_call in tool_calls: + if isinstance(tool_call, dict) and "function" in tool_call: + function = tool_call["function"] + if "arguments" in function: + # Modify arguments to uppercase JSON string + try: + args_dict = json.loads(function["arguments"]) + # Uppercase all string values + for key, value in args_dict.items(): + if isinstance(value, str): + args_dict[key] = value.upper() + function["arguments"] = json.dumps(args_dict) + except json.JSONDecodeError: + # If not JSON, just uppercase the string + function["arguments"] = function["arguments"].upper() + + return modified_texts, [] + + +class TestOpenAIChatCompletionsHandlerToolCallsInput: + """Test input processing with tool calls""" + + @pytest.mark.asyncio + async def test_extract_tool_calls_from_input_messages(self): + """Test that tool calls are extracted from input messages""" + handler = OpenAIChatCompletionsHandler() + guardrail = MockGuardrail() + + # Create input data with tool calls (assistant message) + data = { + "messages": [ + {"role": "user", "content": "What's the weather in SF?"}, + { + "role": "assistant", + "content": None, + "tool_calls": [ + { + "id": "call_123", + "type": "function", + "function": { + "name": "get_weather", + "arguments": json.dumps( + {"location": "San Francisco", "unit": "celsius"} + ), + }, + } + ], + }, + ] + } + + # Process the input + await handler.process_input_messages(data, guardrail) + + # Verify tool calls were extracted and passed to guardrail + assert guardrail.last_inputs is not None + assert "tool_calls" in guardrail.last_inputs + assert len(guardrail.last_inputs["tool_calls"]) == 1 + + tool_call = guardrail.last_inputs["tool_calls"][0] + assert tool_call["id"] == "call_123" + assert tool_call["function"]["name"] == "get_weather" + # Note: tool call arguments may already be modified by guardrail + # Check that it contains location parameter + assert "location" in tool_call["function"]["arguments"] + + # Verify tool call was modified by guardrail + assert guardrail.tool_calls_modified is True + + # Verify the message was updated with modified tool call + modified_tool_call = data["messages"][1]["tool_calls"][0] + args = json.loads(modified_tool_call["function"]["arguments"]) + assert args["location"] == "SAN FRANCISCO" # Should be uppercased + assert args["unit"] == "CELSIUS" # Should be uppercased + + @pytest.mark.asyncio + async def test_extract_tool_calls_and_text_from_input_messages(self): + """Test that both tool calls and text content are extracted""" + handler = OpenAIChatCompletionsHandler() + guardrail = MockGuardrail() + + # Create input data with both text and tool calls + data = { + "messages": [ + {"role": "user", "content": "What's the weather?"}, + { + "role": "assistant", + "content": "Let me check that for you.", + "tool_calls": [ + { + "id": "call_456", + "type": "function", + "function": { + "name": "get_current_weather", + "arguments": json.dumps({"location": "Boston"}), + }, + } + ], + }, + ] + } + + # Process the input + await handler.process_input_messages(data, guardrail) + + # Verify both texts and tool calls were extracted + assert guardrail.last_inputs is not None + assert "texts" in guardrail.last_inputs + assert "tool_calls" in guardrail.last_inputs + + # Should have 2 texts (user message + assistant message) + assert len(guardrail.last_inputs["texts"]) == 2 + assert "What's the weather?" in guardrail.last_inputs["texts"] + assert "Let me check that for you." in guardrail.last_inputs["texts"] + + # Should have 1 tool call + assert len(guardrail.last_inputs["tool_calls"]) == 1 + assert ( + guardrail.last_inputs["tool_calls"][0]["function"]["name"] + == "get_current_weather" + ) + + # Verify text content was modified + assert data["messages"][0]["content"] == "WHAT'S THE WEATHER?" + assert data["messages"][1]["content"] == "LET ME CHECK THAT FOR YOU." + + # Verify tool call was modified + modified_tool_call = data["messages"][1]["tool_calls"][0] + args = json.loads(modified_tool_call["function"]["arguments"]) + assert args["location"] == "BOSTON" + + @pytest.mark.asyncio + async def test_extract_multiple_tool_calls_from_input(self): + """Test extraction of multiple tool calls""" + handler = OpenAIChatCompletionsHandler() + guardrail = MockGuardrail() + + # Create input data with multiple tool calls + data = { + "messages": [ + { + "role": "assistant", + "content": None, + "tool_calls": [ + { + "id": "call_1", + "type": "function", + "function": { + "name": "get_weather", + "arguments": json.dumps({"location": "NYC"}), + }, + }, + { + "id": "call_2", + "type": "function", + "function": { + "name": "get_time", + "arguments": json.dumps({"timezone": "EST"}), + }, + }, + ], + } + ] + } + + # Process the input + await handler.process_input_messages(data, guardrail) + + # Verify multiple tool calls were extracted + assert guardrail.last_inputs is not None + assert "tool_calls" in guardrail.last_inputs + assert len(guardrail.last_inputs["tool_calls"]) == 2 + + # Verify both tool calls + tool_calls = guardrail.last_inputs["tool_calls"] + assert tool_calls[0]["function"]["name"] == "get_weather" + assert tool_calls[1]["function"]["name"] == "get_time" + + # Verify both were modified + modified_tool_calls = data["messages"][0]["tool_calls"] + args1 = json.loads(modified_tool_calls[0]["function"]["arguments"]) + args2 = json.loads(modified_tool_calls[1]["function"]["arguments"]) + assert args1["location"] == "NYC" + assert args2["timezone"] == "EST" + + @pytest.mark.asyncio + async def test_tool_calls_separate_from_texts(self): + """Test that tool calls are passed as a separate parameter, not mixed with texts""" + handler = OpenAIChatCompletionsHandler() + guardrail = MockGuardrail() + + data = { + "messages": [ + {"role": "user", "content": "Get weather for LA"}, + { + "role": "assistant", + "content": "Sure!", + "tool_calls": [ + { + "id": "call_xyz", + "type": "function", + "function": { + "name": "get_weather", + "arguments": json.dumps({"city": "Los Angeles"}), + }, + } + ], + }, + ] + } + + # Process the input + await handler.process_input_messages(data, guardrail) + + # Verify tool calls and texts are separate + assert guardrail.last_inputs is not None + texts = guardrail.last_inputs.get("texts", []) + tool_calls = guardrail.last_inputs.get("tool_calls", []) + + # Texts should only contain the content strings + assert len(texts) == 2 + assert "Get weather for LA" in texts + assert "Sure!" in texts + + # Tool call arguments should NOT be in texts + assert not any("Los Angeles" in text for text in texts) + + # Tool calls should be separate + assert len(tool_calls) == 1 + assert tool_calls[0]["function"]["name"] == "get_weather" + # Check that it contains city parameter (may be modified by guardrail) + assert "city" in tool_calls[0]["function"]["arguments"] + + @pytest.mark.asyncio + async def test_no_tool_calls_in_input(self): + """Test that messages without tool calls work correctly""" + handler = OpenAIChatCompletionsHandler() + guardrail = MockGuardrail() + + # Create input data without tool calls + data = { + "messages": [ + {"role": "user", "content": "Hello"}, + {"role": "assistant", "content": "Hi there!"}, + ] + } + + # Process the input + await handler.process_input_messages(data, guardrail) + + # Verify no tool calls were passed to guardrail + assert guardrail.last_inputs is not None + tool_calls = guardrail.last_inputs.get("tool_calls", []) + assert len(tool_calls) == 0 + + # Verify text was still processed + assert len(guardrail.last_inputs["texts"]) == 2 + assert data["messages"][0]["content"] == "HELLO" + assert data["messages"][1]["content"] == "HI THERE!" + + @pytest.mark.asyncio + async def test_empty_tool_calls_list(self): + """Test that empty tool_calls list is handled correctly""" + handler = OpenAIChatCompletionsHandler() + guardrail = MockGuardrail() + + data = { + "messages": [ + {"role": "assistant", "content": "Hello", "tool_calls": []}, + ] + } + + # Process the input + await handler.process_input_messages(data, guardrail) + + # Verify empty tool_calls doesn't cause issues + assert guardrail.last_inputs is not None + tool_calls = guardrail.last_inputs.get("tool_calls", []) + assert len(tool_calls) == 0 + + +class TestOpenAIChatCompletionsHandlerToolCallsOutput: + """Test output processing with tool calls""" + + @pytest.mark.asyncio + async def test_extract_tool_calls_from_output_response(self): + """Test that tool calls are extracted from output responses""" + handler = OpenAIChatCompletionsHandler() + guardrail = MockGuardrail() + + # Create a mock response with tool calls + response = ModelResponse( + id="chatcmpl-123", + created=1234567890, + model="gpt-4", + object="chat.completion", + choices=[ + Choices( + finish_reason="tool_calls", + index=0, + message=Message( + content=None, + role="assistant", + tool_calls=[ + ChatCompletionMessageToolCall( + id="call_789", + type="function", + function=Function( + name="search_database", + arguments=json.dumps({"query": "python tutorials"}), + ), + ) + ], + ), + ) + ], + ) + + # Process the output + await handler.process_output_response(response, guardrail) + + # Verify tool calls were extracted and passed to guardrail + assert guardrail.last_inputs is not None + assert "tool_calls" in guardrail.last_inputs + assert len(guardrail.last_inputs["tool_calls"]) == 1 + + tool_call = guardrail.last_inputs["tool_calls"][0] + assert tool_call["function"]["name"] == "search_database" + # Check that it contains query parameter (may be modified by guardrail) + assert "query" in tool_call["function"]["arguments"] + + # Verify tool call was modified in response + response_tool_call = response.choices[0].message.tool_calls[0] + args = json.loads(response_tool_call.function.arguments) + assert args["query"] == "PYTHON TUTORIALS" # Should be uppercased + + @pytest.mark.asyncio + async def test_extract_tool_calls_and_content_from_output(self): + """Test extraction of both content and tool calls from output""" + handler = OpenAIChatCompletionsHandler() + guardrail = MockGuardrail() + + response = ModelResponse( + id="chatcmpl-456", + created=1234567890, + model="gpt-4", + object="chat.completion", + choices=[ + Choices( + finish_reason="tool_calls", + index=0, + message=Message( + content="I'll search for that information.", + role="assistant", + tool_calls=[ + ChatCompletionMessageToolCall( + id="call_999", + type="function", + function=Function( + name="web_search", + arguments=json.dumps( + {"keywords": "litellm documentation"} + ), + ), + ) + ], + ), + ) + ], + ) + + # Process the output + await handler.process_output_response(response, guardrail) + + # Verify both texts and tool calls were extracted + assert guardrail.last_inputs is not None + assert "texts" in guardrail.last_inputs + assert "tool_calls" in guardrail.last_inputs + + assert len(guardrail.last_inputs["texts"]) == 1 + assert "I'll search for that information." in guardrail.last_inputs["texts"] + + assert len(guardrail.last_inputs["tool_calls"]) == 1 + + # Verify both were modified + assert ( + response.choices[0].message.content == "I'LL SEARCH FOR THAT INFORMATION." + ) + response_tool_call = response.choices[0].message.tool_calls[0] + args = json.loads(response_tool_call.function.arguments) + assert args["keywords"] == "LITELLM DOCUMENTATION" + + @pytest.mark.asyncio + async def test_extract_multiple_tool_calls_from_output(self): + """Test extraction of multiple tool calls from output""" + handler = OpenAIChatCompletionsHandler() + guardrail = MockGuardrail() + + response = ModelResponse( + id="chatcmpl-789", + created=1234567890, + model="gpt-4", + object="chat.completion", + choices=[ + Choices( + finish_reason="tool_calls", + index=0, + message=Message( + content=None, + role="assistant", + tool_calls=[ + ChatCompletionMessageToolCall( + id="call_1", + type="function", + function=Function( + name="get_weather", + arguments=json.dumps({"location": "Tokyo"}), + ), + ), + ChatCompletionMessageToolCall( + id="call_2", + type="function", + function=Function( + name="get_news", + arguments=json.dumps({"topic": "technology"}), + ), + ), + ], + ), + ) + ], + ) + + # Process the output + await handler.process_output_response(response, guardrail) + + # Verify multiple tool calls were extracted + assert guardrail.last_inputs is not None + assert "tool_calls" in guardrail.last_inputs + assert len(guardrail.last_inputs["tool_calls"]) == 2 + + # Verify both tool calls + tool_calls = guardrail.last_inputs["tool_calls"] + assert tool_calls[0]["function"]["name"] == "get_weather" + assert tool_calls[1]["function"]["name"] == "get_news" + + # Verify both were modified + response_tool_calls = response.choices[0].message.tool_calls + args1 = json.loads(response_tool_calls[0].function.arguments) + args2 = json.loads(response_tool_calls[1].function.arguments) + assert args1["location"] == "TOKYO" + assert args2["topic"] == "TECHNOLOGY" + + +if __name__ == "__main__": + # Run the tests + pytest.main([__file__, "-v"]) diff --git a/tests/test_litellm/proxy/guardrails/guardrail_hooks/content_filter/test_content_filter.py b/tests/test_litellm/proxy/guardrails/guardrail_hooks/content_filter/test_content_filter.py index 2b192dad5d5..b5c093fdf06 100644 --- a/tests/test_litellm/proxy/guardrails/guardrail_hooks/content_filter/test_content_filter.py +++ b/tests/test_litellm/proxy/guardrails/guardrail_hooks/content_filter/test_content_filter.py @@ -191,7 +191,9 @@ class TestContentFilterGuardrail: with pytest.raises(HTTPException) as exc_info: await guardrail.apply_guardrail( - texts=["My SSN is 123-45-6789"], request_data={}, input_type="request" + inputs={"texts": ["My SSN is 123-45-6789"]}, + request_data={}, + input_type="request", ) assert exc_info.value.status_code == 400 @@ -215,11 +217,12 @@ class TestContentFilterGuardrail: patterns=patterns, ) - result, _ = await guardrail.apply_guardrail( - texts=["Contact me at test@example.com"], + guardrailed_inputs = await guardrail.apply_guardrail( + inputs={"texts": ["Contact me at test@example.com"]}, request_data={}, input_type="request", ) + result = guardrailed_inputs.get("texts", []) assert result is not None assert len(result) == 1 @@ -243,11 +246,12 @@ class TestContentFilterGuardrail: blocked_words=blocked_words, ) - result, _ = await guardrail.apply_guardrail( - texts=["This is PROPRIETARY information"], + guardrailed_inputs = await guardrail.apply_guardrail( + inputs={"texts": ["This is PROPRIETARY information"]}, request_data={}, input_type="request", ) + result = guardrailed_inputs.get("texts", []) assert result is not None assert len(result) == 1 @@ -277,11 +281,12 @@ class TestContentFilterGuardrail: patterns=patterns, ) - result, _ = await guardrail.apply_guardrail( - texts=["Contact user@test.com or SSN: 123-45-6789"], + guardrailed_inputs = await guardrail.apply_guardrail( + inputs={"texts": ["Contact user@test.com or SSN: 123-45-6789"]}, request_data={}, input_type="request", ) + result = guardrailed_inputs.get("texts", []) assert result is not None assert len(result) == 1 diff --git a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_generic_guardrail_api.py b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_generic_guardrail_api.py index e6ebada4d2e..f3578fbcefa 100644 --- a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_generic_guardrail_api.py +++ b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_generic_guardrail_api.py @@ -4,6 +4,7 @@ Tests for Generic Guardrail API integration This test file tests the Generic Guardrail API implementation, specifically focusing on metadata extraction and passing. """ + import os from unittest.mock import AsyncMock, MagicMock, patch @@ -108,9 +109,14 @@ class TestGenericGuardrailAPIConfiguration: headers={"Authorization": "Bearer test-key"}, additional_provider_specific_params={"custom_param": "value"}, ) - assert guardrail.api_base == "https://api.test.guardrail.com/beta/litellm_basic_guardrail_api" + assert ( + guardrail.api_base + == "https://api.test.guardrail.com/beta/litellm_basic_guardrail_api" + ) assert guardrail.headers == {"Authorization": "Bearer test-key"} - assert guardrail.additional_provider_specific_params == {"custom_param": "value"} + assert guardrail.additional_provider_specific_params == { + "custom_param": "value" + } def test_init_with_env_vars(self): """Test initialization with environment variables""" @@ -121,7 +127,10 @@ class TestGenericGuardrailAPIConfiguration: }, ): guardrail = GenericGuardrailAPI() - assert guardrail.api_base == "https://env.api.guardrail.com/beta/litellm_basic_guardrail_api" + assert ( + guardrail.api_base + == "https://env.api.guardrail.com/beta/litellm_basic_guardrail_api" + ) def test_init_without_api_base_raises_error(self): """Test that initialization without API base raises ValueError""" @@ -134,14 +143,20 @@ class TestGenericGuardrailAPIConfiguration: guardrail = GenericGuardrailAPI( api_base="https://api.test.guardrail.com/v1", ) - assert guardrail.api_base == "https://api.test.guardrail.com/v1/beta/litellm_basic_guardrail_api" + assert ( + guardrail.api_base + == "https://api.test.guardrail.com/v1/beta/litellm_basic_guardrail_api" + ) def test_api_base_not_duplicated(self): """Test that endpoint path is not duplicated if already present""" guardrail = GenericGuardrailAPI( api_base="https://api.test.guardrail.com/beta/litellm_basic_guardrail_api", ) - assert guardrail.api_base == "https://api.test.guardrail.com/beta/litellm_basic_guardrail_api" + assert ( + guardrail.api_base + == "https://api.test.guardrail.com/beta/litellm_basic_guardrail_api" + ) class TestMetadataExtraction: @@ -164,7 +179,7 @@ class TestMetadataExtraction: generic_guardrail.async_handler, "post", return_value=mock_response ) as mock_post: await generic_guardrail.apply_guardrail( - texts=["Who is Ishaan?"], + inputs=GenericGuardrailAPIInputs(texts=["Who is Ishaan?"]), request_data=mock_request_data_input, input_type="request", ) @@ -180,7 +195,10 @@ class TestMetadataExtraction: request_metadata = json_payload["request_data"] # Verify metadata was extracted from request_data["metadata"] - assert request_metadata["user_api_key_hash"] == "88dc28d0f030c55ed4ab77ed8faf098196cb1c05df778539800c9f1243fe6b4b" + assert ( + request_metadata["user_api_key_hash"] + == "88dc28d0f030c55ed4ab77ed8faf098196cb1c05df778539800c9f1243fe6b4b" + ) assert request_metadata["user_api_key_user_id"] == "default_user_id" assert request_metadata["user_api_key_user_email"] == "test@example.com" assert request_metadata["user_api_key_team_id"] == "test-team" @@ -192,7 +210,7 @@ class TestMetadataExtraction: """Test extracting metadata from output response (litellm_metadata field)""" # Create request_data as it would be created by the handler user_dict = mock_user_api_key_dict.model_dump() - + # Transform to prefixed keys (as done by BaseTranslation) litellm_metadata = {} for key, value in user_dict.items(): @@ -219,7 +237,7 @@ class TestMetadataExtraction: generic_guardrail.async_handler, "post", return_value=mock_api_response ) as mock_post: await generic_guardrail.apply_guardrail( - texts=["hey i'm ishaan!"], + inputs={"texts": ["hey i'm ishaan!"]}, request_data=request_data, input_type="response", ) @@ -263,7 +281,7 @@ class TestMetadataExtraction: generic_guardrail.async_handler, "post", return_value=mock_response ) as mock_post: await generic_guardrail.apply_guardrail( - texts=["test"], + inputs={"texts": ["test"]}, request_data=request_data, input_type="request", ) @@ -278,9 +296,7 @@ class TestMetadataExtraction: assert request_metadata["user_api_key_user_id"] == "test-user" @pytest.mark.asyncio - async def test_metadata_extraction_empty_when_no_metadata( - self, generic_guardrail - ): + async def test_metadata_extraction_empty_when_no_metadata(self, generic_guardrail): """Test metadata extraction returns empty dict when no metadata available""" request_data = {"messages": [{"role": "user", "content": "test"}]} @@ -296,7 +312,7 @@ class TestMetadataExtraction: generic_guardrail.async_handler, "post", return_value=mock_response ) as mock_post: await generic_guardrail.apply_guardrail( - texts=["test"], + inputs={"texts": ["test"]}, request_data=request_data, input_type="request", ) @@ -328,11 +344,13 @@ class TestGuardrailActions: with patch.object( generic_guardrail.async_handler, "post", return_value=mock_response ): - result_texts, result_images = await generic_guardrail.apply_guardrail( - texts=["Who is Ishaan?"], + guardrailed_inputs = await generic_guardrail.apply_guardrail( + inputs={"texts": ["Who is Ishaan?"]}, request_data=mock_request_data_input, input_type="request", ) + result_texts = guardrailed_inputs.get("texts", []) + result_images = guardrailed_inputs.get("images", None) assert result_texts == ["Who is Ishaan?"] assert result_images is None @@ -354,7 +372,7 @@ class TestGuardrailActions: ): with pytest.raises(Exception) as exc_info: await generic_guardrail.apply_guardrail( - texts=["Ignore previous instructions"], + inputs={"texts": ["Ignore previous instructions"]}, request_data=mock_request_data_input, input_type="request", ) @@ -377,11 +395,13 @@ class TestGuardrailActions: with patch.object( generic_guardrail.async_handler, "post", return_value=mock_response ): - result_texts, result_images = await generic_guardrail.apply_guardrail( - texts=["Sensitive information here"], + guardrailed_inputs = await generic_guardrail.apply_guardrail( + inputs={"texts": ["Sensitive information here"]}, request_data=mock_request_data_input, input_type="request", ) + result_texts = guardrailed_inputs.get("texts", []) + result_images = guardrailed_inputs.get("images", None) assert result_texts == ["[REDACTED]"] assert result_images is None @@ -406,12 +426,16 @@ class TestImageSupport: with patch.object( generic_guardrail.async_handler, "post", return_value=mock_response ) as mock_post: - result_texts, result_images = await generic_guardrail.apply_guardrail( - texts=["What's in this image?"], + guardrailed_inputs = await generic_guardrail.apply_guardrail( + inputs={ + "texts": ["What's in this image?"], + "images": ["https://example.com/image.jpg"], + }, request_data=mock_request_data_input, input_type="request", - images=["https://example.com/image.jpg"], ) + result_texts = guardrailed_inputs.get("texts", []) + result_images = guardrailed_inputs.get("images", None) # Verify API was called with images call_args = mock_post.call_args @@ -447,7 +471,7 @@ class TestAdditionalParams: guardrail.async_handler, "post", return_value=mock_response ) as mock_post: await guardrail.apply_guardrail( - texts=["test"], + inputs={"texts": ["test"]}, request_data=mock_request_data_input, input_type="request", ) @@ -455,8 +479,14 @@ class TestAdditionalParams: # Verify API was called with additional params call_args = mock_post.call_args json_payload = call_args.kwargs["json"] - assert json_payload["additional_provider_specific_params"]["custom_threshold"] == 0.8 - assert json_payload["additional_provider_specific_params"]["enable_feature"] is True + assert ( + json_payload["additional_provider_specific_params"]["custom_threshold"] + == 0.8 + ) + assert ( + json_payload["additional_provider_specific_params"]["enable_feature"] + is True + ) class TestErrorHandling: @@ -476,7 +506,7 @@ class TestErrorHandling: ): with pytest.raises(Exception) as exc_info: await generic_guardrail.apply_guardrail( - texts=["test"], + inputs={"texts": ["test"]}, request_data=mock_request_data_input, input_type="request", ) @@ -495,10 +525,9 @@ class TestErrorHandling: ): with pytest.raises(Exception) as exc_info: await generic_guardrail.apply_guardrail( - texts=["test"], + inputs={"texts": ["test"]}, request_data=mock_request_data_input, input_type="request", ) assert "Generic Guardrail API failed" in str(exc_info.value) - diff --git a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_presidio.py b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_presidio.py index 3e19437fe83..a6b2ae5b3a0 100644 --- a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_presidio.py +++ b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_presidio.py @@ -571,7 +571,7 @@ async def test_presidio_sets_guardrail_information_in_request_data(): with patch.object(presidio, "check_pii", mock_check_pii): await presidio.apply_guardrail( - texts=["Test message"], + inputs={"texts": ["Test message"]}, request_data=request_data, input_type="request", ) @@ -623,7 +623,7 @@ async def test_request_data_flows_to_apply_guardrail(): with patch.object(presidio, "check_pii", mock_check_pii): result = await presidio.apply_guardrail( - texts=["Test message"], + inputs={"texts": ["Test message"]}, request_data=request_data, input_type="request", ) From 636f26d8a1641bf0871a5a6fffdf2fa06828f165 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Wed, 3 Dec 2025 21:21:27 -0800 Subject: [PATCH 086/107] e2e changes --- tests/proxy_admin_ui_tests/e2e_ui_tests/login_to_ui.spec.ts | 1 + .../e2e_ui_tests/require_auth_for_dashboard.spec.ts | 2 +- tests/proxy_admin_ui_tests/e2e_ui_tests/search_users.spec.ts | 4 ++++ .../e2e_ui_tests/view_internal_user.spec.ts | 3 +++ tests/proxy_admin_ui_tests/utils/login.ts | 2 ++ 5 files changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/proxy_admin_ui_tests/e2e_ui_tests/login_to_ui.spec.ts b/tests/proxy_admin_ui_tests/e2e_ui_tests/login_to_ui.spec.ts index b9c16299cc0..c4a1dc77c33 100644 --- a/tests/proxy_admin_ui_tests/e2e_ui_tests/login_to_ui.spec.ts +++ b/tests/proxy_admin_ui_tests/e2e_ui_tests/login_to_ui.spec.ts @@ -11,6 +11,7 @@ import { test, expect } from "@playwright/test"; test("admin login test", async ({ page }) => { // Go to the specified URL await page.goto("http://localhost:4000/ui"); + await page.waitForLoadState("networkidle"); await page.screenshot({ path: "test-results/login_before.png" }); diff --git a/tests/proxy_admin_ui_tests/e2e_ui_tests/require_auth_for_dashboard.spec.ts b/tests/proxy_admin_ui_tests/e2e_ui_tests/require_auth_for_dashboard.spec.ts index 2b198d6ab7e..4e4bd2fcd93 100644 --- a/tests/proxy_admin_ui_tests/e2e_ui_tests/require_auth_for_dashboard.spec.ts +++ b/tests/proxy_admin_ui_tests/e2e_ui_tests/require_auth_for_dashboard.spec.ts @@ -10,7 +10,7 @@ test.describe("Authentication Checks", () => { page.on("console", (msg) => console.log("PAGE LOG:", msg.text())); const protectedPageUrl = "http://localhost:4000/ui?page=llm-playground"; - const expectedRedirectUrl = "http://localhost:4000/ui/login"; + const expectedRedirectUrl = "http://localhost:4000/ui/login/"; console.log( `Attempting to navigate to protected page: ${protectedPageUrl}` diff --git a/tests/proxy_admin_ui_tests/e2e_ui_tests/search_users.spec.ts b/tests/proxy_admin_ui_tests/e2e_ui_tests/search_users.spec.ts index a384248a5f7..66d0ad29658 100644 --- a/tests/proxy_admin_ui_tests/e2e_ui_tests/search_users.spec.ts +++ b/tests/proxy_admin_ui_tests/e2e_ui_tests/search_users.spec.ts @@ -21,8 +21,11 @@ test("user search test", async ({ page }) => { // Login first await page.goto("http://localhost:4000/ui"); + await page.waitForLoadState("networkidle"); console.log("Navigated to login page"); + page.screenshot({ path: "test-results/search_users_before_login.png" }); + // Wait for login form to be visible await page.waitForSelector('input[name="username"]', { timeout: 10000 }); console.log("Login form is visible"); @@ -128,6 +131,7 @@ test("user filter test", async ({ page }) => { // Login first await page.goto("http://localhost:4000/ui"); + await page.waitForLoadState("networkidle"); console.log("Navigated to login page"); // Wait for login form to be visible diff --git a/tests/proxy_admin_ui_tests/e2e_ui_tests/view_internal_user.spec.ts b/tests/proxy_admin_ui_tests/e2e_ui_tests/view_internal_user.spec.ts index dd6f812df4a..a7843d52916 100644 --- a/tests/proxy_admin_ui_tests/e2e_ui_tests/view_internal_user.spec.ts +++ b/tests/proxy_admin_ui_tests/e2e_ui_tests/view_internal_user.spec.ts @@ -7,6 +7,9 @@ import { test, expect } from "@playwright/test"; test("view internal user page", async ({ page }) => { // Go to the specified URL await page.goto("http://localhost:4000/ui"); + await page.waitForLoadState("networkidle"); + + page.screenshot({ path: "test-results/view_internal_user_before_login.png" }); // Enter "admin" in the username input field await page.fill('input[name="username"]', "admin"); diff --git a/tests/proxy_admin_ui_tests/utils/login.ts b/tests/proxy_admin_ui_tests/utils/login.ts index 716efe58c76..5749006cdea 100644 --- a/tests/proxy_admin_ui_tests/utils/login.ts +++ b/tests/proxy_admin_ui_tests/utils/login.ts @@ -3,8 +3,10 @@ import { Page, expect } from "@playwright/test"; export async function loginToUI(page: Page) { // Login first await page.goto("http://localhost:4000/ui"); + await page.waitForLoadState("networkidle"); console.log("Navigated to login page"); + page.screenshot({ path: "test-results/login_utils_before.png" }); // Wait for login form to be visible await page.waitForSelector('input[name="username"]', { timeout: 10000 }); console.log("Login form is visible"); From adfbb1c30891ddaa8b09db92471d89c73303c0ea Mon Sep 17 00:00:00 2001 From: codgician <15964984+codgician@users.noreply.github.com> Date: Thu, 4 Dec 2025 13:22:08 +0800 Subject: [PATCH 087/107] docs: document responses and embedding api for github copilot (#17456) --- README.md | 2 +- .../docs/providers/github_copilot.md | 42 ++++++++++++++++++- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 20483ce70c0..4f1a86a648f 100644 --- a/README.md +++ b/README.md @@ -348,7 +348,7 @@ curl 'http://0.0.0.0:4000/key/generate' \ | [Fireworks AI (`fireworks_ai`)](https://docs.litellm.ai/docs/providers/fireworks_ai) | ✅ | ✅ | ✅ | | | | | | | | | [FriendliAI (`friendliai`)](https://docs.litellm.ai/docs/providers/friendliai) | ✅ | ✅ | ✅ | | | | | | | | | [Galadriel (`galadriel`)](https://docs.litellm.ai/docs/providers/galadriel) | ✅ | ✅ | ✅ | | | | | | | | -| [GitHub Copilot (`github_copilot`)](https://docs.litellm.ai/docs/providers/github_copilot) | ✅ | ✅ | ✅ | | | | | | | | +| [GitHub Copilot (`github_copilot`)](https://docs.litellm.ai/docs/providers/github_copilot) | ✅ | ✅ | ✅ | ✅ | | | | | | | | [GitHub Models (`github`)](https://docs.litellm.ai/docs/providers/github) | ✅ | ✅ | ✅ | | | | | | | | | [Google - PaLM](https://docs.litellm.ai/docs/providers/palm) | ✅ | ✅ | ✅ | | | | | | | | | [Google - Vertex AI (`vertex_ai`)](https://docs.litellm.ai/docs/providers/vertex) | ✅ | ✅ | ✅ | ✅ | ✅ | | | | | | diff --git a/docs/my-website/docs/providers/github_copilot.md b/docs/my-website/docs/providers/github_copilot.md index 2ebe6eacb1c..306c9f949ec 100644 --- a/docs/my-website/docs/providers/github_copilot.md +++ b/docs/my-website/docs/providers/github_copilot.md @@ -15,7 +15,7 @@ https://docs.github.com/en/copilot |-------|-------| | Description | GitHub Copilot Chat API provides access to GitHub's AI-powered coding assistant. | | Provider Route on LiteLLM | `github_copilot/` | -| Supported Endpoints | `/chat/completions` | +| Supported Endpoints | `/chat/completions`, `/embeddings` | | API Reference | [GitHub Copilot docs](https://docs.github.com/en/copilot) | ## Authentication @@ -62,6 +62,34 @@ for chunk in stream: print(chunk.choices[0].delta.content, end="") ``` +### Responses + +For GPT Codex models, only responses API is supported. + +```python showLineNumbers title="GitHub Copilot Responses" +import litellm + +response = await litellm.aresponses( + model="github_copilot/gpt-5.1-codex", + input="Write a Python hello world", + max_output_tokens=500 +) + +print(response) +``` + +### Embedding + +```python showLineNumbers title="GitHub Copilot Embedding" +import litellm + +response = litellm.embedding( + model="github_copilot/text-embedding-3-small", + input=["good morning from litellm"] +) +print(response) +``` + ## Usage - LiteLLM Proxy Add the following to your LiteLLM Proxy configuration file: @@ -71,6 +99,16 @@ model_list: - model_name: github_copilot/gpt-4 litellm_params: model: github_copilot/gpt-4 + - model_name: github_copilot/gpt-5.1-codex + model_info: + mode: responses + litellm_params: + model: github_copilot/gpt-5.1-codex + - model_name: github_copilot/text-embedding-ada-002 + model_info: + mode: embedding + litellm_params: + model: github_copilot/text-embedding-ada-002 ``` Start your LiteLLM Proxy server: @@ -180,7 +218,7 @@ extra_headers = { "editor-version": "vscode/1.85.1", # Editor version "editor-plugin-version": "copilot/1.155.0", # Plugin version "Copilot-Integration-Id": "vscode-chat", # Integration ID - "user-agent": "GithubCopilot/1.155.0" # User agent + "user-agent": "GithubCopilot/1.155.0" # User agent } ``` From a6ce1189899912581714bd80cbf7785b20d1e093 Mon Sep 17 00:00:00 2001 From: Devaj Mody Date: Thu, 4 Dec 2025 00:23:09 -0500 Subject: [PATCH 088/107] fix(openai): add clear error message for empty LLM endpoint responses (#17445) Fixes #17209 - Add validation after response.parse() to detect empty/invalid responses - Raise clear OpenAIError with actionable message - Add 3 unit tests --- litellm/llms/openai/openai.py | 12 +++ .../llms/openai/test_openai_empty_response.py | 97 +++++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 tests/test_litellm/llms/openai/test_openai_empty_response.py diff --git a/litellm/llms/openai/openai.py b/litellm/llms/openai/openai.py index 3282b7665c0..20842525e59 100644 --- a/litellm/llms/openai/openai.py +++ b/litellm/llms/openai/openai.py @@ -444,6 +444,11 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM): else: headers = {} response = raw_response.parse() + if not hasattr(response, "model_dump"): + raise OpenAIError( + status_code=500, + message=f"Empty or invalid response from LLM endpoint. Received: {response!r}. Check the reverse proxy or model server configuration.", + ) return headers, response except openai.APITimeoutError as e: end_time = time.time() @@ -477,7 +482,14 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM): else: headers = {} response = raw_response.parse() + if not hasattr(response, "model_dump"): + raise OpenAIError( + status_code=500, + message=f"Empty or invalid response from LLM endpoint. Received: {response!r}. Check the reverse proxy or model server configuration.", + ) return headers, response + except OpenAIError: + raise except Exception as e: if raw_response is not None: raise Exception( diff --git a/tests/test_litellm/llms/openai/test_openai_empty_response.py b/tests/test_litellm/llms/openai/test_openai_empty_response.py new file mode 100644 index 00000000000..fb42918f381 --- /dev/null +++ b/tests/test_litellm/llms/openai/test_openai_empty_response.py @@ -0,0 +1,97 @@ +""" +Test for issue #17209: Clearer error when LLM endpoint returns empty response +""" + +import os +import sys +from unittest.mock import MagicMock, patch + +import pytest + +sys.path.insert(0, os.path.abspath("../../../..")) + +from litellm.llms.openai.openai import OpenAIChatCompletion +from litellm.llms.openai.common_utils import OpenAIError + +class TestEmptyResponseHandling: + """Test that empty/invalid responses from LLM endpoints produce clear error messages""" + + def test_sync_empty_string_response_raises_clear_error(self): + """ + Test that when an OpenAI-compatible endpoint returns an empty string, + we get a clear error instead of "'str' object has no attribute 'model_dump'" + """ + openai_chat = OpenAIChatCompletion() + + # Mock the raw response to return an empty string from parse() + mock_raw_response = MagicMock() + mock_raw_response.headers = {} + mock_raw_response.parse.return_value = "" # Empty string response + + mock_client = MagicMock() + mock_client.chat.completions.with_raw_response.create.return_value = ( + mock_raw_response + ) + + with pytest.raises(OpenAIError) as exc_info: + openai_chat.make_sync_openai_chat_completion_request( + openai_client=mock_client, + data={"messages": [{"role": "user", "content": "test"}]}, + timeout=30, + logging_obj=MagicMock(), + ) + + assert "Empty or invalid response from LLM endpoint" in str(exc_info.value) + assert "Check the reverse proxy or model server configuration" in str( + exc_info.value + ) + + def test_sync_none_response_raises_clear_error(self): + """Test that None response also produces a clear error""" + openai_chat = OpenAIChatCompletion() + + mock_raw_response = MagicMock() + mock_raw_response.headers = {} + mock_raw_response.parse.return_value = None + + mock_client = MagicMock() + mock_client.chat.completions.with_raw_response.create.return_value = ( + mock_raw_response + ) + + with pytest.raises(OpenAIError) as exc_info: + openai_chat.make_sync_openai_chat_completion_request( + openai_client=mock_client, + data={"messages": [{"role": "user", "content": "test"}]}, + timeout=30, + logging_obj=MagicMock(), + ) + + assert "Empty or invalid response from LLM endpoint" in str(exc_info.value) + + def test_valid_response_passes_through(self): + """Test that a valid response with model_dump passes through correctly""" + openai_chat = OpenAIChatCompletion() + + # Create a mock response that has model_dump (like a real Pydantic model) + mock_response = MagicMock() + mock_response.model_dump.return_value = {"choices": []} + + mock_raw_response = MagicMock() + mock_raw_response.headers = {"x-request-id": "123"} + mock_raw_response.parse.return_value = mock_response + + mock_client = MagicMock() + mock_client.chat.completions.with_raw_response.create.return_value = ( + mock_raw_response + ) + + headers, response = openai_chat.make_sync_openai_chat_completion_request( + openai_client=mock_client, + data={"messages": [{"role": "user", "content": "test"}]}, + timeout=30, + logging_obj=MagicMock(), + ) + + assert response == mock_response + assert headers == {"x-request-id": "123"} From 06d42fbd3086fea30b623227c81a465c5942f422 Mon Sep 17 00:00:00 2001 From: Haiyi Date: Thu, 4 Dec 2025 16:25:06 +1100 Subject: [PATCH 089/107] Fix: Skip empty text blocks in Anthropic system messages (#17442) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When translating system messages for the Anthropic API, empty text content blocks cause the error "messages: text content blocks must be non-empty". This fix skips empty string content and empty text blocks in list content to prevent this error. Fixes issue with Vertex AI Anthropic API calls. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude --- litellm/llms/anthropic/chat/transformation.py | 9 +- .../test_anthropic_chat_transformation.py | 87 ++++++++++++++++++- 2 files changed, 94 insertions(+), 2 deletions(-) diff --git a/litellm/llms/anthropic/chat/transformation.py b/litellm/llms/anthropic/chat/transformation.py index 4221dfacf34..bdc986ae27f 100644 --- a/litellm/llms/anthropic/chat/transformation.py +++ b/litellm/llms/anthropic/chat/transformation.py @@ -786,6 +786,9 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): valid_content: bool = False system_message_block = ChatCompletionSystemMessage(**message) if isinstance(system_message_block["content"], str): + # Skip empty text blocks - Anthropic API raises errors for empty text + if not system_message_block["content"]: + continue anthropic_system_message_content = AnthropicSystemMessageContent( type="text", text=system_message_block["content"], @@ -800,10 +803,14 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): valid_content = True elif isinstance(message["content"], list): for _content in message["content"]: + # Skip empty text blocks - Anthropic API raises errors for empty text + text_value = _content.get("text") + if _content.get("type") == "text" and not text_value: + continue anthropic_system_message_content = ( AnthropicSystemMessageContent( type=_content.get("type"), - text=_content.get("text"), + text=text_value, ) ) if "cache_control" in _content: diff --git a/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_transformation.py b/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_transformation.py index 09c16add77a..6e05ca564fc 100644 --- a/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_transformation.py +++ b/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_transformation.py @@ -1290,10 +1290,95 @@ def test_effort_with_other_features(): litellm_params={}, headers={} ) - + # Verify all features are present assert "output_config" in result assert result["output_config"]["effort"] == "low" assert "tools" in result assert len(result["tools"]) > 0 assert "thinking" in result + + +def test_translate_system_message_skips_empty_string_content(): + """ + Test that translate_system_message skips system messages with empty string content. + + Fixes: Vertex AI Anthropic API error "messages: text content blocks must be non-empty" + """ + config = AnthropicConfig() + + # Test empty string content - should not produce any anthropic system message content + messages = [ + {"role": "system", "content": ""}, + {"role": "user", "content": "Hello"}, + ] + + result = config.translate_system_message(messages) + + # Empty system message should produce no anthropic content blocks + assert len(result) == 0 + + +def test_translate_system_message_skips_empty_list_content(): + """ + Test that translate_system_message skips empty text blocks in list content. + + Fixes: Vertex AI Anthropic API error "messages: text content blocks must be non-empty" + """ + config = AnthropicConfig() + + # Test list content with empty text block + messages = [ + {"role": "system", "content": [ + {"type": "text", "text": ""}, + {"type": "text", "text": "Valid content"}, + {"type": "text", "text": ""}, + ]}, + {"role": "user", "content": "Hello"}, + ] + + result = config.translate_system_message(messages) + + # Only non-empty text blocks should be included + assert len(result) == 1 + assert result[0]["text"] == "Valid content" + + +def test_translate_system_message_preserves_valid_content(): + """ + Test that translate_system_message preserves valid system message content. + """ + config = AnthropicConfig() + + # Test valid string content + messages = [ + {"role": "system", "content": "You are a helpful assistant."}, + {"role": "user", "content": "Hello"}, + ] + + result = config.translate_system_message(messages) + + assert len(result) == 1 + assert result[0]["type"] == "text" + assert result[0]["text"] == "You are a helpful assistant." + + +def test_translate_system_message_preserves_cache_control(): + """ + Test that translate_system_message preserves cache_control on valid content. + """ + config = AnthropicConfig() + + # Test list content with cache_control + messages = [ + {"role": "system", "content": [ + {"type": "text", "text": "Cached content", "cache_control": {"type": "ephemeral"}}, + ]}, + {"role": "user", "content": "Hello"}, + ] + + result = config.translate_system_message(messages) + + assert len(result) == 1 + assert result[0]["text"] == "Cached content" + assert result[0]["cache_control"] == {"type": "ephemeral"} From 18796235025767b84479811437cdf7d27e0c9279 Mon Sep 17 00:00:00 2001 From: Andrew Truong Date: Thu, 4 Dec 2025 00:26:22 -0500 Subject: [PATCH 090/107] feat: Basic Weave OTEL integration (#17439) * test * testfix --- litellm/__init__.py | 1 + litellm/integrations/opentelemetry.py | 5 + litellm/integrations/weave/__init__.py | 7 + litellm/integrations/weave/weave_otel.py | 329 ++++++++++++++++++ .../custom_logger_registry.py | 1 + litellm/litellm_core_utils/litellm_logging.py | 25 ++ litellm/types/integrations/weave_otel.py | 31 ++ litellm/types/utils.py | 4 + poetry.lock | 2 +- .../integrations/test_weave_otel.py | 147 ++++++++ 10 files changed, 551 insertions(+), 1 deletion(-) create mode 100644 litellm/integrations/weave/__init__.py create mode 100644 litellm/integrations/weave/weave_otel.py create mode 100644 litellm/types/integrations/weave_otel.py create mode 100644 tests/test_litellm/integrations/test_weave_otel.py diff --git a/litellm/__init__.py b/litellm/__init__.py index 3d40a9b15cf..fdae470fe6a 100644 --- a/litellm/__init__.py +++ b/litellm/__init__.py @@ -151,6 +151,7 @@ _custom_logger_compatible_callbacks_literal = Literal[ "mlflow", "langfuse", "langfuse_otel", + "weave_otel", "pagerduty", "humanloop", "gcs_pubsub", diff --git a/litellm/integrations/opentelemetry.py b/litellm/integrations/opentelemetry.py index 23bffc6c7e5..9f9d45d0e7d 100644 --- a/litellm/integrations/opentelemetry.py +++ b/litellm/integrations/opentelemetry.py @@ -1081,6 +1081,11 @@ class OpenTelemetry(CustomLogger): span, kwargs, response_obj ) return + elif self.callback_name == "weave_otel": + from litellm.integrations.weave.weave_otel import set_weave_otel_attributes + + set_weave_otel_attributes(span, kwargs, response_obj) + return from litellm.proxy._types import SpanAttributes optional_params = kwargs.get("optional_params", {}) diff --git a/litellm/integrations/weave/__init__.py b/litellm/integrations/weave/__init__.py new file mode 100644 index 00000000000..49af77b55e8 --- /dev/null +++ b/litellm/integrations/weave/__init__.py @@ -0,0 +1,7 @@ +""" +Weave (W&B) integration for LiteLLM via OpenTelemetry. +""" + +from litellm.integrations.weave.weave_otel import WeaveOtelLogger + +__all__ = ["WeaveOtelLogger"] diff --git a/litellm/integrations/weave/weave_otel.py b/litellm/integrations/weave/weave_otel.py new file mode 100644 index 00000000000..167deaf2cdc --- /dev/null +++ b/litellm/integrations/weave/weave_otel.py @@ -0,0 +1,329 @@ +from __future__ import annotations + +import base64 +import json +import os +from typing import TYPE_CHECKING, Any, Optional + +from opentelemetry.trace import Status, StatusCode +from typing_extensions import override + +from litellm._logging import verbose_logger +from litellm.integrations._types.open_inference import SpanAttributes as OpenInferenceSpanAttributes +from litellm.integrations.arize import _utils +from litellm.integrations.opentelemetry import OpenTelemetry, OpenTelemetryConfig +from litellm.integrations.opentelemetry_utils.base_otel_llm_obs_attributes import ( + BaseLLMObsOTELAttributes, + safe_set_attribute, +) +from litellm.litellm_core_utils.safe_json_dumps import safe_dumps +from litellm.types.integrations.weave_otel import WeaveOtelConfig, WeaveSpanAttributes +from litellm.types.utils import StandardCallbackDynamicParams + +if TYPE_CHECKING: + from opentelemetry.trace import Span + + +# Weave OTEL endpoint +# Multi-tenant cloud: https://trace.wandb.ai/otel/v1/traces +# Dedicated cloud: https://.wandb.io/traces/otel/v1/traces +WEAVE_BASE_URL = "https://trace.wandb.ai" +WEAVE_OTEL_ENDPOINT = "/otel/v1/traces" + + +class WeaveLLMObsOTELAttributes(BaseLLMObsOTELAttributes): + """ + Weave-specific LLM observability OTEL attributes. + + Weave automatically maps attributes from multiple frameworks including + GenAI, OpenInference, Langfuse, and others. + """ + + @staticmethod + @override + def set_messages(span: "Span", kwargs: dict[str, Any]): + """Set input messages as span attributes using OpenInference conventions.""" + + messages = kwargs.get("messages") or [] + optional_params = kwargs.get("optional_params") or {} + + prompt = {"messages": messages} + functions = optional_params.get("functions") + tools = optional_params.get("tools") + if functions is not None: + prompt["functions"] = functions + if tools is not None: + prompt["tools"] = tools + safe_set_attribute(span, OpenInferenceSpanAttributes.INPUT_VALUE, json.dumps(prompt)) + + +def _set_weave_specific_attributes(span: Span, kwargs: dict[str, Any], response_obj: Any): + """ + Sets Weave-specific metadata attributes onto the OTEL span. + + Based on Weave's OTEL attribute mappings from: + https://github.com/wandb/weave/blob/master/weave/trace_server/opentelemetry/constants.py + """ + + # Extract all needed data upfront + litellm_params = kwargs.get("litellm_params") or {} + # optional_params = kwargs.get("optional_params") or {} + metadata = kwargs.get("metadata") or {} + model = kwargs.get("model") or "" + custom_llm_provider = litellm_params.get("custom_llm_provider") or "" + + # Weave supports a custom display name and will default to the model name if not provided. + display_name = metadata.get("display_name") + if not display_name and model: + if custom_llm_provider: + display_name = f"{custom_llm_provider}/{model}" + else: + display_name = model + if display_name: + display_name = display_name.replace("/", "__") + safe_set_attribute(span, WeaveSpanAttributes.DISPLAY_NAME.value, display_name) + + # Weave threads are OpenInference sessions. + if (session_id := metadata.get("session_id")) is not None: + if isinstance(session_id, (list, dict)): + session_id = safe_dumps(session_id) + safe_set_attribute(span, WeaveSpanAttributes.THREAD_ID.value, session_id) + safe_set_attribute(span, WeaveSpanAttributes.IS_TURN.value, True) + + # Response attributes are already set by _utils.set_attributes, + # but we override them here to better match Weave's expectations + if response_obj: + output_dict = None + if hasattr(response_obj, "model_dump"): + output_dict = response_obj.model_dump() + elif hasattr(response_obj, "get"): + output_dict = response_obj + + if output_dict: + safe_set_attribute(span, OpenInferenceSpanAttributes.OUTPUT_VALUE, safe_dumps(output_dict)) + + +def _get_weave_authorization_header(api_key: str) -> str: + """ + Get the authorization header for Weave OpenTelemetry. + + Weave uses Basic auth with format: api: + """ + auth_string = f"api:{api_key}" + auth_header = base64.b64encode(auth_string.encode()).decode() + return f"Basic {auth_header}" + + +def get_weave_otel_config() -> WeaveOtelConfig: + """ + Retrieves the Weave OpenTelemetry configuration based on environment variables. + + Environment Variables: + WANDB_API_KEY: Required. W&B API key for authentication. + WANDB_PROJECT_ID: Required. Project ID in format /. + WANDB_HOST: Optional. Custom Weave host URL. Defaults to cloud endpoint. + + Returns: + WeaveOtelConfig: A Pydantic model containing Weave OTEL configuration. + + Raises: + ValueError: If required environment variables are missing. + """ + api_key = os.getenv("WANDB_API_KEY") + project_id = os.getenv("WANDB_PROJECT_ID") + host = os.getenv("WANDB_HOST") + + if not api_key: + raise ValueError("WANDB_API_KEY must be set for Weave OpenTelemetry integration.") + + if not project_id: + raise ValueError( + "WANDB_PROJECT_ID must be set for Weave OpenTelemetry integration. Format: /" + ) + + if host: + if not host.startswith("http"): + host = "https://" + host + # Self-managed instances use a different path + endpoint = host.rstrip("/") + WEAVE_OTEL_ENDPOINT + verbose_logger.debug(f"Using Weave OTEL endpoint from host: {endpoint}") + else: + endpoint = WEAVE_BASE_URL + WEAVE_OTEL_ENDPOINT + verbose_logger.debug(f"Using Weave cloud endpoint: {endpoint}") + + # Weave uses Basic auth with format: api: + auth_header = _get_weave_authorization_header(api_key=api_key) + otlp_auth_headers = f"Authorization={auth_header},project_id={project_id}" + + # Set standard OTEL environment variables + os.environ["OTEL_EXPORTER_OTLP_ENDPOINT"] = endpoint + os.environ["OTEL_EXPORTER_OTLP_HEADERS"] = otlp_auth_headers + + return WeaveOtelConfig( + otlp_auth_headers=otlp_auth_headers, + endpoint=endpoint, + project_id=project_id, + protocol="otlp_http", + ) + + +def set_weave_otel_attributes(span: Span, kwargs: dict[str, Any], response_obj: Any): + """ + Sets OpenTelemetry span attributes for Weave observability. + Uses the same attribute setting logic as other OTEL integrations for consistency. + """ + _utils.set_attributes(span, kwargs, response_obj, WeaveLLMObsOTELAttributes) + _set_weave_specific_attributes(span=span, kwargs=kwargs, response_obj=response_obj) + + +class WeaveOtelLogger(OpenTelemetry): + """ + Weave (W&B) OpenTelemetry Logger for LiteLLM. + + Sends LLM traces to Weave via the OpenTelemetry Protocol (OTLP). + + Environment Variables: + WANDB_API_KEY: Required. Weights & Biases API key for authentication. + WANDB_PROJECT_ID: Required. Project ID in format /. + WANDB_HOST: Optional. Custom Weave host URL. Defaults to cloud endpoint. + + Usage: + litellm.callbacks = ["weave_otel"] + + Or manually: + from litellm.integrations.weave.weave_otel import WeaveOtelLogger + weave_logger = WeaveOtelLogger(callback_name="weave_otel") + litellm.callbacks = [weave_logger] + + Reference: + https://docs.wandb.ai/weave/guides/tracking/otel + """ + + def __init__( + self, + config: Optional[OpenTelemetryConfig] = None, + callback_name: Optional[str] = "weave_otel", + **kwargs, + ): + """ + Initialize WeaveOtelLogger. + + If config is not provided, automatically configures from environment variables + (WANDB_API_KEY, WANDB_PROJECT_ID, WANDB_HOST) via get_weave_otel_config(). + """ + if config is None: + # Auto-configure from Weave environment variables + weave_config = get_weave_otel_config() + + config = OpenTelemetryConfig( + exporter=weave_config.protocol, + endpoint=weave_config.endpoint, + headers=weave_config.otlp_auth_headers, + ) + + super().__init__(config=config, callback_name=callback_name, **kwargs) + + def _maybe_log_raw_request(self, kwargs, response_obj, start_time, end_time, parent_span): + """ + Override to skip creating the raw_gen_ai_request child span. + + For Weave, we only want a single span per LLM call. The parent span + already contains all the necessary attributes, so the child span + is redundant. + """ + pass + + def _start_primary_span( + self, + kwargs, + response_obj, + start_time, + end_time, + context, + parent_span=None, + ): + """ + Override to always create a child span instead of reusing the parent span. + + This ensures that wrapper spans (like "B", "C", "D", "E") remain separate + from the LiteLLM LLM call spans, creating proper nesting in Weave. + """ + + otel_tracer = self.get_tracer_to_use_for_request(kwargs) + # Always create a new child span, even if parent_span is provided + # This ensures wrapper spans remain separate from LLM call spans + span = otel_tracer.start_span( + name=self._get_span_name(kwargs), + start_time=self._to_ns(start_time), + context=context, + ) + span.set_status(Status(StatusCode.OK)) + self.set_attributes(span, kwargs, response_obj) + span.end(end_time=self._to_ns(end_time)) + return span + + def _handle_success(self, kwargs, response_obj, start_time, end_time): + """ + Override to prevent ending externally created parent spans. + + When wrapper spans (like "B", "C", "D", "E") are provided as parent spans, + they should be managed by the user code, not ended by LiteLLM. + """ + + verbose_logger.debug( + "Weave OpenTelemetry Logger: Logging kwargs: %s, OTEL config settings=%s", + kwargs, + self.config, + ) + ctx, parent_span = self._get_span_context(kwargs) + + # Always create a child span (handled by _start_primary_span override) + primary_span_parent = None + + # 1. Primary span + span = self._start_primary_span(kwargs, response_obj, start_time, end_time, ctx, primary_span_parent) + + # 2. Raw-request sub-span (skipped for Weave via _maybe_log_raw_request override) + self._maybe_log_raw_request(kwargs, response_obj, start_time, end_time, span) + + # 3. Guardrail span + self._create_guardrail_span(kwargs=kwargs, context=ctx) + + # 4. Metrics & cost recording + self._record_metrics(kwargs, response_obj, start_time, end_time) + + # 5. Semantic logs. + if self.config.enable_events: + self._emit_semantic_logs(kwargs, response_obj, span) + + # 6. Don't end parent span - it's managed by user code + # Since we always create a child span (never reuse parent), the parent span + # lifecycle is owned by the user. This prevents double-ending of wrapper spans + # like "B", "C", "D", "E" that users create and manage themselves. + + def construct_dynamic_otel_headers( + self, standard_callback_dynamic_params: StandardCallbackDynamicParams + ) -> dict | None: + """ + Construct dynamic Weave headers from standard callback dynamic params. + + This is used for team/key based logging. + + Returns: + dict: A dictionary of dynamic Weave headers + """ + dynamic_headers = {} + + dynamic_wandb_api_key = standard_callback_dynamic_params.get("wandb_api_key") + dynamic_weave_project_id = standard_callback_dynamic_params.get("weave_project_id") + + if dynamic_wandb_api_key: + auth_header = _get_weave_authorization_header( + api_key=dynamic_wandb_api_key, + ) + dynamic_headers["Authorization"] = auth_header + + if dynamic_weave_project_id: + dynamic_headers["project_id"] = dynamic_weave_project_id + + return dynamic_headers if dynamic_headers else None diff --git a/litellm/litellm_core_utils/custom_logger_registry.py b/litellm/litellm_core_utils/custom_logger_registry.py index 538ef6be283..fdc9f374553 100644 --- a/litellm/litellm_core_utils/custom_logger_registry.py +++ b/litellm/litellm_core_utils/custom_logger_registry.py @@ -75,6 +75,7 @@ class CustomLoggerRegistry: "langfuse_otel": OpenTelemetry, "arize_phoenix": OpenTelemetry, "langtrace": OpenTelemetry, + "weave_otel": OpenTelemetry, "mlflow": MlflowLogger, "langfuse": LangfusePromptManagement, "otel": OpenTelemetry, diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index 6440e5f6dee..2b52d58e29d 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -3802,6 +3802,31 @@ def _init_custom_logger_compatible_class( # noqa: PLR0915 ) _in_memory_loggers.append(_otel_logger) return _otel_logger # type: ignore + elif logging_integration == "weave_otel": + from litellm.integrations.opentelemetry import ( + OpenTelemetryConfig, + ) + from litellm.integrations.weave.weave_otel import WeaveOtelLogger, get_weave_otel_config + + weave_otel_config = get_weave_otel_config() + + otel_config = OpenTelemetryConfig( + exporter=weave_otel_config.protocol, + endpoint=weave_otel_config.endpoint, + headers=weave_otel_config.otlp_auth_headers, + ) + + for callback in _in_memory_loggers: + if ( + isinstance(callback, WeaveOtelLogger) + and callback.callback_name == "weave_otel" + ): + return callback # type: ignore + _otel_logger = WeaveOtelLogger( + config=otel_config, callback_name="weave_otel" + ) + _in_memory_loggers.append(_otel_logger) + return _otel_logger # type: ignore elif logging_integration == "pagerduty": for callback in _in_memory_loggers: if isinstance(callback, PagerDutyAlerting): diff --git a/litellm/types/integrations/weave_otel.py b/litellm/types/integrations/weave_otel.py new file mode 100644 index 00000000000..5b40ff85340 --- /dev/null +++ b/litellm/types/integrations/weave_otel.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +from enum import Enum +from typing import Literal + +from pydantic import BaseModel + + +class WeaveOtelConfig(BaseModel): + """Configuration for Weave OpenTelemetry integration.""" + + otlp_auth_headers: str | None = None + endpoint: str | None = None + project_id: str | None = None + protocol: Literal["otlp_grpc", "otlp_http"] = "otlp_http" + + +class WeaveSpanAttributes(str, Enum): + """ + Weave-specific span attributes for OpenTelemetry traces. + + Based on Weave's OTEL attribute mappings from: + https://github.com/wandb/weave/blob/master/weave/trace_server/opentelemetry/constants.py + """ + + DISPLAY_NAME = "wandb.display_name" + + # Thread organization, similar to OpenInference session_id. + THREAD_ID = "wandb.thread_id" + IS_TURN = "wandb.is_turn" + diff --git a/litellm/types/utils.py b/litellm/types/utils.py index 3d92e78d494..65457fd3039 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -2676,6 +2676,10 @@ class StandardCallbackDynamicParams(TypedDict, total=False): posthog_api_key: Optional[str] posthog_api_url: Optional[str] + # Weave (W&B) dynamic params + wandb_api_key: Optional[str] + weave_project_id: Optional[str] + # Logging settings turn_off_message_logging: Optional[bool] # when true will not log messages litellm_disabled_callbacks: Optional[List[str]] diff --git a/poetry.lock b/poetry.lock index 15140856781..711ab0bbeae 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. [[package]] name = "aiofiles" diff --git a/tests/test_litellm/integrations/test_weave_otel.py b/tests/test_litellm/integrations/test_weave_otel.py new file mode 100644 index 00000000000..440c0888512 --- /dev/null +++ b/tests/test_litellm/integrations/test_weave_otel.py @@ -0,0 +1,147 @@ +import os +from unittest.mock import MagicMock, patch + +import pytest + +from litellm.integrations.weave.weave_otel import ( + _set_weave_specific_attributes, + get_weave_otel_config, +) +from litellm.types.integrations.weave_otel import WeaveOtelConfig, WeaveSpanAttributes + + +def test_get_weave_otel_config(): + """Test config creation with required env vars and error cases for missing vars.""" + # Test successful config creation with required environment variables + with patch.dict( + os.environ, + { + "WANDB_API_KEY": "test_api_key", + "WANDB_PROJECT_ID": "test-entity/test-project", + }, + clear=True, + ): + config = get_weave_otel_config() + + assert isinstance(config, WeaveOtelConfig) + assert config.protocol == "otlp_http" + assert config.project_id == "test-entity/test-project" + assert config.otlp_auth_headers is not None + assert "Authorization=" in config.otlp_auth_headers + assert "project_id=test-entity/test-project" in config.otlp_auth_headers + assert config.endpoint == "https://trace.wandb.ai/otel/v1/traces" + + # Verify environment variables were set + assert os.environ["OTEL_EXPORTER_OTLP_ENDPOINT"] == "https://trace.wandb.ai/otel/v1/traces" + assert os.environ["OTEL_EXPORTER_OTLP_HEADERS"] == config.otlp_auth_headers + + # Test ValueError when WANDB_API_KEY is missing + with patch.dict(os.environ, {"WANDB_PROJECT_ID": "test-entity/test-project"}, clear=True): + with pytest.raises(ValueError, match="WANDB_API_KEY must be set"): + get_weave_otel_config() + + # Test ValueError when WANDB_PROJECT_ID is missing + with patch.dict(os.environ, {"WANDB_API_KEY": "test_api_key"}, clear=True): + with pytest.raises(ValueError, match="WANDB_PROJECT_ID must be set"): + get_weave_otel_config() + + +def test_get_weave_otel_config_with_custom_host(): + """Test config creation with custom WANDB_HOST.""" + # Test with host that already has https:// + with patch.dict( + os.environ, + { + "WANDB_API_KEY": "test_api_key", + "WANDB_PROJECT_ID": "test-entity/test-project", + "WANDB_HOST": "https://custom.wandb.io", + }, + clear=True, + ): + config = get_weave_otel_config() + assert config.endpoint == "https://custom.wandb.io/otel/v1/traces" + assert os.environ["OTEL_EXPORTER_OTLP_ENDPOINT"] == "https://custom.wandb.io/otel/v1/traces" + + # Test with host without http:// or https:// + with patch.dict( + os.environ, + { + "WANDB_API_KEY": "test_api_key", + "WANDB_PROJECT_ID": "test-entity/test-project", + "WANDB_HOST": "custom.wandb.io", + }, + clear=True, + ): + config = get_weave_otel_config() + assert config.endpoint == "https://custom.wandb.io/otel/v1/traces" + + # Test with host with trailing slash + with patch.dict( + os.environ, + { + "WANDB_API_KEY": "test_api_key", + "WANDB_PROJECT_ID": "test-entity/test-project", + "WANDB_HOST": "https://custom.wandb.io/", + }, + clear=True, + ): + config = get_weave_otel_config() + assert config.endpoint == "https://custom.wandb.io/otel/v1/traces" + + + + + +def test_set_weave_specific_attributes_display_name_from_metadata(): + """Test _set_weave_specific_attributes sets display_name from metadata.""" + mock_span = MagicMock() + kwargs = { + "metadata": {"display_name": "custom-display-name"}, + "model": "gpt-4", + } + + with patch("litellm.integrations.weave.weave_otel.safe_set_attribute") as mock_safe_set: + _set_weave_specific_attributes(mock_span, kwargs, None) + + # Should set display_name from metadata + mock_safe_set.assert_any_call( + mock_span, WeaveSpanAttributes.DISPLAY_NAME.value, "custom-display-name" + ) + + +def test_set_weave_specific_attributes_display_name_from_model(): + """Test _set_weave_specific_attributes sets display_name from model when not in metadata.""" + mock_span = MagicMock() + kwargs = { + "model": "openai/gpt-4o-mini", + "metadata": {}, + } + + with patch("litellm.integrations.weave.weave_otel.safe_set_attribute") as mock_safe_set: + _set_weave_specific_attributes(mock_span, kwargs, None) + + # Should set display_name from model + mock_safe_set.assert_any_call( + mock_span, WeaveSpanAttributes.DISPLAY_NAME.value, "openai__gpt-4o-mini" + ) + + + +def test_set_weave_specific_attributes_thread_id_and_is_turn(): + """Test _set_weave_specific_attributes sets thread_id and is_turn from session_id.""" + mock_span = MagicMock() + kwargs = { + "metadata": {"session_id": "session-123"}, + } + + with patch("litellm.integrations.weave.weave_otel.safe_set_attribute") as mock_safe_set: + _set_weave_specific_attributes(mock_span, kwargs, None) + + # Should set thread_id and is_turn + mock_safe_set.assert_any_call( + mock_span, WeaveSpanAttributes.THREAD_ID.value, "session-123" + ) + mock_safe_set.assert_any_call( + mock_span, WeaveSpanAttributes.IS_TURN.value, True + ) + From 6d831ffff4e279e859c733ed823ed7214da0ecea Mon Sep 17 00:00:00 2001 From: Cesar Garcia <128240629+Chesars@users.noreply.github.com> Date: Thu, 4 Dec 2025 02:28:13 -0300 Subject: [PATCH 091/107] fix(ocr): add missing ocr and aocr to CallTypes enum (#17435) Add `ocr` and `aocr` entries to the CallTypes enum to fix the ValueError that occurs when using the /v1/ocr endpoint with guardrails enabled. The OCR endpoint uses route_type="aocr", but the CallTypes enum was missing these values, causing guardrail hooks to fail when trying to instantiate CallTypes("aocr"). Fixes #17381 --- litellm/types/utils.py | 6 +++++ tests/test_litellm/test_main.py | 40 +++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/litellm/types/utils.py b/litellm/types/utils.py index 65457fd3039..4861510da24 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -340,6 +340,12 @@ class CallTypes(str, Enum): generate_content_stream = "generate_content_stream" agenerate_content_stream = "agenerate_content_stream" + ######################################################### + # OCR Call Types + ######################################################### + ocr = "ocr" + aocr = "aocr" + ######################################################### # MCP Call Types ######################################################### diff --git a/tests/test_litellm/test_main.py b/tests/test_litellm/test_main.py index 71ede0958f5..d2416f8db8c 100644 --- a/tests/test_litellm/test_main.py +++ b/tests/test_litellm/test_main.py @@ -1461,3 +1461,43 @@ async def test_async_mock_completion_stream_with_model_response(): accumulated_content += chunk.choices[0].delta.content assert "This is an async test response" in accumulated_content or len(chunks) > 0 + + +class TestCallTypesOCR: + """Test that OCR call types are properly defined in CallTypes enum. + + Fixes https://github.com/BerriAI/litellm/issues/17381 + """ + + def test_ocr_call_type_exists(self): + """Test that CallTypes.ocr exists and has correct value.""" + from litellm.types.utils import CallTypes + + assert hasattr(CallTypes, "ocr") + assert CallTypes.ocr.value == "ocr" + + def test_aocr_call_type_exists(self): + """Test that CallTypes.aocr exists and has correct value.""" + from litellm.types.utils import CallTypes + + assert hasattr(CallTypes, "aocr") + assert CallTypes.aocr.value == "aocr" + + def test_ocr_call_type_from_string(self): + """Test that CallTypes can be constructed from 'ocr' string.""" + from litellm.types.utils import CallTypes + + call_type = CallTypes("ocr") + assert call_type == CallTypes.ocr + + def test_aocr_call_type_from_string(self): + """Test that CallTypes can be constructed from 'aocr' string. + + This is the actual use case that was failing - the OCR endpoint + uses route_type='aocr' and guardrails try to instantiate + CallTypes('aocr'). + """ + from litellm.types.utils import CallTypes + + call_type = CallTypes("aocr") + assert call_type == CallTypes.aocr From b05f6d3cfdb6c77be37e4a39eaa7dc2fb41a1f0a Mon Sep 17 00:00:00 2001 From: Cesar Garcia <128240629+Chesars@users.noreply.github.com> Date: Thu, 4 Dec 2025 02:31:13 -0300 Subject: [PATCH 092/107] fix(groq): check supports_response_schema before using json_tool_call workaround (#17438) Fixes issue where LiteLLM used the json_tool_call workaround for all Groq models, even those that support native json_schema (e.g., gpt-oss-120b, llama-4, kimi-k2). This caused errors when users passed their own tools alongside structured outputs. Changes: - Check `supports_response_schema()` before using the workaround - Only use json_tool_call workaround for models without native support - Add clear error message when using workaround with user-provided tools - Update model config to set `supports_response_schema: false` for models that don't support native json_schema - Add unit tests for structured output handling --- litellm/llms/groq/chat/transformation.py | 53 +++++-- ...odel_prices_and_context_window_backup.json | 36 ++--- model_prices_and_context_window.json | 36 ++--- tests/llm_translation/test_groq.py | 134 ++++++++++++++++++ 4 files changed, 209 insertions(+), 50 deletions(-) diff --git a/litellm/llms/groq/chat/transformation.py b/litellm/llms/groq/chat/transformation.py index 20e0d412edc..a75ecd8cc7b 100644 --- a/litellm/llms/groq/chat/transformation.py +++ b/litellm/llms/groq/chat/transformation.py @@ -218,22 +218,47 @@ class GroqChatConfig(OpenAILikeChatConfig): When using tools in this way: - https://docs.anthropic.com/en/docs/build-with-claude/tool-use#json-mode - You usually want to provide a single tool - You should set tool_choice (see Forcing tool use) to instruct the model to explicitly use that tool - - Remember that the model will pass the input to the tool, so the name of the tool and description should be from the model’s perspective. + - Remember that the model will pass the input to the tool, so the name of the tool and description should be from the model's perspective. + + Note: This workaround is only for models that don't support native json_schema. + Models like gpt-oss-120b, llama-4, kimi-k2 support native json_schema and should + pass response_format directly to Groq. + See: https://console.groq.com/docs/structured-outputs#supported-models """ if json_schema is not None: - _tool_choice = { - "type": "function", - "function": {"name": "json_tool_call"}, - } - _tool = self._create_json_tool_call_for_response_format( - json_schema=json_schema, - ) - optional_params["tools"] = [_tool] - optional_params["tool_choice"] = _tool_choice - optional_params["json_mode"] = True - non_default_params.pop( - "response_format", None - ) # only remove if it's a json_schema - handled via using groq's tool calling params. + # Check if model supports native response_schema + if not litellm.supports_response_schema( + model=model, custom_llm_provider="groq" + ): + # Check if user is also passing tools - this combination won't work + # See: https://console.groq.com/docs/structured-outputs + # "Streaming and tool use are not currently supported with Structured Outputs" + if "tools" in non_default_params: + raise litellm.BadRequestError( + message=f"Groq model '{model}' does not support native structured outputs. " + "LiteLLM uses a tool-calling workaround for structured outputs on this model, " + "which is incompatible with user-provided tools. " + "Either use a model that supports native structured outputs " + "(e.g., gpt-oss-120b, llama-4, kimi-k2), or remove the tools parameter. " + "See: https://console.groq.com/docs/structured-outputs#supported-models", + model=model, + llm_provider="groq", + ) + # Use workaround only for models without native support + _tool_choice = { + "type": "function", + "function": {"name": "json_tool_call"}, + } + _tool = self._create_json_tool_call_for_response_format( + json_schema=json_schema, + ) + optional_params["tools"] = [_tool] + optional_params["tool_choice"] = _tool_choice + optional_params["json_mode"] = True + non_default_params.pop( + "response_format", None + ) # only remove if it's a json_schema - handled via using groq's tool calling params. + # else: model supports native json_schema, let response_format pass through optional_params = super().map_openai_params( non_default_params, optional_params, model, drop_params ) diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index dbaa60e0f1d..903b86484cd 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -16881,7 +16881,7 @@ "output_cost_per_token": 9.9e-07, "supports_function_calling": true, "supports_reasoning": true, - "supports_response_schema": true, + "supports_response_schema": false, "supports_tool_choice": true }, "groq/distil-whisper-large-v3-en": { @@ -16900,7 +16900,7 @@ "mode": "chat", "output_cost_per_token": 7e-08, "supports_function_calling": true, - "supports_response_schema": true, + "supports_response_schema": false, "supports_tool_choice": true }, "groq/gemma2-9b-it": { @@ -16912,7 +16912,7 @@ "mode": "chat", "output_cost_per_token": 2e-07, "supports_function_calling": false, - "supports_response_schema": true, + "supports_response_schema": false, "supports_tool_choice": false }, "groq/llama-3.1-405b-reasoning": { @@ -16924,7 +16924,7 @@ "mode": "chat", "output_cost_per_token": 7.9e-07, "supports_function_calling": true, - "supports_response_schema": true, + "supports_response_schema": false, "supports_tool_choice": true }, "groq/llama-3.1-70b-versatile": { @@ -16937,7 +16937,7 @@ "mode": "chat", "output_cost_per_token": 7.9e-07, "supports_function_calling": true, - "supports_response_schema": true, + "supports_response_schema": false, "supports_tool_choice": true }, "groq/llama-3.1-8b-instant": { @@ -16949,7 +16949,7 @@ "mode": "chat", "output_cost_per_token": 8e-08, "supports_function_calling": true, - "supports_response_schema": true, + "supports_response_schema": false, "supports_tool_choice": true }, "groq/llama-3.2-11b-text-preview": { @@ -16962,7 +16962,7 @@ "mode": "chat", "output_cost_per_token": 1.8e-07, "supports_function_calling": true, - "supports_response_schema": true, + "supports_response_schema": false, "supports_tool_choice": true }, "groq/llama-3.2-11b-vision-preview": { @@ -16975,7 +16975,7 @@ "mode": "chat", "output_cost_per_token": 1.8e-07, "supports_function_calling": true, - "supports_response_schema": true, + "supports_response_schema": false, "supports_tool_choice": true, "supports_vision": true }, @@ -16989,7 +16989,7 @@ "mode": "chat", "output_cost_per_token": 4e-08, "supports_function_calling": true, - "supports_response_schema": true, + "supports_response_schema": false, "supports_tool_choice": true }, "groq/llama-3.2-3b-preview": { @@ -17002,7 +17002,7 @@ "mode": "chat", "output_cost_per_token": 6e-08, "supports_function_calling": true, - "supports_response_schema": true, + "supports_response_schema": false, "supports_tool_choice": true }, "groq/llama-3.2-90b-text-preview": { @@ -17015,7 +17015,7 @@ "mode": "chat", "output_cost_per_token": 9e-07, "supports_function_calling": true, - "supports_response_schema": true, + "supports_response_schema": false, "supports_tool_choice": true }, "groq/llama-3.2-90b-vision-preview": { @@ -17028,7 +17028,7 @@ "mode": "chat", "output_cost_per_token": 9e-07, "supports_function_calling": true, - "supports_response_schema": true, + "supports_response_schema": false, "supports_tool_choice": true, "supports_vision": true }, @@ -17052,7 +17052,7 @@ "mode": "chat", "output_cost_per_token": 7.9e-07, "supports_function_calling": true, - "supports_response_schema": true, + "supports_response_schema": false, "supports_tool_choice": true }, "groq/llama-guard-3-8b": { @@ -17073,7 +17073,7 @@ "mode": "chat", "output_cost_per_token": 8e-07, "supports_function_calling": true, - "supports_response_schema": true, + "supports_response_schema": false, "supports_tool_choice": true }, "groq/llama3-groq-70b-8192-tool-use-preview": { @@ -17086,7 +17086,7 @@ "mode": "chat", "output_cost_per_token": 8.9e-07, "supports_function_calling": true, - "supports_response_schema": true, + "supports_response_schema": false, "supports_tool_choice": true }, "groq/llama3-groq-8b-8192-tool-use-preview": { @@ -17099,7 +17099,7 @@ "mode": "chat", "output_cost_per_token": 1.9e-07, "supports_function_calling": true, - "supports_response_schema": true, + "supports_response_schema": false, "supports_tool_choice": true }, "groq/meta-llama/llama-4-maverick-17b-128e-instruct": { @@ -17145,7 +17145,7 @@ "mode": "chat", "output_cost_per_token": 2.4e-07, "supports_function_calling": true, - "supports_response_schema": true, + "supports_response_schema": false, "supports_tool_choice": true }, "groq/moonshotai/kimi-k2-instruct": { @@ -17221,7 +17221,7 @@ "output_cost_per_token": 5.9e-07, "supports_function_calling": true, "supports_reasoning": true, - "supports_response_schema": true, + "supports_response_schema": false, "supports_tool_choice": true }, "groq/whisper-large-v3": { diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index dbaa60e0f1d..903b86484cd 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -16881,7 +16881,7 @@ "output_cost_per_token": 9.9e-07, "supports_function_calling": true, "supports_reasoning": true, - "supports_response_schema": true, + "supports_response_schema": false, "supports_tool_choice": true }, "groq/distil-whisper-large-v3-en": { @@ -16900,7 +16900,7 @@ "mode": "chat", "output_cost_per_token": 7e-08, "supports_function_calling": true, - "supports_response_schema": true, + "supports_response_schema": false, "supports_tool_choice": true }, "groq/gemma2-9b-it": { @@ -16912,7 +16912,7 @@ "mode": "chat", "output_cost_per_token": 2e-07, "supports_function_calling": false, - "supports_response_schema": true, + "supports_response_schema": false, "supports_tool_choice": false }, "groq/llama-3.1-405b-reasoning": { @@ -16924,7 +16924,7 @@ "mode": "chat", "output_cost_per_token": 7.9e-07, "supports_function_calling": true, - "supports_response_schema": true, + "supports_response_schema": false, "supports_tool_choice": true }, "groq/llama-3.1-70b-versatile": { @@ -16937,7 +16937,7 @@ "mode": "chat", "output_cost_per_token": 7.9e-07, "supports_function_calling": true, - "supports_response_schema": true, + "supports_response_schema": false, "supports_tool_choice": true }, "groq/llama-3.1-8b-instant": { @@ -16949,7 +16949,7 @@ "mode": "chat", "output_cost_per_token": 8e-08, "supports_function_calling": true, - "supports_response_schema": true, + "supports_response_schema": false, "supports_tool_choice": true }, "groq/llama-3.2-11b-text-preview": { @@ -16962,7 +16962,7 @@ "mode": "chat", "output_cost_per_token": 1.8e-07, "supports_function_calling": true, - "supports_response_schema": true, + "supports_response_schema": false, "supports_tool_choice": true }, "groq/llama-3.2-11b-vision-preview": { @@ -16975,7 +16975,7 @@ "mode": "chat", "output_cost_per_token": 1.8e-07, "supports_function_calling": true, - "supports_response_schema": true, + "supports_response_schema": false, "supports_tool_choice": true, "supports_vision": true }, @@ -16989,7 +16989,7 @@ "mode": "chat", "output_cost_per_token": 4e-08, "supports_function_calling": true, - "supports_response_schema": true, + "supports_response_schema": false, "supports_tool_choice": true }, "groq/llama-3.2-3b-preview": { @@ -17002,7 +17002,7 @@ "mode": "chat", "output_cost_per_token": 6e-08, "supports_function_calling": true, - "supports_response_schema": true, + "supports_response_schema": false, "supports_tool_choice": true }, "groq/llama-3.2-90b-text-preview": { @@ -17015,7 +17015,7 @@ "mode": "chat", "output_cost_per_token": 9e-07, "supports_function_calling": true, - "supports_response_schema": true, + "supports_response_schema": false, "supports_tool_choice": true }, "groq/llama-3.2-90b-vision-preview": { @@ -17028,7 +17028,7 @@ "mode": "chat", "output_cost_per_token": 9e-07, "supports_function_calling": true, - "supports_response_schema": true, + "supports_response_schema": false, "supports_tool_choice": true, "supports_vision": true }, @@ -17052,7 +17052,7 @@ "mode": "chat", "output_cost_per_token": 7.9e-07, "supports_function_calling": true, - "supports_response_schema": true, + "supports_response_schema": false, "supports_tool_choice": true }, "groq/llama-guard-3-8b": { @@ -17073,7 +17073,7 @@ "mode": "chat", "output_cost_per_token": 8e-07, "supports_function_calling": true, - "supports_response_schema": true, + "supports_response_schema": false, "supports_tool_choice": true }, "groq/llama3-groq-70b-8192-tool-use-preview": { @@ -17086,7 +17086,7 @@ "mode": "chat", "output_cost_per_token": 8.9e-07, "supports_function_calling": true, - "supports_response_schema": true, + "supports_response_schema": false, "supports_tool_choice": true }, "groq/llama3-groq-8b-8192-tool-use-preview": { @@ -17099,7 +17099,7 @@ "mode": "chat", "output_cost_per_token": 1.9e-07, "supports_function_calling": true, - "supports_response_schema": true, + "supports_response_schema": false, "supports_tool_choice": true }, "groq/meta-llama/llama-4-maverick-17b-128e-instruct": { @@ -17145,7 +17145,7 @@ "mode": "chat", "output_cost_per_token": 2.4e-07, "supports_function_calling": true, - "supports_response_schema": true, + "supports_response_schema": false, "supports_tool_choice": true }, "groq/moonshotai/kimi-k2-instruct": { @@ -17221,7 +17221,7 @@ "output_cost_per_token": 5.9e-07, "supports_function_calling": true, "supports_reasoning": true, - "supports_response_schema": true, + "supports_response_schema": false, "supports_tool_choice": true }, "groq/whisper-large-v3": { diff --git a/tests/llm_translation/test_groq.py b/tests/llm_translation/test_groq.py index fc84580404c..33c1a425870 100644 --- a/tests/llm_translation/test_groq.py +++ b/tests/llm_translation/test_groq.py @@ -9,6 +9,7 @@ import pytest # ) # noqa # ) # Adds the parent directory to the system path +import litellm from base_llm_unit_tests import BaseLLMChatTest from litellm.llms.groq.chat.transformation import GroqChatConfig @@ -30,3 +31,136 @@ class TestGroq(BaseLLMChatTest): """Test that reasoning_effort is in the list of supported parameters for Groq""" supported_params = GroqChatConfig().get_supported_openai_params(model=model) assert "reasoning_effort" in supported_params + + +class TestGroqStructuredOutputs: + """ + Tests for Groq structured outputs handling. + Related issues: + - https://github.com/BerriAI/litellm/issues/11001 + - https://github.com/openai/openai-agents-python/issues/2140 + """ + + def test_structured_output_with_tools_raises_error_for_non_native_models(self): + """ + Test that using structured outputs + tools with models that don't support + native json_schema raises a clear error message. + + Groq does not support structured outputs + tools together. + See: https://console.groq.com/docs/structured-outputs + "Streaming and tool use are not currently supported with Structured Outputs" + """ + config = GroqChatConfig() + + # Model that doesn't support native json_schema + model = "llama-3.3-70b-versatile" + + non_default_params = { + "response_format": { + "type": "json_schema", + "json_schema": { + "name": "test", + "schema": { + "type": "object", + "properties": {"name": {"type": "string"}}, + "required": ["name"] + } + } + }, + "tools": [ + { + "type": "function", + "function": { + "name": "get_weather", + "parameters": {"type": "object", "properties": {}} + } + } + ] + } + + with pytest.raises(litellm.BadRequestError) as exc_info: + config.map_openai_params( + non_default_params=non_default_params, + optional_params={}, + model=model, + drop_params=False, + ) + + assert "does not support native structured outputs" in str(exc_info.value) + assert "incompatible with user-provided tools" in str(exc_info.value) + + def test_structured_output_without_tools_uses_workaround_for_non_native_models(self): + """ + Test that structured outputs without tools works using the json_tool_call workaround + for models that don't support native json_schema. + """ + config = GroqChatConfig() + + model = "llama-3.3-70b-versatile" + + non_default_params = { + "response_format": { + "type": "json_schema", + "json_schema": { + "name": "test", + "schema": { + "type": "object", + "properties": {"name": {"type": "string"}}, + "required": ["name"] + } + } + } + } + + result = config.map_openai_params( + non_default_params=non_default_params, + optional_params={}, + model=model, + drop_params=False, + ) + + # Should use the workaround (json_tool_call) + assert "tools" in result + assert len(result["tools"]) == 1 + assert result["tools"][0]["function"]["name"] == "json_tool_call" + assert result["tool_choice"]["function"]["name"] == "json_tool_call" + assert result.get("json_mode") is True + + def test_structured_output_passes_through_for_native_models(self): + """ + Test that structured outputs pass through directly for models that + support native json_schema (e.g., gpt-oss-120b). + """ + config = GroqChatConfig() + + # Model that supports native json_schema + model = "openai/gpt-oss-120b" + + non_default_params = { + "response_format": { + "type": "json_schema", + "json_schema": { + "name": "test", + "schema": { + "type": "object", + "properties": {"name": {"type": "string"}}, + "required": ["name"] + } + } + } + } + + result = config.map_openai_params( + non_default_params=non_default_params, + optional_params={}, + model=model, + drop_params=False, + ) + + # Should NOT use the workaround - response_format should pass through + # The workaround sets json_mode=True, so if it's not set, we know it passed through + assert result.get("json_mode") is not True + # Should not have the json_tool_call tool + if "tools" in result: + tool_names = [t.get("function", {}).get("name") for t in result["tools"]] + assert "json_tool_call" not in tool_names From 664b384dce19d0189c26ace5189326d27308f1a6 Mon Sep 17 00:00:00 2001 From: Cesar Garcia <128240629+Chesars@users.noreply.github.com> Date: Thu, 4 Dec 2025 02:31:49 -0300 Subject: [PATCH 093/107] fix(pricing): add missing Moonshot turbo models and fix incorrect pricing (#17432) Fixes #17417 - Add moonshot/kimi-k2-turbo-preview ($1.15/$8.00 input/output) - Add moonshot/kimi-k2-thinking-turbo ($1.15/$8.00 input/output) - Add moonshot/kimi-k2-0905-preview ($0.60/$2.50 input/output, 262K context) - Fix moonshot/kimi-thinking-preview pricing ($30/$30 -> $0.60/$2.50) - Fix wandb/moonshotai/Kimi-K2-Instruct absurd pricing ($135K/$400K -> $0.60/$2.50) --- model_prices_and_context_window.json | 53 +++++++++++++++++++++++++--- 1 file changed, 48 insertions(+), 5 deletions(-) diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 903b86484cd..6cc6e7772af 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -18896,6 +18896,34 @@ "supports_tool_choice": true, "supports_web_search": true }, + "moonshot/kimi-k2-0905-preview": { + "cache_read_input_token_cost": 1.5e-07, + "input_cost_per_token": 6e-07, + "litellm_provider": "moonshot", + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "max_tokens": 262144, + "mode": "chat", + "output_cost_per_token": 2.5e-06, + "source": "https://platform.moonshot.ai/docs/pricing/chat#generation-model-kimi-k2", + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_web_search": true + }, + "moonshot/kimi-k2-turbo-preview": { + "cache_read_input_token_cost": 1.5e-07, + "input_cost_per_token": 1.15e-06, + "litellm_provider": "moonshot", + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "max_tokens": 262144, + "mode": "chat", + "output_cost_per_token": 8e-06, + "source": "https://platform.moonshot.ai/docs/pricing/chat#generation-model-kimi-k2", + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_web_search": true + }, "moonshot/kimi-latest": { "cache_read_input_token_cost": 1.5e-07, "input_cost_per_token": 2e-06, @@ -18953,14 +18981,15 @@ "supports_vision": true }, "moonshot/kimi-thinking-preview": { - "input_cost_per_token": 3e-05, + "cache_read_input_token_cost": 1.5e-07, + "input_cost_per_token": 6e-07, "litellm_provider": "moonshot", "max_input_tokens": 131072, "max_output_tokens": 131072, "max_tokens": 131072, "mode": "chat", - "output_cost_per_token": 3e-05, - "source": "https://platform.moonshot.ai/docs/pricing", + "output_cost_per_token": 2.5e-06, + "source": "https://platform.moonshot.ai/docs/pricing/chat#generation-model-kimi-k2", "supports_vision": true }, "moonshot/kimi-k2-thinking": { @@ -18977,6 +19006,20 @@ "supports_tool_choice": true, "supports_web_search": true }, + "moonshot/kimi-k2-thinking-turbo": { + "cache_read_input_token_cost": 1.5e-7, + "input_cost_per_token": 1.15e-6, + "litellm_provider": "moonshot", + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "max_tokens": 262144, + "mode": "chat", + "output_cost_per_token": 8e-6, + "source": "https://platform.moonshot.ai/docs/pricing/chat#generation-model-kimi-k2", + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_web_search": true + }, "moonshot/moonshot-v1-128k": { "input_cost_per_token": 2e-06, "litellm_provider": "moonshot", @@ -26470,8 +26513,8 @@ "max_tokens": 128000, "max_input_tokens": 128000, "max_output_tokens": 128000, - "input_cost_per_token": 0.135, - "output_cost_per_token": 0.4, + "input_cost_per_token": 6e-07, + "output_cost_per_token": 2.5e-06, "litellm_provider": "wandb", "mode": "chat" }, From 3134fc825ba26df90dd30854de831284b1f7b96c Mon Sep 17 00:00:00 2001 From: Daniel Klein <37589721+danielmklein@users.noreply.github.com> Date: Wed, 3 Dec 2025 23:34:50 -0600 Subject: [PATCH 094/107] fix: make bedrock converse messages respect modify_params as expected (#17427) --- .../prompt_templates/factory.py | 54 ++++++++----------- 1 file changed, 21 insertions(+), 33 deletions(-) diff --git a/litellm/litellm_core_utils/prompt_templates/factory.py b/litellm/litellm_core_utils/prompt_templates/factory.py index 262692d6d1a..04c8c235557 100644 --- a/litellm/litellm_core_utils/prompt_templates/factory.py +++ b/litellm/litellm_core_utils/prompt_templates/factory.py @@ -3446,8 +3446,25 @@ class BedrockConverseMessagesProcessor: @staticmethod def _initial_message_setup( messages: List, + model: str, + llm_provider: str, user_continue_message: Optional[ChatCompletionUserMessage] = None, ) -> List: + # gracefully handle base case of no messages at all + if len(messages) == 0: + if user_continue_message is not None: + messages.append(user_continue_message) + elif litellm.modify_params: + messages.append(DEFAULT_USER_CONTINUE_MESSAGE) + else: + raise litellm.BadRequestError( + message=BAD_MESSAGE_ERROR_STR + + "bedrock requires at least one non-system message", + model=model, + llm_provider=llm_provider, + ) + + # if initial message is assistant message if messages[0].get("role") is not None and messages[0]["role"] == "assistant": if user_continue_message is not None: messages.insert(0, user_continue_message) @@ -3475,18 +3492,8 @@ class BedrockConverseMessagesProcessor: contents: List[BedrockMessageBlock] = [] msg_i = 0 - ## BASE CASE ## - if len(messages) == 0: - raise litellm.BadRequestError( - message=BAD_MESSAGE_ERROR_STR - + "bedrock requires at least one non-system message", - model=model, - llm_provider=llm_provider, - ) - - # if initial message is assistant message messages = BedrockConverseMessagesProcessor._initial_message_setup( - messages, user_continue_message + messages, model, llm_provider, user_continue_message ) while msg_i < len(messages): @@ -3847,28 +3854,9 @@ def _bedrock_converse_messages_pt( # noqa: PLR0915 contents: List[BedrockMessageBlock] = [] msg_i = 0 - ## BASE CASE ## - if len(messages) == 0: - raise litellm.BadRequestError( - message=BAD_MESSAGE_ERROR_STR - + "bedrock requires at least one non-system message", - model=model, - llm_provider=llm_provider, - ) - - # if initial message is assistant message - if messages[0].get("role") is not None and messages[0]["role"] == "assistant": - if user_continue_message is not None: - messages.insert(0, user_continue_message) - elif litellm.modify_params: - messages.insert(0, DEFAULT_USER_CONTINUE_MESSAGE) - - # if final message is assistant message - if messages[-1].get("role") is not None and messages[-1]["role"] == "assistant": - if user_continue_message is not None: - messages.append(user_continue_message) - elif litellm.modify_params: - messages.append(DEFAULT_USER_CONTINUE_MESSAGE) + messages = BedrockConverseMessagesProcessor._initial_message_setup( + messages, model, llm_provider, user_continue_message + ) while msg_i < len(messages): user_content: List[BedrockContentBlock] = [] From 4710e772be8d6c1256ad7de041425d3816b64904 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Thu, 4 Dec 2025 11:08:57 +0530 Subject: [PATCH 095/107] Add support for bedrock qwen 2 imported model --- docs/my-website/docs/providers/bedrock.md | 2 +- .../docs/providers/bedrock_imported.md | 65 ++++ litellm/__init__.py | 3 + litellm/constants.py | 1 + .../amazon_qwen2_transformation.py | 98 ++++++ litellm/llms/bedrock/common_utils.py | 2 + .../test_amazon_qwen2_transformation.py | 292 ++++++++++++++++++ 7 files changed, 462 insertions(+), 1 deletion(-) create mode 100644 litellm/llms/bedrock/chat/invoke_transformations/amazon_qwen2_transformation.py create mode 100644 tests/test_litellm/llms/bedrock/chat/invoke_transformations/test_amazon_qwen2_transformation.py diff --git a/docs/my-website/docs/providers/bedrock.md b/docs/my-website/docs/providers/bedrock.md index 7fc7758b15c..17c0d38111d 100644 --- a/docs/my-website/docs/providers/bedrock.md +++ b/docs/my-website/docs/providers/bedrock.md @@ -7,7 +7,7 @@ ALL Bedrock models (Anthropic, Meta, Deepseek, Mistral, Amazon, etc.) are Suppor | Property | Details | |-------|-------| | Description | Amazon Bedrock is a fully managed service that offers a choice of high-performing foundation models (FMs). | -| Provider Route on LiteLLM | `bedrock/`, [`bedrock/converse/`](#set-converse--invoke-route), [`bedrock/invoke/`](#set-invoke-route), [`bedrock/converse_like/`](#calling-via-internal-proxy), [`bedrock/llama/`](#deepseek-not-r1), [`bedrock/deepseek_r1/`](#deepseek-r1), [`bedrock/qwen3/`](#qwen3-imported-models), [`bedrock/openai/`](./bedrock_imported.md#openai-compatible-imported-models-qwen-25-vl-etc) | +| Provider Route on LiteLLM | `bedrock/`, [`bedrock/converse/`](#set-converse--invoke-route), [`bedrock/invoke/`](#set-invoke-route), [`bedrock/converse_like/`](#calling-via-internal-proxy), [`bedrock/llama/`](#deepseek-not-r1), [`bedrock/deepseek_r1/`](#deepseek-r1), [`bedrock/qwen3/`](#qwen3-imported-models), [`bedrock/qwen2/`](./bedrock_imported.md#qwen2-imported-models), [`bedrock/openai/`](./bedrock_imported.md#openai-compatible-imported-models-qwen-25-vl-etc) | | Provider Doc | [Amazon Bedrock ↗](https://docs.aws.amazon.com/bedrock/latest/userguide/what-is-bedrock.html) | | Supported OpenAI Endpoints | `/chat/completions`, `/completions`, `/embeddings`, `/images/generations` | | Rerank Endpoint | `/rerank` | diff --git a/docs/my-website/docs/providers/bedrock_imported.md b/docs/my-website/docs/providers/bedrock_imported.md index 8b0dd721c3c..0784f716925 100644 --- a/docs/my-website/docs/providers/bedrock_imported.md +++ b/docs/my-website/docs/providers/bedrock_imported.md @@ -203,6 +203,71 @@ curl --location 'http://0.0.0.0:4000/chat/completions' \ +### Qwen2 Imported Models + +| Property | Details | +|----------|---------| +| Provider Route | `bedrock/qwen2/{model_arn}` | +| Provider Documentation | [Bedrock Imported Models](https://docs.aws.amazon.com/bedrock/latest/userguide/model-customization-import-model.html) | +| Note | Qwen2 and Qwen3 architectures are mostly similar. The main difference is in the response format: Qwen2 uses "text" field while Qwen3 uses "generation" field. | + + + + +```python +from litellm import completion +import os + +response = completion( + model="bedrock/qwen2/arn:aws:bedrock:us-east-1:086734376398:imported-model/your-qwen2-model", # bedrock/qwen2/{your-model-arn} + messages=[{"role": "user", "content": "Tell me a joke"}], + max_tokens=100, + temperature=0.7 +) +``` + + + + + +**1. Add to config** + +```yaml +model_list: + - model_name: Qwen2-72B + litellm_params: + model: bedrock/qwen2/arn:aws:bedrock:us-east-1:086734376398:imported-model/your-qwen2-model + +``` + +**2. Start proxy** + +```bash +litellm --config /path/to/config.yaml + +# RUNNING at http://0.0.0.0:4000 +``` + +**3. Test it!** + +```bash +curl --location 'http://0.0.0.0:4000/chat/completions' \ + --header 'Authorization: Bearer sk-1234' \ + --header 'Content-Type: application/json' \ + --data '{ + "model": "Qwen2-72B", # 👈 the 'model_name' in config + "messages": [ + { + "role": "user", + "content": "what llm are you" + } + ], + }' +``` + + + + ### OpenAI-Compatible Imported Models (Qwen 2.5 VL, etc.) Use this route for Bedrock imported models that follow the **OpenAI Chat Completions API spec**. This includes models like Qwen 2.5 VL that accept OpenAI-formatted messages with support for vision (images), tool calling, and other OpenAI features. diff --git a/litellm/__init__.py b/litellm/__init__.py index 3d40a9b15cf..03e4cee9ff3 100644 --- a/litellm/__init__.py +++ b/litellm/__init__.py @@ -1163,6 +1163,9 @@ from .llms.bedrock.chat.invoke_transformations.amazon_ai21_transformation import from .llms.bedrock.chat.invoke_transformations.amazon_nova_transformation import ( AmazonInvokeNovaConfig, ) +from .llms.bedrock.chat.invoke_transformations.amazon_qwen2_transformation import ( + AmazonQwen2Config, +) from .llms.bedrock.chat.invoke_transformations.amazon_qwen3_transformation import ( AmazonQwen3Config, ) diff --git a/litellm/constants.py b/litellm/constants.py index 5ab56bcaa2f..fa9f1d527af 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -876,6 +876,7 @@ BEDROCK_INVOKE_PROVIDERS_LITERAL = Literal[ "nova", "deepseek_r1", "qwen3", + "qwen2", "twelvelabs", "openai", ] diff --git a/litellm/llms/bedrock/chat/invoke_transformations/amazon_qwen2_transformation.py b/litellm/llms/bedrock/chat/invoke_transformations/amazon_qwen2_transformation.py new file mode 100644 index 00000000000..c532d8ea27c --- /dev/null +++ b/litellm/llms/bedrock/chat/invoke_transformations/amazon_qwen2_transformation.py @@ -0,0 +1,98 @@ +""" +Handles transforming requests for `bedrock/invoke/{qwen2} models` + +Inherits from `AmazonQwen3Config` since Qwen2 and Qwen3 architectures are mostly similar. +The main difference is in the response format: Qwen2 uses "text" field while Qwen3 uses "generation" field. + +Qwen2 + Invoke API Tutorial: https://docs.aws.amazon.com/bedrock/latest/userguide/invoke-imported-model.html +""" + +from typing import Any, List, Optional + +import httpx + +from litellm.llms.bedrock.chat.invoke_transformations.amazon_qwen3_transformation import ( + AmazonQwen3Config, +) +from litellm.llms.bedrock.chat.invoke_transformations.base_invoke_transformation import ( + LiteLLMLoggingObj, +) +from litellm.types.llms.openai import AllMessageValues +from litellm.types.utils import ModelResponse + + +class AmazonQwen2Config(AmazonQwen3Config): + """ + Config for sending `qwen2` requests to `/bedrock/invoke/` + + Inherits from AmazonQwen3Config since Qwen2 and Qwen3 architectures are mostly similar. + The main difference is in the response format: Qwen2 uses "text" field while Qwen3 uses "generation" field. + + Reference: https://docs.aws.amazon.com/bedrock/latest/userguide/invoke-imported-model.html + """ + + 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 Qwen2 Bedrock response to OpenAI format + + Qwen2 uses "text" field, but we also support "generation" field for compatibility. + """ + try: + if hasattr(raw_response, 'json'): + response_data = raw_response.json() + else: + response_data = raw_response + + # Extract the generated text - Qwen2 uses "text" field, but also support "generation" for compatibility + generated_text = response_data.get("generation", "") or response_data.get("text", "") + + # Clean up the response (remove assistant start token if present) + if generated_text.startswith("<|im_start|>assistant\n"): + generated_text = generated_text[len("<|im_start|>assistant\n"):] + if generated_text.endswith("<|im_end|>"): + generated_text = generated_text[:-len("<|im_end|>")] + + # Set the content in the existing model_response structure + if hasattr(model_response, 'choices') and len(model_response.choices) > 0: + choice = model_response.choices[0] + if hasattr(choice, 'message'): + choice.message.content = generated_text + choice.finish_reason = "stop" + else: + # Handle streaming choices + choice.delta.content = generated_text + choice.finish_reason = "stop" + + # Set usage information if available in response + if "usage" in response_data: + usage_data = response_data["usage"] + if hasattr(model_response, 'usage'): + model_response.usage.prompt_tokens = usage_data.get("prompt_tokens", 0) + model_response.usage.completion_tokens = usage_data.get("completion_tokens", 0) + model_response.usage.total_tokens = usage_data.get("total_tokens", 0) + + return model_response + + except Exception as e: + if logging_obj: + logging_obj.post_call( + input=messages, + api_key=api_key, + original_response=raw_response, + additional_args={"error": str(e)}, + ) + raise e + diff --git a/litellm/llms/bedrock/common_utils.py b/litellm/llms/bedrock/common_utils.py index 03ca7a5ad29..21a78c30343 100644 --- a/litellm/llms/bedrock/common_utils.py +++ b/litellm/llms/bedrock/common_utils.py @@ -635,6 +635,8 @@ def get_bedrock_chat_config(model: str): return litellm.AmazonInvokeNovaConfig() elif bedrock_invoke_provider == "qwen3": return litellm.AmazonQwen3Config() + elif bedrock_invoke_provider == "qwen2": + return litellm.AmazonQwen2Config() elif bedrock_invoke_provider == "twelvelabs": return litellm.AmazonTwelveLabsPegasusConfig() else: diff --git a/tests/test_litellm/llms/bedrock/chat/invoke_transformations/test_amazon_qwen2_transformation.py b/tests/test_litellm/llms/bedrock/chat/invoke_transformations/test_amazon_qwen2_transformation.py new file mode 100644 index 00000000000..737e1279e65 --- /dev/null +++ b/tests/test_litellm/llms/bedrock/chat/invoke_transformations/test_amazon_qwen2_transformation.py @@ -0,0 +1,292 @@ +import asyncio +import json +import os +import sys +from unittest.mock import Mock + +import pytest + +# Ensure the project root is on the import path so `litellm` can be imported when +# tests are executed from any working directory. +sys.path.insert(0, os.path.abspath("../../../../../..")) + +from litellm.llms.bedrock.chat.invoke_transformations.amazon_qwen2_transformation import ( + AmazonQwen2Config, +) +from litellm.types.utils import ModelResponse + + +def test_qwen2_get_supported_params(): + """Test that Qwen2 config returns correct supported parameters""" + config = AmazonQwen2Config() + params = config.get_supported_openai_params(model="qwen2/test-model") + + expected_params = ["max_tokens", "temperature", "top_p", "top_k", "stop", "stream"] + for param in expected_params: + assert param in params + + +def test_qwen2_map_openai_params(): + """Test that OpenAI parameters are correctly mapped to Qwen2 format""" + config = AmazonQwen2Config() + non_default_params = { + "max_tokens": 100, + "temperature": 0.7, + "top_p": 0.9, + "top_k": 40, + "stop": ["
    ", "<|im_end|>"], + "stream": True + } + optional_params = {} + + result = config.map_openai_params( + non_default_params=non_default_params, + optional_params=optional_params, + model="qwen2/test-model", + drop_params=False + ) + + assert result["max_tokens"] == 100 + assert result["temperature"] == 0.7 + assert result["top_p"] == 0.9 + assert result["top_k"] == 40 + assert result["stop"] == ["
    ", "<|im_end|>"] + assert result["stream"] is True + + +def test_qwen2_convert_messages_to_prompt(): + """Test that messages are correctly converted to Qwen2 prompt format""" + config = AmazonQwen2Config() + + messages = [ + {"role": "system", "content": "You are a helpful assistant."}, + {"role": "user", "content": "Hello, how are you?"}, + {"role": "assistant", "content": "I'm doing well, thank you!"}, + {"role": "user", "content": "What's the weather like?"} + ] + + prompt = config._convert_messages_to_prompt(messages) + + expected_prompt = """<|im_start|>system +You are a helpful assistant.<|im_end|> +<|im_start|>user +Hello, how are you?<|im_end|> +<|im_start|>assistant +I'm doing well, thank you!<|im_end|> +<|im_start|>user +What's the weather like?<|im_end|> +<|im_start|>assistant +""" + + assert prompt == expected_prompt + + +def test_qwen2_transform_request(): + """Test that the request is correctly transformed to Qwen2 format""" + config = AmazonQwen2Config() + + messages = [ + {"role": "user", "content": "Hello, world!"} + ] + + optional_params = { + "max_tokens": 50, + "temperature": 0.8, + "top_p": 0.9 + } + + request_body = config.transform_request( + model="qwen2/test-model", + messages=messages, + optional_params=optional_params, + litellm_params={}, + headers={} + ) + + assert "prompt" in request_body + assert request_body["max_gen_len"] == 50 + assert request_body["temperature"] == 0.8 + assert request_body["top_p"] == 0.9 + + # Check that the prompt contains the expected format + assert "<|im_start|>user" in request_body["prompt"] + assert "Hello, world!" in request_body["prompt"] + assert "<|im_end|>" in request_body["prompt"] + + +def test_qwen2_transform_response_with_text_field(): + """Test that Qwen2 response with 'text' field is correctly transformed to OpenAI format""" + config = AmazonQwen2Config() + + # Mock response data with 'text' field (Qwen2 format) + mock_response_data = { + "text": "<|im_start|>assistant\nHello! How can I help you today?<|im_end|>", + "usage": { + "prompt_tokens": 10, + "completion_tokens": 15, + "total_tokens": 25 + } + } + + # Mock the raw response + mock_raw_response = Mock() + mock_raw_response.json.return_value = mock_response_data + + model_response = ModelResponse() + messages = [{"role": "user", "content": "Hello!"}] + + result = config.transform_response( + model="qwen2/test-model", + messages=messages, + raw_response=mock_raw_response, + model_response=model_response, + logging_obj=Mock(), + optional_params={}, + litellm_params={}, + api_key="test-key", + request_data={}, + encoding=None + ) + + # Check that the response is correctly formatted + assert len(result.choices) == 1 + assert result.choices[0]["message"]["role"] == "assistant" + assert result.choices[0]["message"]["content"] == "Hello! How can I help you today?" + assert result.choices[0]["finish_reason"] == "stop" + + # Check usage information + assert result.usage["prompt_tokens"] == 10 + assert result.usage["completion_tokens"] == 15 + assert result.usage["total_tokens"] == 25 + + +def test_qwen2_transform_response_with_generation_field(): + """Test that Qwen2 response also supports 'generation' field for compatibility""" + config = AmazonQwen2Config() + + # Mock response data with 'generation' field (Qwen3 format, but Qwen2 should handle it) + mock_response_data = { + "generation": "<|im_start|>assistant\nHello! How can I help you today?<|im_end|>", + "usage": { + "prompt_tokens": 10, + "completion_tokens": 15, + "total_tokens": 25 + } + } + + # Mock the raw response + mock_raw_response = Mock() + mock_raw_response.json.return_value = mock_response_data + + model_response = ModelResponse() + messages = [{"role": "user", "content": "Hello!"}] + + result = config.transform_response( + model="qwen2/test-model", + messages=messages, + raw_response=mock_raw_response, + model_response=model_response, + logging_obj=Mock(), + optional_params={}, + litellm_params={}, + api_key="test-key", + request_data={}, + encoding=None + ) + + # Check that the response is correctly formatted + assert len(result.choices) == 1 + assert result.choices[0]["message"]["role"] == "assistant" + assert result.choices[0]["message"]["content"] == "Hello! How can I help you today?" + assert result.choices[0]["finish_reason"] == "stop" + + +def test_qwen2_transform_response_prefers_generation_over_text(): + """Test that Qwen2 prefers 'generation' field over 'text' when both are present""" + config = AmazonQwen2Config() + + # Mock response data with both fields + mock_response_data = { + "generation": "This is from generation field", + "text": "This is from text field", + "usage": { + "prompt_tokens": 10, + "completion_tokens": 15, + "total_tokens": 25 + } + } + + # Mock the raw response + mock_raw_response = Mock() + mock_raw_response.json.return_value = mock_response_data + + model_response = ModelResponse() + messages = [{"role": "user", "content": "Hello!"}] + + result = config.transform_response( + model="qwen2/test-model", + messages=messages, + raw_response=mock_raw_response, + model_response=model_response, + logging_obj=Mock(), + optional_params={}, + litellm_params={}, + api_key="test-key", + request_data={}, + encoding=None + ) + + # Should prefer 'generation' field + assert result.choices[0]["message"]["content"] == "This is from generation field" + + +def test_qwen2_transform_response_without_usage(): + """Test response transformation when usage information is not provided""" + config = AmazonQwen2Config() + + # Mock response data without usage + mock_response_data = { + "text": "Hello! How can I help you today?" + } + + # Mock the raw response + mock_raw_response = Mock() + mock_raw_response.json.return_value = mock_response_data + + model_response = ModelResponse() + messages = [{"role": "user", "content": "Hello!"}] + + result = config.transform_response( + model="qwen2/test-model", + messages=messages, + raw_response=mock_raw_response, + model_response=model_response, + logging_obj=Mock(), + optional_params={}, + litellm_params={}, + api_key="test-key", + request_data={}, + encoding=None + ) + + # Check that the response is correctly formatted + assert len(result.choices) == 1 + assert result.choices[0]["message"]["role"] == "assistant" + assert result.choices[0]["message"]["content"] == "Hello! How can I help you today?" + assert result.choices[0]["finish_reason"] == "stop" + + +def test_qwen2_provider_detection(): + """Test that Qwen2 provider is correctly detected from model names""" + from litellm.utils import ProviderConfigManager + from litellm.types.utils import LlmProviders + + # Test with qwen2/ prefix + config = ProviderConfigManager.get_provider_chat_config( + model="qwen2/arn:aws:bedrock:us-east-1:123456789012:imported-model/test-qwen2", + provider=LlmProviders.BEDROCK + ) + + assert config is not None + assert isinstance(config, AmazonQwen2Config) + From c634c08818cdaa66c05852de9d3fa4717a693c0c Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Wed, 3 Dec 2025 21:51:45 -0800 Subject: [PATCH 096/107] Find inputs by placeholder --- .../e2e_ui_tests/login_to_ui.spec.ts | 4 ++-- .../e2e_ui_tests/search_users.spec.ts | 16 ++++++++++------ .../e2e_ui_tests/view_internal_user.spec.ts | 4 ++-- tests/proxy_admin_ui_tests/utils/login.ts | 8 +++++--- 4 files changed, 19 insertions(+), 13 deletions(-) diff --git a/tests/proxy_admin_ui_tests/e2e_ui_tests/login_to_ui.spec.ts b/tests/proxy_admin_ui_tests/e2e_ui_tests/login_to_ui.spec.ts index c4a1dc77c33..e5a397a6a66 100644 --- a/tests/proxy_admin_ui_tests/e2e_ui_tests/login_to_ui.spec.ts +++ b/tests/proxy_admin_ui_tests/e2e_ui_tests/login_to_ui.spec.ts @@ -16,10 +16,10 @@ test("admin login test", async ({ page }) => { await page.screenshot({ path: "test-results/login_before.png" }); // Enter "admin" in the username input field - await page.fill('input[name="username"]', "admin"); + await page.fill('input[placeholder="Enter your username"]', "admin"); // Enter "gm" in the password input field - await page.fill('input[name="password"]', "gm"); + await page.fill('input[placeholder="Enter your password"]', "gm"); page.screenshot({ path: "test-results/login_after_inputs.png" }); diff --git a/tests/proxy_admin_ui_tests/e2e_ui_tests/search_users.spec.ts b/tests/proxy_admin_ui_tests/e2e_ui_tests/search_users.spec.ts index 66d0ad29658..d72c44ab8cc 100644 --- a/tests/proxy_admin_ui_tests/e2e_ui_tests/search_users.spec.ts +++ b/tests/proxy_admin_ui_tests/e2e_ui_tests/search_users.spec.ts @@ -27,11 +27,13 @@ test("user search test", async ({ page }) => { page.screenshot({ path: "test-results/search_users_before_login.png" }); // Wait for login form to be visible - await page.waitForSelector('input[name="username"]', { timeout: 10000 }); + await page.waitForSelector('input[placeholder="Enter your username"]', { + timeout: 10000, + }); console.log("Login form is visible"); - await page.fill('input[name="username"]', "admin"); - await page.fill('input[name="password"]', "gm"); + await page.fill('input[placeholder="Enter your username"]', "admin"); + await page.fill('input[placeholder="Enter your password"]', "gm"); console.log("Filled login credentials"); const loginButton = page.getByRole("button", { name: "Login" }); @@ -135,11 +137,13 @@ test("user filter test", async ({ page }) => { console.log("Navigated to login page"); // Wait for login form to be visible - await page.waitForSelector('input[name="username"]', { timeout: 10000 }); + await page.waitForSelector('input[placeholder="Enter your username"]', { + timeout: 10000, + }); console.log("Login form is visible"); - await page.fill('input[name="username"]', "admin"); - await page.fill('input[name="password"]', "gm"); + await page.fill('input[placeholder="Enter your username"]', "admin"); + await page.fill('input[placeholder="Enter your password"]', "gm"); console.log("Filled login credentials"); const loginButton = page.getByRole("button", { name: "Login" }); diff --git a/tests/proxy_admin_ui_tests/e2e_ui_tests/view_internal_user.spec.ts b/tests/proxy_admin_ui_tests/e2e_ui_tests/view_internal_user.spec.ts index a7843d52916..2aae9e2bb54 100644 --- a/tests/proxy_admin_ui_tests/e2e_ui_tests/view_internal_user.spec.ts +++ b/tests/proxy_admin_ui_tests/e2e_ui_tests/view_internal_user.spec.ts @@ -12,10 +12,10 @@ test("view internal user page", async ({ page }) => { page.screenshot({ path: "test-results/view_internal_user_before_login.png" }); // Enter "admin" in the username input field - await page.fill('input[name="username"]', "admin"); + await page.fill('input[placeholder="Enter your username"]', "admin"); // Enter "gm" in the password input field - await page.fill('input[name="password"]', "gm"); + await page.fill('input[placeholder="Enter your password"]', "gm"); // Click the login button const loginButton = page.getByRole("button", { name: "Login" }); diff --git a/tests/proxy_admin_ui_tests/utils/login.ts b/tests/proxy_admin_ui_tests/utils/login.ts index 5749006cdea..25858d9f570 100644 --- a/tests/proxy_admin_ui_tests/utils/login.ts +++ b/tests/proxy_admin_ui_tests/utils/login.ts @@ -8,11 +8,13 @@ export async function loginToUI(page: Page) { page.screenshot({ path: "test-results/login_utils_before.png" }); // Wait for login form to be visible - await page.waitForSelector('input[name="username"]', { timeout: 10000 }); + await page.waitForSelector('input[placeholder="Enter your username"]', { + timeout: 10000, + }); console.log("Login form is visible"); - await page.fill('input[name="username"]', "admin"); - await page.fill('input[name="password"]', "gm"); + await page.fill('input[placeholder="Enter your username"]', "admin"); + await page.fill('input[placeholder="Enter your password"]', "gm"); console.log("Filled login credentials"); const loginButton = page.getByRole("button", { name: "Login" }); From dc7c2b9b058a2fd3be3c2ac555f1dd3b6709a4a4 Mon Sep 17 00:00:00 2001 From: Krish Dholakia Date: Wed, 3 Dec 2025 21:59:45 -0800 Subject: [PATCH 097/107] Update docs to link agent hub (#17462) * Docs: Add AI Hub agent registry documentation Co-authored-by: krrishdholakia * Fix: Update AI Hub link in A2A documentation Co-authored-by: krrishdholakia --------- Co-authored-by: Cursor Agent --- docs/my-website/docs/a2a.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/my-website/docs/a2a.md b/docs/my-website/docs/a2a.md index 7851f50d88e..ef19e22dab3 100644 --- a/docs/my-website/docs/a2a.md +++ b/docs/my-website/docs/a2a.md @@ -196,3 +196,9 @@ LiteLLM follows the [A2A JSON-RPC 2.0 specification](https://github.com/google/A } } ``` + +## Agent Registry + +Want to create a central registry so your team can discover what agents are available within your company? + +Use the [AI Hub](./proxy/ai_hub) to make agents public and discoverable across your organization. This allows developers to browse available agents without needing to rebuild them. From 7519a3e30b640683111e19693bb14896c1e67aff Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Wed, 3 Dec 2025 22:08:41 -0800 Subject: [PATCH 098/107] Change credentials to use react-query --- .../hooks/credentials/useCredentials.ts | 13 ++ .../ModelsAndEndpointsView.tsx | 23 +--- .../components/model_add/credentials.test.tsx | 111 ++++++++++++------ .../src/components/model_add/credentials.tsx | 31 ++--- 4 files changed, 101 insertions(+), 77 deletions(-) create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/hooks/credentials/useCredentials.ts diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/credentials/useCredentials.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/credentials/useCredentials.ts new file mode 100644 index 00000000000..aa0a6c2c9fb --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/credentials/useCredentials.ts @@ -0,0 +1,13 @@ +import { credentialListCall, CredentialsResponse } from "@/components/networking"; +import { useQuery } from "@tanstack/react-query"; +import { createQueryKeys } from "../common/queryKeysFactory"; + +const credentialsKeys = createQueryKeys("credentials"); + +export const useCredentials = (accessToken: string | null) => { + return useQuery({ + queryKey: credentialsKeys.list({}), + queryFn: async () => await credentialListCall(accessToken!), + enabled: Boolean(accessToken), + }); +}; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView.tsx index 4f33ba6585d..4cd1f549e69 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect, useRef } from "react"; import { Text, Grid, Col } from "@tremor/react"; import { useQueryClient } from "@tanstack/react-query"; -import { CredentialItem, credentialListCall, CredentialsResponse } from "@/components/networking"; +import { CredentialItem } from "@/components/networking"; import { handleAddModelSubmit } from "@/components/add_model/handle_add_model_submit"; @@ -23,6 +23,7 @@ import { allEndUsersCall, } from "@/components/networking"; import { useModelsInfo } from "@/app/(dashboard)/hooks/models/useModels"; +import { useCredentials } from "@/app/(dashboard)/hooks/credentials/useCredentials"; import { Form } from "antd"; import { Typography } from "antd"; import { RefreshIcon } from "@heroicons/react/outline"; @@ -134,8 +135,6 @@ const ModelsAndEndpointsView: React.FC = ({ const [allEndUsers, setAllEndUsers] = useState([]); - const [credentialsList, setCredentialsList] = useState([]); - // Model Group Alias state const [modelGroupAlias, setModelGroupAlias] = useState<{ [key: string]: string }>({}); @@ -160,21 +159,14 @@ const ModelsAndEndpointsView: React.FC = ({ isLoading: isLoadingModels, refetch: refetchModels, } = useModelsInfo(accessToken, userID, userRole); + const { data: credentialsResponse } = useCredentials(accessToken); + const credentialsList = credentialsResponse?.credentials || []; const setProviderModelsFn = (provider: Providers) => { const _providerModels = getProviderModels(provider, modelMap); setProviderModels(_providerModels); }; - const fetchCredentials = async (accessToken: string) => { - try { - const response: CredentialsResponse = await credentialListCall(accessToken); - setCredentialsList(response.credentials); - } catch (error) { - console.error("Error fetching credentials:", error); - } - }; - useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { @@ -686,12 +678,7 @@ const ModelsAndEndpointsView: React.FC = ({ /> - + ({ + default: () => mockUseAuthorized(), +})); + +vi.mock("@/app/(dashboard)/hooks/credentials/useCredentials", () => ({ + useCredentials: () => mockUseCredentials(), +})); + +const createQueryClient = () => + new QueryClient({ + defaultOptions: { + queries: { + retry: false, + gcTime: 0, + }, + }, + }); + describe("CredentialsPanel", () => { it("should render", () => { - const fetchCredentials = vi.fn(() => Promise.resolve()); + mockUseAuthorized.mockReturnValue({ accessToken: "test-token" }); + mockUseCredentials.mockReturnValue({ + data: { credentials: [] }, + refetch: vi.fn(), + }); 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(fetchCredentials).toHaveBeenCalledWith("test-token"); - }); - }); - it("should display provided credentials", () => { - const fetchCredentials = vi.fn(() => Promise.resolve()); const credentials: CredentialItem[] = [ { credential_name: "openai-key", @@ -49,30 +54,58 @@ describe("CredentialsPanel", () => { }, ]; + mockUseAuthorized.mockReturnValue({ accessToken: "test-token" }); + mockUseCredentials.mockReturnValue({ + data: { credentials }, + refetch: vi.fn(), + }); + render( - , + + + , ); expect(screen.getByText("openai-key")).toBeInTheDocument(); }); it("should display empty state when no credentials are provided", () => { - const fetchCredentials = vi.fn(() => Promise.resolve()); + mockUseAuthorized.mockReturnValue({ accessToken: "test-token" }); + mockUseCredentials.mockReturnValue({ + data: { credentials: [] }, + refetch: vi.fn(), + }); render( - , + + + , ); expect(screen.getByText("No credentials configured")).toBeInTheDocument(); }); + + it("should open add modal when add button is clicked", async () => { + mockUseAuthorized.mockReturnValue({ accessToken: "test-token" }); + mockUseCredentials.mockReturnValue({ + data: { credentials: [] }, + refetch: vi.fn(), + }); + + render( + + + , + ); + + const addButton = screen.getByRole("button", { name: /add credential/i }); + + act(() => { + fireEvent.click(addButton); + }); + + await waitFor(() => { + expect(screen.getByText("Add New Credential")).toBeInTheDocument(); + }); + }); }); diff --git a/ui/litellm-dashboard/src/components/model_add/credentials.tsx b/ui/litellm-dashboard/src/components/model_add/credentials.tsx index 1bba14bef97..3887e340daa 100644 --- a/ui/litellm-dashboard/src/components/model_add/credentials.tsx +++ b/ui/litellm-dashboard/src/components/model_add/credentials.tsx @@ -19,24 +19,22 @@ import { } from "@tremor/react"; import { Form } from "antd"; import { UploadProps } from "antd/es/upload"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import DeleteResourceModal from "../common_components/DeleteResourceModal"; import NotificationsManager from "../molecules/notifications_manager"; import AddCredentialsTab from "./AddCredentialModal"; import EditCredentialsModal from "./EditCredentialModal"; +import { useCredentials } from "@/app/(dashboard)/hooks/credentials/useCredentials"; +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; interface CredentialsPanelProps { - accessToken: string | null; uploadProps: UploadProps; - credentialList: CredentialItem[]; - fetchCredentials: (accessToken: string) => Promise; } -const CredentialsPanel: React.FC = ({ - accessToken, - uploadProps, - credentialList, - fetchCredentials, -}) => { +const CredentialsPanel: React.FC = ({ uploadProps }) => { + const { accessToken } = useAuthorized(); + const { data: credentialsResponse, refetch: refetchCredentials } = useCredentials(accessToken); + const credentialList = credentialsResponse?.credentials || []; + const [isAddModalOpen, setIsAddModalOpen] = useState(false); const [isUpdateModalOpen, setIsUpdateModalOpen] = useState(false); const [selectedCredential, setSelectedCredential] = useState(null); @@ -66,7 +64,7 @@ const CredentialsPanel: React.FC = ({ await credentialUpdateCall(accessToken, values.credential_name, newCredential); NotificationsManager.success("Credential updated successfully"); setIsUpdateModalOpen(false); - await fetchCredentials(accessToken); + await refetchCredentials(); }; const handleAddCredential = async (values: any) => { @@ -90,16 +88,9 @@ const CredentialsPanel: React.FC = ({ await credentialCreateCall(accessToken, newCredential); NotificationsManager.success("Credential added successfully"); setIsAddModalOpen(false); - await fetchCredentials(accessToken); + await refetchCredentials(); }; - useEffect(() => { - if (!accessToken) { - return; - } - fetchCredentials(accessToken); - }, [accessToken]); - const renderProviderBadge = (provider: string) => { const providerColors: Record = { openai: "blue", @@ -124,7 +115,7 @@ const CredentialsPanel: React.FC = ({ try { await credentialDeleteCall(accessToken, credentialToDelete.credential_name); NotificationsManager.success("Credential deleted successfully"); - await fetchCredentials(accessToken); + await refetchCredentials(); } catch (error) { NotificationsManager.error("Failed to delete credential"); } finally { From b8190c6b6dff54a499c8326e4c67976968af08a8 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Wed, 3 Dec 2025 22:15:19 -0800 Subject: [PATCH 099/107] resolving build issues --- .../ModelsAndEndpointsView.tsx | 59 +++++++++---------- .../components/templates/model_dashboard.tsx | 7 +-- 2 files changed, 29 insertions(+), 37 deletions(-) diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView.tsx index 4cd1f549e69..8135580e29e 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView.tsx @@ -1,48 +1,45 @@ -import React, { useState, useEffect, useRef } from "react"; -import { Text, Grid, Col } from "@tremor/react"; import { useQueryClient } from "@tanstack/react-query"; -import { CredentialItem } from "@/components/networking"; +import { Col, Grid, Text } from "@tremor/react"; +import React, { useEffect, useRef, useState } from "react"; import { handleAddModelSubmit } from "@/components/add_model/handle_add_model_submit"; +import { useCredentials } from "@/app/(dashboard)/hooks/credentials/useCredentials"; +import { useModelsInfo } from "@/app/(dashboard)/hooks/models/useModels"; +import { Team } from "@/components/key_team_helpers/key_list"; import CredentialsPanel from "@/components/model_add/credentials"; -import { getDisplayModelName } from "@/components/view_model/model_name_display"; -import { TabPanel, TabPanels, TabGroup, TabList, Tab, Icon } from "@tremor/react"; -import { DateRangePickerValue } from "@tremor/react"; import { - modelCostMap, - modelMetricsCall, - streamingModelMetricsCall, - modelExceptionsCall, - modelMetricsSlowResponsesCall, - getCallbacksCall, - setCallbacksCall, - modelSettingsCall, adminGlobalActivityExceptions, adminGlobalActivityExceptionsPerDeployment, allEndUsersCall, + getCallbacksCall, + modelCostMap, + modelExceptionsCall, + modelMetricsCall, + modelMetricsSlowResponsesCall, + modelSettingsCall, + setCallbacksCall, + streamingModelMetricsCall, } from "@/components/networking"; -import { useModelsInfo } from "@/app/(dashboard)/hooks/models/useModels"; -import { useCredentials } from "@/app/(dashboard)/hooks/credentials/useCredentials"; -import { Form } from "antd"; -import { Typography } from "antd"; -import { RefreshIcon } from "@heroicons/react/outline"; -import type { UploadProps } from "antd"; -import { Team } from "@/components/key_team_helpers/key_list"; -import TeamInfoView from "../../../components/team/team_info"; import { Providers, getPlaceholder, getProviderModels } from "@/components/provider_info_helpers"; -import ModelInfoView from "../../../components/model_info_view"; +import { getDisplayModelName } from "@/components/view_model/model_name_display"; +import { RefreshIcon } from "@heroicons/react/outline"; +import { DateRangePickerValue, Icon, Tab, TabGroup, TabList, TabPanel, TabPanels } from "@tremor/react"; +import type { UploadProps } from "antd"; +import { Form, Typography } from "antd"; import AddModelTab from "../../../components/add_model/add_model_tab"; +import ModelInfoView from "../../../components/model_info_view"; +import TeamInfoView from "../../../components/team/team_info"; -import HealthCheckComponent from "../../../components/model_dashboard/HealthCheckComponent"; -import PassThroughSettings from "../../../components/pass_through_settings"; -import ModelGroupAliasSettings from "../../../components/model_group_alias_settings"; -import { all_admin_roles } from "@/utils/roles"; -import NotificationsManager from "../../../components/molecules/notifications_manager"; import AllModelsTab from "@/app/(dashboard)/models-and-endpoints/components/AllModelsTab"; -import PriceDataManagementTab from "@/app/(dashboard)/models-and-endpoints/components/PriceDataManagementTab"; -import ModelRetrySettingsTab from "@/app/(dashboard)/models-and-endpoints/components/ModelRetrySettingsTab"; import ModelAnalyticsTab from "@/app/(dashboard)/models-and-endpoints/components/ModelAnalyticsTab/ModelAnalyticsTab"; +import ModelRetrySettingsTab from "@/app/(dashboard)/models-and-endpoints/components/ModelRetrySettingsTab"; +import PriceDataManagementTab from "@/app/(dashboard)/models-and-endpoints/components/PriceDataManagementTab"; +import { all_admin_roles } from "@/utils/roles"; +import HealthCheckComponent from "../../../components/model_dashboard/HealthCheckComponent"; +import ModelGroupAliasSettings from "../../../components/model_group_alias_settings"; +import NotificationsManager from "../../../components/molecules/notifications_manager"; +import PassThroughSettings from "../../../components/pass_through_settings"; interface ModelDashboardProps { accessToken: string | null; @@ -678,7 +675,7 @@ const ModelsAndEndpointsView: React.FC = ({ /> - + = ({ /> - + Date: Wed, 3 Dec 2025 22:20:04 -0800 Subject: [PATCH 100/107] view_user_info debugging --- .../e2e_ui_tests/view_user_info.spec.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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 742dbd14d44..53c73593882 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 @@ -5,7 +5,9 @@ test.describe("User Info View", () => { test.beforeEach(async ({ page }) => { await loginToUI(page); // Navigate to users page + page.screenshot({ path: "test-results/view_user_info_before_nav.png" }); await page.goto("http://localhost:4000/ui?page=users"); + page.screenshot({ path: "test-results/view_user_info_after_nav.png" }); }); test("should display user info when clicking on user ID", async ({ @@ -16,9 +18,12 @@ test.describe("User Info View", () => { state: "hidden", timeout: 10000, }); + page.screenshot({ path: "test-results/view_user_info_after_loading.png" }); // Wait for users table to load await page.waitForSelector("table"); - + page.screenshot({ + path: "test-results/view_user_info_after_table_load.png", + }); // Get the first user ID cell const firstUserIdCell = page.locator( "table tbody tr:first-child td:first-child" From 19dc5d750c409a88a5b64e05505c10a89a8a7c56 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Wed, 3 Dec 2025 23:10:32 -0800 Subject: [PATCH 101/107] Adding waits --- tests/proxy_admin_ui_tests/e2e_ui_tests/view_user_info.spec.ts | 2 ++ 1 file changed, 2 insertions(+) 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 53c73593882..cba7ddad7cc 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 @@ -18,6 +18,7 @@ test.describe("User Info View", () => { state: "hidden", timeout: 10000, }); + await page.waitForLoadState("networkidle"); page.screenshot({ path: "test-results/view_user_info_after_loading.png" }); // Wait for users table to load await page.waitForSelector("table"); @@ -33,6 +34,7 @@ test.describe("User Info View", () => { // Click on the user ID await firstUserIdCell.click(); + await page.waitForLoadState("networkidle"); // Check for tabs await expect(page.locator('button:has-text("Overview")')).toBeVisible({ From 0483336c53cfd6249d31bd44ba064a2fa6246689 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Wed, 3 Dec 2025 23:21:09 -0800 Subject: [PATCH 102/107] change test to follow other logins --- .../e2e_ui_tests/view_user_info.spec.ts | 36 ++++++++++++++----- 1 file changed, 28 insertions(+), 8 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 cba7ddad7cc..22d37b19ecd 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 @@ -2,17 +2,37 @@ import { test, expect } from "@playwright/test"; import { loginToUI } from "../utils/login"; test.describe("User Info View", () => { - test.beforeEach(async ({ page }) => { - await loginToUI(page); - // Navigate to users page - page.screenshot({ path: "test-results/view_user_info_before_nav.png" }); - await page.goto("http://localhost:4000/ui?page=users"); - page.screenshot({ path: "test-results/view_user_info_after_nav.png" }); - }); - test("should display user info when clicking on user ID", async ({ page, }) => { + test.setTimeout(60000); + + // Enable console logging + page.on("console", (msg) => console.log("PAGE LOG:", msg.text())); + + // Login first + await page.goto("http://localhost:4000/ui"); + await page.waitForLoadState("networkidle"); + console.log("Navigated to login page"); + + // Wait for login form to be visible + await page.waitForSelector('input[placeholder="Enter your username"]', { + timeout: 10000, + }); + console.log("Login form is visible"); + + await page.fill('input[placeholder="Enter your username"]', "admin"); + await page.fill('input[placeholder="Enter your password"]', "gm"); + console.log("Filled login credentials"); + + const loginButton = page.getByRole("button", { name: "Login" }); + await expect(loginButton).toBeEnabled(); + await loginButton.click(); + console.log("Clicked login button"); + + // Wait for navigation to complete and dashboard to load + await page.waitForLoadState("networkidle"); + await page.goto("http://localhost:4000/ui?page=users"); // Wait for loading state to disappear await page.waitForSelector('text="🚅 Loading users..."', { state: "hidden", From 59b32c28a860b0cdb43ee85c29983106441acefa Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Wed, 3 Dec 2025 23:37:15 -0800 Subject: [PATCH 103/107] Change selector in test --- .../e2e_ui_tests/view_user_info.spec.ts | 39 +++++++++++-------- 1 file changed, 23 insertions(+), 16 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 22d37b19ecd..adda3088f12 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 @@ -5,40 +5,47 @@ test.describe("User Info View", () => { test("should display user info when clicking on user ID", async ({ page, }) => { - test.setTimeout(60000); - - // Enable console logging - page.on("console", (msg) => console.log("PAGE LOG:", msg.text())); - - // Login first await page.goto("http://localhost:4000/ui"); await page.waitForLoadState("networkidle"); - console.log("Navigated to login page"); - // Wait for login form to be visible - await page.waitForSelector('input[placeholder="Enter your username"]', { - timeout: 10000, + page.screenshot({ + path: "test-results/view_user_info_before_login.png", }); - console.log("Login form is visible"); + // Enter "admin" in the username input field await page.fill('input[placeholder="Enter your username"]', "admin"); - await page.fill('input[placeholder="Enter your password"]', "gm"); - console.log("Filled login credentials"); + page.screenshot({ + path: "test-results/view_user_info_after_username_input.png", + }); + // Enter "gm" in the password input field + await page.fill('input[placeholder="Enter your password"]', "gm"); + page.screenshot({ + path: "test-results/view_user_info_after_password_input.png", + }); + + // Click the login button const loginButton = page.getByRole("button", { name: "Login" }); await expect(loginButton).toBeEnabled(); await loginButton.click(); - console.log("Clicked login button"); + page.screenshot({ + path: "test-results/view_user_info_after_login_button_click.png", + }); // Wait for navigation to complete and dashboard to load await page.waitForLoadState("networkidle"); - await page.goto("http://localhost:4000/ui?page=users"); + const tabElement = page.locator("span.ant-menu-title-content", { + hasText: "Internal User", + }); + await tabElement.click(); + page.screenshot({ + path: "test-results/view_user_info_after_internal_user_tab_click.png", + }); // Wait for loading state to disappear await page.waitForSelector('text="🚅 Loading users..."', { state: "hidden", timeout: 10000, }); - await page.waitForLoadState("networkidle"); page.screenshot({ path: "test-results/view_user_info_after_loading.png" }); // Wait for users table to load await page.waitForSelector("table"); From 5496e622c14eb3068d1292b2a8719119b021e762 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Wed, 3 Dec 2025 23:46:09 -0800 Subject: [PATCH 104/107] Unit tests --- ui/litellm-dashboard/tests/CreateKeyPage.expiredToken.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/litellm-dashboard/tests/CreateKeyPage.expiredToken.test.tsx b/ui/litellm-dashboard/tests/CreateKeyPage.expiredToken.test.tsx index 9287e203182..c3c5ae59237 100644 --- a/ui/litellm-dashboard/tests/CreateKeyPage.expiredToken.test.tsx +++ b/ui/litellm-dashboard/tests/CreateKeyPage.expiredToken.test.tsx @@ -193,7 +193,7 @@ describe("CreateKeyPage auth behavior", () => { // Assert: we eventually redirect to SSO login (single replace, not assign/href) await waitFor(() => { - expect(window.location.replace).toHaveBeenCalledWith("https://example.com/sso/key/generate"); + expect(window.location.replace).toHaveBeenCalledWith("https://example.com/ui/login"); }); // And we attempted to clear the cookie (defensive deletion) From edd392b50d611829d05ee64e45fab1aee4f0bb8c Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Thu, 4 Dec 2025 13:27:53 +0530 Subject: [PATCH 105/107] Add support for file content download for bedrock batches --- docs/my-website/docs/files_endpoints.md | 13 ++ .../docs/providers/bedrock_batches.md | 91 ++++++++ litellm/files/main.py | 17 +- litellm/llms/bedrock/files/handler.py | 206 ++++++++++++++++++ .../files/test_bedrock_files_integration.py | 110 ++++++++++ 5 files changed, 434 insertions(+), 3 deletions(-) create mode 100644 litellm/llms/bedrock/files/handler.py create mode 100644 tests/test_litellm/llms/bedrock/files/test_bedrock_files_integration.py diff --git a/docs/my-website/docs/files_endpoints.md b/docs/my-website/docs/files_endpoints.md index fc0484e9219..30677c748a9 100644 --- a/docs/my-website/docs/files_endpoints.md +++ b/docs/my-website/docs/files_endpoints.md @@ -301,6 +301,17 @@ content = await litellm.afile_content( print("file content=", content) ``` +**Get File Content (Bedrock)** +```python +# For Bedrock batch output files stored in S3 +content = await litellm.afile_content( + file_id="s3://bucket-name/path/to/file.jsonl", # S3 URI or unified file ID + custom_llm_provider="bedrock", + aws_region_name="us-west-2" +) +print("file content=", content.text) +``` + @@ -313,4 +324,6 @@ print("file content=", content) ### [Vertex AI](./providers/vertex#batch-apis) +### [Bedrock](./providers/bedrock_batches#4-retrieve-batch-results) + ## [Swagger API Reference](https://litellm-api.up.railway.app/#/files) diff --git a/docs/my-website/docs/providers/bedrock_batches.md b/docs/my-website/docs/providers/bedrock_batches.md index a1116f41076..19446fda837 100644 --- a/docs/my-website/docs/providers/bedrock_batches.md +++ b/docs/my-website/docs/providers/bedrock_batches.md @@ -172,6 +172,97 @@ curl http://localhost:4000/v1/batches \ +### 4. Retrieve batch results + +Once the batch job is completed, download the results from S3: + + + + +```python showLineNumbers title="bedrock_batch.py" +... +# Wait for batch completion (check status periodically) +batch_status = client.batches.retrieve(batch_id=batch.id) + +if batch_status.status == "completed": + # Download the output file + result = client.files.content( + file_id=batch_status.output_file_id, + extra_headers={"custom-llm-provider": "bedrock"} + ) + + # Save or process the results + with open("batch_output.jsonl", "wb") as f: + f.write(result.content) + + # Parse JSONL results + for line in result.text.strip().split('\n'): + record = json.loads(line) + print(f"Record ID: {record['recordId']}") + print(f"Output: {record.get('modelOutput', {})}") +``` + + + + +```bash showLineNumbers title="Download Batch Results" +# First retrieve batch to get output_file_id +curl http://localhost:4000/v1/batches/batch_abc123 \ + -H "Authorization: Bearer sk-1234" + +# Then download the output file +curl http://localhost:4000/v1/files/{output_file_id}/content \ + -H "Authorization: Bearer sk-1234" \ + -H "custom-llm-provider: bedrock" \ + -o batch_output.jsonl +``` + + + + +```python showLineNumbers title="bedrock_batch.py" +import litellm +from litellm import file_content + +# Download using litellm directly (bypasses proxy managed files) +result = file_content( + file_id=batch_status.output_file_id, # Can be S3 URI or unified file ID + custom_llm_provider="bedrock", + aws_region_name="us-west-2", +) + +# Process results +print(result.text) +``` + + + + +**Output Format:** + +The batch output file is in JSONL format with each line containing: + +```json +{ + "recordId": "request-1", + "modelInput": { + "messages": [...], + "max_tokens": 1000 + }, + "modelOutput": { + "content": [...], + "id": "msg_abc123", + "model": "claude-3-5-sonnet-20240620-v1:0", + "role": "assistant", + "stop_reason": "end_turn", + "usage": { + "input_tokens": 15, + "output_tokens": 10 + } + } +} +``` + ## FAQ ### Where are my files written? diff --git a/litellm/files/main.py b/litellm/files/main.py index 71139001e52..9378715a472 100644 --- a/litellm/files/main.py +++ b/litellm/files/main.py @@ -18,6 +18,7 @@ from litellm import get_secret_str from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj from litellm.llms.azure.files.handler import AzureOpenAIFilesAPI +from litellm.llms.bedrock.files.handler import BedrockFilesHandler from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler from litellm.llms.custom_httpx.llm_http_handler import BaseLLMHTTPHandler from litellm.llms.openai.openai import FileDeleted, FileObject, OpenAIFilesAPI @@ -47,6 +48,7 @@ base_llm_http_handler = BaseLLMHTTPHandler() openai_files_instance = OpenAIFilesAPI() azure_files_instance = AzureOpenAIFilesAPI() vertex_ai_files_instance = VertexAIFilesHandler() +bedrock_files_instance = BedrockFilesHandler() ################################################# @@ -755,7 +757,7 @@ def file_list( @client async def afile_content( file_id: str, - custom_llm_provider: Literal["openai", "azure", "vertex_ai", "hosted_vllm"] = "openai", + custom_llm_provider: Literal["openai", "azure", "vertex_ai", "bedrock", "hosted_vllm"] = "openai", extra_headers: Optional[Dict[str, str]] = None, extra_body: Optional[Dict[str, str]] = None, **kwargs, @@ -800,7 +802,7 @@ def file_content( file_id: str, model: Optional[str] = None, custom_llm_provider: Optional[ - Union[Literal["openai", "azure", "vertex_ai", "hosted_vllm"], str] + Union[Literal["openai", "azure", "vertex_ai", "bedrock", "hosted_vllm"], str] ] = None, extra_headers: Optional[Dict[str, str]] = None, extra_body: Optional[Dict[str, str]] = None, @@ -938,9 +940,18 @@ def file_content( timeout=timeout, max_retries=optional_params.max_retries, ) + elif custom_llm_provider == "bedrock": + response = bedrock_files_instance.file_content( + _is_async=_is_async, + file_content_request=_file_content_request, + api_base=optional_params.api_base, + optional_params=litellm_params_dict, + timeout=timeout, + max_retries=optional_params.max_retries, + ) else: raise litellm.exceptions.BadRequestError( - message="LiteLLM doesn't support {} for 'custom_llm_provider'. Supported providers are 'openai', 'azure', 'vertex_ai'.".format( + message="LiteLLM doesn't support {} for 'custom_llm_provider'. Supported providers are 'openai', 'azure', 'vertex_ai', 'bedrock'.".format( custom_llm_provider ), model="n/a", diff --git a/litellm/llms/bedrock/files/handler.py b/litellm/llms/bedrock/files/handler.py new file mode 100644 index 00000000000..d6177e090d5 --- /dev/null +++ b/litellm/llms/bedrock/files/handler.py @@ -0,0 +1,206 @@ +import asyncio +import base64 +from typing import Any, Coroutine, Optional, Tuple, Union + +import httpx + +from litellm import LlmProviders +from litellm.llms.custom_httpx.http_handler import get_async_httpx_client +from litellm.types.llms.openai import ( + FileContentRequest, + HttpxBinaryResponseContent, +) +from litellm.types.utils import SpecialEnums + +from ..base_aws_llm import BaseAWSLLM + + +class BedrockFilesHandler(BaseAWSLLM): + """ + Handles downloading files from S3 for Bedrock batch processing. + + This implementation downloads files from S3 buckets where Bedrock + stores batch output files. + """ + + def __init__(self): + super().__init__() + self.async_httpx_client = get_async_httpx_client( + llm_provider=LlmProviders.BEDROCK, + ) + + def _extract_s3_uri_from_file_id(self, file_id: str) -> str: + """ + Extract S3 URI from encoded file ID. + + The file ID can be in two formats: + 1. Base64-encoded unified file ID containing: llm_output_file_id,s3://bucket/path + 2. Direct S3 URI: s3://bucket/path + + Args: + file_id: Encoded file ID or direct S3 URI + + Returns: + S3 URI (e.g., "s3://bucket-name/path/to/file") + """ + # First, try to decode if it's a base64-encoded unified file ID + try: + # Add padding if needed + padded = file_id + "=" * (-len(file_id) % 4) + decoded = base64.urlsafe_b64decode(padded).decode() + + # Check if it's a unified file ID format + if decoded.startswith(SpecialEnums.LITELM_MANAGED_FILE_ID_PREFIX.value): + # Extract llm_output_file_id from the decoded string + if "llm_output_file_id," in decoded: + s3_uri = decoded.split("llm_output_file_id,")[1].split(";")[0] + return s3_uri + except Exception: + pass + + # If not base64 encoded or doesn't contain llm_output_file_id, assume it's already an S3 URI + if file_id.startswith("s3://"): + return file_id + + # If it doesn't start with s3://, assume it's a direct S3 URI and add the prefix + return f"s3://{file_id}" + + def _parse_s3_uri(self, s3_uri: str) -> Tuple[str, str]: + """ + Parse S3 URI to extract bucket name and object key. + + Args: + s3_uri: S3 URI (e.g., "s3://bucket-name/path/to/file") + + Returns: + Tuple of (bucket_name, object_key) + """ + if not s3_uri.startswith("s3://"): + raise ValueError(f"Invalid S3 URI format: {s3_uri}. Expected format: s3://bucket-name/path/to/file") + + # Remove 's3://' prefix + path = s3_uri[5:] + + if "/" in path: + bucket_name, object_key = path.split("/", 1) + else: + bucket_name = path + object_key = "" + + return bucket_name, object_key + + async def afile_content( + self, + file_content_request: FileContentRequest, + optional_params: dict, + timeout: Union[float, httpx.Timeout], + max_retries: Optional[int], + ) -> HttpxBinaryResponseContent: + """ + Download file content from S3 bucket for Bedrock files. + + Args: + file_content_request: Contains file_id (encoded or S3 URI) + optional_params: Optional parameters containing AWS credentials + timeout: Request timeout + max_retries: Max retry attempts + + Returns: + HttpxBinaryResponseContent: Binary content wrapped in compatible response format + """ + import boto3 + from botocore.credentials import Credentials + + file_id = file_content_request.get("file_id") + if not file_id: + raise ValueError("file_id is required in file_content_request") + + # Extract S3 URI from file ID + s3_uri = self._extract_s3_uri_from_file_id(file_id) + bucket_name, object_key = self._parse_s3_uri(s3_uri) + + # Get AWS credentials + aws_region_name = self._get_aws_region_name( + optional_params=optional_params, model="" + ) + credentials: Credentials = self.get_credentials( + aws_access_key_id=optional_params.get("aws_access_key_id"), + aws_secret_access_key=optional_params.get("aws_secret_access_key"), + aws_session_token=optional_params.get("aws_session_token"), + aws_region_name=aws_region_name, + aws_session_name=optional_params.get("aws_session_name"), + aws_profile_name=optional_params.get("aws_profile_name"), + aws_role_name=optional_params.get("aws_role_name"), + aws_web_identity_token=optional_params.get("aws_web_identity_token"), + aws_sts_endpoint=optional_params.get("aws_sts_endpoint"), + ) + + # Create S3 client + s3_client = boto3.client( + "s3", + aws_access_key_id=credentials.access_key, + aws_secret_access_key=credentials.secret_key, + aws_session_token=credentials.token, + region_name=aws_region_name, + ) + + # Download file from S3 + try: + response = s3_client.get_object(Bucket=bucket_name, Key=object_key) + file_content = response["Body"].read() + except Exception as e: + raise ValueError(f"Failed to download file from S3: {s3_uri}. Error: {str(e)}") + + # Create mock HTTP response + mock_response = httpx.Response( + status_code=200, + content=file_content, + headers={"content-type": "application/octet-stream"}, + request=httpx.Request(method="GET", url=s3_uri), + ) + + return HttpxBinaryResponseContent(response=mock_response) + + def file_content( + self, + _is_async: bool, + file_content_request: FileContentRequest, + api_base: Optional[str], + optional_params: dict, + timeout: Union[float, httpx.Timeout], + max_retries: Optional[int], + ) -> Union[ + HttpxBinaryResponseContent, Coroutine[Any, Any, HttpxBinaryResponseContent] + ]: + """ + Download file content from S3 bucket for Bedrock files. + Supports both sync and async operations. + + Args: + _is_async: Whether to run asynchronously + file_content_request: Contains file_id (encoded or S3 URI) + api_base: API base (unused for S3 operations) + optional_params: Optional parameters containing AWS credentials + timeout: Request timeout + max_retries: Max retry attempts + + Returns: + HttpxBinaryResponseContent or Coroutine: Binary content wrapped in compatible response format + """ + if _is_async: + return self.afile_content( + file_content_request=file_content_request, + optional_params=optional_params, + timeout=timeout, + max_retries=max_retries, + ) + else: + return asyncio.run( + self.afile_content( + file_content_request=file_content_request, + optional_params=optional_params, + timeout=timeout, + max_retries=max_retries, + ) + ) + diff --git a/tests/test_litellm/llms/bedrock/files/test_bedrock_files_integration.py b/tests/test_litellm/llms/bedrock/files/test_bedrock_files_integration.py new file mode 100644 index 00000000000..37a0daa1d50 --- /dev/null +++ b/tests/test_litellm/llms/bedrock/files/test_bedrock_files_integration.py @@ -0,0 +1,110 @@ +""" +Test Bedrock files integration with main files API +""" + +import base64 +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +import litellm +from litellm.types.llms.openai import HttpxBinaryResponseContent +from litellm.types.utils import SpecialEnums + + +class TestBedrockFilesIntegration: + """Test integration of Bedrock files with main litellm API""" + + @pytest.mark.asyncio + async def test_litellm_afile_content_bedrock_provider_with_s3_uri(self): + """Test litellm.afile_content with bedrock provider using direct S3 URI""" + file_id = "s3://test-bucket/test-file.jsonl" + expected_content = b'{"recordId": "request-1", "modelInput": {}, "modelOutput": {}}' + + # Mock the bedrock_files_instance.file_content method + with patch( + "litellm.files.main.bedrock_files_instance.file_content", + new_callable=AsyncMock, + ) as mock_file_content: + # Create a mock HttpxBinaryResponseContent response + import httpx + + mock_response = httpx.Response( + status_code=200, + content=expected_content, + headers={"content-type": "application/octet-stream"}, + request=httpx.Request( + method="GET", url="s3://test-bucket/test-file.jsonl" + ), + ) + mock_file_content.return_value = HttpxBinaryResponseContent( + response=mock_response + ) + + # Call litellm.afile_content + result = await litellm.afile_content( + file_id=file_id, + custom_llm_provider="bedrock", + aws_region_name="us-west-2", + ) + + # Verify the result + assert isinstance(result, HttpxBinaryResponseContent) + assert result.response.content == expected_content + assert result.response.status_code == 200 + + # Verify the mock was called with correct parameters + mock_file_content.assert_called_once() + call_kwargs = mock_file_content.call_args.kwargs + assert call_kwargs["_is_async"] is True + assert call_kwargs["file_content_request"]["file_id"] == file_id + + @pytest.mark.asyncio + async def test_litellm_afile_content_bedrock_provider_with_unified_file_id(self): + """Test litellm.afile_content with bedrock provider using unified file ID""" + # Create a unified file ID + s3_uri = "s3://test-bucket/batch-outputs/output.jsonl" + unified_id = "test-unified-id-123" + model_id = "test-model-id-456" + + unified_file_id_str = f"litellm_proxy:application/json;unified_id,{unified_id};target_model_names,;llm_output_file_id,{s3_uri};llm_output_file_model_id,{model_id}" + encoded_file_id = base64.urlsafe_b64encode(unified_file_id_str.encode()).decode().rstrip("=") + + expected_content = b'{"recordId": "request-1", "modelInput": {}, "modelOutput": {}}' + + # Mock the bedrock_files_instance.file_content method + with patch( + "litellm.files.main.bedrock_files_instance.file_content", + new_callable=AsyncMock, + ) as mock_file_content: + # Create a mock HttpxBinaryResponseContent response + import httpx + + mock_response = httpx.Response( + status_code=200, + content=expected_content, + headers={"content-type": "application/octet-stream"}, + request=httpx.Request(method="GET", url=s3_uri), + ) + mock_file_content.return_value = HttpxBinaryResponseContent( + response=mock_response + ) + + # Call litellm.afile_content with unified file ID + result = await litellm.afile_content( + file_id=encoded_file_id, + custom_llm_provider="bedrock", + aws_region_name="us-west-2", + ) + + # Verify the result + assert isinstance(result, HttpxBinaryResponseContent) + assert result.response.content == expected_content + assert result.response.status_code == 200 + + # Verify the mock was called - the handler should extract S3 URI from unified file ID + mock_file_content.assert_called_once() + call_kwargs = mock_file_content.call_args.kwargs + assert call_kwargs["_is_async"] is True + # The handler extracts S3 URI from the unified file ID + assert call_kwargs["file_content_request"]["file_id"] == encoded_file_id From 6751badf3a1cecb094d489cb1518a969e0c607be Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Thu, 4 Dec 2025 21:49:33 +0530 Subject: [PATCH 106/107] fix: test_default_api_base for ragfow --- tests/local_testing/test_get_llm_provider.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/local_testing/test_get_llm_provider.py b/tests/local_testing/test_get_llm_provider.py index 261f9bd0eac..4df910bc73e 100644 --- a/tests/local_testing/test_get_llm_provider.py +++ b/tests/local_testing/test_get_llm_provider.py @@ -137,6 +137,9 @@ def test_default_api_base(): # Get the API base for the given provider if provider == "github_copilot": continue + # Skip ragflow as it requires specific model format: ragflow/chat/{id}/{model} or ragflow/agent/{id}/{model} + if provider == "ragflow": + continue _, _, _, api_base = _get_openai_compatible_provider_info( model=f"{provider}/*", api_base=None, api_key=None, dynamic_api_key=None ) From 8ee8cad4f1f8f472ed7031e668a983cd89e02c3b Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 4 Dec 2025 09:26:55 -0800 Subject: [PATCH 107/107] feat - new model amazon.titan-image-generator-v2:0 (#17488) --- ...odel_prices_and_context_window_backup.json | 62 +++++++++++++++++-- model_prices_and_context_window.json | 9 +++ 2 files changed, 66 insertions(+), 5 deletions(-) diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 903b86484cd..d02a01e3a67 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -425,6 +425,15 @@ "litellm_provider": "bedrock", "mode": "image_generation" }, + "amazon.titan-image-generator-v2:0": { + "input_cost_per_image": 0.0, + "output_cost_per_image": 0.008, + "output_cost_per_image_premium_image": 0.01, + "output_cost_per_image_above_1024_and_1024_pixels": 0.01, + "output_cost_per_image_above_1024_and_1024_pixels_and_premium_image": 0.012, + "litellm_provider": "bedrock", + "mode": "image_generation" + }, "twelvelabs.marengo-embed-2-7-v1:0": { "input_cost_per_token": 7e-05, "litellm_provider": "bedrock", @@ -18896,6 +18905,34 @@ "supports_tool_choice": true, "supports_web_search": true }, + "moonshot/kimi-k2-0905-preview": { + "cache_read_input_token_cost": 1.5e-07, + "input_cost_per_token": 6e-07, + "litellm_provider": "moonshot", + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "max_tokens": 262144, + "mode": "chat", + "output_cost_per_token": 2.5e-06, + "source": "https://platform.moonshot.ai/docs/pricing/chat#generation-model-kimi-k2", + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_web_search": true + }, + "moonshot/kimi-k2-turbo-preview": { + "cache_read_input_token_cost": 1.5e-07, + "input_cost_per_token": 1.15e-06, + "litellm_provider": "moonshot", + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "max_tokens": 262144, + "mode": "chat", + "output_cost_per_token": 8e-06, + "source": "https://platform.moonshot.ai/docs/pricing/chat#generation-model-kimi-k2", + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_web_search": true + }, "moonshot/kimi-latest": { "cache_read_input_token_cost": 1.5e-07, "input_cost_per_token": 2e-06, @@ -18953,14 +18990,15 @@ "supports_vision": true }, "moonshot/kimi-thinking-preview": { - "input_cost_per_token": 3e-05, + "cache_read_input_token_cost": 1.5e-07, + "input_cost_per_token": 6e-07, "litellm_provider": "moonshot", "max_input_tokens": 131072, "max_output_tokens": 131072, "max_tokens": 131072, "mode": "chat", - "output_cost_per_token": 3e-05, - "source": "https://platform.moonshot.ai/docs/pricing", + "output_cost_per_token": 2.5e-06, + "source": "https://platform.moonshot.ai/docs/pricing/chat#generation-model-kimi-k2", "supports_vision": true }, "moonshot/kimi-k2-thinking": { @@ -18977,6 +19015,20 @@ "supports_tool_choice": true, "supports_web_search": true }, + "moonshot/kimi-k2-thinking-turbo": { + "cache_read_input_token_cost": 1.5e-7, + "input_cost_per_token": 1.15e-6, + "litellm_provider": "moonshot", + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "max_tokens": 262144, + "mode": "chat", + "output_cost_per_token": 8e-6, + "source": "https://platform.moonshot.ai/docs/pricing/chat#generation-model-kimi-k2", + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_web_search": true + }, "moonshot/moonshot-v1-128k": { "input_cost_per_token": 2e-06, "litellm_provider": "moonshot", @@ -26470,8 +26522,8 @@ "max_tokens": 128000, "max_input_tokens": 128000, "max_output_tokens": 128000, - "input_cost_per_token": 0.135, - "output_cost_per_token": 0.4, + "input_cost_per_token": 6e-07, + "output_cost_per_token": 2.5e-06, "litellm_provider": "wandb", "mode": "chat" }, diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 6cc6e7772af..d02a01e3a67 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -425,6 +425,15 @@ "litellm_provider": "bedrock", "mode": "image_generation" }, + "amazon.titan-image-generator-v2:0": { + "input_cost_per_image": 0.0, + "output_cost_per_image": 0.008, + "output_cost_per_image_premium_image": 0.01, + "output_cost_per_image_above_1024_and_1024_pixels": 0.01, + "output_cost_per_image_above_1024_and_1024_pixels_and_premium_image": 0.012, + "litellm_provider": "bedrock", + "mode": "image_generation" + }, "twelvelabs.marengo-embed-2-7-v1:0": { "input_cost_per_token": 7e-05, "litellm_provider": "bedrock",
    +
    diff --git a/ui/litellm-dashboard/src/components/model_dashboard/table.tsx b/ui/litellm-dashboard/src/components/model_dashboard/table.tsx index b74060db6c5..344ff2e94f2 100644 --- a/ui/litellm-dashboard/src/components/model_dashboard/table.tsx +++ b/ui/litellm-dashboard/src/components/model_dashboard/table.tsx @@ -100,7 +100,7 @@ export function ModelDataTable({ key={header.id} className={`py-1 h-8 relative ${ header.id === "actions" - ? "sticky right-0 bg-white shadow-[-4px_0_8px_-6px_rgba(0,0,0,0.1)] z-20 w-[120px] ml-8" + ? "sticky right-0 bg-white shadow-[-4px_0_8px_-6px_rgba(0,0,0,0.1)] w-[120px] ml-8" : "" } ${header.column.columnDef.meta?.className || ""}`} style={{ @@ -160,7 +160,7 @@ export function ModelDataTable({ key={cell.id} className={`py-0.5 ${ cell.column.id === "actions" - ? "sticky right-0 bg-white shadow-[-4px_0_8px_-6px_rgba(0,0,0,0.1)] z-20 w-[120px] ml-8" + ? "sticky right-0 bg-white shadow-[-4px_0_8px_-6px_rgba(0,0,0,0.1)] w-[120px] ml-8" : "" } ${cell.column.columnDef.meta?.className || ""}`} style={{ From 9bb292f478dab11c0cd8088fde353fc349fb8e93 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Wed, 3 Dec 2025 12:41:45 -0800 Subject: [PATCH 061/107] V2 login route --- litellm/proxy/proxy_server.py | 47 +++++++++++++++++ tests/test_litellm/proxy/test_proxy_server.py | 52 +++++++++++++++++++ 2 files changed, 99 insertions(+) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 9789f495508..4d971e8ce42 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -8318,6 +8318,53 @@ async def login(request: Request): # noqa: PLR0915 return redirect_response +@router.post( + "/v2/login", include_in_schema=False +) # hidden helper for UI logins via API +async def login_v2(request: Request): # noqa: PLR0915 + global premium_user, general_settings, master_key + from litellm.proxy.auth.login_utils import authenticate_user, create_ui_token_object + from litellm.proxy.utils import get_custom_url + + body = await request.json() + username = str(body.get("username")) + password = str(body.get("password")) + + login_result = await authenticate_user( + username=username, + password=password, + master_key=master_key, + prisma_client=prisma_client, + ) + + returned_ui_token_object = create_ui_token_object( + login_result=login_result, + general_settings=general_settings, + premium_user=premium_user, + ) + + import jwt + + jwt_token = jwt.encode( # type: ignore + cast(dict, returned_ui_token_object), + master_key, + algorithm="HS256", + ) + + litellm_dashboard_ui = get_custom_url(str(request.base_url)) + if litellm_dashboard_ui.endswith("/"): + litellm_dashboard_ui += "ui/" + else: + litellm_dashboard_ui += "/ui/" + litellm_dashboard_ui += "?login=success" + + json_response = JSONResponse( + content={"redirect_url": litellm_dashboard_ui}, + status_code=status.HTTP_200_OK, + ) + json_response.set_cookie(key="token", value=jwt_token) + return json_response + @app.get("/onboarding/get_token", include_in_schema=False) async def onboarding(invite_link: str, request: Request): """ diff --git a/tests/test_litellm/proxy/test_proxy_server.py b/tests/test_litellm/proxy/test_proxy_server.py index 8e87b67933f..7b3157f8ee1 100644 --- a/tests/test_litellm/proxy/test_proxy_server.py +++ b/tests/test_litellm/proxy/test_proxy_server.py @@ -71,6 +71,58 @@ def client_no_auth(): return TestClient(app) +def test_login_v2_returns_redirect_url_and_sets_cookie(monkeypatch): + mock_login_result = {"user_id": "test-user"} + mock_prisma_client = MagicMock() + mock_authenticate_user = AsyncMock(return_value=mock_login_result) + mock_create_ui_token_object = MagicMock(return_value={"user_id": "test-user"}) + mock_jwt_encode = MagicMock(return_value="signed-token") + + monkeypatch.setattr( + "litellm.proxy.auth.login_utils.authenticate_user", + mock_authenticate_user, + ) + monkeypatch.setattr( + "litellm.proxy.auth.login_utils.create_ui_token_object", + mock_create_ui_token_object, + ) + monkeypatch.setattr("jwt.encode", mock_jwt_encode) + monkeypatch.setattr("litellm.proxy.proxy_server.master_key", "test-master-key") + monkeypatch.setattr("litellm.proxy.proxy_server.general_settings", {}) + monkeypatch.setattr("litellm.proxy.proxy_server.premium_user", False) + monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", mock_prisma_client) + + client = TestClient(app) + response = client.post( + "/v2/login", + json={"username": "alice", "password": "secret"}, + ) + + assert response.status_code == 200 + assert ( + response.json() + == {"redirect_url": "http://testserver/ui/?login=success"} + ) + assert response.cookies.get("token") == "signed-token" + + mock_authenticate_user.assert_awaited_once_with( + username="alice", + password="secret", + master_key="test-master-key", + prisma_client=mock_prisma_client, + ) + mock_create_ui_token_object.assert_called_once_with( + login_result=mock_login_result, + general_settings={}, + premium_user=False, + ) + mock_jwt_encode.assert_called_once_with( + {"user_id": "test-user"}, + "test-master-key", + algorithm="HS256", + ) + + @pytest.mark.asyncio async def test_initialize_scheduled_jobs_credentials(monkeypatch): """ From 5f43e7a2d2b2787e26f6f1da4efa30b0aeeb0391 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Mon, 1 Dec 2025 17:15:59 -0800 Subject: [PATCH 062/107] New login page WIP --- .../app/(dashboard)/hooks/login/useLogin.ts | 17 +++ .../(dashboard)/hooks/uiConfig/useUIConfig.ts | 14 ++ ui/litellm-dashboard/src/app/login/page.tsx | 123 ++++++++++++++++++ .../src/components/networking.tsx | 65 ++++++++- 4 files changed, 216 insertions(+), 3 deletions(-) create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/hooks/login/useLogin.ts create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/hooks/uiConfig/useUIConfig.ts create mode 100644 ui/litellm-dashboard/src/app/login/page.tsx diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/login/useLogin.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/login/useLogin.ts new file mode 100644 index 00000000000..486d2e39929 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/login/useLogin.ts @@ -0,0 +1,17 @@ +import { useMutation } from "@tanstack/react-query"; +import { loginCall, LoginRequest } from "@/components/networking"; + +export const useLogin = () => { + return useMutation({ + mutationFn: async ({ username, password }: LoginRequest) => { + const result = await loginCall(username, password); + return result; + }, + onSuccess: (data) => { + // Redirect to UI on successful login + if (data.success && data.redirectUrl) { + window.location.href = data.redirectUrl; + } + }, + }); +}; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/uiConfig/useUIConfig.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/uiConfig/useUIConfig.ts new file mode 100644 index 00000000000..147fdc241b0 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/uiConfig/useUIConfig.ts @@ -0,0 +1,14 @@ +import { getUiConfig, LiteLLMWellKnownUiConfig } from "@/components/networking"; +import { useQuery } from "@tanstack/react-query"; +import { createQueryKeys } from "../common/queryKeysFactory"; + +const uiConfigKeys = createQueryKeys("uiConfig"); + +export const useUIConfig = () => { + return useQuery({ + queryKey: uiConfigKeys.list({}), + queryFn: async () => await getUiConfig(), + staleTime: 24 * 60 * 60 * 1000, // 24 hours - data rarely changes + gcTime: 24 * 60 * 60 * 1000, // 24 hours - keep in cache for 24 hours + }); +}; diff --git a/ui/litellm-dashboard/src/app/login/page.tsx b/ui/litellm-dashboard/src/app/login/page.tsx new file mode 100644 index 00000000000..4ffe7cbfe80 --- /dev/null +++ b/ui/litellm-dashboard/src/app/login/page.tsx @@ -0,0 +1,123 @@ +"use client"; + +import React, { useState } from "react"; +import { Form, Input, Button, Alert, Typography, Card, Spin, Space } from "antd"; +import { InfoCircleOutlined } from "@ant-design/icons"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { useUIConfig } from "@/app/(dashboard)/hooks/uiConfig/useUIConfig"; +import { useLogin } from "@/app/(dashboard)/hooks/login/useLogin"; + +function LoginPageContent() { + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const { isLoading: isConfigLoading } = useUIConfig(); + const loginMutation = useLogin(); + + const handleSubmit = async () => { + loginMutation.mutate({ username, password }); + }; + + const error = loginMutation.error instanceof Error ? loginMutation.error.message : null; + const isLoading = loginMutation.isPending; + + const { Title, Text, Paragraph } = Typography; + + if (isConfigLoading) { + return ( +
    + +
    + ); + } + + return ( +
    + + +
    + 🚅 LiteLLM +
    + +
    + Login + Access your LiteLLM Admin UI. +
    + + + + By default, Username is admin and + Password is your set LiteLLM Proxy + MASTER_KEY. + + + Need to set UI credentials or SSO?{" "} +
    + Check the documentation + + . + + + } + type="info" + icon={} + showIcon + /> + + {error && } + +
    + + setUsername(e.target.value)} + disabled={isLoading} + size="large" + className="rounded-md border-gray-300" + /> + + + + setPassword(e.target.value)} + disabled={isLoading} + size="large" + /> + + + + + +
    + + +
    + ); +} + +export default function LoginPage() { + const queryClient = new QueryClient(); + + return ( + + + + ); +} diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index 14763930aaf..76b79c0f8b8 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -124,7 +124,7 @@ export interface PromptSpec { prompt_info: PromptInfo; created_at?: string; updated_at?: string; - version?: number; // Explicit version number for version history + version?: number; // Explicit version number for version history } export interface PromptTemplateBase { @@ -7414,7 +7414,11 @@ interface RegisterMcpOAuthClientPayload { token_endpoint_auth_method?: string; } -export const registerMcpOAuthClient = async (accessToken: string, serverId: string, payload: RegisterMcpOAuthClientPayload) => { +export const registerMcpOAuthClient = async ( + accessToken: string, + serverId: string, + payload: RegisterMcpOAuthClientPayload, +) => { const base = getProxyBaseUrl(); const normalizedServerId = encodeURIComponent(serverId.trim()); const url = `${base}/v1/mcp/server/oauth/${normalizedServerId}/register`; @@ -7424,7 +7428,7 @@ export const registerMcpOAuthClient = async (accessToken: string, serverId: stri headers: { [globalLitellmHeaderName]: `Bearer ${accessToken}`, "Content-Type": "application/json", - "Accept": "application/json, text/event-stream", + Accept: "application/json, text/event-stream", }, body: JSON.stringify(payload), }); @@ -7969,3 +7973,58 @@ const deriveErrorMessage = (errorData: any): string => { JSON.stringify(errorData) ); }; + +export interface LoginRequest { + username: string; + password: string; +} + +export interface LoginResponse { + success: boolean; + redirectUrl?: string; +} + +export const loginCall = async (username: string, password: string): Promise => { + const proxyBaseUrl = getProxyBaseUrl(); + const loginUrl = proxyBaseUrl ? `${proxyBaseUrl}/login` : "/login"; + + const formData = new FormData(); + formData.append("username", username); + formData.append("password", password); + + const response = await fetch(loginUrl, { + method: "POST", + body: formData, + credentials: "include", + redirect: "manual", + }); + + // Handle redirect status codes (301, 302, 303, 307, 308) + if (response.status >= 300 && response.status < 400) { + const redirectUrl = response.headers.get("Location"); + + if (!redirectUrl) { + throw new Error("Login redirect missing Location header"); + } + + return { + success: true, + redirectUrl, + }; + } + + if (response.ok) { + return { + success: true, + redirectUrl: "/uia/", + }; + } + + // Otherwise, try to extract an error + let errorText = "Invalid username or password"; + try { + errorText = await response.text(); + } catch {} + + throw new Error(errorText || "Login failed"); +}; From 9b3d8302cf9e2bd52df4bcdc49c6e0c08f181e9f Mon Sep 17 00:00:00 2001 From: Ishaan Jaffer Date: Wed, 3 Dec 2025 14:12:50 -0800 Subject: [PATCH 063/107] docs fix stable --- docs/my-website/release_notes/v1.80.5-stable/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/my-website/release_notes/v1.80.5-stable/index.md b/docs/my-website/release_notes/v1.80.5-stable/index.md index 4324fdef776..7c3283ce349 100644 --- a/docs/my-website/release_notes/v1.80.5-stable/index.md +++ b/docs/my-website/release_notes/v1.80.5-stable/index.md @@ -1,5 +1,5 @@ --- -title: "[PREVIEW] v1.80.5.rc.2 - Gemini 3.0 Support" +title: "v1.80.5-stable - Gemini 3.0 Support" slug: "v1-80-5" date: 2025-11-22T10:00:00 authors: @@ -27,7 +27,7 @@ import TabItem from '@theme/TabItem'; docker run \ -e STORE_MODEL_IN_DB=True \ -p 4000:4000 \ -ghcr.io/berriai/litellm:v1.80.5.rc.2 +ghcr.io/berriai/litellm:v1.80.5-stable ``` From 100cfc11acf73e6c8bd92f7f856568f7623f0d4f Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 3 Dec 2025 14:13:28 -0800 Subject: [PATCH 064/107] [Bug Fix] Parallel Request Limiter with /messages (#17426) * fix: use standard_logging_object for parallel request limiter * fix test parallel request limtier --- .../hooks/parallel_request_limiter_v3.py | 34 ++++++++++--------- .../hooks/test_parallel_request_limiter_v3.py | 10 +++--- 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/litellm/proxy/hooks/parallel_request_limiter_v3.py b/litellm/proxy/hooks/parallel_request_limiter_v3.py index 9436df585a3..2abba1d4976 100644 --- a/litellm/proxy/hooks/parallel_request_limiter_v3.py +++ b/litellm/proxy/hooks/parallel_request_limiter_v3.py @@ -1343,19 +1343,19 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger): "INSIDE parallel request limiter ASYNC SUCCESS LOGGING" ) - # Get metadata from kwargs - litellm_metadata = kwargs["litellm_params"].get( - get_metadata_variable_name_from_kwargs(kwargs), {} + # Get metadata from standard_logging_object - this correctly handles both + # 'metadata' and 'litellm_metadata' fields from litellm_params + standard_logging_object = kwargs.get("standard_logging_object") or {} + standard_logging_metadata = standard_logging_object.get("metadata") or {} + + # user_api_key_hash is the same as user_api_key (it's the hash) + user_api_key = standard_logging_metadata.get("user_api_key_hash") + user_api_key_user_id = standard_logging_metadata.get("user_api_key_user_id") + user_api_key_team_id = standard_logging_metadata.get("user_api_key_team_id") + user_api_key_organization_id = standard_logging_metadata.get( + "user_api_key_org_id" ) - if litellm_metadata is None: - return - user_api_key = litellm_metadata.get("user_api_key") - user_api_key_user_id = litellm_metadata.get("user_api_key_user_id") - user_api_key_team_id = litellm_metadata.get("user_api_key_team_id") - user_api_key_organization_id = litellm_metadata.get( - "user_api_key_organization_id" - ) - user_api_key_end_user_id = kwargs.get("user") or litellm_metadata.get( + user_api_key_end_user_id = kwargs.get("user") or standard_logging_metadata.get( "user_api_key_end_user_id" ) model_group = get_model_group_from_litellm_kwargs(kwargs) @@ -1501,10 +1501,12 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger): litellm_parent_otel_span: Union[ Span, None ] = _get_parent_otel_span_from_kwargs(kwargs) - litellm_metadata = kwargs["litellm_params"]["metadata"] - user_api_key = ( - litellm_metadata.get("user_api_key") if litellm_metadata else None - ) + # Get metadata from standard_logging_object - this correctly handles both + # 'metadata' and 'litellm_metadata' fields from litellm_params + standard_logging_object = kwargs.get("standard_logging_object") or {} + standard_logging_metadata = standard_logging_object.get("metadata") or {} + user_api_key = standard_logging_metadata.get("user_api_key_hash") + pipeline_operations: List[RedisPipelineIncrementOperation] = [] if user_api_key: diff --git a/tests/test_litellm/proxy/hooks/test_parallel_request_limiter_v3.py b/tests/test_litellm/proxy/hooks/test_parallel_request_limiter_v3.py index 6574c500fcf..c8c30d41b5e 100644 --- a/tests/test_litellm/proxy/hooks/test_parallel_request_limiter_v3.py +++ b/tests/test_litellm/proxy/hooks/test_parallel_request_limiter_v3.py @@ -22,6 +22,7 @@ from litellm.proxy.hooks.parallel_request_limiter_v3 import ( from litellm.proxy.utils import InternalUsageCache, ProxyLogging, hash_token from litellm.types.utils import ModelResponse, Usage + class TimeController: def __init__(self): self._current = datetime.utcnow() @@ -461,10 +462,11 @@ async def test_token_rate_limit_type_respected_v3(monkeypatch, token_rate_limit_ ) # Create mock kwargs for the success event + # Use standard_logging_object which is the canonical source for metadata mock_kwargs = { - "litellm_params": { + "standard_logging_object": { "metadata": { - "user_api_key": _api_key, + "user_api_key_hash": _api_key, "user_api_key_user_id": None, "user_api_key_team_id": None, "user_api_key_end_user_id": None, @@ -532,8 +534,8 @@ async def test_async_log_failure_event_v3(): internal_usage_cache=InternalUsageCache(local_cache) ) - # Mock kwargs with user_api_key - mock_kwargs = {"litellm_params": {"metadata": {"user_api_key": _api_key}}} + # Mock kwargs with user_api_key via standard_logging_object + mock_kwargs = {"standard_logging_object": {"metadata": {"user_api_key_hash": _api_key}}} # Capture pipeline operations captured_ops = [] From b14b4a71129516c97ae9a23b41668f6602e8ed21 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Wed, 3 Dec 2025 15:05:29 -0800 Subject: [PATCH 065/107] New login screen using v2/login --- .../app/(dashboard)/hooks/login/useLogin.ts | 6 -- ui/litellm-dashboard/src/app/login/page.tsx | 65 ++++++++--- ui/litellm-dashboard/src/app/page.tsx | 102 +++++++----------- .../common_components/LoadingScreen.test.tsx | 10 ++ .../common_components/LoadingScreen.tsx | 15 +++ .../src/components/networking.tsx | 50 +++------ .../src/utils/jwtUtils.test.ts | 53 +++++++++ ui/litellm-dashboard/src/utils/jwtUtils.ts | 14 +++ 8 files changed, 193 insertions(+), 122 deletions(-) create mode 100644 ui/litellm-dashboard/src/components/common_components/LoadingScreen.test.tsx create mode 100644 ui/litellm-dashboard/src/components/common_components/LoadingScreen.tsx create mode 100644 ui/litellm-dashboard/src/utils/jwtUtils.test.ts create mode 100644 ui/litellm-dashboard/src/utils/jwtUtils.ts diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/login/useLogin.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/login/useLogin.ts index 486d2e39929..a15b4a06d13 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/login/useLogin.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/login/useLogin.ts @@ -7,11 +7,5 @@ export const useLogin = () => { const result = await loginCall(username, password); return result; }, - onSuccess: (data) => { - // Redirect to UI on successful login - if (data.success && data.redirectUrl) { - window.location.href = data.redirectUrl; - } - }, }); }; diff --git a/ui/litellm-dashboard/src/app/login/page.tsx b/ui/litellm-dashboard/src/app/login/page.tsx index 4ffe7cbfe80..6248639b063 100644 --- a/ui/litellm-dashboard/src/app/login/page.tsx +++ b/ui/litellm-dashboard/src/app/login/page.tsx @@ -1,33 +1,57 @@ "use client"; -import React, { useState } from "react"; -import { Form, Input, Button, Alert, Typography, Card, Spin, Space } from "antd"; +import { useLogin } from "@/app/(dashboard)/hooks/login/useLogin"; +import { useUIConfig } from "@/app/(dashboard)/hooks/uiConfig/useUIConfig"; +import LoadingScreen from "@/components/common_components/LoadingScreen"; +import { getProxyBaseUrl } from "@/components/networking"; +import { getCookie } from "@/utils/cookieUtils"; +import { isJwtExpired } from "@/utils/jwtUtils"; import { InfoCircleOutlined } from "@ant-design/icons"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { useUIConfig } from "@/app/(dashboard)/hooks/uiConfig/useUIConfig"; -import { useLogin } from "@/app/(dashboard)/hooks/login/useLogin"; +import { Alert, Button, Card, Form, Input, Space, Typography } from "antd"; +import { useRouter } from "next/navigation"; +import { useEffect, useState } from "react"; function LoginPageContent() { const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); + const [isLoading, setIsLoading] = useState(true); const { isLoading: isConfigLoading } = useUIConfig(); const loginMutation = useLogin(); + const router = useRouter(); - const handleSubmit = async () => { - loginMutation.mutate({ username, password }); + useEffect(() => { + if (isConfigLoading) { + return; + } + + const rawToken = getCookie("token"); + if (rawToken && !isJwtExpired(rawToken)) { + router.replace(`${getProxyBaseUrl()}/ui`); + return; + } + + setIsLoading(false); + }, [isConfigLoading, router]); + + const handleSubmit = () => { + loginMutation.mutate( + { username, password }, + { + onSuccess: (data) => { + router.push(data.redirect_url); + }, + }, + ); }; const error = loginMutation.error instanceof Error ? loginMutation.error.message : null; - const isLoading = loginMutation.isPending; + const isLoginLoading = loginMutation.isPending; const { Title, Text, Paragraph } = Typography; - if (isConfigLoading) { - return ( -
    - -
    - ); + if (isConfigLoading || isLoading) { + return ; } return ( @@ -79,7 +103,7 @@ function LoginPageContent() { autoComplete="username" value={username} onChange={(e) => setUsername(e.target.value)} - disabled={isLoading} + disabled={isLoginLoading} size="large" className="rounded-md border-gray-300" /> @@ -95,14 +119,21 @@ function LoginPageContent() { autoComplete="current-password" value={password} onChange={(e) => setPassword(e.target.value)} - disabled={isLoading} + disabled={isLoginLoading} size="large" /> - diff --git a/ui/litellm-dashboard/src/app/page.tsx b/ui/litellm-dashboard/src/app/page.tsx index a71d7dd87d3..68060ae6968 100644 --- a/ui/litellm-dashboard/src/app/page.tsx +++ b/ui/litellm-dashboard/src/app/page.tsx @@ -1,49 +1,47 @@ "use client"; -import React, { Suspense, useEffect, useState } from "react"; -import { useSearchParams } from "next/navigation"; -import { jwtDecode } from "jwt-decode"; -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { Team } from "@/components/key_team_helpers/key_list"; -import Navbar from "@/components/navbar"; -import { ThemeProvider } from "@/contexts/ThemeContext"; -import UserDashboard from "@/components/user_dashboard"; -import OldModelDashboard from "@/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView"; -import ViewUserDashboard from "@/components/view_users"; -import Organizations from "@/components/organizations"; -import { fetchOrganizations } from "@/components/organizations"; -import AdminPanel from "@/components/admins"; -import Settings from "@/components/settings"; -import GeneralSettings from "@/components/general_settings"; -import PassThroughSettings from "@/components/pass_through_settings"; -import BudgetPanel from "@/components/budgets/budget_panel"; -import SpendLogsTable from "@/components/view_logs"; -import ModelHubTable from "@/components/model_hub_table"; -import PublicModelHub from "@/components/public_model_hub"; -import NewUsagePage from "@/components/new_usage"; import APIReferenceView from "@/app/(dashboard)/api-reference/APIReferenceView"; -import PlaygroundPage from "@/app/(dashboard)/playground/page"; -import Usage from "@/components/usage"; -import CacheDashboard from "@/components/cache_dashboard"; -import { getUiConfig, proxyBaseUrl, setGlobalLitellmHeaderName } from "@/components/networking"; -import { Organization } from "@/components/networking"; -import GuardrailsPanel from "@/components/guardrails"; -import AgentsPanel from "@/components/agents"; -import PromptsPanel from "@/components/prompts"; -import TransformRequestPanel from "@/components/transform_request"; -import { fetchUserModels } from "@/components/organisms/create_key_button"; -import { fetchTeams } from "@/components/common_components/fetch_teams"; -import { MCPServers } from "@/components/mcp_tools"; -import TagManagement from "@/components/tag_management"; -import VectorStoreManagement from "@/components/vector_store_management"; -import UIThemeSettings from "@/components/ui_theme_settings"; -import { CostTrackingSettings } from "@/components/CostTrackingSettings"; -import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner"; -import { cx } from "@/lib/cva.config"; import SidebarProvider from "@/app/(dashboard)/components/SidebarProvider"; +import OldModelDashboard from "@/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView"; +import PlaygroundPage from "@/app/(dashboard)/playground/page"; +import AdminPanel from "@/components/admins"; +import AgentsPanel from "@/components/agents"; +import BudgetPanel from "@/components/budgets/budget_panel"; +import CacheDashboard from "@/components/cache_dashboard"; +import { fetchTeams } from "@/components/common_components/fetch_teams"; +import LoadingScreen from "@/components/common_components/LoadingScreen"; +import { CostTrackingSettings } from "@/components/CostTrackingSettings"; +import GeneralSettings from "@/components/general_settings"; +import GuardrailsPanel from "@/components/guardrails"; +import { Team } from "@/components/key_team_helpers/key_list"; +import { MCPServers } from "@/components/mcp_tools"; +import ModelHubTable from "@/components/model_hub_table"; +import Navbar from "@/components/navbar"; +import { getUiConfig, Organization, proxyBaseUrl, setGlobalLitellmHeaderName } from "@/components/networking"; +import NewUsagePage from "@/components/new_usage"; import OldTeams from "@/components/OldTeams"; +import { fetchUserModels } from "@/components/organisms/create_key_button"; +import Organizations, { fetchOrganizations } from "@/components/organizations"; +import PassThroughSettings from "@/components/pass_through_settings"; +import PromptsPanel from "@/components/prompts"; +import PublicModelHub from "@/components/public_model_hub"; import { SearchTools } from "@/components/search_tools"; +import Settings from "@/components/settings"; +import TagManagement from "@/components/tag_management"; +import TransformRequestPanel from "@/components/transform_request"; +import UIThemeSettings from "@/components/ui_theme_settings"; +import Usage from "@/components/usage"; +import UserDashboard from "@/components/user_dashboard"; +import VectorStoreManagement from "@/components/vector_store_management"; +import SpendLogsTable from "@/components/view_logs"; +import ViewUserDashboard from "@/components/view_users"; +import { ThemeProvider } from "@/contexts/ThemeContext"; +import { isJwtExpired } from "@/utils/jwtUtils"; import { isAdminRole } from "@/utils/roles"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { jwtDecode } from "jwt-decode"; +import { useSearchParams } from "next/navigation"; +import { Suspense, useEffect, useState } from "react"; function getCookie(name: string) { // Safer cookie read + decoding; handles '=' inside values @@ -62,19 +60,6 @@ function deleteCookie(name: string, path = "/") { document.cookie = `${name}=; Max-Age=0; Path=${path}`; } -function isJwtExpired(token: string): boolean { - try { - const decoded: any = jwtDecode(token); - if (decoded && typeof decoded.exp === "number") { - return decoded.exp * 1000 <= Date.now(); - } - return false; - } catch { - // If we can't decode, treat as invalid/expired - return true; - } -} - function formatUserRole(userRole: string) { if (!userRole) { return "Undefined Role"; @@ -112,19 +97,6 @@ interface ProxySettings { const queryClient = new QueryClient(); -function LoadingScreen() { - return ( -
    -
    🚅 LiteLLM
    - -
    - - Loading... -
    -
    - ); -} - export default function CreateKeyPage() { const [userRole, setUserRole] = useState(""); const [premiumUser, setPremiumUser] = useState(false); diff --git a/ui/litellm-dashboard/src/components/common_components/LoadingScreen.test.tsx b/ui/litellm-dashboard/src/components/common_components/LoadingScreen.test.tsx new file mode 100644 index 00000000000..5601c53d574 --- /dev/null +++ b/ui/litellm-dashboard/src/components/common_components/LoadingScreen.test.tsx @@ -0,0 +1,10 @@ +import { render, screen } from "@testing-library/react"; +import { describe, it, expect } from "vitest"; +import LoadingScreen from "./LoadingScreen"; + +describe("LoadingScreen", () => { + it("should render", () => { + render(); + expect(screen.getByText("Loading...")).toBeInTheDocument(); + }); +}); diff --git a/ui/litellm-dashboard/src/components/common_components/LoadingScreen.tsx b/ui/litellm-dashboard/src/components/common_components/LoadingScreen.tsx new file mode 100644 index 00000000000..6144fa6aad1 --- /dev/null +++ b/ui/litellm-dashboard/src/components/common_components/LoadingScreen.tsx @@ -0,0 +1,15 @@ +import { cx } from "@/lib/cva.config"; +import { UiLoadingSpinner } from "../ui/ui-loading-spinner"; + +export default function LoadingScreen() { + return ( +
    +
    🚅 LiteLLM
    + +
    + + Loading... +
    +
    + ); +} diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index 76b79c0f8b8..061d62d6700 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -7980,51 +7980,33 @@ export interface LoginRequest { } export interface LoginResponse { - success: boolean; - redirectUrl?: string; + redirect_url: string; } export const loginCall = async (username: string, password: string): Promise => { const proxyBaseUrl = getProxyBaseUrl(); - const loginUrl = proxyBaseUrl ? `${proxyBaseUrl}/login` : "/login"; + const loginUrl = proxyBaseUrl ? `${proxyBaseUrl}/v2/login` : "/v2/login"; - const formData = new FormData(); - formData.append("username", username); - formData.append("password", password); + const body = JSON.stringify({ + username, + password, + }); const response = await fetch(loginUrl, { method: "POST", - body: formData, + body, credentials: "include", - redirect: "manual", + headers: { + "Content-Type": "application/json", + }, }); - // Handle redirect status codes (301, 302, 303, 307, 308) - if (response.status >= 300 && response.status < 400) { - const redirectUrl = response.headers.get("Location"); - - if (!redirectUrl) { - throw new Error("Login redirect missing Location header"); - } - - return { - success: true, - redirectUrl, - }; + if (!response.ok) { + const errorData = await response.json(); + const errorMessage = deriveErrorMessage(errorData); + throw new Error(errorMessage); } - if (response.ok) { - return { - success: true, - redirectUrl: "/uia/", - }; - } - - // Otherwise, try to extract an error - let errorText = "Invalid username or password"; - try { - errorText = await response.text(); - } catch {} - - throw new Error(errorText || "Login failed"); + const data = await response.json(); + return data; }; diff --git a/ui/litellm-dashboard/src/utils/jwtUtils.test.ts b/ui/litellm-dashboard/src/utils/jwtUtils.test.ts new file mode 100644 index 00000000000..d695a3c37bb --- /dev/null +++ b/ui/litellm-dashboard/src/utils/jwtUtils.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, it, vi, beforeEach, afterEach } from "vitest"; +import { isJwtExpired } from "./jwtUtils"; +import { jwtDecode } from "jwt-decode"; + +vi.mock("jwt-decode"); + +describe("jwtUtils", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("should return true if the token is expired", () => { + const mockDateNow = 1716838401000; + vi.spyOn(Date, "now").mockReturnValue(mockDateNow); + vi.mocked(jwtDecode).mockReturnValue({ + exp: Math.floor(mockDateNow / 1000) - 1, + user_id: "test", + }); + + expect(isJwtExpired("any-token")).toBe(true); + }); + + it("should return false if the token is not expired", () => { + const mockDateNow = 1716838401000; + vi.spyOn(Date, "now").mockReturnValue(mockDateNow); + vi.mocked(jwtDecode).mockReturnValue({ + exp: Math.floor(mockDateNow / 1000) + 1000, + user_id: "test", + }); + + expect(isJwtExpired("any-token")).toBe(false); + }); + + it("should return false if the token does not have an exp field", () => { + vi.mocked(jwtDecode).mockReturnValue({ + user_id: "test", + }); + + expect(isJwtExpired("any-token")).toBe(false); + }); + + it("should return true if jwtDecode throws an error", () => { + vi.mocked(jwtDecode).mockImplementation(() => { + throw new Error("Invalid token"); + }); + + expect(isJwtExpired("invalid-token")).toBe(true); + }); +}); diff --git a/ui/litellm-dashboard/src/utils/jwtUtils.ts b/ui/litellm-dashboard/src/utils/jwtUtils.ts new file mode 100644 index 00000000000..d3db41a411a --- /dev/null +++ b/ui/litellm-dashboard/src/utils/jwtUtils.ts @@ -0,0 +1,14 @@ +import { jwtDecode } from "jwt-decode"; + +export function isJwtExpired(token: string): boolean { + try { + const decoded: any = jwtDecode(token); + if (decoded && typeof decoded.exp === "number") { + return decoded.exp * 1000 <= Date.now(); + } + return false; + } catch { + // If we can't decode, treat as invalid/expired + return true; + } +} From 43da0793d34840fcdaedc5c18a1b97797e33f369 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Wed, 3 Dec 2025 15:13:19 -0800 Subject: [PATCH 066/107] Fixed typo --- ui/litellm-dashboard/src/app/login/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/litellm-dashboard/src/app/login/page.tsx b/ui/litellm-dashboard/src/app/login/page.tsx index 6248639b063..bd24240a6cf 100644 --- a/ui/litellm-dashboard/src/app/login/page.tsx +++ b/ui/litellm-dashboard/src/app/login/page.tsx @@ -133,7 +133,7 @@ function LoginPageContent() { block size="large" > - {isLoginLoading ? "Loggin in..." : "Login"} + {isLoginLoading ? "Logging in..." : "Login"} From 462d423d86ce424b887f599d2b6427099936831e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 3 Dec 2025 15:19:34 -0800 Subject: [PATCH 067/107] Bump mcp from 1.10.1 to 1.23.0 in /.circleci (#17363) Bumps [mcp](https://github.com/modelcontextprotocol/python-sdk) from 1.10.1 to 1.23.0. - [Release notes](https://github.com/modelcontextprotocol/python-sdk/releases) - [Changelog](https://github.com/modelcontextprotocol/python-sdk/blob/main/RELEASE.md) - [Commits](https://github.com/modelcontextprotocol/python-sdk/compare/v1.10.1...v1.23.0) --- updated-dependencies: - dependency-name: mcp dependency-version: 1.23.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .circleci/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/requirements.txt b/.circleci/requirements.txt index 2294c84813c..6f1043f59a5 100644 --- a/.circleci/requirements.txt +++ b/.circleci/requirements.txt @@ -13,7 +13,7 @@ google-cloud-aiplatform==1.43.0 google-cloud-iam==2.19.1 fastapi-sso==0.16.0 uvloop==0.21.0 -mcp==1.10.1 # for MCP server +mcp==1.23.0 # for MCP server semantic_router==0.1.10 # for auto-routing with litellm fastuuid==0.12.0 responses==0.25.7 # for proxy client tests \ No newline at end of file From de4b79851ffc09e7140ef6e125fdc7b00e2580b6 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Wed, 3 Dec 2025 15:26:25 -0800 Subject: [PATCH 068/107] Change e2e test to look for Virtual Keys instead of API Keys --- .../e2e_ui_tests/view_internal_user.spec.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/proxy_admin_ui_tests/e2e_ui_tests/view_internal_user.spec.ts b/tests/proxy_admin_ui_tests/e2e_ui_tests/view_internal_user.spec.ts index 1d263e50511..e578ab57e3f 100644 --- a/tests/proxy_admin_ui_tests/e2e_ui_tests/view_internal_user.spec.ts +++ b/tests/proxy_admin_ui_tests/e2e_ui_tests/view_internal_user.spec.ts @@ -34,10 +34,10 @@ test("view internal user page", async ({ page }) => { // The UI renders badges in each row - we just verify the column structure exists const rowCount = await page.locator("tbody tr").count(); expect(rowCount).toBeGreaterThan(0); - - // Verify table headers are present (including API Keys column) - const apiKeysHeader = page.locator("th", { hasText: "API Keys" }); - await expect(apiKeysHeader).toBeVisible(); + + // Verify table headers are present (including Virtual Keys column) + const virtualKeysHeader = page.locator("th", { hasText: "Virtual Keys" }); + await expect(virtualKeysHeader).toBeVisible(); // test pagination // Wait for pagination controls to be visible From 37c598441f6906e62b40c8aebacff1ed51d50c0f Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Wed, 3 Dec 2025 15:48:50 -0800 Subject: [PATCH 069/107] Change is_sso_configured to auto_redirect_to_sso --- docs/my-website/docs/proxy/config_settings.md | 1 + .../ui_discovery_endpoints.py | 6 +- .../ui_discovery_endpoints.py | 2 +- .../test_ui_discovery_endpoints.py | 78 ++++++++++++++++--- 4 files changed, 76 insertions(+), 11 deletions(-) diff --git a/docs/my-website/docs/proxy/config_settings.md b/docs/my-website/docs/proxy/config_settings.md index 82955dadb53..b71e100e157 100644 --- a/docs/my-website/docs/proxy/config_settings.md +++ b/docs/my-website/docs/proxy/config_settings.md @@ -377,6 +377,7 @@ router_settings: | ATHINA_API_KEY | API key for Athina service | ATHINA_BASE_URL | Base URL for Athina service (defaults to `https://log.athina.ai`) | AUTH_STRATEGY | Strategy used for authentication (e.g., OAuth, API key) +| AUTO_REDIRECT_UI_LOGIN_TO_SSO | Flag to enable automatic redirect of UI login page to SSO when SSO is configured. Default is **true** | ANTHROPIC_API_KEY | API key for Anthropic service | ANTHROPIC_API_BASE | Base URL for Anthropic API. Default is https://api.anthropic.com | AWS_ACCESS_KEY_ID | Access Key ID for AWS services diff --git a/litellm/proxy/discovery_endpoints/ui_discovery_endpoints.py b/litellm/proxy/discovery_endpoints/ui_discovery_endpoints.py index b297634d6be..9aaa2fb8381 100644 --- a/litellm/proxy/discovery_endpoints/ui_discovery_endpoints.py +++ b/litellm/proxy/discovery_endpoints/ui_discovery_endpoints.py @@ -1,4 +1,5 @@ #### Analytics Endpoints ##### +import os from fastapi import APIRouter from litellm.types.proxy.discovery_endpoints.ui_discovery_endpoints import ( @@ -15,8 +16,11 @@ router = APIRouter() async def get_ui_config(): from litellm.proxy.utils import get_proxy_base_url, get_server_root_path from litellm.proxy.auth.auth_utils import _has_user_setup_sso + + auto_redirect_ui_login_to_sso = os.getenv("AUTO_REDIRECT_UI_LOGIN_TO_SSO", "true").lower() == "true" + return UiDiscoveryEndpoints( server_root_path=get_server_root_path(), proxy_base_url=get_proxy_base_url(), - is_sso_configured=_has_user_setup_sso(), + auto_redirect_to_sso=_has_user_setup_sso() and auto_redirect_ui_login_to_sso, ) diff --git a/litellm/types/proxy/discovery_endpoints/ui_discovery_endpoints.py b/litellm/types/proxy/discovery_endpoints/ui_discovery_endpoints.py index 88f0627f533..f100dd35fa6 100644 --- a/litellm/types/proxy/discovery_endpoints/ui_discovery_endpoints.py +++ b/litellm/types/proxy/discovery_endpoints/ui_discovery_endpoints.py @@ -6,4 +6,4 @@ from pydantic import BaseModel class UiDiscoveryEndpoints(BaseModel): server_root_path: str proxy_base_url: Optional[str] - is_sso_configured: bool + auto_redirect_to_sso: bool diff --git a/tests/test_litellm/proxy/discovery_endpoints/test_ui_discovery_endpoints.py b/tests/test_litellm/proxy/discovery_endpoints/test_ui_discovery_endpoints.py index b6e064823c5..599d5437589 100644 --- a/tests/test_litellm/proxy/discovery_endpoints/test_ui_discovery_endpoints.py +++ b/tests/test_litellm/proxy/discovery_endpoints/test_ui_discovery_endpoints.py @@ -20,7 +20,8 @@ def test_ui_discovery_endpoints_with_defaults(): with patch("litellm.proxy.utils.get_server_root_path", return_value="/"), \ patch("litellm.proxy.utils.get_proxy_base_url", return_value=None), \ - patch("litellm.proxy.auth.auth_utils._has_user_setup_sso", return_value=False): + patch("litellm.proxy.auth.auth_utils._has_user_setup_sso", return_value=False), \ + patch.dict(os.environ, {}, clear=False): response = client.get("/.well-known/litellm-ui-config") @@ -28,7 +29,7 @@ def test_ui_discovery_endpoints_with_defaults(): data = response.json() assert data["server_root_path"] == "/" assert data["proxy_base_url"] is None - assert data["is_sso_configured"] is False + assert data["auto_redirect_to_sso"] is False def test_ui_discovery_endpoints_with_custom_server_root_path(): @@ -38,7 +39,8 @@ def test_ui_discovery_endpoints_with_custom_server_root_path(): with patch("litellm.proxy.utils.get_server_root_path", return_value="/litellm"), \ patch("litellm.proxy.utils.get_proxy_base_url", return_value=None), \ - patch("litellm.proxy.auth.auth_utils._has_user_setup_sso", return_value=False): + patch("litellm.proxy.auth.auth_utils._has_user_setup_sso", return_value=False), \ + patch.dict(os.environ, {}, clear=False): response = client.get("/.well-known/litellm-ui-config") @@ -46,7 +48,7 @@ def test_ui_discovery_endpoints_with_custom_server_root_path(): data = response.json() assert data["server_root_path"] == "/litellm" assert data["proxy_base_url"] is None - assert data["is_sso_configured"] is False + assert data["auto_redirect_to_sso"] is False def test_ui_discovery_endpoints_with_proxy_base_url_when_set(): @@ -56,7 +58,8 @@ def test_ui_discovery_endpoints_with_proxy_base_url_when_set(): with patch("litellm.proxy.utils.get_server_root_path", return_value="/"), \ patch("litellm.proxy.utils.get_proxy_base_url", return_value="https://proxy.example.com"), \ - patch("litellm.proxy.auth.auth_utils._has_user_setup_sso", return_value=False): + patch("litellm.proxy.auth.auth_utils._has_user_setup_sso", return_value=False), \ + patch.dict(os.environ, {}, clear=False): response = client.get("/litellm/.well-known/litellm-ui-config") @@ -64,17 +67,18 @@ def test_ui_discovery_endpoints_with_proxy_base_url_when_set(): data = response.json() assert data["server_root_path"] == "/" assert data["proxy_base_url"] == "https://proxy.example.com" - assert data["is_sso_configured"] is False + assert data["auto_redirect_to_sso"] is False -def test_ui_discovery_endpoints_with_sso_configured_when_sso_is_setup(): +def test_ui_discovery_endpoints_with_sso_configured_and_auto_redirect_enabled(): app = FastAPI() app.include_router(router) client = TestClient(app) with patch("litellm.proxy.utils.get_server_root_path", return_value="/litellm"), \ patch("litellm.proxy.utils.get_proxy_base_url", return_value="https://proxy.example.com"), \ - patch("litellm.proxy.auth.auth_utils._has_user_setup_sso", return_value=True): + patch("litellm.proxy.auth.auth_utils._has_user_setup_sso", return_value=True), \ + patch.dict(os.environ, {"AUTO_REDIRECT_UI_LOGIN_TO_SSO": "true"}, clear=False): response = client.get("/.well-known/litellm-ui-config") @@ -82,5 +86,61 @@ def test_ui_discovery_endpoints_with_sso_configured_when_sso_is_setup(): data = response.json() assert data["server_root_path"] == "/litellm" assert data["proxy_base_url"] == "https://proxy.example.com" - assert data["is_sso_configured"] is True + assert data["auto_redirect_to_sso"] is True + + +def test_ui_discovery_endpoints_with_sso_configured_but_auto_redirect_disabled(): + app = FastAPI() + app.include_router(router) + client = TestClient(app) + + with patch("litellm.proxy.utils.get_server_root_path", return_value="/litellm"), \ + patch("litellm.proxy.utils.get_proxy_base_url", return_value="https://proxy.example.com"), \ + patch("litellm.proxy.auth.auth_utils._has_user_setup_sso", return_value=True), \ + patch.dict(os.environ, {"AUTO_REDIRECT_UI_LOGIN_TO_SSO": "false"}, clear=False): + + response = client.get("/.well-known/litellm-ui-config") + + assert response.status_code == 200 + data = response.json() + assert data["server_root_path"] == "/litellm" + assert data["proxy_base_url"] == "https://proxy.example.com" + assert data["auto_redirect_to_sso"] is False + + +def test_ui_discovery_endpoints_with_sso_not_configured_but_auto_redirect_enabled(): + app = FastAPI() + app.include_router(router) + client = TestClient(app) + + with patch("litellm.proxy.utils.get_server_root_path", return_value="/"), \ + patch("litellm.proxy.utils.get_proxy_base_url", return_value=None), \ + patch("litellm.proxy.auth.auth_utils._has_user_setup_sso", return_value=False), \ + patch.dict(os.environ, {"AUTO_REDIRECT_UI_LOGIN_TO_SSO": "true"}, clear=False): + + response = client.get("/.well-known/litellm-ui-config") + + assert response.status_code == 200 + data = response.json() + assert data["server_root_path"] == "/" + assert data["proxy_base_url"] is None + assert data["auto_redirect_to_sso"] is False + + +def test_ui_discovery_endpoints_both_routes_return_same_data(): + app = FastAPI() + app.include_router(router) + client = TestClient(app) + + with patch("litellm.proxy.utils.get_server_root_path", return_value="/litellm"), \ + patch("litellm.proxy.utils.get_proxy_base_url", return_value="https://proxy.example.com"), \ + patch("litellm.proxy.auth.auth_utils._has_user_setup_sso", return_value=True), \ + patch.dict(os.environ, {"AUTO_REDIRECT_UI_LOGIN_TO_SSO": "true"}, clear=False): + + response1 = client.get("/.well-known/litellm-ui-config") + response2 = client.get("/litellm/.well-known/litellm-ui-config") + + assert response1.status_code == 200 + assert response2.status_code == 200 + assert response1.json() == response2.json() From d215576477d613c1f0c4c5c30524b50b2099f087 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Wed, 3 Dec 2025 17:07:12 -0800 Subject: [PATCH 070/107] Add auto redirect to SSO to new login page --- litellm/proxy/proxy_server.py | 5 +- .../e2e_ui_tests/login_to_ui.spec.ts | 4 +- .../src/app/login/LoginPage.test.tsx | 172 ++++++++++++++++++ .../src/app/login/LoginPage.tsx | 159 ++++++++++++++++ ui/litellm-dashboard/src/app/login/page.tsx | 153 +--------------- .../src/components/networking.tsx | 1 + 6 files changed, 339 insertions(+), 155 deletions(-) create mode 100644 ui/litellm-dashboard/src/app/login/LoginPage.test.tsx create mode 100644 ui/litellm-dashboard/src/app/login/LoginPage.tsx diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 4d971e8ce42..40436e1716b 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -555,10 +555,11 @@ else: global_max_parallel_request_retry_timeout_env ) -ui_link = f"{server_root_path}/ui/" +ui_link = f"{server_root_path}/ui/login" +fallback_login_link = f"{server_root_path}/fallback/login" model_hub_link = f"{server_root_path}/ui/model_hub_table" ui_message = ( - f"👉 [```LiteLLM Admin Panel on /ui```]({ui_link}). Create, Edit Keys with SSO" + f"👉 [```LiteLLM Admin Panel on /ui```]({ui_link}). Create, Edit Keys with SSO. Having issues? Try [```Fallback Login```]({fallback_login_link})" ) ui_message += "\n\n💸 [```LiteLLM Model Cost Map```](https://models.litellm.ai/)." diff --git a/tests/proxy_admin_ui_tests/e2e_ui_tests/login_to_ui.spec.ts b/tests/proxy_admin_ui_tests/e2e_ui_tests/login_to_ui.spec.ts index f691389de53..fc4d4653246 100644 --- a/tests/proxy_admin_ui_tests/e2e_ui_tests/login_to_ui.spec.ts +++ b/tests/proxy_admin_ui_tests/e2e_ui_tests/login_to_ui.spec.ts @@ -10,7 +10,7 @@ import { test, expect } from "@playwright/test"; test("admin login test", async ({ page }) => { // Go to the specified URL - await page.goto("http://localhost:4000/ui"); + await page.goto("http://localhost:4000/ui/login"); // Enter "admin" in the username input field await page.fill('input[name="username"]', "admin"); @@ -19,7 +19,7 @@ test("admin login test", async ({ page }) => { await page.fill('input[name="password"]', "gm"); // Optionally, you can add an assertion to verify the login button is enabled - const loginButton = page.locator('input[type="submit"]'); + const loginButton = page.locator('button[type="submit"]'); await expect(loginButton).toBeEnabled(); // Optionally, you can click the login button to submit the form diff --git a/ui/litellm-dashboard/src/app/login/LoginPage.test.tsx b/ui/litellm-dashboard/src/app/login/LoginPage.test.tsx new file mode 100644 index 00000000000..cce063eceb7 --- /dev/null +++ b/ui/litellm-dashboard/src/app/login/LoginPage.test.tsx @@ -0,0 +1,172 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { render, screen, waitFor } from "@testing-library/react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import LoginPage from "./LoginPage"; + +const mockPush = vi.fn(); +const mockReplace = vi.fn(); + +vi.mock("next/navigation", () => ({ + useRouter: vi.fn(() => ({ + push: mockPush, + replace: mockReplace, + })), +})); + +vi.mock("@/app/(dashboard)/hooks/uiConfig/useUIConfig", () => ({ + useUIConfig: vi.fn(), +})); + +vi.mock("@/utils/cookieUtils", () => ({ + getCookie: vi.fn(), +})); + +vi.mock("@/utils/jwtUtils", () => ({ + isJwtExpired: vi.fn(), +})); + +vi.mock("@/components/networking", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + getProxyBaseUrl: vi.fn().mockReturnValue("http://localhost:4000"), + }; +}); + +vi.mock("@/app/(dashboard)/hooks/login/useLogin", () => ({ + useLogin: vi.fn(() => ({ + mutate: vi.fn(), + isPending: false, + error: null, + })), +})); + +import { useUIConfig } from "@/app/(dashboard)/hooks/uiConfig/useUIConfig"; +import { getCookie } from "@/utils/cookieUtils"; +import { isJwtExpired } from "@/utils/jwtUtils"; + +const createQueryClient = () => + new QueryClient({ + defaultOptions: { + queries: { + retry: false, + gcTime: 0, + }, + }, + }); + +describe("LoginPage", () => { + beforeEach(() => { + vi.clearAllMocks(); + mockPush.mockClear(); + mockReplace.mockClear(); + }); + + it("should render", async () => { + (useUIConfig as ReturnType).mockReturnValue({ + data: { auto_redirect_to_sso: false, server_root_path: "/", proxy_base_url: null }, + isLoading: false, + }); + (getCookie as ReturnType).mockReturnValue(null); + + const queryClient = createQueryClient(); + render( + + + , + ); + + await waitFor(() => { + expect(screen.getByRole("heading", { name: "Login" })).toBeInTheDocument(); + }); + }); + + it("should call router.replace to dashboard when jwt is valid", async () => { + const validToken = "valid-token"; + (useUIConfig as ReturnType).mockReturnValue({ + data: { auto_redirect_to_sso: false, server_root_path: "/", proxy_base_url: null }, + isLoading: false, + }); + (getCookie as ReturnType).mockReturnValue(validToken); + (isJwtExpired as ReturnType).mockReturnValue(false); + + const queryClient = createQueryClient(); + render( + + + , + ); + + await waitFor(() => { + expect(mockReplace).toHaveBeenCalledWith("http://localhost:4000/ui"); + }); + }); + + it("should call router.push to SSO when jwt is invalid and auto_redirect_to_sso is true", async () => { + const invalidToken = "invalid-token"; + (useUIConfig as ReturnType).mockReturnValue({ + data: { auto_redirect_to_sso: true, server_root_path: "/", proxy_base_url: null }, + isLoading: false, + }); + (getCookie as ReturnType).mockReturnValue(invalidToken); + (isJwtExpired as ReturnType).mockReturnValue(true); + + const queryClient = createQueryClient(); + render( + + + , + ); + + await waitFor(() => { + expect(mockPush).toHaveBeenCalledWith("http://localhost:4000/sso/key/generate"); + }); + }); + + it("should not call router when jwt is invalid and auto_redirect_to_sso is false", async () => { + const invalidToken = "invalid-token"; + (useUIConfig as ReturnType).mockReturnValue({ + data: { auto_redirect_to_sso: false, server_root_path: "/", proxy_base_url: null }, + isLoading: false, + }); + (getCookie as ReturnType).mockReturnValue(invalidToken); + (isJwtExpired as ReturnType).mockReturnValue(true); + + const queryClient = createQueryClient(); + render( + + + , + ); + + await waitFor(() => { + expect(screen.getByRole("heading", { name: "Login" })).toBeInTheDocument(); + }); + + expect(mockPush).not.toHaveBeenCalled(); + expect(mockReplace).not.toHaveBeenCalled(); + }); + + it("should send user to dashboard when jwt is valid even if auto_redirect_to_sso is true", async () => { + const validToken = "valid-token"; + (useUIConfig as ReturnType).mockReturnValue({ + data: { auto_redirect_to_sso: true, server_root_path: "/", proxy_base_url: null }, + isLoading: false, + }); + (getCookie as ReturnType).mockReturnValue(validToken); + (isJwtExpired as ReturnType).mockReturnValue(false); + + const queryClient = createQueryClient(); + render( + + + , + ); + + await waitFor(() => { + expect(mockReplace).toHaveBeenCalledWith("http://localhost:4000/ui"); + }); + + expect(mockPush).not.toHaveBeenCalled(); + }); +}); diff --git a/ui/litellm-dashboard/src/app/login/LoginPage.tsx b/ui/litellm-dashboard/src/app/login/LoginPage.tsx new file mode 100644 index 00000000000..85f2c6dd870 --- /dev/null +++ b/ui/litellm-dashboard/src/app/login/LoginPage.tsx @@ -0,0 +1,159 @@ +"use client"; + +import { useLogin } from "@/app/(dashboard)/hooks/login/useLogin"; +import { useUIConfig } from "@/app/(dashboard)/hooks/uiConfig/useUIConfig"; +import LoadingScreen from "@/components/common_components/LoadingScreen"; +import { getProxyBaseUrl } from "@/components/networking"; +import { getCookie } from "@/utils/cookieUtils"; +import { isJwtExpired } from "@/utils/jwtUtils"; +import { InfoCircleOutlined } from "@ant-design/icons"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { Alert, Button, Card, Form, Input, Space, Typography } from "antd"; +import { useRouter } from "next/navigation"; +import { useEffect, useState } from "react"; + +function LoginPageContent() { + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [isLoading, setIsLoading] = useState(true); + const { data: uiConfig, isLoading: isConfigLoading } = useUIConfig(); + const loginMutation = useLogin(); + const router = useRouter(); + + useEffect(() => { + if (isConfigLoading) { + return; + } + + const rawToken = getCookie("token"); + if (rawToken && !isJwtExpired(rawToken)) { + router.replace(`${getProxyBaseUrl()}/ui`); + return; + } + + if (uiConfig && uiConfig.auto_redirect_to_sso) { + router.push(`${getProxyBaseUrl()}/sso/key/generate`); + return; + } + + setIsLoading(false); + }, [isConfigLoading, router, uiConfig]); + + const handleSubmit = () => { + loginMutation.mutate( + { username, password }, + { + onSuccess: (data) => { + router.push(data.redirect_url); + }, + }, + ); + }; + + const error = loginMutation.error instanceof Error ? loginMutation.error.message : null; + const isLoginLoading = loginMutation.isPending; + + const { Title, Text, Paragraph } = Typography; + + if (isConfigLoading || isLoading) { + return ; + } + + return ( +
    + + +
    + 🚅 LiteLLM +
    + +
    + Login + Access your LiteLLM Admin UI. +
    + + + + By default, Username is admin and + Password is your set LiteLLM Proxy + MASTER_KEY. + + + Need to set UI credentials or SSO?{" "} + + Check the documentation + + . + + + } + type="info" + icon={} + showIcon + /> + + {error && } + +
    + + setUsername(e.target.value)} + disabled={isLoginLoading} + size="large" + className="rounded-md border-gray-300" + /> + + + + setPassword(e.target.value)} + disabled={isLoginLoading} + size="large" + /> + + + + + +
    +
    +
    +
    + ); +} + +export default function LoginPage() { + const queryClient = new QueryClient(); + + return ( + + + + ); +} diff --git a/ui/litellm-dashboard/src/app/login/page.tsx b/ui/litellm-dashboard/src/app/login/page.tsx index bd24240a6cf..a539f87ee5e 100644 --- a/ui/litellm-dashboard/src/app/login/page.tsx +++ b/ui/litellm-dashboard/src/app/login/page.tsx @@ -1,154 +1,5 @@ "use client"; -import { useLogin } from "@/app/(dashboard)/hooks/login/useLogin"; -import { useUIConfig } from "@/app/(dashboard)/hooks/uiConfig/useUIConfig"; -import LoadingScreen from "@/components/common_components/LoadingScreen"; -import { getProxyBaseUrl } from "@/components/networking"; -import { getCookie } from "@/utils/cookieUtils"; -import { isJwtExpired } from "@/utils/jwtUtils"; -import { InfoCircleOutlined } from "@ant-design/icons"; -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { Alert, Button, Card, Form, Input, Space, Typography } from "antd"; -import { useRouter } from "next/navigation"; -import { useEffect, useState } from "react"; +import LoginPage from "./LoginPage"; -function LoginPageContent() { - const [username, setUsername] = useState(""); - const [password, setPassword] = useState(""); - const [isLoading, setIsLoading] = useState(true); - const { isLoading: isConfigLoading } = useUIConfig(); - const loginMutation = useLogin(); - const router = useRouter(); - - useEffect(() => { - if (isConfigLoading) { - return; - } - - const rawToken = getCookie("token"); - if (rawToken && !isJwtExpired(rawToken)) { - router.replace(`${getProxyBaseUrl()}/ui`); - return; - } - - setIsLoading(false); - }, [isConfigLoading, router]); - - const handleSubmit = () => { - loginMutation.mutate( - { username, password }, - { - onSuccess: (data) => { - router.push(data.redirect_url); - }, - }, - ); - }; - - const error = loginMutation.error instanceof Error ? loginMutation.error.message : null; - const isLoginLoading = loginMutation.isPending; - - const { Title, Text, Paragraph } = Typography; - - if (isConfigLoading || isLoading) { - return ; - } - - return ( -
    - - -
    - 🚅 LiteLLM -
    - -
    - Login - Access your LiteLLM Admin UI. -
    - - - - By default, Username is admin and - Password is your set LiteLLM Proxy - MASTER_KEY. - - - Need to set UI credentials or SSO?{" "} - - Check the documentation - - . - - - } - type="info" - icon={} - showIcon - /> - - {error && } - -
    - - setUsername(e.target.value)} - disabled={isLoginLoading} - size="large" - className="rounded-md border-gray-300" - /> - - - - setPassword(e.target.value)} - disabled={isLoginLoading} - size="large" - /> - - - - - -
    -
    -
    -
    - ); -} - -export default function LoginPage() { - const queryClient = new QueryClient(); - - return ( - - - - ); -} +export default LoginPage; diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index 061d62d6700..0e45c0f3a91 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -205,6 +205,7 @@ export interface PublicModelHubInfo { export interface LiteLLMWellKnownUiConfig { server_root_path: string; proxy_base_url: string | null; + auto_redirect_to_sso: boolean; } export interface CredentialsResponse { From f035984dd79082bd0bc07154615606be3594e32d Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 3 Dec 2025 18:54:31 -0800 Subject: [PATCH 071/107] fix: cyberark allow setting ssl verfiy to false (#17433) --- .../docs/secret_managers/cyberark.md | 19 +++++++++++ .../cyberark_secret_manager.py | 33 ++++++++++++++----- 2 files changed, 43 insertions(+), 9 deletions(-) diff --git a/docs/my-website/docs/secret_managers/cyberark.md b/docs/my-website/docs/secret_managers/cyberark.md index 37aa1086691..c33aa286703 100644 --- a/docs/my-website/docs/secret_managers/cyberark.md +++ b/docs/my-website/docs/secret_managers/cyberark.md @@ -41,6 +41,7 @@ CYBERARK_CLIENT_KEY="path/to/client.key" # OPTIONAL CYBERARK_REFRESH_INTERVAL="300" # defaults to 300 seconds (5 minutes), frequency of token refresh +CYBERARK_SSL_VERIFY="true" # defaults to true, set to "false" to disable SSL verification (for self-signed certificates) ``` **Step 2.** Add to proxy config.yaml @@ -172,6 +173,24 @@ If these commands work successfully against your CyberArk instance, then CyberAr - The `CYBERARK_API_BASE` URL is accessible from your LiteLLM instance - Your API key or certificates have the necessary permissions in CyberArk +### SSL Certificate Errors + +If you encounter SSL certificate verification errors like: + +``` +RuntimeError: Could not authenticate to CyberArk Conjur: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate in certificate chain +``` + +This typically occurs when your CyberArk Conjur instance uses a self-signed certificate. You can disable SSL verification by setting: + +```bash +CYBERARK_SSL_VERIFY="false" +``` + +:::warning +Disabling SSL verification is insecure and should only be used for testing or development environments with self-signed certificates. For production, configure your certificate chain properly or use certificate-based authentication with `CYBERARK_CLIENT_CERT` and `CYBERARK_CLIENT_KEY`. +::: + ## Video Walkthrough This video walks through using CyberArk Conjur as a secret manager with LiteLLM. We create a virtual key in the LiteLLM Admin UI and verify it exists in CyberArk. Then we rotate the secret key and verify it exists in CyberArk. diff --git a/litellm/secret_managers/cyberark_secret_manager.py b/litellm/secret_managers/cyberark_secret_manager.py index 5f1e8b5cd07..2745df00778 100644 --- a/litellm/secret_managers/cyberark_secret_manager.py +++ b/litellm/secret_managers/cyberark_secret_manager.py @@ -16,6 +16,7 @@ from litellm.llms.custom_httpx.http_handler import ( from litellm.proxy._types import KeyManagementSystem from .base_secret_manager import BaseSecretManager +from .main import str_to_bool class CyberArkSecretManager(BaseSecretManager): @@ -32,6 +33,11 @@ class CyberArkSecretManager(BaseSecretManager): self.tls_cert_path = os.getenv("CYBERARK_CLIENT_CERT", "") self.tls_key_path = os.getenv("CYBERARK_CLIENT_KEY", "") + # SSL verification - can be disabled for self-signed certificates + # Set CYBERARK_SSL_VERIFY=false to disable SSL verification + ssl_verify_env = str_to_bool(os.getenv("CYBERARK_SSL_VERIFY")) + self.ssl_verify: bool = ssl_verify_env if ssl_verify_env is not None else True + # Validate environment if not self.conjur_api_key and not ( self.tls_cert_path and self.tls_key_path @@ -52,6 +58,11 @@ class CyberArkSecretManager(BaseSecretManager): f"CyberArk secret manager is only available for premium users. {CommonProxyErrors.not_premium_user.value}" ) + if not self.ssl_verify: + verbose_logger.warning( + "CyberArk SSL verification is disabled. This is insecure and should only be used for testing with self-signed certificates." + ) + def _authenticate(self) -> str: """ Authenticate with CyberArk Conjur and get a session token. @@ -71,13 +82,16 @@ class CyberArkSecretManager(BaseSecretManager): try: if self.tls_cert_path and self.tls_key_path: - # Certificate-based authentication - http_client = httpx.Client(cert=(self.tls_cert_path, self.tls_key_path)) + # Certificate-based authentication - need custom client for cert + http_client = httpx.Client( + cert=(self.tls_cert_path, self.tls_key_path), + verify=self.ssl_verify, + ) resp = http_client.post(auth_url, content=self.conjur_api_key) else: # API key authentication - http_handler = _get_httpx_client() - resp = http_handler.post(auth_url, content=self.conjur_api_key) + http_handler = _get_httpx_client(params={"ssl_verify": self.ssl_verify}) + resp = http_handler.client.post(auth_url, content=self.conjur_api_key) resp.raise_for_status() @@ -117,8 +131,8 @@ class CyberArkSecretManager(BaseSecretManager): policy_yaml = f"- !variable {secret_name}\n" try: - client = _get_httpx_client() - resp = client.post( + client = _get_httpx_client(params={"ssl_verify": self.ssl_verify}) + resp = client.client.post( policy_url, headers={ **self._get_request_headers(), @@ -180,6 +194,7 @@ class CyberArkSecretManager(BaseSecretManager): async_client = get_async_httpx_client( llm_provider=httpxSpecialProvider.SecretManager, + params={"ssl_verify": self.ssl_verify}, ) try: @@ -227,11 +242,11 @@ class CyberArkSecretManager(BaseSecretManager): if self.cache.get_cache(secret_name) is not None: return self.cache.get_cache(secret_name) - sync_client = _get_httpx_client() + sync_client = _get_httpx_client(params={"ssl_verify": self.ssl_verify}) try: url = self.get_url(secret_name) - response = sync_client.get(url, headers=self._get_request_headers()) + response = sync_client.client.get(url, headers=self._get_request_headers()) response.raise_for_status() # CyberArk Conjur returns the raw secret value as text @@ -278,7 +293,7 @@ class CyberArkSecretManager(BaseSecretManager): """ async_client = get_async_httpx_client( llm_provider=httpxSpecialProvider.SecretManager, - params={"timeout": timeout}, + params={"ssl_verify": self.ssl_verify}, ) try: From 4370f6fb74aa2d51d32c542e0314f078b65ec75e Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 3 Dec 2025 18:54:55 -0800 Subject: [PATCH 072/107] [Feat] Agent Gateway - Allow invoking agents through AI Gateway (#17440) * init litellm A2a client * simpler a2a client interface * test a2a * move a2a invoking tests * test fix * ensure a2a send message is tracked n logs * rename tags * add streaming handlng * add a2a invocation --- .../proxy/vector_stores/endpoints.py | 337 ++++++++++++++++++ litellm/a2a/__init__.py | 59 +++ litellm/a2a/client.py | 107 ++++++ litellm/a2a/main.py | 261 ++++++++++++++ litellm/litellm_core_utils/litellm_logging.py | 2 + .../proxy/agent_endpoints/a2a_endpoints.py | 189 ++++++++++ litellm/proxy/agent_endpoints/endpoints.py | 14 +- litellm/proxy/proxy_server.py | 2 + litellm/types/agents.py | 47 ++- litellm/types/llms/custom_http.py | 1 + tests/agent_tests/test_a2a.py | 138 +++++++ 11 files changed, 1148 insertions(+), 9 deletions(-) create mode 100644 enterprise/litellm_enterprise/proxy/vector_stores/endpoints.py create mode 100644 litellm/a2a/__init__.py create mode 100644 litellm/a2a/client.py create mode 100644 litellm/a2a/main.py create mode 100644 litellm/proxy/agent_endpoints/a2a_endpoints.py create mode 100644 tests/agent_tests/test_a2a.py diff --git a/enterprise/litellm_enterprise/proxy/vector_stores/endpoints.py b/enterprise/litellm_enterprise/proxy/vector_stores/endpoints.py new file mode 100644 index 00000000000..fdb1dba372f --- /dev/null +++ b/enterprise/litellm_enterprise/proxy/vector_stores/endpoints.py @@ -0,0 +1,337 @@ +""" +VECTOR STORE MANAGEMENT + +All /vector_store management endpoints + +/vector_store/new +/vector_store/delete +/vector_store/list +""" + +import copy +import json +from typing import List, Optional + +from fastapi import APIRouter, Depends, HTTPException + +import litellm +from litellm._logging import verbose_proxy_logger +from litellm.litellm_core_utils.safe_json_dumps import safe_dumps +from litellm.proxy._types import ( + LiteLLM_ManagedVectorStoresTable, + ResponseLiteLLM_ManagedVectorStore, + UserAPIKeyAuth, +) +from litellm.proxy.auth.user_api_key_auth import user_api_key_auth +from litellm.types.vector_stores import ( + LiteLLM_ManagedVectorStore, + LiteLLM_ManagedVectorStoreListResponse, + VectorStoreDeleteRequest, + VectorStoreInfoRequest, + VectorStoreUpdateRequest, +) +from litellm.vector_stores.vector_store_registry import VectorStoreRegistry + +router = APIRouter() + + +######################################################## +# Management Endpoints +######################################################## +@router.post( + "/vector_store/new", + tags=["vector store management"], + dependencies=[Depends(user_api_key_auth)], +) +async def new_vector_store( + vector_store: LiteLLM_ManagedVectorStore, + user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), +): + """ + Create a new vector store. + + Parameters: + - vector_store_id: str - Unique identifier for the vector store + - custom_llm_provider: str - Provider of the vector store + - vector_store_name: Optional[str] - Name of the vector store + - vector_store_description: Optional[str] - Description of the vector store + - vector_store_metadata: Optional[Dict] - Additional metadata for the vector store + """ + from litellm.proxy.proxy_server import prisma_client + from litellm.types.router import GenericLiteLLMParams + + if prisma_client is None: + raise HTTPException(status_code=500, detail="Database not connected") + + try: + # Check if vector store already exists + existing_vector_store = ( + await prisma_client.db.litellm_managedvectorstorestable.find_unique( + where={"vector_store_id": vector_store.get("vector_store_id")} + ) + ) + if existing_vector_store is not None: + raise HTTPException( + status_code=400, + detail=f"Vector store with ID {vector_store.get('vector_store_id')} already exists", + ) + + if vector_store.get("vector_store_metadata") is not None: + vector_store["vector_store_metadata"] = safe_dumps( + vector_store.get("vector_store_metadata") + ) + + # Safely handle JSON serialization of litellm_params + litellm_params_json: Optional[str] = None + _input_litellm_params: dict = vector_store.get("litellm_params", {}) or {} + if _input_litellm_params is not None: + litellm_params_dict = GenericLiteLLMParams( + **_input_litellm_params + ).model_dump(exclude_none=True) + litellm_params_json = safe_dumps(litellm_params_dict) + del vector_store["litellm_params"] + + _new_vector_store = ( + await prisma_client.db.litellm_managedvectorstorestable.create( + data={ + **vector_store, + "litellm_params": litellm_params_json, + } + ) + ) + + new_vector_store: LiteLLM_ManagedVectorStore = LiteLLM_ManagedVectorStore( + **_new_vector_store.model_dump() + ) + + # Add vector store to registry + if litellm.vector_store_registry is not None: + litellm.vector_store_registry.add_vector_store_to_registry( + vector_store=new_vector_store + ) + + return { + "status": "success", + "message": f"Vector store {vector_store.get('vector_store_id')} created successfully", + "vector_store": new_vector_store, + } + except Exception as e: + verbose_proxy_logger.exception(f"Error creating vector store: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.get( + "/vector_store/list", + tags=["vector store management"], + dependencies=[Depends(user_api_key_auth)], + response_model=LiteLLM_ManagedVectorStoreListResponse, +) +async def list_vector_stores( + user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), + page: int = 1, + page_size: int = 100, +): + """ + List all available vector stores with optional filtering and pagination. + Combines both in-memory vector stores and those stored in the database. + + Parameters: + - page: int - Page number for pagination (default: 1) + - page_size: int - Number of items per page (default: 100) + """ + from litellm.proxy.proxy_server import prisma_client + + seen_vector_store_ids = set() + + try: + # Get in-memory vector stores + in_memory_vector_stores: List[LiteLLM_ManagedVectorStore] = [] + if litellm.vector_store_registry is not None: + in_memory_vector_stores = copy.deepcopy( + litellm.vector_store_registry.vector_stores + ) + + # Get vector stores from database + vector_stores_from_db = await VectorStoreRegistry._get_vector_stores_from_db( + prisma_client=prisma_client + ) + + # Combine in-memory and database vector stores + combined_vector_stores: List[LiteLLM_ManagedVectorStore] = [] + for vector_store in in_memory_vector_stores + vector_stores_from_db: + vector_store_id = vector_store.get("vector_store_id", None) + if vector_store_id not in seen_vector_store_ids: + combined_vector_stores.append(vector_store) + seen_vector_store_ids.add(vector_store_id) + + total_count = len(combined_vector_stores) + total_pages = (total_count + page_size - 1) // page_size + + # Format response using LiteLLM_ManagedVectorStoreListResponse + response = LiteLLM_ManagedVectorStoreListResponse( + object="list", + data=combined_vector_stores, + total_count=total_count, + current_page=page, + total_pages=total_pages, + ) + + return response + except Exception as e: + verbose_proxy_logger.exception(f"Error listing vector stores: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.post( + "/vector_store/delete", + tags=["vector store management"], + dependencies=[Depends(user_api_key_auth)], +) +async def delete_vector_store( + data: VectorStoreDeleteRequest, + user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), +): + """ + Delete a vector store. + + Parameters: + - vector_store_id: str - ID of the vector store to delete + """ + from litellm.proxy.proxy_server import prisma_client + + if prisma_client is None: + raise HTTPException(status_code=500, detail="Database not connected") + + try: + # Check if vector store exists + existing_vector_store = ( + await prisma_client.db.litellm_managedvectorstorestable.find_unique( + where={"vector_store_id": data.vector_store_id} + ) + ) + if existing_vector_store is None: + raise HTTPException( + status_code=404, + detail=f"Vector store with ID {data.vector_store_id} not found", + ) + + # Delete vector store + await prisma_client.db.litellm_managedvectorstorestable.delete( + where={"vector_store_id": data.vector_store_id} + ) + + # Delete vector store from registry + if litellm.vector_store_registry is not None: + litellm.vector_store_registry.delete_vector_store_from_registry( + vector_store_id=data.vector_store_id + ) + + return {"message": f"Vector store {data.vector_store_id} deleted successfully"} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@router.post( + "/vector_store/info", + tags=["vector store management"], + dependencies=[Depends(user_api_key_auth)], + response_model=ResponseLiteLLM_ManagedVectorStore, +) +async def get_vector_store_info( + data: VectorStoreInfoRequest, + user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), +): + """Return a single vector store's details""" + from litellm.proxy.proxy_server import prisma_client + + if prisma_client is None: + raise HTTPException(status_code=500, detail="Database not connected") + + try: + if litellm.vector_store_registry is not None: + vector_store = litellm.vector_store_registry.get_litellm_managed_vector_store_from_registry( + vector_store_id=data.vector_store_id + ) + if vector_store is not None: + vector_store_metadata = vector_store.get("vector_store_metadata") + # Parse metadata if it's a JSON string + parsed_metadata: Optional[dict] = None + if isinstance(vector_store_metadata, str): + parsed_metadata = json.loads(vector_store_metadata) + elif isinstance(vector_store_metadata, dict): + parsed_metadata = vector_store_metadata + + vector_store_pydantic_obj = LiteLLM_ManagedVectorStoresTable( + vector_store_id=vector_store.get("vector_store_id") or "", + custom_llm_provider=vector_store.get("custom_llm_provider") or "", + vector_store_name=vector_store.get("vector_store_name") or None, + vector_store_description=vector_store.get( + "vector_store_description" + ) + or None, + vector_store_metadata=parsed_metadata, + created_at=vector_store.get("created_at") or None, + updated_at=vector_store.get("updated_at") or None, + litellm_credential_name=vector_store.get("litellm_credential_name"), + litellm_params=vector_store.get("litellm_params") or None, + ) + return {"vector_store": vector_store_pydantic_obj} + + vector_store = ( + await prisma_client.db.litellm_managedvectorstorestable.find_unique( + where={"vector_store_id": data.vector_store_id} + ) + ) + if vector_store is None: + raise HTTPException( + status_code=404, + detail=f"Vector store with ID {data.vector_store_id} not found", + ) + + vector_store_dict = vector_store.model_dump() # type: ignore[attr-defined] + return {"vector_store": vector_store_dict} + except Exception as e: + verbose_proxy_logger.exception(f"Error getting vector store info: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.post( + "/vector_store/update", + tags=["vector store management"], + dependencies=[Depends(user_api_key_auth)], +) +async def update_vector_store( + data: VectorStoreUpdateRequest, + user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), +): + """Update vector store details""" + from litellm.proxy.proxy_server import prisma_client + + if prisma_client is None: + raise HTTPException(status_code=500, detail="Database not connected") + + try: + update_data = data.model_dump(exclude_unset=True) + vector_store_id = update_data.pop("vector_store_id") + if update_data.get("vector_store_metadata") is not None: + update_data["vector_store_metadata"] = safe_dumps( + update_data["vector_store_metadata"] + ) + + updated = await prisma_client.db.litellm_managedvectorstorestable.update( + where={"vector_store_id": vector_store_id}, + data=update_data, + ) + + updated_vs = LiteLLM_ManagedVectorStore(**updated.model_dump()) + + if litellm.vector_store_registry is not None: + litellm.vector_store_registry.update_vector_store_in_registry( + vector_store_id=vector_store_id, + updated_data=updated_vs, + ) + + return {"vector_store": updated_vs} + except Exception as e: + verbose_proxy_logger.exception(f"Error updating vector store: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) diff --git a/litellm/a2a/__init__.py b/litellm/a2a/__init__.py new file mode 100644 index 00000000000..a2e9770cd20 --- /dev/null +++ b/litellm/a2a/__init__.py @@ -0,0 +1,59 @@ +""" +LiteLLM A2A - Wrapper for invoking A2A protocol agents. + +This module provides a thin wrapper around the official `a2a` SDK that: +- Handles httpx client creation and agent card resolution +- Adds LiteLLM logging via @client decorator +- Matches the A2A SDK interface (SendMessageRequest, SendMessageResponse, etc.) + +Example usage (standalone functions with @client decorator): + ```python + from litellm.a2a import asend_message + from a2a.types import SendMessageRequest, MessageSendParams + from uuid import uuid4 + + request = SendMessageRequest( + id=str(uuid4()), + params=MessageSendParams( + message={ + "role": "user", + "parts": [{"kind": "text", "text": "Hello!"}], + "messageId": uuid4().hex, + } + ) + ) + response = await asend_message( + base_url="http://localhost:10001", + request=request, + ) + print(response.model_dump(mode='json', exclude_none=True)) + ``` + +Example usage (class-based): + ```python + from litellm.a2a import A2AClient + + client = A2AClient(base_url="http://localhost:10001") + response = await client.send_message(request) + ``` +""" + +from litellm.a2a.client import A2AClient +from litellm.a2a.main import ( + aget_agent_card, + asend_message, + asend_message_streaming, + create_a2a_client, + send_message, +) +from litellm.types.agents import LiteLLMSendMessageResponse + +__all__ = [ + "A2AClient", + "asend_message", + "send_message", + "asend_message_streaming", + "aget_agent_card", + "create_a2a_client", + "LiteLLMSendMessageResponse", +] diff --git a/litellm/a2a/client.py b/litellm/a2a/client.py new file mode 100644 index 00000000000..0c3b8fff2ba --- /dev/null +++ b/litellm/a2a/client.py @@ -0,0 +1,107 @@ +""" +LiteLLM A2A Client class. + +Provides a class-based interface for A2A agent invocation. +""" + +from typing import TYPE_CHECKING, AsyncIterator, Dict, Optional + +from litellm.types.agents import LiteLLMSendMessageResponse + +if TYPE_CHECKING: + from a2a.client import A2AClient as A2AClientType + from a2a.types import ( + AgentCard, + SendMessageRequest, + SendStreamingMessageRequest, + SendStreamingMessageResponse, + ) + + +class A2AClient: + """ + LiteLLM wrapper for A2A agent invocation. + + Creates the underlying A2A client once on first use and reuses it. + + Example: + ```python + from litellm.a2a import A2AClient + from a2a.types import SendMessageRequest, MessageSendParams + from uuid import uuid4 + + client = A2AClient(base_url="http://localhost:10001") + + request = SendMessageRequest( + id=str(uuid4()), + params=MessageSendParams( + message={ + "role": "user", + "parts": [{"kind": "text", "text": "Hello!"}], + "messageId": uuid4().hex, + } + ) + ) + response = await client.send_message(request) + ``` + """ + + def __init__( + self, + base_url: str, + timeout: float = 60.0, + extra_headers: Optional[Dict[str, str]] = None, + ): + """ + Initialize the A2A client wrapper. + + Args: + base_url: The base URL of the A2A agent (e.g., "http://localhost:10001") + timeout: Request timeout in seconds (default: 60.0) + extra_headers: Optional additional headers to include in requests + """ + self.base_url = base_url + self.timeout = timeout + self.extra_headers = extra_headers + self._a2a_client: Optional["A2AClientType"] = None + + async def _get_client(self) -> "A2AClientType": + """Get or create the underlying A2A client.""" + if self._a2a_client is None: + from litellm.a2a.main import create_a2a_client + + self._a2a_client = await create_a2a_client( + base_url=self.base_url, + timeout=self.timeout, + extra_headers=self.extra_headers, + ) + return self._a2a_client + + async def get_agent_card(self) -> "AgentCard": + """Fetch the agent card from the server.""" + from litellm.a2a.main import aget_agent_card + + return await aget_agent_card( + base_url=self.base_url, + timeout=self.timeout, + extra_headers=self.extra_headers, + ) + + async def send_message( + self, request: "SendMessageRequest" + ) -> LiteLLMSendMessageResponse: + """Send a message to the A2A agent.""" + from litellm.a2a.main import asend_message + + a2a_client = await self._get_client() + return await asend_message(a2a_client=a2a_client, request=request) + + async def send_message_streaming( + self, request: "SendStreamingMessageRequest" + ) -> AsyncIterator["SendStreamingMessageResponse"]: + """Send a streaming message to the A2A agent.""" + from litellm.a2a.main import asend_message_streaming + + a2a_client = await self._get_client() + async for chunk in asend_message_streaming(a2a_client=a2a_client, request=request): + yield chunk diff --git a/litellm/a2a/main.py b/litellm/a2a/main.py new file mode 100644 index 00000000000..7e293dec81e --- /dev/null +++ b/litellm/a2a/main.py @@ -0,0 +1,261 @@ +""" +LiteLLM A2A SDK functions. + +Provides standalone functions with @client decorator for LiteLLM logging integration. +""" + +import asyncio +from typing import TYPE_CHECKING, Any, AsyncIterator, Coroutine, Dict, Optional, Union + +from litellm._logging import verbose_logger +from litellm.llms.custom_httpx.http_handler import ( + get_async_httpx_client, + httpxSpecialProvider, +) +from litellm.types.agents import LiteLLMSendMessageResponse +from litellm.utils import client + +if TYPE_CHECKING: + from a2a.client import A2AClient as A2AClientType + from a2a.types import ( + AgentCard, + SendMessageRequest, + SendStreamingMessageRequest, + SendStreamingMessageResponse, + ) + +# Runtime imports with availability check +A2A_SDK_AVAILABLE = False +A2ACardResolver: Any = None +_A2AClient: Any = None + +try: + from a2a.client import A2ACardResolver # type: ignore[no-redef] + from a2a.client import A2AClient as _A2AClient # type: ignore[no-redef] + + A2A_SDK_AVAILABLE = True +except ImportError: + pass + + +@client +async def asend_message( + a2a_client: "A2AClientType", + request: "SendMessageRequest", + **kwargs: Any, +) -> LiteLLMSendMessageResponse: + """ + Async: Send a message to an A2A agent. + + Uses the @client decorator for LiteLLM logging and tracking. + + Args: + a2a_client: An initialized a2a.client.A2AClient instance + request: SendMessageRequest from a2a.types + **kwargs: Additional arguments passed to the client decorator + + Returns: + LiteLLMSendMessageResponse (wraps a2a SendMessageResponse with _hidden_params) + + Example: + ```python + from litellm.a2a import asend_message, create_a2a_client + from a2a.types import SendMessageRequest, MessageSendParams + from uuid import uuid4 + + # Create client once + a2a_client = await create_a2a_client(base_url="http://localhost:10001") + + # Use it for multiple requests + request = SendMessageRequest( + id=str(uuid4()), + params=MessageSendParams( + message={ + "role": "user", + "parts": [{"kind": "text", "text": "Hello!"}], + "messageId": uuid4().hex, + } + ) + ) + response = await asend_message(a2a_client=a2a_client, request=request) + ``` + """ + verbose_logger.info(f"A2A send_message request_id={request.id}") + + a2a_response = await a2a_client.send_message(request) + + verbose_logger.info(f"A2A send_message completed, request_id={request.id}") + + # Wrap in LiteLLM response type for _hidden_params support + response = LiteLLMSendMessageResponse.from_a2a_response(a2a_response) + + return response + + +@client +def send_message( + a2a_client: "A2AClientType", + request: "SendMessageRequest", + **kwargs: Any, +) -> Union[LiteLLMSendMessageResponse, Coroutine[Any, Any, LiteLLMSendMessageResponse]]: + """ + Sync: Send a message to an A2A agent. + + Uses the @client decorator for LiteLLM logging and tracking. + + Args: + a2a_client: An initialized a2a.client.A2AClient instance + request: SendMessageRequest from a2a.types + **kwargs: Additional arguments passed to the client decorator + + Returns: + LiteLLMSendMessageResponse (wraps a2a SendMessageResponse with _hidden_params) + """ + try: + loop = asyncio.get_running_loop() + except RuntimeError: + loop = None + + if loop is not None: + return asend_message(a2a_client=a2a_client, request=request, **kwargs) + else: + return asyncio.run(asend_message(a2a_client=a2a_client, request=request, **kwargs)) + + +async def asend_message_streaming( + a2a_client: "A2AClientType", + request: "SendStreamingMessageRequest", +) -> AsyncIterator["SendStreamingMessageResponse"]: + """ + Async: Send a streaming message to an A2A agent. + + Args: + a2a_client: An initialized a2a.client.A2AClient instance + request: SendStreamingMessageRequest from a2a.types + + Yields: + SendStreamingMessageResponse chunks from the agent + """ + verbose_logger.info(f"A2A send_message_streaming request_id={request.id}") + + stream = a2a_client.send_message_streaming(request) + + chunk_count = 0 + async for chunk in stream: + chunk_count += 1 + yield chunk + + verbose_logger.info( + f"A2A send_message_streaming completed, request_id={request.id}, chunks={chunk_count}" + ) + + +async def create_a2a_client( + base_url: str, + timeout: float = 60.0, + extra_headers: Optional[Dict[str, str]] = None, +) -> "A2AClientType": + """ + Create an A2A client for the given agent URL. + + This resolves the agent card and returns a ready-to-use A2A client. + The client can be reused for multiple requests. + + Args: + base_url: The base URL of the A2A agent (e.g., "http://localhost:10001") + timeout: Request timeout in seconds (default: 60.0) + extra_headers: Optional additional headers to include in requests + + Returns: + An initialized a2a.client.A2AClient instance + + Example: + ```python + from litellm.a2a import create_a2a_client, asend_message + + # Create client once + client = await create_a2a_client(base_url="http://localhost:10001") + + # Reuse for multiple requests + response1 = await asend_message(a2a_client=client, request=request1) + response2 = await asend_message(a2a_client=client, request=request2) + ``` + """ + if not A2A_SDK_AVAILABLE: + raise ImportError( + "The 'a2a' package is required for A2A agent invocation. " + "Install it with: pip install a2a" + ) + + verbose_logger.info(f"Creating A2A client for {base_url}") + + # Use LiteLLM's cached httpx client + http_handler = get_async_httpx_client( + llm_provider=httpxSpecialProvider.A2A, + params={"timeout": timeout}, + ) + httpx_client = http_handler.client + + # Resolve agent card + resolver = A2ACardResolver( + httpx_client=httpx_client, + base_url=base_url, + ) + agent_card = await resolver.get_agent_card() + + verbose_logger.debug( + f"Resolved agent card: {agent_card.name if hasattr(agent_card, 'name') else 'unknown'}" + ) + + # Create and return A2A client + a2a_client = _A2AClient( + httpx_client=httpx_client, + agent_card=agent_card, + ) + + verbose_logger.info(f"A2A client created for {base_url}") + + return a2a_client + + +async def aget_agent_card( + base_url: str, + timeout: float = 60.0, + extra_headers: Optional[Dict[str, str]] = None, +) -> "AgentCard": + """ + Fetch the agent card from an A2A agent. + + Args: + base_url: The base URL of the A2A agent (e.g., "http://localhost:10001") + timeout: Request timeout in seconds (default: 60.0) + extra_headers: Optional additional headers to include in requests + + Returns: + AgentCard from the A2A agent + """ + if not A2A_SDK_AVAILABLE: + raise ImportError( + "The 'a2a' package is required for A2A agent invocation. " + "Install it with: pip install a2a" + ) + + verbose_logger.info(f"Fetching agent card from {base_url}") + + # Use LiteLLM's cached httpx client + http_handler = get_async_httpx_client( + llm_provider=httpxSpecialProvider.A2A, + params={"timeout": timeout}, + ) + httpx_client = http_handler.client + + resolver = A2ACardResolver( + httpx_client=httpx_client, + base_url=base_url, + ) + agent_card = await resolver.get_agent_card() + + verbose_logger.info( + f"Fetched agent card: {agent_card.name if hasattr(agent_card, 'name') else 'unknown'}" + ) + return agent_card diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index 0b0f483ff74..4fd2c988797 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -71,6 +71,7 @@ from litellm.litellm_core_utils.redact_messages import ( from litellm.llms.base_llm.ocr.transformation import OCRResponse from litellm.llms.base_llm.search.transformation import SearchResponse from litellm.responses.utils import ResponseAPILoggingUtils +from litellm.types.agents import LiteLLMSendMessageResponse from litellm.types.containers.main import ContainerObject from litellm.types.llms.openai import ( AllMessageValues, @@ -1738,6 +1739,7 @@ class Logging(LiteLLMLoggingBaseClass): and logging_result.get("object") == "search" # Search API (dict format) or isinstance(logging_result, VideoObject) or isinstance(logging_result, ContainerObject) + or isinstance(logging_result, LiteLLMSendMessageResponse) # A2A or (self.call_type == CallTypes.call_mcp_tool.value) ): return True diff --git a/litellm/proxy/agent_endpoints/a2a_endpoints.py b/litellm/proxy/agent_endpoints/a2a_endpoints.py new file mode 100644 index 00000000000..4534e313c29 --- /dev/null +++ b/litellm/proxy/agent_endpoints/a2a_endpoints.py @@ -0,0 +1,189 @@ +""" +A2A Protocol endpoints for LiteLLM Proxy. + +Allows clients to invoke agents through LiteLLM using the A2A protocol. +The A2A SDK can point to LiteLLM's URL and invoke agents registered with LiteLLM. +""" + +import json +from typing import Any, Optional + +from fastapi import APIRouter, Depends, HTTPException, Request +from fastapi.responses import JSONResponse, StreamingResponse + +from litellm._logging import verbose_proxy_logger +from litellm.proxy._types import UserAPIKeyAuth +from litellm.proxy.auth.user_api_key_auth import user_api_key_auth + +router = APIRouter() + + +def _jsonrpc_error( + request_id: Optional[str], + code: int, + message: str, + status_code: int = 400, +) -> JSONResponse: + """Create a JSON-RPC 2.0 error response.""" + return JSONResponse( + content={ + "jsonrpc": "2.0", + "id": request_id, + "error": {"code": code, "message": message}, + }, + status_code=status_code, + ) + + +def _get_agent(agent_id: str): + """Look up an agent by ID or name. Returns None if not found.""" + from litellm.proxy.agent_endpoints.agent_registry import global_agent_registry + + agent = global_agent_registry.get_agent_by_id(agent_id=agent_id) + if agent is None: + agent = global_agent_registry.get_agent_by_name(agent_name=agent_id) + return agent + + +async def _handle_send_message( + a2a_client: Any, + request_id: str, + params: dict, +) -> JSONResponse: + """Handle message/send method.""" + from a2a.types import MessageSendParams, SendMessageRequest + + from litellm.a2a import asend_message + + a2a_request = SendMessageRequest( + id=request_id, + params=MessageSendParams(**params), + ) + response = await asend_message(a2a_client=a2a_client, request=a2a_request) + return JSONResponse(content=response.model_dump(mode="json", exclude_none=True)) + + +async def _handle_stream_message( + a2a_client: Any, + request_id: str, + params: dict, +) -> StreamingResponse: + """Handle message/stream method.""" + from a2a.types import MessageSendParams, SendStreamingMessageRequest + + a2a_request = SendStreamingMessageRequest( + id=request_id, + params=MessageSendParams(**params), + ) + + async def stream_response(): + try: + async for chunk in a2a_client.send_message_streaming(a2a_request): + yield json.dumps(chunk.model_dump(mode="json", exclude_none=True)) + "\n" + except Exception as e: + verbose_proxy_logger.exception(f"Error streaming A2A response: {e}") + yield json.dumps({ + "jsonrpc": "2.0", + "id": request_id, + "error": {"code": -32603, "message": f"Streaming error: {str(e)}"}, + }) + "\n" + + return StreamingResponse(stream_response(), media_type="application/x-ndjson") + + +@router.get( + "/a2a/{agent_id}/.well-known/agent-card.json", + tags=["[beta] A2A Agents"], + dependencies=[Depends(user_api_key_auth)], +) +async def get_agent_card( + agent_id: str, + request: Request, + user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), +): + """ + Get the agent card for an agent (A2A discovery endpoint). + + The URL in the agent card is rewritten to point to the LiteLLM proxy, + so all subsequent A2A calls go through LiteLLM for logging and cost tracking. + """ + try: + agent = _get_agent(agent_id) + if agent is None: + raise HTTPException(status_code=404, detail=f"Agent '{agent_id}' not found") + + # Copy and rewrite URL to point to LiteLLM proxy + agent_card = dict(agent.agent_card_params) + agent_card["url"] = f"{str(request.base_url).rstrip('/')}/a2a/{agent_id}" + + verbose_proxy_logger.debug( + f"Returning agent card for '{agent_id}' with proxy URL: {agent_card['url']}" + ) + return JSONResponse(content=agent_card) + + except HTTPException: + raise + except Exception as e: + verbose_proxy_logger.exception(f"Error getting agent card: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.post( + "/a2a/{agent_id}", + tags=["[beta] A2A Agents"], + dependencies=[Depends(user_api_key_auth)], +) +async def invoke_agent_a2a( + agent_id: str, + request: Request, + user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), +): + """ + Invoke an agent using the A2A protocol (JSON-RPC 2.0). + + Supported methods: + - message/send: Send a message and get a response + - message/stream: Send a message and stream the response + """ + from litellm.a2a import create_a2a_client + + body = {} + try: + body = await request.json() + verbose_proxy_logger.debug(f"A2A request for agent '{agent_id}': {body}") + + # Validate JSON-RPC format + if body.get("jsonrpc") != "2.0": + return _jsonrpc_error(body.get("id"), -32600, "Invalid Request: jsonrpc must be '2.0'") + + request_id = body.get("id") + method = body.get("method") + params = body.get("params", {}) + + # Find the agent + agent = _get_agent(agent_id) + if agent is None: + return _jsonrpc_error(request_id, -32000, f"Agent '{agent_id}' not found", 404) + + # Get backend URL + agent_url = agent.agent_card_params.get("url") + if not agent_url: + return _jsonrpc_error(request_id, -32000, f"Agent '{agent_id}' has no URL configured", 500) + + verbose_proxy_logger.info(f"Proxying A2A request to agent '{agent_id}' at {agent_url}") + + # Create A2A client and dispatch to handler + a2a_client = await create_a2a_client(base_url=agent_url) + + if method == "message/send": + return await _handle_send_message(a2a_client, request_id, params) + elif method == "message/stream": + return await _handle_stream_message(a2a_client, request_id, params) + else: + return _jsonrpc_error(request_id, -32601, f"Method '{method}' not found") + + except HTTPException: + raise + except Exception as e: + verbose_proxy_logger.exception(f"Error invoking agent: {e}") + return _jsonrpc_error(body.get("id"), -32603, f"Internal error: {str(e)}", 500) diff --git a/litellm/proxy/agent_endpoints/endpoints.py b/litellm/proxy/agent_endpoints/endpoints.py index 489c8e82302..90688036d84 100644 --- a/litellm/proxy/agent_endpoints/endpoints.py +++ b/litellm/proxy/agent_endpoints/endpoints.py @@ -29,7 +29,7 @@ router = APIRouter() @router.get( "/v1/agents", - tags=["[beta] Agents"], + tags=["[beta] A2A Agents"], dependencies=[Depends(user_api_key_auth)], response_model=List[AgentResponse], ) @@ -101,7 +101,7 @@ from litellm.proxy.agent_endpoints.agent_registry import ( @router.post( "/v1/agents", - tags=["[beta] Agents"], + tags=["[beta] A2A Agents"], dependencies=[Depends(user_api_key_auth)], response_model=AgentResponse, ) @@ -196,7 +196,7 @@ async def create_agent( @router.get( "/v1/agents/{agent_id}", - tags=["[beta] Agents"], + tags=["[beta] A2A Agents"], dependencies=[Depends(user_api_key_auth)], response_model=AgentResponse, ) @@ -239,7 +239,7 @@ async def get_agent_by_id(agent_id: str): @router.put( "/v1/agents/{agent_id}", - tags=["[beta] Agents"], + tags=["[beta] A2A Agents"], dependencies=[Depends(user_api_key_auth)], response_model=AgentResponse, ) @@ -328,7 +328,7 @@ async def update_agent( @router.patch( "/v1/agents/{agent_id}", - tags=["[beta] Agents"], + tags=["[beta] A2A Agents"], dependencies=[Depends(user_api_key_auth)], response_model=AgentResponse, ) @@ -471,7 +471,7 @@ async def delete_agent(agent_id: str): @router.post( "/v1/agents/{agent_id}/make_public", - tags=["[beta] Agents"], + tags=["[beta] A2A Agents"], dependencies=[Depends(user_api_key_auth)], response_model=AgentMakePublicResponse, ) @@ -585,7 +585,7 @@ async def make_agent_public( @router.post( "/v1/agents/make_public", - tags=["[beta] Agents"], + tags=["[beta] A2A Agents"], dependencies=[Depends(user_api_key_auth)], response_model=AgentMakePublicResponse, ) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 4d971e8ce42..8e2c229c914 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -188,6 +188,7 @@ from litellm.proxy._experimental.mcp_server.tool_registry import ( global_mcp_tool_registry, ) from litellm.proxy._types import * +from litellm.proxy.agent_endpoints.a2a_endpoints import router as a2a_router from litellm.proxy.agent_endpoints.agent_registry import global_agent_registry from litellm.proxy.agent_endpoints.endpoints import router as agent_endpoints_router from litellm.proxy.analytics_endpoints.analytics_endpoints import ( @@ -10068,6 +10069,7 @@ app.include_router(user_agent_analytics_router) app.include_router(enterprise_router) app.include_router(ui_discovery_endpoints_router) app.include_router(agent_endpoints_router) +app.include_router(a2a_router) ######################################################## # MCP Server ######################################################## diff --git a/litellm/types/agents.py b/litellm/types/agents.py index 850dab0ea7f..2eb26dc6227 100644 --- a/litellm/types/agents.py +++ b/litellm/types/agents.py @@ -1,9 +1,14 @@ from datetime import datetime -from typing import Any, Dict, List, Literal, Optional, Union +from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Union -from pydantic import BaseModel +from pydantic import BaseModel, PrivateAttr from typing_extensions import Required, TypedDict +from litellm.types.llms.base import LiteLLMPydanticObjectBase + +if TYPE_CHECKING: + from a2a.types import SendMessageResponse + # AgentProvider class AgentProvider(TypedDict, total=False): @@ -200,3 +205,41 @@ class AgentMakePublicResponse(BaseModel): class MakeAgentsPublicRequest(BaseModel): agent_ids: List[str] + + +class LiteLLMSendMessageResponse(LiteLLMPydanticObjectBase): + """ + LiteLLM wrapper for A2A SendMessageResponse. + + Wraps the a2a SDK's SendMessageResponse with LiteLLM's _hidden_params + for cost tracking and logging integration. + """ + + # A2A response fields + id: str + jsonrpc: str = "2.0" + result: Optional[Dict[str, Any]] = None + error: Optional[Dict[str, Any]] = None + + model_config = {"extra": "allow"} + + # LiteLLM private attributes for logging/cost tracking + _hidden_params: dict = PrivateAttr(default_factory=dict) + + @classmethod + def from_a2a_response( + cls, response: "SendMessageResponse" + ) -> "LiteLLMSendMessageResponse": + """ + Create a LiteLLMSendMessageResponse from an a2a SDK SendMessageResponse. + + Args: + response: The a2a SDK SendMessageResponse + + Returns: + LiteLLMSendMessageResponse with _hidden_params support + """ + # Convert the a2a response to a dict + response_dict = response.model_dump(mode="json", exclude_none=True) + + return cls(**response_dict) diff --git a/litellm/types/llms/custom_http.py b/litellm/types/llms/custom_http.py index 4d5ca02f032..9ed25005c05 100644 --- a/litellm/types/llms/custom_http.py +++ b/litellm/types/llms/custom_http.py @@ -24,6 +24,7 @@ class httpxSpecialProvider(str, Enum): Search = "search" MCP = "mcp" RAG = "rag" + A2A = "a2a" VerifyTypes = Union[str, bool, ssl.SSLContext] diff --git a/tests/agent_tests/test_a2a.py b/tests/agent_tests/test_a2a.py new file mode 100644 index 00000000000..fb5620e44a1 --- /dev/null +++ b/tests/agent_tests/test_a2a.py @@ -0,0 +1,138 @@ +""" +Test for LiteLLM A2A module. + +Run with: + pytest tests/agent_tests/test_a2a.py -v -s +""" + +import asyncio +import os +import sys +import json +from typing import Optional +from uuid import uuid4 + +import pytest + +import litellm +from litellm.integrations.custom_logger import CustomLogger +from litellm.types.utils import StandardLoggingPayload + +sys.path.insert( + 0, os.path.abspath("../..") +) # Adds the parent directory to the system path + +from a2a.types import MessageSendParams, SendMessageRequest + + +@pytest.mark.asyncio +async def test_asend_message_with_client_decorator(): + """ + Test asend_message standalone function with @client decorator. + This tests the LiteLLM logging integration. + """ + litellm._turn_on_debug() + from litellm.a2a import asend_message, create_a2a_client + + # Create the A2A client first + a2a_client = await create_a2a_client(base_url="http://localhost:10001") + + # Build the request matching A2A SDK spec + send_message_payload = { + "message": { + "role": "user", + "parts": [ + { + "kind": "text", + "text": "Hello from @client decorated function!", + } + ], + "messageId": uuid4().hex, + }, + } + + request = SendMessageRequest( + id=str(uuid4()), + params=MessageSendParams(**send_message_payload), + ) + + # Send message using standalone function with @client decorator + response = await asend_message(a2a_client=a2a_client, request=request) + + # Print response for debugging + print("\n=== A2A Response (standalone with @client) ===") + print(response.model_dump(mode="json", exclude_none=True)) + + # Basic assertions + assert response is not None + + +class TestA2ALogger(CustomLogger): + """Custom logger to capture A2A logging payloads for testing.""" + + def __init__(self): + self.standard_logging_payload: Optional[StandardLoggingPayload] = None + self.logged_kwargs: Optional[dict] = None + self.log_success_called = False + super().__init__() + + async def async_log_success_event( + self, kwargs, response_obj, start_time, end_time + ): + print("TestA2ALogger: async_log_success_event called") + self.log_success_called = True + self.logged_kwargs = kwargs + self.standard_logging_payload = kwargs.get("standard_logging_object", None) + print(f"Captured standard_logging_payload: {self.standard_logging_payload}") + + +@pytest.mark.asyncio +async def test_a2a_logging_payload(): + """ + Test that A2A calls create a standard logging payload. + Validates the @client decorator integration with LiteLLM logging. + """ + # Reset callbacks and set up custom logger + litellm.logging_callback_manager._reset_all_callbacks() + test_logger = TestA2ALogger() + litellm.callbacks = [test_logger] + + from litellm.a2a import asend_message, create_a2a_client + + # Create the A2A client first + a2a_client = await create_a2a_client(base_url="http://localhost:10001") + + # Build the request + send_message_payload = { + "message": { + "role": "user", + "parts": [ + { + "kind": "text", + "text": "Hello! Testing logging payload.", + } + ], + "messageId": uuid4().hex, + }, + } + + request = SendMessageRequest( + id=str(uuid4()), + params=MessageSendParams(**send_message_payload), + ) + + # Send message + response = await asend_message(a2a_client=a2a_client, request=request) + + # Give async logging time to complete + await asyncio.sleep(1) + + # Print debug info + print("\n=== Logging Validation ===") + print(f"log_success_called: {test_logger.log_success_called}") + print(f"standard_logging_payload: {test_logger.standard_logging_payload}") + print(f"logged kwargs: {json.dumps(test_logger.logged_kwargs, indent=4, default=str)}") + + assert test_logger.standard_logging_payload is not None + + \ No newline at end of file From 585aee2ae465ce051c0d769aa71b31e94e17a1c9 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 3 Dec 2025 18:57:18 -0800 Subject: [PATCH 073/107] [Feat] Agent Gateway - Allow tracking request / response in "Logs" Page (#17449) * init litellm A2a client * simpler a2a client interface * test a2a * move a2a invoking tests * test fix * ensure a2a send message is tracked n logs * rename tags * add streaming handlng * add a2a invocation * add a2a invocation i cost calc * test_a2a_logging_payload * update invoke_agent_a2a * test_invoke_agent_a2a_adds_litellm_data * add A2a agent --- litellm/a2a/cost_calculator.py | 36 ++++++ litellm/a2a/main.py | 41 ++++++- litellm/cost_calculator.py | 11 ++ .../out/assets/logos/a2a_agent.png | Bin 0 -> 72568 bytes .../proxy/agent_endpoints/a2a_endpoints.py | 67 ++++++---- litellm/types/utils.py | 11 ++ tests/agent_tests/test_a2a.py | 31 ++++- .../proxy/agent_endpoints/__init__.py | 0 .../agent_endpoints/test_a2a_endpoints.py | 114 ++++++++++++++++++ .../public/assets/logos/a2a_agent.png | Bin 0 -> 72568 bytes .../src/components/provider_info_helpers.tsx | 4 + 11 files changed, 289 insertions(+), 26 deletions(-) create mode 100644 litellm/a2a/cost_calculator.py create mode 100644 litellm/proxy/_experimental/out/assets/logos/a2a_agent.png create mode 100644 tests/test_litellm/proxy/agent_endpoints/__init__.py create mode 100644 tests/test_litellm/proxy/agent_endpoints/test_a2a_endpoints.py create mode 100644 ui/litellm-dashboard/public/assets/logos/a2a_agent.png diff --git a/litellm/a2a/cost_calculator.py b/litellm/a2a/cost_calculator.py new file mode 100644 index 00000000000..2d821bb9c3b --- /dev/null +++ b/litellm/a2a/cost_calculator.py @@ -0,0 +1,36 @@ +""" +Cost calculator for A2A (Agent-to-Agent) calls. +""" + +from typing import TYPE_CHECKING, Any, Optional + +if TYPE_CHECKING: + from litellm.litellm_core_utils.litellm_logging import ( + Logging as LitellmLoggingObject, + ) +else: + LitellmLoggingObject = Any + + +class A2ACostCalculator: + @staticmethod + def calculate_a2a_cost( + litellm_logging_obj: Optional[LitellmLoggingObject], + ) -> float: + """ + Calculate the cost of an A2A send_message call. + + Default is 0.0. In the future, users can configure cost per agent call. + """ + if litellm_logging_obj is None: + return 0.0 + + # Check if user set a custom response cost + response_cost = litellm_logging_obj.model_call_details.get( + "response_cost", None + ) + if response_cost is not None: + return response_cost + + # Default to 0.0 for A2A calls + return 0.0 diff --git a/litellm/a2a/main.py b/litellm/a2a/main.py index 7e293dec81e..7827581ceac 100644 --- a/litellm/a2a/main.py +++ b/litellm/a2a/main.py @@ -38,6 +38,38 @@ except ImportError: pass +def _get_a2a_model_info(a2a_client: Any, kwargs: Dict[str, Any]) -> str: + """ + Extract agent info and set model/custom_llm_provider for cost tracking. + + Sets model info on the litellm_logging_obj if available. + Returns the agent name for logging. + """ + agent_name = "unknown" + + # Try to get agent card from our stored attribute first, then fallback to SDK attribute + agent_card = getattr(a2a_client, "_litellm_agent_card", None) + if agent_card is None: + agent_card = getattr(a2a_client, "agent_card", None) + + if agent_card is not None: + agent_name = getattr(agent_card, "name", "unknown") or "unknown" + + # Build model string + model = f"a2a_agent/{agent_name}" + custom_llm_provider = "a2a_agent" + + # Set on litellm_logging_obj if available (for standard logging payload) + litellm_logging_obj = kwargs.get("litellm_logging_obj") + if litellm_logging_obj is not None: + litellm_logging_obj.model = model + litellm_logging_obj.custom_llm_provider = custom_llm_provider + litellm_logging_obj.model_call_details["model"] = model + litellm_logging_obj.model_call_details["custom_llm_provider"] = custom_llm_provider + + return agent_name + + @client async def asend_message( a2a_client: "A2AClientType", @@ -80,7 +112,9 @@ async def asend_message( response = await asend_message(a2a_client=a2a_client, request=request) ``` """ - verbose_logger.info(f"A2A send_message request_id={request.id}") + agent_name = _get_a2a_model_info(a2a_client, kwargs) + + verbose_logger.info(f"A2A send_message request_id={request.id}, agent={agent_name}") a2a_response = await a2a_client.send_message(request) @@ -207,12 +241,15 @@ async def create_a2a_client( f"Resolved agent card: {agent_card.name if hasattr(agent_card, 'name') else 'unknown'}" ) - # Create and return A2A client + # Create A2A client a2a_client = _A2AClient( httpx_client=httpx_client, agent_card=agent_card, ) + # Store agent_card on client for later retrieval (SDK doesn't expose it) + a2a_client._litellm_agent_card = agent_card # type: ignore[attr-defined] + verbose_logger.info(f"A2A client created for {base_url}") return a2a_client diff --git a/litellm/cost_calculator.py b/litellm/cost_calculator.py index 9ef26d23ce2..943eb069565 100644 --- a/litellm/cost_calculator.py +++ b/litellm/cost_calculator.py @@ -934,6 +934,17 @@ def completion_cost( # noqa: PLR0915 prompt_tokens = token_counter(model=model, text=prompt) completion_tokens = token_counter(model=model, text=completion) + # Handle A2A calls before model check - A2A doesn't require a model + if call_type in ( + CallTypes.asend_message.value, + CallTypes.send_message.value, + ): + from litellm.a2a.cost_calculator import A2ACostCalculator + + return A2ACostCalculator.calculate_a2a_cost( + litellm_logging_obj=litellm_logging_obj + ) + if model is None: raise ValueError( f"Model is None and does not exist in passed completion_response. Passed completion_response={completion_response}, model={model}" diff --git a/litellm/proxy/_experimental/out/assets/logos/a2a_agent.png b/litellm/proxy/_experimental/out/assets/logos/a2a_agent.png new file mode 100644 index 0000000000000000000000000000000000000000..305ae1acaf434f9b4151cdabdc86038abdd34f00 GIT binary patch literal 72568 zcmY&=cOV>J_cu#|Akm{+-Re=J_qJ-5u!KYxokes)kmzmony^HochRG+8a;&QEky72 z9p9(CzsLTuvv=;CJ9p-u^7))|h3jak5I&%OfPsNQsHUn2!@vL~VPIfk;NAyXJfbs~ zfiFxqn2J0`=@9KE@I%K6u4b*NiNOgR<6?j?Nr28k7vO{O-WCJ7mt%0 z^S^}rM~TpM9)1&n+rU;2sUKRy4a?cd`_^4$&oe{SYqGySI(n5y&xNxuI+ zHt7dcFBfqzFk~^*6y@~1Ft_K{t1P>`W%iiy6l2tlMtQdrlJ6yFnxzoPV+UEPC6ha( zyqe*=dUW->p)bJ9z>pXH#*ic-j_3nek*ItnGbPi^5D`O?d)Anf>?>s~GB7Lg@p547 zdEwBk@A}Zu`J2;lC@TDa z8~GivgY~p%zMsAQUm@V6CpiC=_|K_=XxX?vTLnsn{}n)#FR=E%m$p?CNGC0xK8hRxmmccZ>e^%JaTHp2`rN_p9>W zDybViKKbP=y4!N;bm7YupQdUpaQLc1fE|%Pc>Io=B_|sY1u%GpLk{a9MU#8x8 z4Q$c|mQK-`*LkZ`QB;EInj+IICO7oK{+;i{SISfCTv%2CpP=s9y}*WNN~5Fknl`aQbFTvJoQIPH!p5Y`mMb4Yugb zoO0V@VO6HjMGf>GmXBTYrf&|H>|3O(LlN!{;n({Fw`X8f)F#cT4yvF7=O z)TmlH=@~cSxnK|Fq&Y*D@yFsf2}+g;l1w?h`Hx@uRd{crQbv8@VD|n3?!1HMpYNk% zYCxC=`GkS3{`%$ zhm+loLn)*DIj~|Kc3N+5+@)^LmcwPOF$s$(Bla`-grz;$BF_1=u2R=eflOwtObmRKSzmHz?35Mq%yFH!*bW06B4-5d&AKi8P z;e)%g;~MQb8H>?AAOOMqApCaAcFM%l@uAGsp7rhZsq@-ETH=y$Gds3-U{6MHhF}Gx zUXle+KDT#@59$`B7*j%4DVL%(_X%W4Qo1bu*v*bOSVh?Cad;u%> zCib}xyZO&ed3hG%Uf-Ptc0-I#rf{`{BF)b@@!P};UcOw!vixq6Y@%XyPhn{CF4mec zEeVu{o6~1-s%{2og@g~Ul4Ex5<9m82ly^#`frUzE+cG9HbEd8{@E+)|Jce!lSp=2} zo99on_tvpeC#u;=-iK?fRzK$3Yp#l;o+q7_FGj$91)Zhow%9 z5#D20dG|m;ZC{9GX|0l-s|H2FjVnzfI#@bcG0-~$$-&P9N4;*7j6XF+%qvr(Ke4JO z3q6v#a!GhroWR$5gsE zf|xZ~p6-g~=R6*Rq#$TT`>p=nx0kuB@1^hG30jvnOf&)=QREkYph&j-q4`Siw;U(? zpoF}{<6G2UH)a#JL19Sm-0Y;5=yB`yI5X0lq%YUc}j@0EB+OE}SR4PSh}tKIZq z30WK#DZT&E2(rDT5_`+i1~1q3J8QNu5hWYwLtbLHmo5dbajN-uZ+I`KlN-W*cA58` z-kSl{;1^~GDD@P!uPg%Iz-mUp)iC!Kmv%6?Ufu^`DZh15rYBZg{h%ebid;2Z4iM!{ z#zedqh}2+;n|c-K?{++4P?)O58hj;XIrf&_!p{%9*NPjbZYTm#avylo!_sB2jrz1z zoV+{>IOZtnpl!H-+z!;~GY@)#M?X9RGOM=Sq_{M#5{+SwKIZYdIU3V;ZaG5np%%PL z4{AmGxWzgxKC$SqC4NVE=`mSX{uKmgmsw)>f*+K|(baf)KZo21~W@qc>-n?K!+G$!Vc~&!%%+mv2 zoM=JL3z*5+N052u^AKXUhSwvX6ou$axL){W&iqV(Ix(-$RAS5``gt~wXjIrqR=6<$ z6_w4U#U?_}z4=vQSZ*q(pi?JILu;!~uX=Sd=Q*EcJSXy{FegBq6(=mCF;k363J)7> zstNN69mIQPhWyS-rkmo@8&UMFtG>RgCq|Dc>O-E~`X6PE2@hRf4y|NdMq*s#dkhfz z*nifw2h~ZJw%{05$daL#q(jfaj#H^s`9m33%llBkhYZ)9>8&v|zbb&L7%@S)GRpYK zc**XtRw~}cK^66rnF3?wNslIiD-9!lWUfJI$pftO5QNPy=l4SnhZWWU+&>DALijwr z&i)xS{=J5{0?PZc>1mf%rM@!m(9wRhHZFEpxNNO)U%zQh=My50Cn-=8G`+JM@XX`- zXeEj`_MRJrx)vaXWx-+aHB6lBP`zXjtCRmIOJzb7cE}oAE^Ew-#cuHe3<{%a;!{?*wtRgSdm%6qhBoj2dXvMdX@5(7`+=U zfDuk;quAa#mE$-R`rr%Jp$B1?o~g<00`A0gbgxUl7R88cVi%otQE)@ErsSWg^|Xeq z=kqYr4nNt!wM4~F$sLp(|7`<(l!OwAJt=nlC|SamWT#cB1?XYTra57+;oN-*BOvxf zMz*nKI3&y}sapL?aI3GeijK%qF*d#@WzFn4YR;6+D}kW99eWqjHLsc=r)@SnS)COfk1L7K3;}&xZD2 zOPnMFd>J$!n~p}+b>$9?J(zG;_tuU4EWrE(*e;LkU*Rd7-UTC&1f(_l(T5rb1$*)$ zmN?ssh*X=ceADTamp+ht-|NiUmZ$)CA!{1RQ>$9I@%ens-jyS zQ3CtTpsvt`v=gq&y&^S83@o}%Br{WCG087ak?5R-QRQh&YG1il%rAo2bvjS%J@Q$_ z@BH#;JG4~5UOP@9OLn@HUhIFK( zi!WD9vGEVSfc?m|KCUv2-$>QoBG)&{IaonAjJe3vN(BP*;uNnaxH>tXU5WcMsiOQ< z8eFA;z=OmbmO2tbjFBh`K!H6N-^y~56`SsDPglKgMLaZgU(fOsk-6EA)rH3xH9QNf zEcK>pCd9rUDk~CElrv}^KJ0gMnB%-Qm@&m6q4(lzx*ih}`-t4Yc;xD8T}GPqBPCT-g1U!cUwhL7@ zKAs;^d*xt2a9CsgLs{K#AOGu8xj=YLK1pNB$Y54J%X>KM6Fjqzv`5JB;YZjPu^9NM zpK9S^TEj)nA#s|)>gq+g3^#l4!*!sPdKW^G)?Ug= z@L3#&=T*uaa**jZq=vamg1~N9X~-RdP?uo^`4GirCYB(fcoPL#+REvYMwOOo2e}+{ zEFFTltCq;6^*f)Xh|$W(?q&M?gDhtfgZNsi#TT)wkDM^PJ=&iUItXtrh852n3LXF< z{ZV*(zoq>?O+G0boHI+{*&$j?1cVA6&m~LmSf!&!UaVOiJtd+f2NBq;gpGbsMJ3p& z(7Gyeis^lGC=S!JmPgBbk0?DBmN{#GM6v<*zw?5hA7kqGr3u=w)t#0$?ig9Q_$a?J z9w*h7jPGTEsl5#So-q>c3#g-eB*yU9e7fw1Wsv#zbZ_8Q=W7{{P3r2XVc`v575Tyr zUfry|f?ZkSZaEd&H1QE}kYWapx<>WkpwFx;ifev|wci5Kc7B4~3XKgpojYVo`l6EQ^QWlG$322QsXCAX$|^P_eYwq2 z-yuI;W=owziW~J1Qy*HI*?cm?HBnG}Ook@_ogEt#ouADFg1AlYT@D(dZ{Fndw7Av8 zm%^PA}-2; zDt=&w`ksOL5Y{8fxIg`XBk~7?>iF5ypf{YsI?^D^K@-opqz@?gQKGiA7;%4srAlI;7s>}| zVB+z-%4>fK8E*JXP%#h-2rB4s7c{>+!y$Cejs3ihQ9>=1lrYGGi9V^psAJt+GHBt~ zla+lkU>(O16KU*FD)&5LO0M3Tp=KNsFJePke*YJP67>KRTFOiZMAHYB6=X(2_LIfW zJPOZ`dRrQnBMn&?03(9krztu&-gI-ZgtEUH_$6YQqM43A;%jX9_OjJXyWmqhZO4$ugV*x!bB9*q zt?f%1$i@d#=56G5Y9e)Z-^+@!*Znk)T>GK$NYhhhYJ34Kq{5AQM`z-$}iz|Z+c4fD-W`lUmtp)|&8h29_hCxse zxz=2lU-RLRMEF~hCBwK=b7B6vL7VB{?->HTd`!kZ{M<&0ORQ(Qb!(jqzv*RQZBvqt zm&kCeoj!;(CgJLbCXn9mMHTO@-MS46aH?8qa+$jLvt|esvE&KZvgEm7BQ^nh;nj3ft?%W7%Qt(V`>9>OsxGR01}&2U7!47FD(b?jRFsedg7}y_Sh&rA1^28 z9=_kMJ#@B}sXEI)aUCCwMzErjxtC>1S6eDcl`SDX zX|w&bwAfj;4{h5mjwjJW>!xA!wd2pKDobN~ts3zUV>Pz-YWz;o3s$Zy`AL{AmAC?G z9pGw1P<_2)+q9-)<7pscb&q2$!HxG|g@d6y$$*mLSg0&hTADH9R{C&25N*fqaBt)j zM`P*xX_wIMa)E<5mz+Vse~P8$uP`MdUs%jQ zQm$SlYoAvw(c>Or1xd0Gin%$Tgdh}n4KpmYlL6YzLKn^Q*eS>|TDPNYVdvaJ0%uY?(OO^EnBR*&C!#?Zms1qM=bj8 z{ODauT2M%xi1%+-Po{J-`biCz!z6>3YLl2sZFG8j8+ow>Ie6zo$J>GpN)XM%_2Dut zM)M>T2A^SN{sax#DtY_>0_|17>6Sy~>jv)e@O9>>efQ%1J~~IS&95IC)%|xa7ATHf-uq;fiBPxY2f%Kpz8Qj z#3|#M{!{%E!~IKH)1SDht!oAS^w4ug3opaW_nM3>DahNth_ZRZDMx4S2Ki*ChSg+6 z?#1se!W$xbFf}lKX;-$@GXcZ8ES(u0&7*13H6Qt3ik_~_=l(Hr)JL*C>o}lmMGUl} zh%s)#c~sO>pZy>ldeyBA@Z;r9o*2LM(~cj=%k#{T%2em@5XXe)A{P2Rl1?~u?mATf zHBmVD(+fr$HwZWbP2AKeNq^o+vBQjQ#uPYji4{E#g@$oR4{9))I2H#zGR2)dz0BWh zL(f@cnbZVOza(x2QhS$mH6wqz)FYxBHJ#&aEHQrO2EP!ahB1Wa=ex`R>}DU>N;==wutA z=73iPL1)=LZ z=JR|sr3G1xQ7qDHiCydYiLC5b3`S6>V&%(-Dduq;*3SE(tll1{bDr$?>IrG0pU57d z3oK%#MB)QnXKSMSbGs#^>#lKAw@zM1kgpo3QrllEJvJFPcI_fZv$lNUq^M!)R3DJ= zn^U}onnCd?xGCWrqdKar1scB!$+@#s&z&tIWHfWUO*B6zYKcRv8W2ycJYl#yMky9Y zzF%-t56$^Xf@acTEUGA&e2udJkT*CBsSdJeN%rzp+!3z zqq9>t9{_oyF$;^Pv2A5H@Ouf?p#kCM=hGfQ*S>9|jcC*f?wz{jTpAyC@dZ3-9cCYuVe|u&h!*Pm%<{3WlLQw z%O5^khMOhfS=DuqL`grw!t@pW%F0DQ#h(CKOip<&cPv8L=^`-9L^dKv#yL`dF?}D( znKhPX&%&7E_%tG3oyObUZ`z>cAnAS)S;~lW?fkG@%GR`M2VE1Ncmt3w#Z+l{U85OyAQE@IoR#N zNy1@&eiB(C=sjm$(SLceE4yJ@e^WL0$sFa|ZiaK+`yhq+ofH-5Vn2B+JGNxxYc+}D zq$L&N{($PBI4GXjKqq$+wns^HvbnrtK(WBgIZ_RMhlvM4-v>Q=^Thb5~X@K@BdSjJLb-m|s zl%+?~E#%)dgttMxoIt=Aq`~Z(T(RvG`Y!9$wtMD3Q=!L((|GB>{!8L+*MGJq9G}{e zhyZvOc2N@L@Tzww{D2*l>F|I+-qIF9s6N8Iq}F@*$#c*X%+|A|$Z);B+~NvMU4V6b z+i)&}o))&utOyWRvbpNmaB4UQntM8~MyQX^K757N<}Bhm=8mE2P?9D1g#9w5er*$e z7WV5fHZWtXgLbahPHZlH$-hdk5{B^qst!4;>OKRd%M=7$Y!-^%<)1hME?hU33jx;R zH7UrFoG0sjXxGQ&giC$AD)!uxyom4pJ4q0>6TwSLu9@|$`Q|GY&f?E01*ucdHgu5KjL05#Lv ziw{G=_wX+Q-Z(d^NM`dAZSaBYOKSA109eRs9xm~@TPLF{;n&NvVF3T9M!g$DTkql* zjbkb%XiMkBPS2asY!{|sXnoS*T=;1b>u5ckZ8Dcn$t}OX864N~mwnQhc@?^4T6w*%=EU`el{FnU^lsN|_YxUyI4LF%lFrKNGDBdlo4j zg9Q?_9#HkBw8{2fLp%loI-V;Phx0oZ%tJ5g2um}W=SIfxeo=b5-mVOO-j}keIrhN_ zNtITQ1Ny7T33FTH4lnx-X+aL5s|${twj6N{s7W z%wT6%rOC?oIkCKZ=sfeAQj6HOH8S)N{$poZ%8VQSU+AdF_l6pU@W#FIO9J)sh~{Z< z2_9o2)VW#Jt$0eEu@j<5)WrgCu39n!o*0iW{{jWQVQy1JJr!pr*OqrYE`j`-z9&6# zpgP8uq?#|}zFm9YvDFRAg_9d0=~gqPJNV|$)qocru;i?m-|BEObV4Bxg~`Rvc$hKa zZnSv_c{3EZZ?@8IHz{T(qOAD@+WlpwkAN|$Kbc4UL7km+z)p=OaJ!yd?xmmnG@}iD z?vPgxPk3Hb05sS^fd22uQUR0aQJMI^B)3n_~z zc#Mz_SrcRhhZqpY~Ma2AMaYdS;XeU%VPwxXt zf>Fd$6$b@Xhb7P1{ejKoe}@`&d{$!UA;;WGOak;$sl4DgqDO2vvkSqw7&XR}1TX&7 z9xHjEDvni^jA!|lu$r0DMjN?;D{t7}t0aRJw8USBDX5`9U|?hj!O(PK&# zX2n3pI3Mv5QDTE8i#Z9S3~DBt*%;#cia6$g2Ro}e)KpxgHsnf{_tq1O;m#0sV*$jS zQUL2l6M$QF5xWF55Gel^i;~yQY{N(FN4^+Km!KdB*mUj-w;PaIK`raqKd2yeYjNKk zg?Q%zbGJQLWyLg6&s&s;ATFA<`&uF|J0;*UGAx_>2=_-*)c{kg9q#nA?MxLV*vIs5 z|2)Zu*pwwmPBQEDw@F6Sd_phR|2$XLO4GYwQhJxBIwA4}h36u(5~l!rqmSJ2kT0*p#e?;OO%J^rh_|Kto_*MT2;?j-={=CY?Av|l&j%LCl|XVxp+BX4*N3M;#0rLIpPzuL46_E`tEYj3e1Hm*GHVz6 zHqi@8Tx1Le7mAY{n(ICH^@R=Du!?m!>JA7jpZ$DBAnRuX=GVmT)UVVRFMrzwd#X+t zjYD05{P9yA;1~29=_3CgPNK7%>g+%1=&;5*|TEVToCr}z}sswZ8hT3 zAe~@jQU{urY|)6DmRuur!yL>ngkAh%i}IG}g<(HLLh=XFg+2Cov_ONiA-)`bN=;oQ z$c3t;HA5Afb$*Hsk_{x**)UFs`5M-JEakQ~up<7oN*t)qb>o3R>oJU5iU) zed~FE%T>h#Z1JAPLl2vEiJqoIk9FUfE2FCwiQT3|NiOJvi66Z@*-6u+Y4gHK%!cH( zS%+1J9yd8d4b$i1w^BPE%s<4|vdRv1Jz~7qx2{f&cGKS70d+JXKf&u)ew#eI}kC2W0yu~XFk9&GcLy-S4EASi>rzm`p`pm4_|%&TF64EA`eT2U~{^ zT`MyM4DxEQg1Gay@2c3!gB)2smMKnrz>_~$F*d9l*zedC{77CzW8LeeXqk3Ey`za!t} zrF%Kcx|o@SYGPY&GUgL@dM)tYp}6WN%bpmC$0O(dw&UwEC9&aybQL67&1!(013S#n zmx>X8e5o3oW$q53L(w~^%9J3Y38EfRd@3O<+Zm)&`!pW!X|JN&N(LUUQ~&&oY<(d! zw)*X>W#=f!!y)Ttz!)XQi7*hUg*H1bWGy@2c*e-wQy2+^CNFmkV9Dt6&p>(6TwCvm z-kT5-dc#si3+%4!?AI7Wr(B&^#sS=(#!cTHG?7YM^>L;;DpvUh{0NyvDDo3bOc3z{ zNL?hZeNn`zob?ShPL^Qamg-I(QOi=WaDE66{E)w2r|tbHt$<29zyD`~Qa1x;o-rI} z?d^VDpy4Com{#eGWihRpna_-5Jzn8og(&8#A@NX9kjse`N|Y37I)o=K?377Sq{Pv_ z0F?V^Mv|pfn?_?{W~;6I>F(&PnuY;WXs2BMvM7#@-lEXN|5Qb>LhY4nA));L}fKhB`yLirL7-uDG^p&|f_ zX(+rL&!fvtvogk0H=D3#U;F9QJcJ*|Y{6~d{DpI~a0B6>uB)vUJ05F&Q(6>9f|}uG zO%G*YWu&|V&zrHR*5>s6&NF-y!IJ|f_^x5uVC+o#I+KkQ)+ZCvB1ym$G=6WgfvLG* z1XJ`e!F=+jf4IOc?8aP72`%(L15s&y1Mu$0LUkzL6|Wu(+?O8vZlK@A7HNFuki5jYaqNrP4$`R zw7>=J$$oNm1m8;$PPrt<}yNGy`oqJ=+hM4_T)_$Q5 zB?CoHKVPe4JT&7-4;fBwCYze}%HWm1M^`D0CWx%;yH! z%!RWejeEynK7z5mL^RR@-Z$s#?4aInza#JYKOaW-b(x1&8(~FJ-*L7Ptqyq}{?Fma z6VZd-GiKG+i;1#yuF`rYD$e4@0HMksHdKMW$eL}N#TQG?_J^fUW}dP}evCXwU4~u; zO@O7bre25>>rVeQ+DX%+Hq9sTbc@GWBTJQV-Ge?bGU2<@@jHequ;INS z$-@Clyif3PN?rjf=6Xh^8WxI~*YIe=DD|Fs<#!9Nx~~vu*1_Sxn?uE``&RxRDa(T! zbiZkdZ{{m<+f~6ev-4_hqI4cW7NstyhXW>!y;qaaT&A~wJOvB4wOcweE!@QpdYMV( zzN%HUDfCVP+Qi#Mn4-n`_YqRe!m$-j7yBnokAw+sPne%@zoSzl<0IYpC^vmb!ep!t z!k0AnzPrw#=Zw2;8e4RZZSV)HO`2c1{8SU`MoqUQi3ZkmPfvKBZahn4kR_Zd@Aq7kKvQ2=wyw>99Z3(rZ5GqL*kB9j|_N zKbA=PIpSS~t2N!4m|+&qR~wiu*wITjJdskwMc|&5(S#+cS_f~2Tje(%ZCx6{E@|~<*9}; z!Xm>KSf6q$T*iqiI|NPh;-B_hzz`O2a1L+N-uKnX4zmH>)SrP4-M5D_w~0t_C#+sp zv-8tRg^9_0l;EQ_AQwAf?9zS@jS=krQ{n!cpJN!+o#HI%6+9Ny<-v%RENQ}@t* zPQ$~&h3GQjrj(b(2AU6})hEm!wf;<jC!`>TLga369JwVUWxy@6|6@_w88QCxQqpMoq}_*`~>Wki-E zSu5`385LC=O=035H0|pi{ynd9K{^~+{-|d4rb@9V+t&WN6o%Xoi-%|M>etU;A?l~a z73JxLO(bNeqvv{S*ijF^+Dm0k*bqrI1yy4v&uVZ6?HJby!#R2M>PIgOIpe>X*#~4T z+a3^6df9h4o8iJPL_P50|~!Vi(H^T$;%18w=b_uFzAc= zfX`LKxPHDNl^jWed1nS&kBia3`~h93m4K-X!4AyO8xiN==xhqkJ4SE?}q?YNlYM* zx(|cA#4O@&ev=liO*vDZMXB>CLo26V&h|5fJ>jy9)V|o&tu>(#4wU)tTA?)FyU&nC z)pZCgRv6fHQzd;b#m&-vhtt9UU&C(%LNjHpU7Sqzw;oE*Q*sO1z}O{L%&M`s-K@ze5LLz-XNs!m%QBAmjcSw0?? zix>ErQTj3{B1=u4ko7)`4z&G>~!$-h3*B~9970z_I1`% zAF3wK`hsUb+pX<6LU2tWQ9Y-S)#7P`^NL=rUZbZ1FI!+Yf50SP&AHhejqAKm(O#rn!S09sR!6gAg(k*qLJ zR69DQrypR)|JHD~9=h4hcQEhsCoJzsgR}8^AxU${>^hrbfpa1W>B_GfBHRxZ#Y(<) z{^^-7@S^D3hYSQiG=jfjnvd^7{N>x(GdArvwm$b% z{n~wR)$;8+=UH=E*uDy<)#r$!Eq&aW2g6s*eYofgUa8VPNnsr5>CH;OHXI~?aXddLJt0`cqVw&1;TaZE4JF6b&`$Evz1$C7Px8ZV)3BJa_);|Q z^-ZxJg~KM{xd%wGXyttYU#hX>>S*}Vl(5i&`%tFXpmm0${uM31L3~6)KRuP3z3t(k zC?EYW^_tBNbDDDwLfRjFskc)WFe&>M&25ajpFPTRYR^+drkDB7<4}=n->IS!CU4a8Kk1#W ztFjp}w2TL50UK*ZD8TaRN+p)4U{C{)-~OVy>eTpGpRlO9=x4bJO2j@qfgZ4t%$CC{ zlc9g&uuz!R9=&lM-CV+u@)&)<)@)z&-HY{WqV{bGV0D= z52oimVQF|eq70IDaRkYNHR|k1hFGa$Zq`*mut(7VcVxpBUfS{i{NJDRss7UpEF4-F z*-8we2v%YLG4^D4l(WMOt4JveDkK{ynsiTB4)@>QCjH)_5cGL!>6%yqkqO{0j(!;eu^}-fp`Z<@{`etE{mYR6N5!88-IFiAkAqn~`*B~TEx>CPf zuK^la_MW-RX7PLI^MwYcDD@kY-?vmUnHbO0xqHnXku6Y@3D2(FQCBMS$&x!4VK_R$ zjf-qR^uhp-{L1!-5&?PJx^myZ@_OOjBQ5y~mZ{*-K6!LP*d7u02vAszsaZr+XY`<3 zHDrGtNOdHT&(Z!>{(QucH4*qp_~wo~$fGj2-5F`^pjH|9HgTgezbtdw=2vdJ3zS@% zI16-aG-_%Wvrnx9$)I4phZyb$Z<)-lh}AFUAbmv65&JTWA*5fP1T1h2!ECNL8>lQ0 zImdMw%|!tDfje5MUZ+1}Nsufj=*24fPPso@!qbU~!g!H&!5$Q@DgOpO0v#^k@{Tf? z5_NkEs{pD_*zQ?)>@sxEm$XDgqyb8$NO_gNJZ;Alu1@x0MinK- z_-r}%-9zZ)F^tUAvS&t)^XSQpDN#8e)vdxqF%~KsXj}Srbqvv5D5<_(SQ#v7e&`vo z`Z-V^`ds(M0h~X&FoJ8RHYv-Qg=MTh5ZvX`NvuMah*vCwef&u&lB9tnXyXlUpGb3U zEw>SN_H(cCilh*MDeh19SB>NxdDMwOt*=xP=_keH-^I*3Vo;V(FFWr442OVvm^j`L zw^JS)*SvxILu8gV<+VIw^aEdY%w&dEt;jpMw9;4!GIU+#_#L2&Wai*w%G0mrc~~Ct zwQJjbDf&fND`m(TWCnt8wN>i-RJA{?EO9?XbSF)!JiXi4gI;}I5%ncJOP=FcDF^yP zRVB2myybECYy?o$==a@E@B2)F#>WL^Nr1aaP%1e(zG=gHl&_dIs$ep}s^4dvj#HXP zyUTnoO8^j z${3SL^!xC_3l%v-HLc*jcYWLf#YOO5_%3XON(Fza*gY~C0*8&|ER>rzDCOMOf zvw0v3Flb-aETUIjAl?f1vDPN~Xp}(DE>qZGDk!QFBes^xQW!!hHu(Cd+l0^J!Q>;W zGP|k^t?t_7oUX|S;h*4OWuW4I>&K}fc8~ZdPJY5fb@ILqilxj2Nlk*k_$3oA+KtLq zLq!iSM2|*H{{5bZ2et{J!&fFGeQ211=kRf(6|c^HiPmBj=RQw)G>?1(z*@7*x07q@ z#LhHN{Pn6^^rtA-&j{fr5Ywv;ed8$Hj_}4TNIjQkqV#Pe4}0p&8=9t(jacqq<#_T3t1*X z9bTW#Yu8Ye$m!RaMYnndE)So56jy@mH_t_pDojQ4wLEoKD$Z!p42TsDO(7VEg})x> ze+x3R#rlAns;O6o3&1fCh!cT*BaTeT?*=Rjk5>_lIhV8R$&FI>l(0!TB9{(z&A? zB}qgG@<$0HAMZbNz+SVMI6k8NOtoDy2`xf;UjX!Gg~^Iu`xaf^&Fk_|^<{m$XaqWxhC{es{)A-MU{5F?zLE z*6>W6gF(bcpnuKnmt&nUNnwz{00I7vFl1BBlhl8Nuiq$#my1;zfULgb=t7g$Yw%vkiP}O>loab%Y4|9?w3)( zY|CF`M=;Sy&N%WA4&kCK@vLovFYk7{fN0ceO$)!`rGwd`_E);a@2ZQG3pHagVnxKU zqh$KOX(nb2pXqWFeMXTk5^=jmQfYdlnU+sdc7`&pXfM9%o7U1-zWjZ+L6!!vckH^7x@9Zh zDbqOs!dI86fYh5Ssx)Zdk~*{QRJR$NQ7h$G^BbDqPqd7yMZ+*qrNb;r%n(=XfA z(eG0OlFd+;VA8d z@-q7AtF$%Cq%*Nre0jqmf=Zjzv6aLMGXfx|;5&LIbM!+2jc~m3o%r!yx(Ijw&8}LGMpPsJM|(OzKlFK z2lLREvf@Y9B%yD^*|SxLJMGX>JF^zpMi}LUB0UyNxYc5oK^d~O`NRBbaZtrkHk&HJ zQaO(I6&j7VF-;ZJ3>Lk8R_{D~qQshkxXxY33|PWKIifDqE&Fx)hi7ih2xsQ_H4L8d z`KZ9U;JizA%2^3wJ4Q^tragIbDr8(amC^rW?=9b=iodpRz)@+2Q~{BOA(fO4$srwD z8VP9$6{HbFx`r-khi*iX25A%|B@_mbmXH>n@Akf~`?-Gq!1Lz0UU+yoj^pgj%-(CA z>pVZp2i@2pT+>O9a(Vr=eO4aU%a!r23E9+WS7p&3XoIe*Uh&qP4kdAHlIpQb%UOqi zVBzU?bnXdii2f6i{rNcYAzE+W#4UhTa7S+fv#VTWcsKeIBxBIh{g)`lz2n3eXv(FX zec$9=Q4U{a-fVQ=eWoP&2faJAi-}|Fnhv9w4d%(<#iWE+a7>`Fr-o_~K_>$r}v%Gs2|J>@^%482mY!XSzTj9!D zI<{DuJVnFGC|xjeb0yW*^<|fvFK^={cwD1=AE&$8fT))*E1zoD-9y zRB6EjI>d4#9jb3JNwu&EN!~Dd+)P`p<4nJrB$c=;I%0G)nuRTO{DY1rm^dC=ZaH8_ ztWn{zv;nYMk8^{=xx&lrDET~u0h*`l!+|XrJoIo>ezXSzJL#L4>fb>+FCWy!A?2gv zN{#L^bzMsfte2}6U?!*GEH|f6wrkD`{}B*JK{OZKm}|rQbxQ%^7xlR?AkBpNRmk=x zL21e^M)8*q18&)Zicga%r;sr)7UZ!p30A_!oQuY)W4m$igKzsnd|d z4C;0tv|Uqge;RFEEBEy{=`N-nY46L4D=9hqaY-RTk~{B*)$LdjGki~q!QGd8u`;~O zqk|=%fZM*~WX>HLvfzk-mnPFCSi0O}f#9f8XU2R4>hK6>6mEx__$Hk4VDgR_hB$eSgE+5K>Y>Q(vS^W@xnuRss)kw(fC;C)} zNJP5qOjD}j&58-vWqR9aCoy5{E&h+kftU!GMXJ&DU2N^4d%W4GU_Jhd0!vHU1xnz{ zc_kO9he9zmHqMOJ8zMs2;*8$d@H>MakcI9>OnnUvQ@G(w6fvKUC-1B81A#q0SArT? z-lpz>!l1Re$8>E(O?o(6yR8pdAOIOUqR8;%cjEf!T;4Nfy=7^sdCjgHHki=9pq4>{ z!MfUE%2c&WNZ2gHHU=&JIS^mMtNi1USChICN%L^zJ+unj4KL#jon&d!udT8cRQUKK zmW3uDEhuhT@;YiKic%h-T0CjJ369phOo*(JG!hKa-$s-w@YuvnW z;};p&&8kdRMcXC^2ZuX?H10u1wDb3QQQ<>1y5c`RiI8N9z1&^wwk$8RFH$!DOcTWO zHnjZRqPCi3UGRct!p{p*lvu-!&4pw;;RFOe{`iKj@>rn>-uWL)?2`bXOv|4@%L6ZmJiaPuHmwVuJc2ZY|>NjH`?H zV{8&pa~CxOd}`Zuw80n3;Re1h81Hhy@U02NA=g{UAN|OU3@0kJzQ4Q|&Fm-lOR8gb zNMH`ifgNgQrv4|(h^}FKe_`Pohl}%*zCTjbWCwMsg{e$N(NOC=+T}=*S(emKlofDs zf_T$Flk!M|Qs|TxU;PGd`!*8asMNLP#FqN%NS~b1g4k5n_`eTU9UX26D*LsyML|M+ zjR-f}NdIh;N8*i_tOW_)o&f3KNW)_oXCkF@FD1qyJq-uXKE5AJ&+EOHO zTSVbEY49WoC*B>e7E&sX5Ny(#EMVV$VeBxf@_FA9Cj5BO2L$windStTEQLX0*9;YW ztHQxS1q-4Msn!#^)IaTwx$Lonh3k6E$)9NY>4ks9IMx|uJ)1DXT$pCo39gfegJksu z8ScizpFaR6rkcw&xPac80;Bu+1sCn*WiGrD%ISN;3|X@_{N}naey!)r{H1EcO!>XM zW_I&-X1_3X_TZzvK48QRY-GJkTrZtTs;x^}fTc78xCM&{j~+SSR1UZjWJYcg@41(h z$eH`pEx`+&iU6q(ZubuiTs(`^!7{#CLFS&7&na9!i25TJLrta%llL!xD+HP2rIH zc1s~Jk9#b`Zr`BwRBx~B+pohOXG4TXFzGFQVvM!GkkJp)hc}Fk5<}a+6?->>px|1W zW|}s{2NHix(MN1Gt@#tKrALZ4u~1_(FnanJL#1YRb8Sp6=^Cxkgu3QXTxc~i?$@picpJ>_ctuthY7y^N*>GNO8G7b!& zZdI~1aE6J=Xw6;;qJ_(HmJ13$?nae#X@6HWD6znLcy#_T?JR@tQ=gdP2-t|0r|oj) zAcI-7LKZDuBZZ_gmO|GVhXECm3<@t{?!t*oxuHG<-ds7(xxHA$u z8L#AUf;`VMwvW`+BeS@~l;7wvmgP zYLS*Mjm=~iQeAAA&UT6YA~Bnr#Lj*oa;ZtZ+)dB;q+U3Y!;y87?R|^Qvn?p1F;Ua7 z8h&vNiu1;xc#*fzv-9<#!(X<&?Vp32z3xV|8sfh!^O>?0y#OLH4rQQ0)bxukcdn2C z+Cm@Ml^|Dj$CIB+)MwGFc(0Uj$lf0G|1LV_Gpb?rMmeyf%h2-A zdU>dQPN^#d;V4Wkj2qRr9*7=KrB34DVqW7^rtEiA{)|~=rLp|5d;LTLd_ALB@Qmy! zA3(Gvkyx1vuSe!oA0E--sSb>A{QCB&xRItgKPRZ|E_v0enq<)d;ZPG$w-PuCZRH7M z6eyG*@Z4tmUMT_xtvlj#Er|HCzSsr1)$h&VeB-Sa@r^WS-Vmc&JjUFH(w-o* z-r0KtYzxnVO2=|ie1s2Ul>-dl$x($D1lcopKo9L|z;TE3ivW8PN&>`!$8h04KNVl*=twP~cP zlh$P>QtQUT-(Rc_{RS~@^;?U@neIpdrcxUq-*G(J6^9?Q;)Mt}FQmUMJ5XF*B=@qg~@XUrC{DL;o^0oWm%H*6Lto*!u{}gn~4Yzyq6S75+Qbx>}-Qu0xIjwfKgG0+%vl>Y7#GjJ(Ig0aOoH$DpqNs47Qc z4fB}P`V;LX@*Fy-vTZhlG8iJz-LOOv3~FhAXP2Y<@x>OSto{`i>k5I_Q8!~~(PfFm zmhy8BNm|^rSLb!B7v!JC4fq=){Okg@vbnt?c)V=XxScj7pDL6czK6x8hqLLNK+%7m zWS$gAFl7F^{_OR#)zw61e_?%mLk_EWL!2%xv**(`h=GrXw5}6}q!OEPLKn9LZ53|i zR;Q2eRyoTCI=MIJS=0Y{R|$}Gp4hxDE96r;FC+tpS&~t$ZQ)aeTMNYguka^9a_d00 z?D=9ed;ntLG(|f~O>mNFubKkP)K7Nyjxt#;ckf=NFxFBw_i0y3kJY2k8?tY8VNXsb zx^BOi{d#|b=X$fTpfHhBhhgB^`N@8wNhv}#mYe_MKC{lJZ%LGYX<6~C$tp4KVc<&R z6S1K`5%r9s&nfX4MGTf2K!=WghTX|FBkMq}eS9s?B^6h2Td`AcSDw$Y!jzzS*fxRU z8{W&bFGePx|glavVC0n>y%w8iZaiEtql* z8BO1@BgpMrkf1`XsS)syu~3>gNblDWe-RO}J^^YH((N&_h*RR=WF$b^rlayVRkcZm=do3uk)%-QniVGIhlQJptZQ+ z4ar@~aDl!hRxw!0S?ji{UE$t~nmt`2!m%gx4Mw}JhEm2F8DtP64 zn45^ii+HZZ8ChCK@RfN+lgG%tE44}8p<_-gZYQLNN;QE7dntzWs z&EUUSAQ7*N{!HeptA0+N(Lo$U>plF@rX_tRBLxg4t#F zt$5h@W_|aEhCul}<{{qP;lZ3bEXt7*Y+d1!+_exEjzfXS!3`IxBv;NAm1HM1wM+s) zvk&tUe9_FkTfEq~!Gw2$Q6iqGwcI^b3CKNJ3v{LxNUNHNX`mdCd@Ewjgk!Sgt@C`4 zE``=o8dP$&Kn~U5cxEj(j~MJU&5n;cV}vt$A>``3J80J!tKt>^7$Ow#t+P8P?gmdk zUyh5TX~?rqok33k*ehr7{&@8F2p(ZEI(>>O8-*m(MG7LSLY^l^eiDDS-}joT6mg?} zq}x8o^6Mt=W!;}tR|-EgD& zOA4}0FGk`SR*R;5rti%?7$bU-$0<|niGg}JO{~pXS$rmKs#36~r5~_qTW)ry?5kqx zrfE-K-ga&#??paZopZdgn7Wgu@F63y_o|%w(hJoO_mu6=CJ>fe!yknC949{1Y*Uf4 z7nBdAr&rtNQfb-qAV|0!S8%R&`~jQ?VXGQjz)ZAG2w|w~b5#v1yMv{85Olh_F?- z1;vVp!1eYFMxLwqw(M!Y2lcPWf8Thr6pe$M73%K4W!G`*)?i9~3cvmVPhGqcUPj#W z8#gusd?(%uKWh2&VkOOv-Bn)W&+Yvw?ltSN`k&9gI}P^Cb(hLFOjW`Ac@jCBsmx)5 z18h4%@e%@>3^pBW+2OYA*>i^+^OKse-U4+7iSoFWlBrF45nBe^t2bPK)hQ)7F99WC zgVe4!tjaLMU#3h%0JVlo&V03lK%BB0YgteKS;KKJaIDv9h3}ptEw1_#%6WFe(KU*& zK4csSM1Sd`T~SD%y*&0lz=w~*go2zkY%~^TYbI){l$dL6B2b;kwadJ}w{SP!Fe~ee zy&g|5#iU*D(d!Xiwh({3QS0=ZZ@XtUPC?+hrqS-78AXwY3AMg^(lvvM`XmFqDQC1t zHE_lZ&Qfe*ldANxn371^$7zAJXZ*;AMq1*>^M-0@$+Y?5qZPfIho`g%3~QNvHAjxG zJF`|D9Jhhpuuh9vma?`N?y5lVpLSofC19_0Ni?V}hf+|0)X3Yz{OiVfi(6~rLyGl_ zL)S|YglDg|Ml|)Kae{5vnk)ALDrbNpOaH~f!Sq$~Yl;x_TceS3(7 zjew4JN!)JD!A71P!QuNXePx!ENG%Z*3UdWUJd}J@_ME1jH7;|4Vk;59tK*cFeK$ka zLK{C$YcW#`G&{PR%?(uvPK74TUT(-5+PV&gJ$r4zd(xtCLoqxo6@L+z0O_NL4Q{&q zJH4uGsglhC4{F+*ex|lG`CZR_Sm|rH$PXcr@O|lVE!k)1RoFybob^R&bOj1f@{)4o zFAjHiqjT_rL>SvL5-2@bWz{n2lFGP(b!l~XlRy$(C0J_~Hk4kPvI3~HptlIJWMRCf zO=1-K>czB8N<6_DMp|?lK@+`^aiq-isCNvLckA*7J~|vHKNA%K7@$r|Kk!t=^O@0b zuGhNj#G?0WdAHKgnS9Rlv$DcRST1=y!EXNcf$6jjW*c zVxv_P&+pry&vGYi00B7E1}0g9bBD}RTQPF$Kn+1gA%~JjNS96`d^F*)SvV-0H z0axKz@!ii@P`4DfYEgUA9glnqU&q6a8=am`wq{2ZOg=$yS_bBmfw%WX3U3_|k*}N3 zsY(Sq;#^AnuygWx0aF(Hm?o=_Pekg!dZn~^sIA{8yZlYIX0n6AXA6mR4|qY^bc!_5 z4v{E71BIzpMP@Jljsn6Af*!YmZZd1{&oX~Nlh}{ID}#W zYyeF3Q}ot<+4}U&o_G`u25F%a5@Ei}8X=;EY*C5LR&!XnsDPQdT)%4@k6nNMl2pyc5MdBzcrFF!yc49( zdAWP%@*dmr8w_hB(q%jj$w+Y=yn6e=K~kkJTUNKvu-K{)6ee;HPa*a^fWDdia2$JQ%!(RK@42If;a9Z4*CQt2h7uS&^faVef)%z0<;4>42 zu*!xAiR)6}Lr=}r_M~Wr#4Mj8^wmv=$o+~0-4(G8>KH?Yfb(4fFPb;tGgtw#5C+V{ z0#!O1j?oRH+ahzGW4ivo)=Mij-4tu?j<3E z;UC<;cINq!luye4g+1Xl}exJ{TEKM83B?A$-LExWM5S{fo`m1hw?y?3Y~j+irLxZ)^>G zof+?1!31)tQn1I0d~7uRB0|+@73ECUYNC0~zZ-%7ynZ91m!*>+g99M)H|sRgoT&0Kx1^y5YA zndyEPrB(r!{_})Vs+JP_c985e{twQ@kAMd;f?ZEWcmA2XCQooO5sxJ6_KY$tdB8E8 zAo3L4hziABS}*=oCT4Jk+${h|#Vz^3Hkurv7u&NdP()o*=I;So15q@(F!QCCFIkl> zBy$%MI~sphLvT%f7TF6<)1E0dU9CRGVy~YZpt67NnEffKVDwj&3M0BkYL0Cm3fHXF zg+o-%@jBuZFUJ)(NfpL8Si>S=PSTS3>&bFO61uE| z`{DG1YXqMF&Pg*OL-)=Lz*~NT4ftjjWF@d(&X#l{ciW?@ZjgF~H5*E-G)1xQLa9nj zk+C7K;3GFO*t&wOST*2t6+xGHid@z@Bb=FUy$EVvdJVN0F;(^4{{vfP$7?RKrp%|Y zG4K_g7V=^KXzaHo(lC*Td}V84#64a+YZ-QX^ce{2ih?wvITy`&#=<$f0LSJC^M-+;JVgV=owWYghw-j2te5>> z!tgpx1aw2;7$ImBO<3D;c780!C2>7=JmKmqfvsbFiV^iS&X2>W>qJu z@OojFA;)(W&S<^oK8bofN%*!9o05|VAPSA0sukUzaZ`8A4Warn1o)hJRN1)0+Hcs| z>j?Zh+i4AON4SOa0~(_dA4|NQNwS=z9SOc+n)GyKRAaBWB#~SoaD{0DsYFAhJUr+w z&DIDlw zyR`;{EhjVZiG-FAL_N(ShQ74qfIlpr8)+GFl;{Ld`&{Aool`WpgU@v+JJz+hPdo2l zXhtQtKZqcN!0Bs33`fw7!6t(HKK;|^rNK9L3|H~c($=Vz0tI^EIcsf-nKC@s>3t!A z)mLQFD#&dp%k!-g54-KM%g~an@LwFI=9{^g8Svi?uT4X z(!;ze%Y;hi=M+SRZ;zo2CxKhC+H^9~s|p?hH^+UtTM7l^86$fOrdat{_4|10{<_T0 zs@_6PFZ{b&YNREZVfWJPvhC@oZ|d;~pul-KjSY+wOZ#pzli(7kGP(V6CmW0YlG*V3 z$3d`dTz}HrZgECB`fyKT+Ml1S`x30%bva_5Doe)?RGC%4x{jYQeqqC#RX{$i!nyBe z1r5^h+U7n1=tW_HxV%)z^F(a)O*QL;(T#K0?k|vqZyD*TTl33(u^TWN^cZF5<0P;N zI;X5Kl)EY90d;Xv@SvZY<$eW@v_Ed`*slTpRFo+dcK-%?5=nmI10Nu5TeM;Pas_Wz zHRjgs+^;X37Vw}jPf`iv4r>riZ{C`UagDcjA z>7kknB5hEdF>& zoqfN5m-0(DKVFa)qwGYSgjGYl@@@h);dBBCcXuOd&BYHhIfePI-2kzdr~za^Mjsnd z&|`)i(Co7I61Z-2+-R?uw=ukHK=}CFl9*f;KJLhp*%WVL#ctYL*!H4=l_roe@skFe zMzxE)iPguDzmMtj*AKmHFS-PIQa@SJ?YM~I^)jV(o~qD1blP9Gb5{*bWlWqKdc~MD z_p`3!g$)tz2Ag9W5r1=B@UI+zPVrm}ol?LnK9{#Y&v&DAx=AM!Fa+0 zo_O_?$&mN0)$C6TeFE_Jafy|Dv9j>DYqjzbk)G8;%n@%`%kP9^=WU}|d;gY?g$l)K zc868a;MRNf(cchlI({k;?wA?IHi>0RtTAEE;2gqt0BA)|7$-)BDGe)6(RXJ;SwPj7 zm7;Ght2{pU$Dyn4E8Rn~fh2#x)0uk+6BYMEQKSx zSm=`)+XPE2h4+dk`?61Ou|28MxsMjU^@)PWa-_)~Z7z0n*qtDrHOG*-kN3t&SGh)o zBCui0ah*l3B4v0}4xheoQza-Fxe+v+mv;38Abe6nnkfD8pXqDifjjQqCt#{!9S4*Z zcj_j}Z<=vWzn@=hkij^a##F(&=*Q~&o2{O=j~|N9vbF4^){ zynQ*AbMftbMYPo(lI)@7LjS09kRsqR4~8)-ldTue_RkhBTk?v-E1*%5%LLdCV4J8R zV&4E3h1i0cVF+w{NT05?6Qy&md$^C$^5c?Sm;DhNaQ=87q{V~*aLbVajOT?e&7nda zSSB<#7RdsRA6yv&YraaSpLUXD@HR~4O@o|QI@F6#17$1&jAMmzP6qA7rSF+N??(fn zy2cx@$udCSiGlP3iXw=YK(}*i)%G?3+2?3@H~@LXwE-mKgmMPCH=cu8AibTJ{_*At z6tze`#6T`5@7?Yj86eg2$s06&8IY(FdMT*cDpWj3x0how17KC7nKPjA8$D<4?D=#R zL;r*Lcy7l(CFC!NY)nc5FZJ3VsQjYAko>givfbJmtNX{Z8Ui1MdC12Ue$d7=a^}0!#lEzr$ytlfm<+si=Kt7*p#Qvd?~vw` zr_+4HyiuWX2`1D}ew+evPZYu{&1?!LpUj8bH@hTrzgVGGqf~o8v>+~AY?uxYk-Y4| z74#vu3F$~VLhKXe3dQc3^dGsk6{Oe=dUW6Zs=#_Q<5&rFa#GbBSFR}b%-_}IRk69F zobfF1>T8L0U{ZACYr-bfrPZkPbZoMG2@RkkT88#@Wdera(7n&}W1HaIlBlWD$cMfN z=${k3L}-d2_Mt5dPEcM?;Z4zb?&Vo5J{A%|p>+vE;DbF0pIZ7s)3{qp6_0?HxO7lq z6a*d=sk`b=Z^jQmT$~~9{Q6as{u@yaM_>&7X$%ii<>x&qcXc zfRdHY@3*EFZerUhpRuaT;gE9@Kv}+jWA~*4{!_KDr-ty0KjxRhyt?!jbWK>D=gVr04b5QIe^pEV3)`rci|WJdK;`HO_Zd(AKQQicM~KL@Z{ zA+E92wIBkg^8%QfN&uJPn`p;&L?81#+xMX#H2bcL`03U z(Z;xw$V;jh?HX|QeF$8;dCrP zH33{Kttv=YPQ+hx+^QAJw`Gi*i=9KmCgHO3QIHeBC{;c;n(Y!()Jh5=8r~c;c9&jV!g?{>H<2#E^bCG&h6j!nb3Aw&-6R= z@+e=}aAIC%=-q8R(ZLbk(2cS&ZYIi(rT+lTP#Jgr{!;!Y9}9f=D-GrQ%*u_ksjbcA z4cU{OQTpupHwxSZn5E909@K|NWLZCayT)}J^iI9_a2_;qg|U1VUDxYwXJZM1Jnb|v zn7fBA3+qI^^A%kTHSBnJSm<;?ot76}BbXAyoU#Wf7i>ZA+!_o<7v(h&< zLqj0a1#yH#A^6g%82<7PW$K;@{&OR~P`NY%=ZE_GAr@c_S?*}}4jkFB3Nk%Te84)Y zG|iaE4%&=cLFzmr9d({M=v-{Y9-!QH2w8;X}L;hXouT)@Q?jqR4VpSza3d8eoJ zjMXE8n!Xy_v`A=ob=4H3cTu^X_DZ*cFe~YNC|F1pws^5=C!@wD?Q{+B1ofEWOqslE z`}-tHPje_PZS!k(XWqU46dLfQ=db@%bldZ|!05!P>5wbo&6tf=M)TPQ5$@vO-@Ne197}NyfInKIY1HtSvh#_yu4x$; zUZBZ@ZMj0Pkx3{yKF8ZSCA!;qZ*DlU8SQlO{G_gD+qQQ1?WQ&;fR{h?qEr$MUiK5b zg=h|LX3aN|w2iDX5f z@`WSlluad)ku)zeCg0I<=eR4)`j{-%3-99;fg7p4d*b-SrO085uvr2cOOvZv-{0I+#6QHz5t{F5=#UVQEd(jV#{XTB(-)|nt z%)?>E$tT=RB^;i(qE<>_L@y-_cOO}r=Fmvz!;A%{SnqGR74zH6TN{S07 zd8>@Gv_*hRBc1l?jeKex7EDIs^Ts;8aGIr zo;usCrFx%mNf~+=@p*;uI?b|3J!6to6noG}2dnDm6+I&IUq8v0${#2Mj>i%e2t_XR zGgjSWN`zU4C(CZchrYHUP%SNVtLBQ?VaN#Z&VQ{!Xpjq=Sj=M98AKR-=doNKiktTd zQTqXZQ|OcND6XGDDN!+qC}eGB1t5e|N%45_Pi}sug|!ZipW9VeHziN~P*JJXosj<$ zN6m*6V51t}vrY2uN)5!(E<)xQoPI{B;u0$sHlLt-Z|z&fqr6GpIW-1J^OMwVk|M2n2MG^_4O@L~ zp}~m-GH2(OLCHpCm~wa{O)w5~yQ%S&Tt?W?-%*09XkBxoOAyu-p?*K{Wl}zQNacgv z!+VuT4u=U2BAZPemNa(L00dZE=0K8N;&O*a#imh3qj`k&?y++^j+CT-^D`X$RGs?w z(nCJ_ZKDKt-IZ{uDo|G*zQSC9*Tdxra*(D_t-Xf4!{O1F8R1uQj!9ZZ?>eYKhpIxs zF7j4VJn%Z9 z$BkZ&j8!ObNT6b8j(?lff{BwU1EhiI=z7nA5)JBH$O2o+xB)73v-L`UW$qie{W;!x z;+Q(=ay^4Ock;Wjz(Uii@t7J_7A9s_osI)}5oO?Sb6r5)+)XJw5}}=b2D9uV5gQu; zJph#!&X4zNMHxjG;EVxYh2_DJuaz!m);UWHNCH{z1rlFLIgY_VUU4J64@bo*9K@*U zE019PD;ciY6$}@=^R)P7RJe3)pcaqhaA2?ijf<*B4xA?mXZp7h8fsw_g4MR{Rzs%1 ziqu5CdJQ_Y)&2TqXgjcZ_kAXAsHa(XTr&%87>ve9UT3?V^H#S31DzBhK@7H{%(8a< z=hww@7TLfgcc}7*sIgpD4seSJT0g#V)5nzc(yL(qQ}khLC#gii5F36QK8@uKbJYnJ z2S8g@g}wFM)2e`S>VrsVT7>_d-XydaeS`_ium=ayMNiP~#coRO?`S zV?*|%+gK(jFjYn$Nz(?t?9w$j!|B4@O_S2QvRBWU!zc$cL@NX5JtsrKluZQuE7Kaj zQ1vy^TFVxqY2|Z3^Qs@6ED`IK zZ+#pQd$A+zyRhmvzav=Tdq9oP_+-a-Rbqf$FOp~1e-j`byWhX;CK);=yGwQIfA)Q0 z9xND^7HmoXsuhdVMr9w~=P?9ha9WRV~ir|31P`_lxj{=8zFAtn>ZjD(%=-ZmjdC$kPLj z*os4!2V4ekEY!Hj#DBeYc$8fBn`VEP6mF494{GObSxnzuxz`B!jHICktx3M!zo4IS z1vca=Qro9B#eu~e+jK>Vp9W52lor09L&jYJ(fYVrk&@@2p`y9|=2^y9GFUIXL#wEn z%sE82;;9m^e$!JCetD+BFU8T%LK_ofM{sbj^5b_QiZfA)<434BScgw%b31&5@kw*n zPa65CC3iWQaO+6~+<-Y*Xy@hR$7`!i@0|H&HtJ9IeRDj9#m4LYI#kX{$om3hgb)O1 z?^~!<60nWLF-2^x0rLaTDe}Yc7o`HHs9W`hp{1Hqf4(i&S2J0|f*!)F9_}(Ql9hd< zrT)}xmHl#$e{eSJcXYuKK;PFoy%nxd&L9yuZ#nu@`3wEf8j+L6fHzTyN4SRAL-hGN zJLhSqVB-qyvdW~Vt92gO6c(=h%| z^|%0A5MTInqid>;u1o(8TFJlj_X*<5+LgZB*YT~Vh?1ewJ{{_O1lv6`9q)|3=tT>` zLiWHMo_AhaK%Wlx>8ArC*D0V0e1SUIAt(t4H7PeoJ@wr9SAqqRygi@6e#R9r(C4`- zUVn=#Kkzn6j1Wn8OlSC`#4y`p zQM&;1$?;OGD71C>-&%E@lg4Y_aRn6T83FsQ{VNLK0B)9!@+qEQPDNb4X2h&ojY`GJ~!?z-(~=`zXzY0?wE2e2>NY_2but zpmt`#F??*kPvs!&pYn>kz+EgvWbYDo6;XvTdJDNvylp6_@GU$}?c6&`%iRwEblGKC z=JGs2!ft6Vpq_M66G0eoxo*(PrIe&m%DqENM(mm(*%k1o@C6s8E&1-T zrsw$?4v~f~#}Q&oNONs2?qIOhxp6#G&{}YUYLjbqVkfSIp?g1a(US*#TK+X&6M7Fg3txXQeSVAklxgo#0-NR2c;PO08hh^iLA9+MCW9y{cTa%y z7rt{g^%)uDx~0_bzE6Q_qL*hmZ|&~S8uGAeuYuJPMB&V~$uOgvY@Ih4_2vWqP6n7# z5t6Y9h!n_OP~UD6h4sGf_7RK5l^9QSPw?_Hy4aII{AMCKLMR4n{=`rbsLmgH+TP5y z{={Iq7I&;C0()|n9z zUb=~bj-M*|@OhvUnV^MA&9DBAGk&@I z^a^}s;`bL~Y86!@`{*`E(>r&Gj;JUMwDUE3bgZF7DD`eDJ&)e|pgHM6n(T3HeLvwF(U%_xjdFL}qz3zxaY9D0nkdd@hSSuS$_?;xCKnWu z9c4TggC_Cl@ zhLYIAP%Pa8Xn{A>_2xS15WP2V<8~oWL!M!14IakSuYb+9?tjEkV-k;GO(-6{u&z8E ze$g@ph1s~SS~V~3n#J&GNl(B0X#6=r8qMG$*HUnuge}^NN>`psS!e8`V=2anqAPo&34p;;6G%=&d?W!t;C`Go>%$^%sxQ7SmW|q9_Dx$`cUo zJ)D`1az$loHC<1t4dU<3(ck2;Cd6gaVM^y$yZ-n_ctL4zRNWncT51d(YMhKm0Q0VkjNP*g=&RlJ$k2{^zvSz(?4aH&de8?tw(v zqY*YKJgwusN6dJ=Y!p8{!XujLw{4E=tx>G);?6b$Gi)CF(!C_NtP1MfZL$&&KGZCC z8Fq_)F`VW+97vPph-VXzsBkdfho{XBsQaVe@UWgFz|sE#sl}93eo>sGkl*`F zlCm$90*-2eN1uaHo-7<6PmU2E7Q}d$2CsA7S%3z1g8ILY!C}E|4&?Y46W-tx|Nr@@ zEG6m|(|`X<@GM!lp)ej(Qt9iv*Z<>Dd`uKEc^F&4Ow!&h+r<58xh`2YD|!h-Zz!0|fZ zdj$TEHv$9iqxb(kdgw??pd%Fy5&MrvWmRmf|DB)zJ_d_%d{FPucF_OFql^Ut0{{Jt ze}4hY((+seB$lNA@hBX1>(+mqp#ME>|KFcBr+cjUP|=n0-F_j=)3vlIWk}?EneQh* z^P%dMt&c=D_CH7VUq9GxG=t4NTWFc|;g~Z&=@4%Hy-G81`00$h^en2%Z@@PaI~NQsSGeR`$t&^^qmUt z#c+;vfD&vu0Yju4YER0WQU2eZYBg$sNmvxZ1J!~Vib$o;+(tSeLDqN=IhyH&IDN1( z9r@aaCJ9mh3k~L=hDQW4QF_NC0%9r3?4(!!BAprD6bQG1P_2-}_WQ^CuSJ`4%lyRK zPLI-}Qpl?=w&*5WCjTuPUzNG^2J1FQcxH5*hJlv;tlbmD;f9R9GFeZznfJ3X824k}fsC$Ce-DiM5G;;QUiQ=E- z%VH)AVa8C-1VsNfsE^&^F*|-cK10AsSfx_r8$)ah3wi`M{DrYFY}s;lbtDB5@D&i6 za|9OhoIFZ+fw$>dIaBy7W30^S*VuG0Z7GJr$NkyCx(z-^N9_i2YDA-I>z{I0(_)>q z3DHk$8p}JL+s!c(_wAyf#cU*ggp&a$DFKKOih1cVEAh(v8fPu#0oZuTzS9L@qse$h zD*mqQQtEIjf7<_SYvA%Hej;r~xv8$+X2Kihfn6{tMPyt6%eOABK$4K zR=YfU?h=XVt?{uB$SL@caou}8qAk@^?p z?ZND`!RoDcut@3+Gp}4PzPw{wAvv)v1@a&DvW(t5-c#_m0BzXN49^8JfkQ>JlS;H- zXLj&!pkMIXXnBL}laXT`{+%98)qfTe)WpYEX4jz&9v=Wo?lED|4YqXAtbW-rCT56Q zk@BLok-?nR?{TW8?hlWuXZVaMl4$JLQ#<_I;ODBS8Lxhdvzr-`@(C;och=MoGSTG+tEP0<68D^i}?lw}z#3a^`U zYM6b1X3K`_Xk1Be0WTgW@dvuW+h6^NJj}t`K*b_Hz(ZNgjfxj2Ily|%HJdj zkugXVyqVMxk9yYwSN2&rL__ImXbZQ=Aq_Un4yE8UD_86u_{J9+886Q*{8$tIMz5dx z!L>%AxZ_}NfZuf+n0Xg>O-YKDh80^H?O}uA-(Y4hNhN?b7QghhfT(Bg(npPpS81Z4 zsta0@=?fY)o{89ukyLs-Dm- zPYzol!F60rX+5_wqLe(EX#PC~?`#kmolv#ZI_WoPK{eY?$k4M3(-Za8SjvF!r_HP6 zN3gM6@`2<6maovl?tQR4Gq(w>TPA!Mu9<~a(Ac?O{^F!q=C8df3v2#iK=vOuspLD$~TSR+ud=#?xTss3+;UDfqM{4fQcTL?)io;4_t%4JFGF49M0 zBVL=Be=TT@*}~&zHN5FZq3?k4JZj?d{d~ll*;aq-e8QcL%R3g-wMP0`OaDFq2{ zOMZclU&b^=KxF1vKJ*}$eWX}F^^tnu&as_Ged*hY<^>N6EyU7O!He7b^uH$y8 zsSuOr5wY&8h2;`CFl=`hA)HIt(4HnYPL7A8t=rM0@?7z1JukOj#m`e9;}w$TOf4Qs zxcGhY_Xq%Jj#%BZEjgW+L}E`@H!<*M{iQ_YESRA1jOO=4%Bj-Mhowq22N63r|0 z+|4#xtvpVWWpn>ZQ3rBuw4uoL> z1-3~L{O8>_iV;~><~7v`s|E@!;2W*MK~6l!FBixb3=D+85RR>7Ityefg!7C9|D&7 zShOmI1o;jnxMp&t5UpgGd#QHP(I1w?w0B67o~|B zD~yGXZmv|&Qgb@VVNq_)IO|Uk*9)Z4LB0&RY;l#ebuK^`;!x-zTFURi^CQ1i#P&Mr z(xM>g(iYb<87j_$93(+2n>=lmE%lngRe$vB96*9{S8Y!Q%un180o6i-;j)ebu=%A> zC}2!iDs86gPJX8O+;|CC;#kE}ji=T4Bi%(DcXZ3mVtjnqm-J_N&^>2xcReqAybuYKs79$L<`BI#lX~ElotJ;wtcT4#-B|o>}&!)<4t1cpa)A ztKQ7a&(V}``Nc2cvSpAi*9J z%a7#)Cen{invSq}^x=`f(n?duhgKB?jvCHd{+1mf$bD9=WIKmjAA3_QyYB%lZc1vYw|^!kSp1#b+%NJs9m2Q*&Jqf1}GP zdoM==&F5CkP1U@4$_cmM_r=w3kmtbpw3@kCpx_R331RO#%I57zCLWs444EBtw)m(q zD@7$Xx-Uz~?S>DSsV66;;xi zkXKZDEryR17P_5b+Zz;yr(=2(Hs^*%0`B-ChuFLO!Cny44o0TdiemwIf+IJE9 z>g=Ny9u#&KAU<1WRNF>xfk#8EyoTt}fuCVH;J|0cLER&$%fbwqPgJ#iDGjT7=u*^! z2X;(!d5aZ35Ufd*5mjZtZUyYos_4~Awcwsxc`Cv#T3W{SflQp(DX;U|>I-S?1M-;{ zYzchM?~Iv+Wks~{GpSLd$hD`v6Ufo3z|<-W%a`YeqGUbAj9eOKVxVH2Z}7GVrt$cox9u z>j00RFtW~f40LC4b47G_hTL@jVZDR)og1Lfet0l>Sh&AK)8RW>TfFR27X)HpW z*W-hfA+l2xquz6a><$Qm64DqpUW?A&E3O(L9uxh)_upG8CnV4IxV4>L?O_Sl-F)bK zqYg8*0##|e%sQS#(P@8KzteI5@`3AgJdXA)6|jkSrk7{)m1aZwoZmeBmJ3$*mR&f18i>;TiSiL~WEYnLb*ehiRAURYH=PgS1dGOLB$7{$J*%i)jrCMj_T zcAtlx$2b~tE9NWScap1b;3gEZJ%Mc6^?>Oyj-jlRCXJMboAfZ5VtD5zD(1uM)#Lag z6cEjWkTkX{fbi|V%xo*7(Ii8~9LQNc`}`izB^}(QEy+S1LzPS{o`_!~H`_Zbv_}#N zu$$N~{Q}<_CL0B>Kq=R*z{HYZT{51l+a6%;4j_Kq-x@Ib_^w#@E(uue7@WHAF4#Bl z^i|g))e=$t_e|CnFIC6S(L>CEa?zvDMyk!E_MR|g2QIWS06k+Gun|38z6`+2JCoR+ z1k_$e7azMev-h$&fQOmsA`mUzK&`!71e|8As44leuu(|nx9P-tK@g1CMyB<)=!z{)ljNq^HxBK1?1w7)%B zYT2}vnG|SA1Ny`l8r;!dCHl;1h#*pvmORfRbg}hn!foAu85y*dDoWt$4wB z-VW=;Jq3(TtDO@D)sH60YqY*in$o8g*A%WMs^G*5LkMYFX;B(qi0EUpTZA3}#yx$r z(64o$tnYyFM)az^@kN=bv<>DDxvC&vvZN`s!p|X4Dz|S0;m{GR{r&S(F_eTUt1;$HVd4zWwg?B*=;DzG)2qh zt!LE9NE+P~$u?5IXQQsrtgG*vXCJWoxu~wEXuUd>BAEaHuTC(hx?z$ZPXI22ORh*r zc1;}c4@qi(US1Gey5w2py}Y67Z~6R4HUd};$}YmI7Q!q!7e@bz0EzE8c>e2;+1VJD zZo8|f0@{@I6I}O@gY_0b0R)VV~R1x&UT|9}^g0GY+#FUXuHhPrhLfEeDV z^$7u0r1Ul6OKnXtw}~@GlmNN!!pKQ`)zkO{+}+?^t%My`xWm#p`x3grWT8 z7T}Q_0s$~a7}<3#ny>R0i!sl0MtK}S(|eQE*V|1koQmhmd2FgAf%Z?E?t4Jath-bV z_Z1FCF-jA12grCqBykuc?91rB$gaJ6znsWZ3RUU5KFxoC`fvf$lBCP}#c7!`eNSTx zkkcRl#5nn$-d}ciUtbWAeDtH!vEF;p{a0Qq7#XOT!_up)su6GS8WqAFfJv6}@@iNS zCN6Kqn4^-<osEB| zMMNMMM4+`izWpflfPUF%O>Doh!={$D&x?_MR#p9})bc+}-pF$8Wt;gy+jJ9$WWj@E zwyDb&dqJhe-at5c6PU-aAx1V`vl2}PRbO;qEU4=oUTw4#WuSczgf(q!-me0xRs+XU zj-anU143K+#FJS0-g^)bHrC5wO*osoZ~dh2S=WBmCwou7i)JT8rTa0q4wG@Ry*mZrrr@G%m_enH2SfQ*O6LqFekMgZ%%bnDJGfNytdx!mNe7te)G=oQ)BifoI z_r6NEyg$Qxv|rE^L6`;bqeYfx>!w?t%k@8v_o^ueh*otm+3oL(P#3W-br#FFa4s{W>|Eu45yc6=`Y4PeU+*Yzsxs z^R3Pr$ETBkoIjbm(X?Gp^-H3TXqrSgvuToQX3gN9&XQ?QYzATA#ny*f7@a0U9J-Gn zTcHpBYw{qL1fNsY4P8FbKiSlv6E#9FcigbOa;>IMoD@5KbBg58z<@_zHJ@YNb`hU8 zFG|K;)LbD>CdE;;t^SQ6ome*-PSV+H_H1u=Le%*QJgYx z6j=>e1Fq#Z?>#;h;R+G1@>5_OLUSed@)bv}FNODl@}eeXM(XmG@LSI(dmJx`^)@e3 zg)ZC2J4=`?Rm%9ji9g9HjIW~4IC_4+g=HQ$WV_)7JM&Qe0n|9Uq4l00552Tlm@Zu@ z1M>!~vRk=hCcft0%ce|L1g!)nNS)01{i*1l#!WGO&`TO6aYwZaX^DodVwEShdUr?a zc0pj;+R=uDbfH@|vssD*3!^iA-hAO=$uCFa^*X0CvC2)O^aBrGqv9wtqLvhdmc84! z&VMlpclT)a)R2y<3Iae=-9;ULXV&s2-j?fBH;J&AE5s2w;=yu0`Qk$@$uk#Tr}L^w zu+5uiA?dow@)-Eq_J=L^FGksn^3Lup7|Ttu#Y5yUp&b69l+poYj1;jMnxR-tCLIw(VcX`ilHL_n_qgs9!@;0t4&TXQa;?Z_+guxyx9;{=AJw;&_85e zZy}!DncE#_7HNh7=Ky~ah&J_o0{CE6UD&#x{F`9%jFmFgU)lWBi2mPK-zEYq(KB~r z51oHeQ#&L8>EwJo@$;X7;Q#&uj}pKWJrSvHEB_Zc1^h)oNawTd_^*S3;Ah&jkGZgA z-G32r?ea7w4JJ^Jum3vuDIstcX?2h@8!+zu=k*mjeC?l{t0E@iVlEYlie;xc$4xrf*XR~v#QUCjfT7VDuA=Z0>e1~>~PylLz6e@{#^#R+`C|9y?X_x``H@jrLz|L(^BM@IkeNBy5C z#s8j-|L=kOza{2B(u)5rG5=ZS|36mhx&fNQ3R~fGfJ-X}1L#RMz~UDpMFI4luFi^doZYXMO~_Cn`@7vc`d_&3h$n+Ur2^iyOb30E*cpRG9p*drj7 zkn7|^z4 zyC(@kp+q4 zLlPJD$S5pMkPG3^I4=w-PNI%2zdJ)ZN&$e;%wtwfIBgu{!2J8D2Y^l+kW3|; z$kuJ$Sc1A^@G;X#1r21lcwtsU(oCu35{Z>S7rU=0wWF1;B?v{!Lp)E+>C%Bb9j8Vl zcq`L}RC{zBw1fJ`4sG)Yx#fs9I9s7D?^<#W9rx zq=26(gj)TCkN~a)AT2LqIZosjIRb$W`BcqMt}DoU0-1-)V(pP!;@0Np>EuL86>TQ! zBSO{Wx}`?@9F9T;Om!V&qS-K!c8Qv1 zWoAG$Drtud34ae@6T8=h7(rdYJJhJ>SpuxhhQkXp%T=uTz7>%z%40;G0E?WBghk=$ z1C4xo!`s*E)x!1TA#zN}fT4eE%*dLXlZFYOmmLg=zusW;E&HmIA$Ii^-xA7l+x*E07s%TdOOSDUzlw>_aS6cxgNH#FOz9tC4(r{Kn zggr9zeHD5Gpi#AOr}KbD6W{%@yNOMrnFnNQa|3IHE?1=CeNZW;AwPsyULcBL@)e!J zfum1Usa^17(>IUO?VbMZaB{FZ()PLqScaV>?sheVI}W@2Jb;n%MQF6$i8UJ+(6l9(3_{^T?w;Ufa%_p}(GUc`GBY1?aI_Xq+WFu+YPb?m3>P5}=>%v@A;D^D$B<=3>SLt$fw5L~6rKBJsPaAjXyMb6A7kRj!zwwJts_H$08Kta z>9Za(uBAw&hm8xjIeA+l^@4g56E#I9m{#$A_*1iRlVzMZVCc26_^k0v{A{XlW%JD; zbBTM*(F5RQzsJsFc zL`Fhi&$dh$Fs^LS(5i6JMnx@y%U?AYDpS=5hOPT>ZhQ5Vao$RE$xFh+cKs@2&Vh;s zhYJ8;-yEOoQVM*pt+l0RtRJ)nD>@?txkV8OVYYGmhUXP}``k6Y*XMp64@^g7lrN|T zm}vZau&;MDf{X*r&q5yE(d`;xJ|dW68rb}fiCRqa_S2E5_HSq+qz;LNGujfmX^V)@ zsW}n*3!RZoKe7xeX;2fFW&v{o0`?;rzO}!D!s*LniDS65ncYsJs=R}di*6&3D5AQ23(U|bzVyR=lm+y8uIxR0pV?EF34Ki}M^O6nyIYq&1 zq^M34)_dSzUcaa;qkewB4I2DR@V1rD7@P{v?K?u62*4Q?#!0Fg*CX%S>?u#X_Xuco z$n#Vi-$I{)U87nJ@R&A^Cq=WqJ4WiNV+_eFCZ)XdnB$-9qpWCNSQS8Z1!55d0M^8O zw_`{az^;YDf}5w9MsZx$fUt*B&gXcgDhH4>dZ{%Q^W+G+TyLae$sG*9;eyB{xZMEz zen@?byW{pg#Zys`wm3=~`N{ep%emi7$4K}F&QP}Fbk0w$-RArB1y%3o5!7}dzE5=m zW6Wjev2x-WgaLB^?4YE8Zy19jvN6xZew|ang|H;L&WrSFAWl$zP`_mP6H=K<%xSrL zUgP~dG}`pU_%n^=%on_EA9$o&(UChf9r}TmTCf1&u29Shluthr7^8n5`3*8UH+5@g_=Ol!yKl#kWAAw^aVi%yNDHz zh%}Tmn%`W+9{~M4uR6Q&he)hCo$zt*CC?7ssTiaLHXD~v@s-}J zNwJWIxvIt=WtX43ld`zBb^1s>V+Jaf& zlybIA4T@;Q5<#GJP|a+8;0DsHV|z`^*N;=Ksb&z2Wb(odRqpeJWh?P?7Mye2wE*Fm zb`%2e-k9bEUuc>#9+?HU4BODq!Sgj;5kRocak1@{H z_bO!5<^7x(ubP}dfj=d%&uOtV69c4;r3($`8_{}T(fPmHz;9gQLu{8a4>SHa(P;+=#&HTXkD%`4J3#KavUX?2I$T(_U20gJKB9O{7TC7|D=)L$b!h|c z*adf-wh1iy9@!Bc;RH~#8c{t-$u`T6>0g3YVgR5r)W83HZ8MrS)t%q5>Qsp>gznD> zf4C_C!^ynCxO+nFTX<0Gk6FC4$8L z6Ol~tMx^dy_mC)gkgfu(k`J~@*terskC+q+hW4CU^WdJI( zPrwD|RVt(htoqX!v$(m3_T57R2Gaf*{_rVZ5s5L$6$qX4GH3@T53r(1OyUqF)rn|n zL%#x1A8(WMgc-%s9$p%~5b6LLeR9UQyP?Dm%GdeZf||3b#IRU8 z>A$zZ@pZkB<=9sQn4X zfXebOKiMjE!5Zc{;PIwyflMEjatD-Ol>u_QWD64Ai11*|mL}lxpFG(XKUl3_tc$_fKr?MAHX zjbt{?puA}V^NuUxYbzG|bflzG*%pjoKFu&DNbHgk=EolQf*oCF$$b@xfr>ri_)#_V zJ~jUzKMZ|7fkfR%gF_xbWySeBNIX2Hh#BjfvlB62Zbt;aOS26F;$qhhc_iLL1F)zG%CC2VZkUlGlt00vZ zN$^tZ{So<5LrCy|vOSR8U`D$VTUdS|tO8@2`--cgBI~8ox{32%i3j8PGA~n@O7XV- zQ>(+S9)A1D8CAn;eWlq=R=P`QTCO}~JMm!PFUO^c62cwR3I2eU4W&mH&C7_fe$SzR z!bHN1VB8gn_288dynX+SU>KS5LsidHMt1r!SRfs6Cl|K;uBeP5_!M*0zP);l2+?23 zpSq0LPCjP1dt7gA81CajT84WkSmG}Gh2vB>9b)}qe8QyAWQ*P7Y7H}bAliq1J?}@N z79MJK?&%cM;)?gMOtqdR#tNkl~z{3BC^k4>b=8t$WU z#dv|&-_A+s2tF?v`z5%i(X?uVu8-V&#XNd0l7C+H{r=W7|J)Doos(G6r}a1Hr=*#N zToD;rk;>t5Czy6Doa&ECswFL+WM0WBA?#jN-=rQrvKjRyActJOrxKvJ>)q59f6C8E z&UYP$f?!QJF44C~eVsQS2}iyvs6w~hMxw#YGX!jSsW zs&bxL=JA{&9#?{q65@oE^EzxGigee~rZG;&3~f>&sQ!wE4g169`6A03{s%mT^D#Y5I{0DB9%k^(kACO^@W6Am z&A>c7H>zxHX19fHp{YgkV9lY=^~Qg@tJRy zwy^5Ix10a;PxA63p*PX^{l_e4kh{cv_Ake^g_@>QI~qqE^o2D447a;B%hz>bLeo!y zQAO}@MM9qfpEO_&{)}IjcmDbou&tW;UfVRT5F0n%Ntz3lung$^8viImMW8}`tsCM( z-k&(C_N(zADBL>??8Ay>FgQwJxU-wKkdpXwls9)!IPnqKjc`V=6}BVl!H-)K>HVgG zuA6Ul^nRkc*>UaNcYaEKK*4_KoO3alISL3BY11f%-cLUaV>wo*HCY*=jSqKgI;WB& z6h;O%DahK(12uo-pvP-EXum8K(QAF;5$tqA3(epY3Hdb{*I7BP ztcK}7`zk`$nLOX?p)$C)kN934u`Ft+fBR9+cHYxB=x5?4feCLI(H-io(FWmP0ODC} z8p&p9@0TR)PppsAJ<asJiJ~ZWN~GL>b)#(g@izz&WQ@k~ zX7W+<@u*;|;3{3?iFa78!PE4-9}S3f6QSCGeUL$5fj>~ktQntLx=HE5moOh!>pNeW zy#U=3lz(P%*}jl=iOgFA1BlDY-)^M+209CbOMB<=8(k!et|MGdzfj1%9FFJt3#5x# z9DOtzbR?jOh0iY2+|uEnvgD^^qpRaTBlva#I4vCkP$g2C%2#s;tDMqO)QL1~r0>n^ z2m+L)Z;Hl~nMW-HSD%GDSxelBeAG_69eJRsTQdBh)xY2)N~G~7T+X?9UO#SuV)ZBj z*~R1Z0T?wJwEkWJKAM0lNq9rRabNGy>yv6%y!U;UGTnjcOurtW^nXr+nUlwVbOFFh zPKvr{`8sAJ#ZS)bY2NmSc5T2f-NAVSzB=gz=MHx%7>`_+xPnSe5Ve- znvI4j8hDHX$tq=o-BT2#ej*h^z!%}DBPLz>+M%mc8_fsSG_2jbH|-?*Z1*UaR#Yq* zz7N1_9*qFHAOvjX3>pD2L>P$W@@ND$j4$|p`c4L-Y1eH>OFFn?4MYx9ziiyicX6BlEsfbNX! z?x_N@eR3RV1Y4y9E=%~E10cg}T>bUFE;O}o)#=L;2geG_Z5Q#&?iOzIr|^bz&?Z<%^z%*!T;EqGl5RRf$Tk8V_ z2nI0;fkOfp;a?O>QsW6bW2tmI$=^VswTkhav9W6SJ6cMD8d)0<%VjjNp8Mzd z*^M1-T25xZPzk+%xaqU$oW|&-{NF*ee`LbN(yq%o5u7fp=B4F%0#7U=r7B6oha#*w zusTMQ;Iyf4f{@5lEm>&fAFtBePm$XV=jP79Ofe3uS}?-U3=bb5-2O!^b>}Z$hju@ z6xhNEnI(|4g;PJ+2;I|?RtuSyXyX9L(Z18}pE}x*%#*e6 zA;Z~9Xea!{#WtgLasEDg#KLb$X9SA~fZJ*Vr`$AQpeP@o+DXP4upw=hQYH)APRC9B zeX+qesxgrhv+N`ES(4VeZM>8+=EdVe-7ut{1_}n=%)*$yUf-`_M(y%|q(0h`OXP}v z3}Z+A^2u-}2Wo|#64sAUnM|J22ogL$d^%$k_Fg`u#Bh#F=T5ej)oB2(1iGD+ngfZr z#|j(#{ZAIa{mGls?S2J@uebpqZ1JY$vbg1wH}@QGiXWIKvzGHAyYy0D^|%*(B6vvY z`l=s}CGUr}LUFiQHV!3(=e#?iC)nWo*`RBUainRxz?B$ zxF}7mPfo5JUY=yBOI*RXy6;*ZV91<8Mzjg~AD6wAwAKJd-y!&Luy)t6ReS%tR%Puw zZP+wES^0Blnpg(e3T@9)b>+vM1eh?{tZZ=Zn&+$<<0yW79rbot_kvaZ0dve$Un+&1 zp+nt;j|5P=lDl;_tHHfbUjsx4Z2^&oRhvdoCk@G*M6oJzrCw3X@}noPbA0#eVQ6SB zKny&9%4=#k=N1W7MU46`#L|$_o-w>8e$^D+-msn=rV{tZ$zzL+EX@yJLNwdats!L?hYL(@x18nt)l`dTl6cxiv4i&=kaq2!#)h>LTQq$XXE__?1hfZ3Et*?-FE%p?N0_z zE3<`Jz-iEpRnTSoSC6?%?{KZfM&1d~R56TWrC2EiAaO3%QTBSM_50ucwdYKQsqNVU znH}OJ2<8p)1IO^E&4xdP^g0g{!@kxx4oy-4xN*1uN85n;y$TlH3H6d4fT7Nu5w4@Z z@B$VOA>@_XlQCbtn(!w1In80}hC?@N8>#>kZ@hW@@~@N!ATP(Qu)%#d!gD*;eLE(T zP8JoA7mH4J9`mx=f>ln;R?sKc-0ku6dugg*Qx(ysh|X9?9I}@l-+2+1=z+HI->93> zy3J2B>C>KmfrNEi&_0$~8;--bz2(QeIp)k1xJKSHbo?H z8|+0adbiQCFD~W+aJmiA%#1=`8#CqcOv;PB;K*_fc%B&aIL?K`^4$WL+B-YGJ{EZS z)#bzeTE=;SVd9gDL0ZLZu>l>CyPu8f;0VNDaIGcJTZ9f$x60VJmA!8QO_SMdF3HDQ zh;49F`1x_WlZhs86uok?56&y=7)IbgCX-dw5gy$~Ql>a}EHz(u+_#Fskcxjec*dCU zCNZu!UbKQ?c*Vn$c~JPAZ}%tUHGdzWA1l+2IeEn*B9f&)jI zt1R!FckZjLK%#l0xP|;}-P51MQp8ea){I5wdBQrHQHvuNb@7rgBj~Agb&TT`Q{3eS zf@8|urx)q*(TYDSF8p-mkitpID5fGQ;VB$?c_KA`Bu7J%Ut&8t?X^&%YrG8dW0rKi zAeGjLOsrHg_mr4g&S;p6jJwnp+^tIetTxn)1y(&bWZ7%Blo&my`~LEJDst4LFR77S z7SvZ?L$NyocsF1SwwNDty<15LYmd9_$gS$BU~Nr!3dQ0*=9;q4Uw;}=eX|O-*anwe z`}O_-xvTRS`U$egP~aUhXq3v?>YKk#M5o3>e;pMv@euIRDM8Rq>TVa=ZFXU3LxlEjBJH^V7u_UTrzI(xbcT(u-FX!_VR2WjE z3u!jeofVs(&P?(WlF>IO=9Vs4j2*(>QCvrWIsIOolv<5wKb}5B5^h)_^kagFDxw4~ zt-x+A*3CTj&zoPOMEB;wzcpZ6XS8p{9t%Fr*$0AE&CP`tg~F!HJK~gmdQqz_e+V?& zwF2gcm;p0cAZWC;22M`K7R9LXXtF?&`hI?Ka5?{at}I)_f0GE|EutCI=kHwpy&7&> z?4SNL#cBVeqm*^l@??KqTLt8K^m=ExlihPBgLVvUO^UDk=QCMFnwW7-oIVRY!>8HJ zZ7Yz4(!DGXxx(GTNNQuX!gBCb#1S&U%#YqdqUjW~65=&Pv7Qvt%rJ;Up=9@UnydL@ z(d6O*GdSG%qx&}J`+cK=7-gS#k=d^oCbTTpOc3h5rH@9PLt8gb~_DW8kkM4UQV(UY04 zuVfvjj*skdJjDn=yHb45H==t+T1qS&?25S6lyoD#_291Nm(_`*&4f<$a_S=J+((6vTOS+c3ND*evVh;i&zj9)7l9`dCTJ`w=FHp}xLHMV zor%=qOK=nwt97%wuitlHeSDK*V)oQi_(vur!Q+o{*RQ-J@PIPRol`57#9~lXmfq?F z)6fL-HC=HlF--6&-^y3q#UJO_5HW`vici{)%S*O>Sh+j?G$hwF@+wXa9)MYb@XAeJ zr$DXpgpG9&l&bLyn7hV5RP4&%HehqU$*(TGWU|>cCb)b2>{S8$U|_7?^S$lE>rmz6 z?rrfy*0vzA?-iFz&#;9a=LM~%x?xN*{7f?utfdB#_DeHXB^u+Rd*f^`E}s^}febms z^aITy!2zlv@lBlYXc`3t+&s$k36XfL;7qOMm;0{!>pj)E!7j0cZ|(=m zrQu&>jsYJvR~v;OW(}xm%CS{uEe9LRI2W^2zN)SWqgEPpeC$8+t2j(3o!@xOANO1| zDIg}rHbgj1QOix)1k3&z!kiD^M^KTo%iYvm<@7KU?qjaF*DTEeqECyOY*iS5PF#Mu z90zfh8R^;*?lKkUc{k@1!M=Js#kd+B#(@8rIAH7~kVDTnr!(SdvaJrYTX-)^w1<0$ zJRJiC5ha)b?0GazIdo}gi?$rrv`=yZa35dcYwMlA#tXm#Wu~Z^(Gw2yUA{Zg{6gJ4 z*dMGr+6`|@^|PlZo>)w^4QF1B=+lvgZG#Qhj)>E~IQ_6L)9TTqc$U&-&Y=BL6Ev1x zF+j8|DpIwlMiz6XOu=N)2xEPhP!pm%WAC{Hble+3_7X>xRRht7ewD&&^ra1Ao6GD% z&)?S{OBe^|kVE~2b^uPckz89@nT~eR+UNHe%Vitu)03bT+F&#+13zBt;kLp(J;wt<;hKaVhUNHlXQ=wc5{?y+C4t0bj~L>u0r&o}Tm!AL&m62q3= zV`o2G6P!9?pDxJc=@HSVS5PGFsD-O;*-yhfYcXjD7L(Wl zhqm?25suEI#acupzsxIdTF6W;n{o4U)W9X6j{GiDZrL|;Smjk%7ONlp7QReB$&(0o zIE(Boi0B*>9A&)h!JH+kiPt22pF@Z`s>7;Lny%PY z|2Iq=nwXS0>Z?c8uRwGvq@i*s-97dHlO09Q~&#JyXma1R+avpHTuUZj7J13jubNf6&U&#iyQRrHbE^dpsO;?lY zo6W??&KKH-=}ADXz5B%&_D3dlycitkR)?Q<9K`fQlx>(>cSnC#s){rbcC7-J#(t_{ zV_T@5@TQ zTV{yf3i}%4Kc^DpsGMzb9N-O5()9F!^Q}e_SZ=BEYcEgcr~gzH0kUsmx~^gn`UUbsd1+&cY{z!8$;T@b)`ggId5&8zIn+&>5e7i zN1L4+l5_VsTLbrHE(wsdc2S?WAULg~hiRKs0#%9elU_=VgDpC(p>nqRO()ca^btA@ zGUB42RXdie>5_*dQ3alO>C5<07A?!skFyru(Rj;2uZd}TPs{-N-riO^ViQQt42$Ponl9N<<_#o)Ob7^Ht_EUB!= zW4q--J!qP&Ld^|(jAy7uG(I-0Y9A8USQkdbC{CYNF+C_USVe*^yAHVQ*%Pv!*5}T1 zz@d#&!O*2#fwmf_DgGih!QJexci&G-Ux!N63WBRecyy)Mr^ptvXc-kGn6yRWWo`65 z9P)I7_l8VN+WT5F#~I751!>X*#V+hOz8UMvpcD(moBcJDY{X{;53L_pv2xf*v;CO< zY-vuiJ##g7cp8n;|DwFA^wVgc(ab2M3&`|~8XXN{daP6$X)t$Uopr%gDz&o^rCTF# zwnE*1DffF0?Z`L&*S=%IJmU37%`q8p8e8wS<#(!bYlNGkNoI3c7ppZbgB*UnsTz^> zs)`2dlz;ZQ!B~MVIGwwFr>ITEx5g&W{nIQ^?@n_=ZoVL}!&>VWEvI%Vx({;F2}X7* z$Y_5K=u)OyTOCO1)v;eAGa#WH3bet4S67AW_9dOFWMpLZB;g;89A+GfYrA0RB9<4UfX6fLFokkYD)K#I*v;!AZn zM1U&53q*$!kS1`|gk;fpIfi999~aJ9CG*Q!e>WF8sI#ax$}wP_`I`L;;&<=Xy*=W( zD78TiO_$YJ%cjtdmZ~VQ-mNEhzz)zEB_UjRaELCjs{bqY^R1u=*DtQRd4IelUHP)h zjDaj53gpov-#tJpR?YQ?)il~LLbpa;W#8;pBj*`t%fQkRkHro2e8tq9$0!{^_a^rjU#^SDT|a)~=xjqEFNjar6?WL)2< zbf-(Vsrc)o-vcTY-Cy=K=xe675BTfz_}BXTCMN2P#n0=XmYaSu9ig>-<5%LUhAz5g zHhcqw`s75pZTN_-9r_HtR>55EG8|j~<_o}EW2ZeJce}Wchs%j7vC}$6V%hbdr@V6> zvxfDkLt`%R&*!xEC{*Tm#p)LM&jAU&w)nTFJo)oOxm~qG)(uaQq_T3 zt*qXfHjM-Jr^U(=C!lpAaDa>t>z9mE$WWVm)SGwQrYqke_fCFkqR+$?qCh$@^@eM(cVxsf3Sxf?4%X zo({tmJ4aQVE}w1^R-0!tS#>}%Gra#6RH{E)=Y6bA4HPyH)McU-dOlc>1cq0A9gu8Z z?~@{S`)8_IXiMKS-}Rb6FHdCvX*K+1u`el!n3i}{W+s*5d1(UYP%gx}N)yD@&wocn zVxcCYAZVEj`Xr8emU`>58n@_6q*6<8pQr0-bJ56~!=9*G-kO?G`MKAALz?QMMp+mD zn5hAzVGo+tydJ9SfHT;}sM!8FYFCAaTC6-Z#hUIsiZ4kJ!PgWwN+4-Is%+#}9c;(> z?eVBd+u|tnBGU}Iu6R~Z zxdbJh;J(R0N`^a9FDr)bJmZJsQku%+g;D8L-!da&G`_uW0^^;y}(uC+<93nJ!X|wPw2ahXDUN^~a=% zo~mc5eB7$aPKUe|)VoCXpj9A0duwf#@w$0Z;wCk~W@Z8(oH|?r9R)O>PDS>XzsBcw z)*1|UO=o=BBU`<3^MG9k9d$)lTMo!5*J)>|4=A3GgE9=k)m)$Dg=b^1-va@g9I+n5 z%9DcQ$~9i@m{U5#B?5Q^>Ddu#`RauxpDmv*gbg^Adt|1K|XWl?pyU zqf)9K8^&aK{Ah7Fjf!o+3Q=|XGXB<3^a;gvz*NahaW(!kl6;K5R4>lQKU4UDb_5Ta zT4*YXtrN*hb`1hqr?jNm+)2ugKs0=8bWtF))y*s6y&PQ<)^u97ao3reLn5xZO1VjM zxYANUtQPO?)MX!7#{5$(l-JsU3<%Cw3$786rTEB{22JG*}<|pmS%LF@CbX$bK3bH!goKv4(lnm4ZNaAS1w& zf&|$7+tXl0N;kO^Rs-6>v&^#YV{I7h!{-2@mcdIiF$Jxd%z#)TjFBoEJ11bNuJ#DT zwV&h1VVAoDtliv&S~w}q2D9rT#1{%@?S?E-mH43!or}f-Kf;=y&uD%XLN`r$vt?YC zfPz}icZQtQSgD$u36fJdyVY=-vd@OkY(JdwH_i7#=iCQDV`Xwj&_9-Df6ONOuR={f zYy^`9FbUr&gI0bCi^#dAE<%RXR#x6Bm)3p3DlP#TXZ9&piHpSh`g`&EFmtMp21m{b z&hdS(9@xct{WRK#9hB8(x~+}&kEcKM$_ zV2c0?Rtvgu_NP5R4ukGk+T~S}CYUY}3I*r0@y6_g#q1>)2j?fsY~U(lS69vjeY4UH zk$1M1>XlUe+93si(U;_<3qpYXB0bNqxqHcnQ(bS}T{q%Kk!3R};^s~fC99xT>=#-2 z7q=gCcz{+ttZOJ&Y?bYL56Ommp$N*GzhVk25Mg>H-@Ys>V7yJW(C3gHE^Vd0r_jwr z*typdf7kxGG>*7c4%6-}yDJ{+ zR81WRd-1waHRG%9<;~)h!_g`^`RaLpQc{+C7PD3O)DZ8?322effewQoKVuPzr^sot zzg_uU5E@@LvsqOx|1q&;fVFnUPiB2KF_F^47&CDJs1mQg0`f1k`YeTV79%!grk{h? z`_|2pV;1WG^uayYE`e!CUEWpA{cU%h3$1sNli4#l?TXCpkfnYH+BBX`G-(zB)jK=T zCObvWuim6lA&7OdL&M)C{H9#u5@!oFP`4TnU(BO7 zWN76X_%sA_o70xJ1hE?2|nXwC5?r{OIDbNBlOyQx#&h`}U+z@e>_l z(K(omgHX&`~&SIx%l(5hywy-bjCD=AUNil&JyyD^GNJ2?y`bO zWIYRB_|@evn4~10NG^%gMWoiAlW&+}JacNteAK*J0nz6rz@lw06V_FKpUoD+i4tQJ z>QR~bLS5THQU~M2GK>fA`9ZH&IQphIY`pDr?+JTk-7Dr0)%a-*wBbtEZ9N0Cd1UTY zxaJ&NqdRNqcvIn|vCx@>GKS0ZcfBU^&m6D>_rJ3~2G5_RqMlCjuY9$W z;^7aKvLGm^gij)CeFiXV$^*X3yh*}drPY{DS`fnFp6}Z$Keok)$V;7}aj1MOMJEqi ztZ?a6vE>CK--iJfL~|HXRb!Rw_rm5BAJ5!DL0*jjhP!h3R;kn!4X2^`!oh(OGK?ks z5(t+lwSfPh-p=wXsz2P~fQTREpWBZ2yQjb@8g#4`_>&{|35*nS&X+eBjV!+|b zE<0!Kq55p;h}dq|V}_~hw>%!ua^AN_JJh(Ha%X>4I^D-a=tmzv*Mde(@1Norw=gDr z@}oIdyyon)at4$R-7xj~?XFKEbfS9MoBZvM_n6WBNO9MH2)*#t6f9>UHhNus52X*JKcG(^>wZQ zSnRDlEGI9!wYAUhK$1pABUgF~2--D-_(Qi`jGCtuw(#Yt)C@4wxaNK?aD1`?)keOJ zh7bZgd{6Z$!GKmB^^#^J62$-49fLxsGaEL|)PfMs+LJRM*vw|_@!-ntr9NmV)8YOw_%^3dNFtulONkCV|(y zGcUI`!Q|sqv+pd^XyZ&bV6)F~6on@0J1RN>YQgw#E4pH!(b0Jh~ANz^$rjCN}Ax{8sZIl zV*wv?M~j9sM*v=h+cFz=P2Srv9oLR$B}L+qLPN|Fh`R68sr9%99}8!?=jD_81S0}{ z?nTUeUCxlY(f;eQ#_UOhNgW1`3sh*ol8$=7(MN&V;rv5`;3x0R?PSR(>inA**))ZN zOAooq)X<|#X0{dcS^e8iQx-z&hoIBB1fM~#KkED&=(4ZNWVu*6M`ri&c0S{N%_Wur zgLDCY1AjQzJ7sg5bCX$rr?M}6Xxv%NKx+kZKzC$feXZIgohmR2&rc|*AZ|f^&zQ^% zg`V&1vK#^f#)Nr)q}_=Jwl0#7A3v`=R5Ew>Fvw}!jKv18tn&Yp#;oQ=d6!uHR1d(H zMS|u`5WAYZ?8qi4S}r*6#V&^f>DfZEQ<^0N_k0o8{DUjxq|Gzgtk3zaAt91%i$y0$ zVD2?u&uSJC?uGuX_!|IT9EDB$yPqB!r>4tXi2ia+$HTC;)|5`hj*@<}>O`6*-pg5P zRd7o+hHx3u{kj>A$W=M4`ouBjKraeKiai$@dzsjgc2M5d>Wm=!-W;N`i=MThM@cVd z(c_9)aBumMEJZo_{eKIrK1bHuZRr^>et7bsg6_HQ zGXo&7oq2qDCFo^|n;Nn4N$cfgDXtL{6d%L03$sv>w!hF^_0!8Kf?DW1mhvixmRIsI zdQ`oHUvsagr+(!^pdm8PJv~P1pARndBq_YFI4jPV^Q((E{<7uM{7stkk+;pX8%DCM=Pd9P7!90d^@n6#Wj z4A|hA+E>kqb-(obI}P0wSb-lYt-Az-&azehp{R>|$7BCC|&JiNTh0Q;<`}YdX)QI#C z0}&{y9kFNX)*ei{+?1nW16DDnaJO+;QbFW4Z%^{qIm~Z@);y!cfe9^Zvv5kKpzhBa z+x7gkWuNTbuJ#_jps4iF8$T%{$av8LgGO2XQ8~wDN#1y#15(7@9ir(coy!5WqGxy( zt3?YGH;NpgnEY#3L})YKc2UAP+WL>k<|)UcwK^9GE3Yp|1kXhdL`I6biJ#RB6mSN^ zcB~Zqp)`mkMRc@685V23lZ}z1npRecNCE!`x3hlMVt#QEBG&X>Vl^6o z9@j1dyM=@OODTo*fTeD*w^s%?nsa2duoP0&*h1DI&GAr`S8CUDl5Clw6MDWmFNZ`wT$6g-EK9BdA>R{Ce|v#WMn^t(tmI1OWp1)7pIYX%6f( zkxg9#N#&PTIYj-i(%zovg5%WvkbY`pGo-7k z?(UKEuT6N02)Bg`D4DPr2=pQQ_yEd2uFu!)-9f1oty=ptvgQI&O zI_G&g#U}8z^=JU_q)%Qq&(^-Eil8dCTm=={jY$A1H$6Vc?XhZck2)4imgKoJ*Th1J z*nn=wPfM#=>!Vt1H8L5pC5BQv)nmKSfrSZH>rQvJ*+HN9*QXMXJC9tsP7AlTqR=9 zl1A}&o!X(mV__gnHyjLiS$4Dh;vf6zARaMGX4zfKk9o85c;}rs^LE0)+P$f}pkQ1nBjy{x+**Yiu~Bfnu(h*G znxzzAaG_!+{R;B=1+V$hUBbZ*LMHmCw-)yXF16|zHKqBMgZ(QF3-+Xkou3N<7UNK@ z2fOj8`!q^oZ&lumR9U)9ERGul)jSh0`30i1NQWFq-;4tBtq83p5Ef#1X0nB7yC%TD zmM}PC*lN~$b`VCx%s}T^oc9)ElNJg09!~Ln)#{LG(Mu0q1eIQXw3f5a%(0!0hTO0P zOGd;eaO^N);sIxE&L{y}uhSo22>b@YfZV~tfO0_UuRSB)K4h3c#JS-FoJ9M83!|IuV$(70;s;&sEvOlP!#rZ^2+n&T|iJ3}y)*r_! zDnQUl-{gt`hw7s22 zA{$XJDpW@=WwK{`#;}|7B;zX}c8c&UsrR1!CF515Wh76W3!T(Pm#hM&`ibpn^*N|s zfvdW9^BHKXff>I~fJ_xmg4^+&{4ENa87lr?AYN9Rrll|1d}>AxQ!ZIK1L;ft zc~k#*wL+4kB{2}*PbX>U$`a7()+pB5Soh11dsOpHR#y0o0Ixipw65-#Dt@ho-oz}( z^+XZTy@xNGO-(PmltJ?xet zPf5Y6I3xpYLI|S`-Q9CR#z-gwXaLFaVs4ST5%3_6z6=r7(W`+?co;Wm1Vd%7=du34x3NHxb*?EDO& zf4=}em}~yf2^%O@Gt&3q3bMXnD%H{Z!FW-6dZS8^p2#Dq;aeoP6*(j1nyu}j(52d$ zRy(XXJF8UQH>(NX7!Q*!Jqs)zhJF4HaiD>uT4Qc)GON6f_-gKq&1-Jtv?BC%C+(>< zN?)()xoto@!h3fBss|r#4k`N$0A87EvmMT+^`IfDj^_;VV8vS8fs}TJ1Zf=4`t!GY z9}|X5b@b>7q_Qj#COA>6#p9o>cIZzy<$<*3IAAxyUYdlaSQQI-_&|sEByFteQ?AT>XMYY89t+A)J%Hhn!(zrv6p> zaxCPzksjU=8GL;96=p#|2n>m?*SPAWAa1w+U9TN?1h zY3!e?Roc&@=SP8DBQ|y3fW@2@L}~+H;FhC_RCB&dxna6p+!@M%^C6#iRdpSEHa8A&BQZ1B`@=KkHlp+F&zK z12tyHuCDvFFS=il6WUI&l!bTgF|qI|g!S3<$5X}fXr+8z0s242N;W_Dya9WK9YB5` zjgP)o#X=;@5G)wN0m7uMm0i#7Fp$=IWJ-S7Ui#T}Ka~he^i1$Irsj8126ptaUvJ)f z*?Eu}^oX$7c<#R(Q#$HUDx^3EF8YB%YrCVA4ZUu3$1lNcavm3N%3JU2L&E@kV92Ec zV?P(VO~`~YCU~}3I1MtE(ml(%I9FAtDd|VDK1A1p8B^W^EQs#ijjR&8jl*ItyWJ+4 zDR6>X#xXK?BP;6S)*&31=^(fE#do*pxUc0`RG$j11xZpF5`Y$#aF}#1uUeg9EF@le zsYx&pPg~-QEufYCG%Ki8kDI+7%IJJ;xUpJj)>rZOwo1gNtFnAH9Gqf0j%NsQ5}OKr zOi+DpTy&N|8^kpf#*-3{11WX|;tV0wDDy>j3US)!k4`u{-Klmf6GDn+G58ioQ~ZbO z3^r5@XPtKpPAy7THk^(QO?!E0+IE!9U?R<^?IO%P)rV#rV{}M=oVE4&U$@xj*~Xut z{PR9RjJ3cX^K5`q$vASot(9%n>PN|kV`Ru8GB9l$h&i!yHDMT?lypDkL#Wl@*DYLe zyD%(lR)%j{8eVZTIRa@MdZgyncrRaAlW+G+gLhxKP10bA9RQ5Ze)X=LRSiAg$bPe% zU8VWJx7&)z#ZrV)e~Kbt&l#f|0rzKpF3-?p^ROPmxn9O1#@BNO2y}`9tCn*XDHxoJ! ze!vw&trc2gab$i>Tv&rukft6{9HAO9L&MLUzm2r3mik*e=9g>6XeZIoY8;GRhjsT> zWPRY@m;J#E-@XXtEYqG9D(LBGQ?vLbCuOlEh>Um~20*Na8Jh@IRzN=iH7U znGu$@TPgb9uRbcMYG#VHDu1?$>a*xnxSvj>Vaz}R9CWx)67ZFP`(0|1mYe}*4gDuK zHI8{pyVL0aL+`RWW;o6i5KB`6c8{5$wvKmEV%z$h39OQ~@WUrmx3kV=jB-_`>kad% zp4Yx`ALY#9L0dczr`y2)_(F_>r9Q5}|OhgYb z9m*BVQzwOBQBt$t6V6MiP*eRZufzd9hErTgt0VmOEdfN{~<=mL$LK~_zYbGV% zSx4k&vJF^?!87awR7cbohF?_}^z+3)-fuxSwe)yyAjblkN4#h_RwE+0jx+D+4si=> z$e2Z+bhAX`qg(i*`1P#Ae$hDGa9Mn~cdF~wq-iN2MYQI0#0ve(iU{yOL+Yvfvo4s3 z=ubcc#}=uTCT;)05_r}@5sAlRL&-g|K9PE4d(_t=aeoaR6cG8zbn&mdECn*Vrnbp8 z=;#iWNy-87z4f_!Z{x{Ao9VL5>rGX6nqxp@Ca~LU59zt~38)+J0Jg65cTA{64Ya za{HZsdQP8)nb)b5xMA?u63NysYXKgjXPc%|dt*{E;pZ&HL#E3;up4I=ZN~DPeQDa+ zB3CHEalLglQWe`mnoB^&u~U& z;qAzNk|5fd`h`IA{n_B41>eCz|BZ0C4+ziU3>it1$l+j|nWgDA7&qkvO}Zn@{4293 zySH-+#o+?2OkZ+*g^Bq-ElI7 z5f>HJxU{&esmS%0$?k=N+#YUB#3NwwkUyUv^{TrEZ~kFCYXFLUf|)=hlF}lO@0jrr z!}kW=pA6YvHF1@bAF`)$+`hyNz71Vkg*kq+jL{QS2b3Jo1*{YdsT6v{%2#-cy>U5q1hw}J3kA~ zW3%W)ePeBCoy8kc(K~naKEyXi(d~>E!r1d^?1f!LZfqK!rqh*IGuZN8Dcoq7&l%+z zFWfwA=WSzFF6P;lT z79d;@mpY_aB!d!DW4Tuk4omS#j)k0hkKV{}ZAsyNF)e`dJB`cI^yga^jdZLJ5^>4> zxK!PLTWAL|b?!T_>L$^H_iQ8i$)Q1K0FT-u175AoWO0=W%E8p5iG5kk-WMYL9c3pi zBR(`U6JT$T=xq##!xreAz1onq!z8sAdUBv$O0)_71%|3tgGI0bo&(6SydyPeA`K^5 z)tVX!ITuDs?Q2PGx5)a6`h5EkM;yT_U+^(try2flJuOXp ztLl~@Ty8EW0je;rSI)cpEZZ1LS3!pQ4WjJRQhe}LOMo$csd`|!`$3wG6kEpY01pjp z)`1B&EFFU>B=*KY)%?4+`>)wnr>lz>6oH=eGYvIix$jrBfd zoEnWNhvni{6`A&Gi>zG6(!PS>A)>DV58u@N&f|Ti677Ip-F)6@eYh9B4=hhI-^eMp zSSK8Xx?hLYQ^`y||9e@_yH^SN5Xu@{zgki}RjtYN^Kd_uQJ`yMDhiVlc)>Gy|019u zS(Lx4>eK?P}?$=E1m3py1w zlPcWVL`pJ49Y+8R2*E?ft31(VGtesH0(uX6;>4e{%{D15DU&3<2{K;U_mBiwTVB{ms|I?SxTDu?2Pp?Yidsafk~GXWa7?(gNd-I%DW`3eH*ZVw<)14X3IiC$L`DmY>CLJj&=`B7L$MgeY5GX*BU$s4u zMItbfdeTdOXbS3t7f=Z!=4Ez3aGcObUg<0Y@9E$M9nrB&*C=WxJUc$I9W2%S`yo!n zzqIWf&Z!sf1ILXqwyS9RQwAI}^CBa>8E&w&YT|mX2O`5mrTJP_ymlJBI3FG~vfnC; zS5ffgd&8d&Y$aX@4FpeyY+aHKh&COR6L1oR->ROe=Gm#XSyE=fF*dAT&;Ps?(`szG z#Qeza@5DFvGx*jS!lC2R3>OObZ=HSb?zJz0W_JCb=s;&pCu0yWo_=uJ&dMcyCG;5N z{WmBZ<)^Awaog=_WCfBY_l)Y3;*N~5ic(QT0Xg>jAsPdQzHy>n3!Fu-=K##w@L{} zEpxQ!*|-O1(9u9Neup{&uC<4vO;x0;^;Gpr`DLPh6kEW)&Tq_3#ElrT3-2?9`_v-* z+Uj5i_=F_!x*?_HuQ-AsW@W@^LWHTI%@F#4O^pa%Qtm8R1v1VezJm%9738=8akww0 zc}&IJ0+c&im|K8}b`iN3K~y#lI6Nkf8v!vgh?W}S8fp0GLv|)-Dmof6>5Xqrtm*T@ z9%XtAg#HP!f<1(c4>*WG6zRV2GG3bnU5nxmI1Eu>IC9hC1nh!Hc*mlhG~C#LY}&q9 z3AX|E{8zw}C@;0V5%%74rDmHrarxqL;peR3&!TO)W;E!}kY`9vl_N*0P3XywIZ+QY)ZR9re)kkB>>_c%%%gt;Gv3p#7D? zBMqw|K*~c*!l=O8#nFJqPh;oFWSB3b0Sj*IR^q9~zbHLrw*KJexX@JK{%qzQ>)sN22VYih1Toim$8p%nl-eG#*uv&j98b=y~}d%>Z(aUugJj zdI=yI>zTGnbgJ`+-kd~ewr1mECRL}=^+r+;lI;uOC|nscp}VpUTTZ*UuJmq|QoSiG ziK9f{{0W*dtdpVRM-!raYEHda0F$m;#*D`AhH+9}00g_l-tb`_q1fwzc+TQdy?PR< zMvv=b#|x90tZ>1Fjv5$;nt$f0$cRXdcAuF;4#NkP)X}tWX^a~mfs;{SO60_%C*2ka zd@12Hfk_v?uCDzJ>|nqwue|o0HjCHe>X5yA*v<7P2Qvd4nMGZ6v{gLoGRl-}UcpBUGGZYc|k~`ZFNi%e^YbnvCsIA6u#&0$yxv}P2mV*}GmYs07$&2ANw&;$!AgA(BY`|1?m4((+uxHexgPi&fk&x-tgeI8A+ah36WIC)J`M# zqW!Hoey>|Qm@B;GpXc3GJgv2?7N^7?Rj*A(+QK@!Ma=<>Q&^c{%j-c5?lcnmjI1K| zuJZ>|5HH-2v=`oBkqmx0J;eByk>9+a!KNRNRNTy+IQfI+o zQBV~RrSl+nJ#6LSs=gS&1yrYPrTgAilTedyWMr`)%!xCqI1}j<^qfgX)J!mlRy$BU$VF_uTitv%40ae~N zA^apTTJvdDvYnKYn8jSgfcqm5nGXF2EXS#AxNG{OE*e*~Gq<<{&yZmxF|9!62A-xO z+h{lYx>2+6jKC#5aa}NDlIQU=UtROK>xs=}K@zByQ(=^OjflGVu4VAKAU-}wNhhak9mCE783aD37~Zk|F>%sVe5 z0fAat+m313xP6YSS9P?1+n*Jszj65w74y6je#w3JM$Z<##C*p?ueVu5xi_U-j@4sD z5PhlsV555Y4tV=x|6GSkm*5IgbwtukQ!r zTqX+kNtdum3$xE&>x7wX=R0VL3nBRt_zeGmoRk_@b~o21cYOpIgl6D9kcm5u%qh42 z`n67BHDh!J7hObpe{!WuPCmXhefa`eMm@jI6dHbg?M?4}>`fzYeS1I#{o)@l{quIe zSpL`xpoR*0?@tvDbcC}#nbArA`Sqv>xz>c&$z_=Q^YoEiGwqGkcn?06NnZWeQY=

    ^a=!o5^urJk=C^kw+dJ+O(=Y=$dCSdYlZuiZP^fgAh32#OZl`6_VlotW$E(_lW% zaKHT6ME>>VZt{-R8zBD1a3o_?w=ga_WqWYc`{5iKC!7u4c!;Q0o42sAklO}F#6n86 zxI!Z|f4(6Z{_o>A{y{ zVHieog<1=ogW%h?KxQG)x;)QhoOrV6ZbtAqvLJ7BGqWN*A8qdDy{BZv!MbJpyQx;U^_!cw!-!UZR6A8& zVJwQ9P4Vp2_+Qx z9dVR8u6Kn%>ql4cQ_*^myQrIxxyL?F&4ZKb%zw6*04~_sjP(B1r&T)*%yeUOZW)6o zVaAp@vPt+5;I3vQ_}v|-+JuLUF)cB3do!3y%BClbznWa4R2*C!&gQ8;*X4+W4iGyg z+f`wf=+&2jZVTw2y*T{wJIf^a6M=KeTY60b$q3_eS@af!>q_rwAIL1K$$_>#NKXQld$j#WlWWA%-_ zO3mMZ#(@0S;8#){c6n7sc6lTg>V!<>fjkzTS3| z`SPo^`;FpO|IO?48#XUP9*hT2?V8;)JQ^;@Ia`kgV{U9B^!rGbbWrHDr9kLhG=*=G z&1_l3+&`6z`E{hcCKSC4EZ3Fym2u;FVR(b$uO$i=KZVzhk@Xv-{VN8{a3PBfN1bKl zkPh*6GJG~_dBB6l?-Ed=U5;td)e^Q@ged{NK|(!$`c0p4=2X}%)^Q(-`8`?td%7WO ze(pL~JJv@%X+pHhb;n-C#lpjYIN2-H?^knja+=Gw5Z%LK@@fHkmEKVLZ2-MhM0qU3&h&&^f zq18mAO@_^2y&hn{4RX1Cnj2uiWKGB5NN#*PY#t&iOz)j+Ffi-FMG z8v6Y@Y;=360VOBF4KQitz5cW++1Ln7WU)bK08YCAxo&qv z>j%=fPnqD8Mh}F0sDi#Ov1_dMBEVq#SFOx#_?_H7f-dtIM29^6T@dTWSsxr>#-@aB@{uw}7#^uzxY+Am5ltLeJF`G!M7iSbY zPcmlOnDgWpJDjAI<)Hjt{yn@GKeYv7MGqVStS;uO&4e&7;)P5+p6jnZ43&|-p~fpN zRGNXygIO)1y8>tjaB5NOH-zQ18t{RUZGb~z1nvD2vr;* z^pJgsY{Cywr1u;h83|LVzT`B^Rq-pmBS>3L65IuPfBF5KJvHzlPR@t zmz2^pBR_Ajw4p`o2^R1lV7XTeY5W+0%93~17sJT>uq1dn8ORVXgdk*9fd`NaVhrM{ zjBCHW6C(3Q{*R^JroyiZd3ojY0b4VBu1$2NM%?q^Ci4)Ur&3mjF%ZrA8^3p~g=zPp zAKo1;)E>Me=*;!_aOlQ*WMGmO#Gc5%C(e82iP@Mk;FTTtl>4iJf%}V0$-nG&X`1+o zh||?+t>3*(^jp`Rama(n?F_<89GWR0%!*V5Vg&T~SrFa8EF; zUSnQy?)we1!Gc@1hPCu8cS%8D#jPLJA1-Epy3muTiNSIhU;chC$x-@b>*zy|kv zspcu&x+l8D6$jR9poZXJZo`Xqe9H96G~k0=x>NJ*i|!x%7pCEOX$7XA0WowKvv~hT zLBu6p2ETd5Rn!MhqU5I((uNFjfbpAZ?Z}7r7^Ho4l^118+ISNEV zP9yDANji8j)uA~mW5&1KBw@C7Uk+v}grUpipVtqn2A`i}V10f;QGD;sFvfyT_Nf`3 zuaf?p+PwpnpUK}Voym}h3|o%LUXD=ZSo6I#&vhv7Q7WwJDoy6opVeRM z2&MpeTMQixdulM{DMf-wW(4IyT77ITs20mTVbkKX+>lvRsK=GZ9)qN@X9TfH`fLBerx{mwu{YTTNLblNU)?R@e6nN(URsf}kGr2i@Zj(12M5w%h=N!z7{(|4%*VW@GBz%y+E z{dzd3rm`8#^*EJVl^6~T?tL`OXqBJ}eE!w!H_~KN&G2C6@5$UR&{XvvT|!wyrA>jT*3x`&0V zH*T~H6$;~k0vE#=7!BqF>|AmR&|;KUW11xZaR#wT@<{adPCt`4{v(|ycAH+UwZ`0` zrwWJ_s^~@s{LYv)6n#jsdZ*5Aw1PF7wJBdYUcsU-Fuk=_9s52)vcT~=*`!GrJ zH-%~9|N9s&wBX!u2bGwD2#U^5#J=onsqD!0y$!+c?lOa)b#Q?T5tC}I!Vgl7UO=`WN$GCL05i+@}<^YAEoJe}Om^qnYV|T>paE z#D6O2c@aLF&=Jr9m$g$lqo>0{4f{T`S=%Emswq2q)T~ap?UWqa4o(b8eUfx8YvF~Q zM9e6s=B#R^(?or*)v0XyNdDchyURnVZcfY=9mxvq!=IqBFGi-Lxrd%w_!aZh-6@Q{ z+;Oe(?bR;kVG%wp5RwLIFwh}L>5hUy%Qg6PxJov&b7PkxRFMZ>-((2Zi+#fo7d@v` z)VRzTUvdo$RPle|+v09jnP#7%D3LT`3+M3f{a0LV-1xy^DCKSc_Q$fMr&k^yt{X`C zSDdazjP>>~vBbg==U1go7F#KTHXZJqrRCm0rY*CMJi*en4Tah? zzu5bXD9GeisF}yYUZCiQ8Cg?_)>9EjM9J8x6gvL!dH3hMlYv)(qjH=lP5s-nh8FRj zkNFQx7|%$-PB+W)6j3K9x#SM=bcEEhj`3)c&L;ag3JkLCbB8{vz-sE zI(u(&Cv;Yt_2-*>B|VEW{|*!k6n-)HEG(njyEvR1kX2-svNGmi%19P24l0P?1K2}Y~E_69Z$q+D+N_xbOy(!(usp)`_p z_H^wcsuQ0q#N{+~z)_4s%Zg^u7Kq{-OV0VQScpSGSWoM}tHmwXj9}7{=O1vV%C>8+ z7$Vo#J%tkZcU^z{cFhsONSlLX5YHDqj3i((p>@)$4m$Qm(eg&dtcjc7o>aN08F#_t zE-jR4yT4wwyUvKBHjk@OT5cdKngXJzm&>(mg-o67LnW<=+U2+1<2)G`o=Ey+^3;ay zZz-QcyKmGjk7F$c6eK%C?s}3O{m<5$si=-W76Uh3b|psSMS3ONbL2{r zx0qWzpQu2ZB3_$+!OS=8eI>bt5;}qJFZ>FZkn>xh#Y{Pa4>h_B#H9=s@CQ>|wrUzF zJs1X~uqH}_?`7iKON9|E8qc6OEj+w6J}`zDFx*f^KtiJ0k{*2YP3}H7>CME)O|Igv z*^7~ejq@|7?2)VCT!fXkQ`2#^Dctx9=os>n@2dP>!+(Vpl{H$w0Ga!PBKzdWUxAi9z;t(%XQTG| zGXsBN%?Mnwl6{6`h3WIq2?RZxk*-xKgm5If05@wB9sj)|gK_}(zJg)%3Kc_Dd-`)$ zW7Zp%%CeMhPQ zW#x0A;$*Y~z(ojvYKpK{p0JE9NuF=4XfH+Ii~;(Rvs<>#H-U3=iZpFdt&TUB_yMmT zLp0_^l#etzTxe}C3b;bL1K866>P(1}hhB+w&Iku?-#hAQy%5%cwyc^$66eVLSjxEk zO`sCWVK`6_=5%wm)vXj_CL`V;wl0vLnll1c)a3JqA<EPVKl|JR+}XNvRr`FUvQlkxC5 zS6wD*1(DzkXAu75p%WLnh$I}JfTv@Sw)PatKk`9Wlr|Yw{NERHW0*f(8fzGfxL*cabDj#@Ou1JlfaNWQ7BY_xiKPf0xhEH zrOsD{r^5Uu+`CoMLm!@{CojM z0RqW#t1y2n@|xf67{-Zhf(qkmHql+FYSrd>-ZHq!D%eeQZmRVZs3{s-;=~bhA7EFcG z^Ricg!&Xh+@vqkgCzT2CxPTbnbooOKvxS&>5kn_^6T)AkwkVUB;GOS}cTZ$E$Vr&~ z^zjooAT}GNgI*ASC->B^dJK__!~7KZ>3eWK{MiSAumyNKVO2MItUv!gTjY#GT0qA@ zMQ~a{_uS81al>bg7cx^l9Rh4s-i(&2vfXXm|Kym`lZ&sk%p4HP6ETJIkyQO$f9l?q z9m?B)RIiQ!su_ErmKiUzeS?1xQlQ2Bmeui>ll)LoG1tN3hOH7M zDhK6`_9n$*;u$O!-<3TlBQ;#+6J`EIOAV=+=Osz*O?w}xkM_&t+80@tbfPNb&OnB+ zsacVI2eVXjsxQM)?*DEc`WYqP^6$tAUclr*6dmLY%Sq0=PK-iMKX3 z=mFyby{k@FMWm7)$6O^NZjO+r3jr!Y3^)F2Bgx-ZtI51r*?+)tM3{+9$f&ZzOIRoV z*A5njd~?11Zwi9cLuW8LNW!pD4VT=pUXZ)l+}^%yyq3=jX8!FbMr4d_2#P~3s1>M^ zYMeb`V6kd-FqHaBD@Qe*YDy6*5uIxqH^h{#p9BIqA%3(m;^o9bW_?kfuNt*^d7Ap( z!?4bb(|_!ER57D%$VnDb8H%2c?0XFfSCjLeE0wH(tBD^7S<4ldVM+y@S`ZJB2y;y;rsspiJP?w literal 0 HcmV?d00001 diff --git a/litellm/proxy/agent_endpoints/a2a_endpoints.py b/litellm/proxy/agent_endpoints/a2a_endpoints.py index 4534e313c29..ffb861a206f 100644 --- a/litellm/proxy/agent_endpoints/a2a_endpoints.py +++ b/litellm/proxy/agent_endpoints/a2a_endpoints.py @@ -8,7 +8,7 @@ The A2A SDK can point to LiteLLM's URL and invoke agents registered with LiteLLM import json from typing import Any, Optional -from fastapi import APIRouter, Depends, HTTPException, Request +from fastapi import APIRouter, Depends, HTTPException, Request, Response from fastapi.responses import JSONResponse, StreamingResponse from litellm._logging import verbose_proxy_logger @@ -45,24 +45,6 @@ def _get_agent(agent_id: str): return agent -async def _handle_send_message( - a2a_client: Any, - request_id: str, - params: dict, -) -> JSONResponse: - """Handle message/send method.""" - from a2a.types import MessageSendParams, SendMessageRequest - - from litellm.a2a import asend_message - - a2a_request = SendMessageRequest( - id=request_id, - params=MessageSendParams(**params), - ) - response = await asend_message(a2a_client=a2a_client, request=a2a_request) - return JSONResponse(content=response.model_dump(mode="json", exclude_none=True)) - - async def _handle_stream_message( a2a_client: Any, request_id: str, @@ -136,6 +118,7 @@ async def get_agent_card( async def invoke_agent_a2a( agent_id: str, request: Request, + fastapi_response: Response, user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), ): """ @@ -145,7 +128,13 @@ async def invoke_agent_a2a( - message/send: Send a message and get a response - message/stream: Send a message and stream the response """ - from litellm.a2a import create_a2a_client + from litellm.a2a import asend_message, create_a2a_client + from litellm.proxy.litellm_pre_call_utils import add_litellm_data_to_request + from litellm.proxy.proxy_server import ( + general_settings, + proxy_config, + version, + ) body = {} try: @@ -165,18 +154,50 @@ async def invoke_agent_a2a( if agent is None: return _jsonrpc_error(request_id, -32000, f"Agent '{agent_id}' not found", 404) - # Get backend URL + # Get backend URL and agent name agent_url = agent.agent_card_params.get("url") + agent_name = agent.agent_card_params.get("name", agent_id) if not agent_url: return _jsonrpc_error(request_id, -32000, f"Agent '{agent_id}' has no URL configured", 500) verbose_proxy_logger.info(f"Proxying A2A request to agent '{agent_id}' at {agent_url}") - # Create A2A client and dispatch to handler + # Set up data dict for litellm processing + body.update({ + "model": f"a2a_agent/{agent_name}", + "custom_llm_provider": "a2a_agent", + }) + + # Add litellm data (user_api_key, user_id, team_id, etc.) + data = await add_litellm_data_to_request( + data=body, + request=request, + user_api_key_dict=user_api_key_dict, + proxy_config=proxy_config, + general_settings=general_settings, + version=version, + ) + + # Create A2A client a2a_client = await create_a2a_client(base_url=agent_url) if method == "message/send": - return await _handle_send_message(a2a_client, request_id, params) + from a2a.types import MessageSendParams, SendMessageRequest + + a2a_request = SendMessageRequest( + id=request_id, + params=MessageSendParams(**params), + ) + + # Pass litellm data through kwargs for proper logging + response = await asend_message( + a2a_client=a2a_client, + request=a2a_request, + metadata=data.get("metadata", {}), + proxy_server_request=data.get("proxy_server_request"), + ) + return JSONResponse(content=response.model_dump(mode="json", exclude_none=True)) + elif method == "message/stream": return await _handle_stream_message(a2a_client, request_id, params) else: diff --git a/litellm/types/utils.py b/litellm/types/utils.py index 6f2277cccd3..cb4d5c4d47f 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -345,6 +345,12 @@ class CallTypes(str, Enum): ######################################################### call_mcp_tool = "call_mcp_tool" + ######################################################### + # A2A Call Types + ######################################################### + asend_message = "asend_message" + send_message = "send_message" + CallTypesLiteral = Literal[ "embedding", @@ -397,6 +403,8 @@ CallTypesLiteral = Literal[ "vector_store_file_delete", "avector_store_file_delete", "call_mcp_tool", + "asend_message", + "send_message", "aresponses", "responses", ] @@ -712,6 +720,8 @@ API_ROUTE_TO_CALL_TYPES = { ], # MCP (Model Context Protocol) "/mcp/call_tool": [CallTypes.call_mcp_tool], + # A2A (Agent-to-Agent) + "/a2a/{agent_id}": [CallTypes.asend_message, CallTypes.send_message], # Passthrough endpoints "/llm_passthrough": [ CallTypes.llm_passthrough_route, @@ -2981,6 +2991,7 @@ class LlmProviders(str, Enum): WANDB = "wandb" OVHCLOUD = "ovhcloud" LEMONADE = "lemonade" + A2A_AGENT = "a2a_agent" # Create a set of all provider values for quick lookup diff --git a/tests/agent_tests/test_a2a.py b/tests/agent_tests/test_a2a.py index fb5620e44a1..675b3b98e4d 100644 --- a/tests/agent_tests/test_a2a.py +++ b/tests/agent_tests/test_a2a.py @@ -133,6 +133,35 @@ async def test_a2a_logging_payload(): print(f"standard_logging_payload: {test_logger.standard_logging_payload}") print(f"logged kwargs: {json.dumps(test_logger.logged_kwargs, indent=4, default=str)}") + # Verify logging was called + assert test_logger.log_success_called is True assert test_logger.standard_logging_payload is not None - \ No newline at end of file + # Verify standard_logging_payload exists + slp = test_logger.standard_logging_payload + assert slp is not None + + # Get values from standard logging payload + logged_model = slp.get("model") if isinstance(slp, dict) else getattr(slp, "model", None) + logged_provider = slp.get("custom_llm_provider") if isinstance(slp, dict) else getattr(slp, "custom_llm_provider", None) + call_type = slp.get("call_type") if isinstance(slp, dict) else getattr(slp, "call_type", None) + response_cost = slp.get("response_cost") if isinstance(slp, dict) else getattr(slp, "response_cost", None) + + print(f"\n=== Standard Logging Payload Validation ===") + print(f"model: {logged_model}") + print(f"custom_llm_provider: {logged_provider}") + print(f"call_type: {call_type}") + print(f"response_cost: {response_cost}") + + # Verify model and custom_llm_provider are set correctly + assert logged_model is not None, "model should be set" + assert "a2a_agent/" in logged_model, f"model should contain 'a2a_agent/', got: {logged_model}" + assert logged_provider == "a2a_agent", f"custom_llm_provider should be 'a2a_agent', got: {logged_provider}" + + # Verify call_type is correct for A2A + assert call_type == "asend_message", f"call_type should be 'asend_message', got: {call_type}" + + # Verify response_cost is set to 0.0 (not None, not an error) + # This confirms the A2A cost calculator is working + assert response_cost is not None, "response_cost should not be None" + assert response_cost == 0.0, f"response_cost should be 0.0 for A2A, got: {response_cost}" diff --git a/tests/test_litellm/proxy/agent_endpoints/__init__.py b/tests/test_litellm/proxy/agent_endpoints/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/test_litellm/proxy/agent_endpoints/test_a2a_endpoints.py b/tests/test_litellm/proxy/agent_endpoints/test_a2a_endpoints.py new file mode 100644 index 00000000000..c9b8917b20c --- /dev/null +++ b/tests/test_litellm/proxy/agent_endpoints/test_a2a_endpoints.py @@ -0,0 +1,114 @@ +""" +Mock tests for A2A endpoints. + +Tests that invoke_agent_a2a properly integrates with add_litellm_data_to_request. +""" + +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + + +@pytest.mark.asyncio +async def test_invoke_agent_a2a_adds_litellm_data(): + """ + Test that invoke_agent_a2a calls add_litellm_data_to_request + and the resulting data includes proxy_server_request. + """ + from litellm.proxy._types import UserAPIKeyAuth + + # Track the data passed to add_litellm_data_to_request + captured_data = {} + + async def mock_add_litellm_data(data, **kwargs): + # Simulate what add_litellm_data_to_request does + data["proxy_server_request"] = { + "url": "http://localhost:4000/a2a/test-agent", + "method": "POST", + "headers": {}, + "body": dict(data), + } + captured_data.update(data) + return data + + # Mock response from asend_message + mock_response = MagicMock() + mock_response.model_dump.return_value = { + "jsonrpc": "2.0", + "id": "test-id", + "result": {"status": "success"}, + } + + # Mock agent + mock_agent = MagicMock() + mock_agent.agent_card_params = { + "url": "http://backend-agent:10001", + "name": "Test Agent", + } + + # Mock request + mock_request = MagicMock() + mock_request.json = AsyncMock(return_value={ + "jsonrpc": "2.0", + "id": "test-id", + "method": "message/send", + "params": { + "message": { + "role": "user", + "parts": [{"kind": "text", "text": "Hello"}], + "messageId": "msg-123", + } + }, + }) + + mock_user_api_key_dict = UserAPIKeyAuth( + api_key="sk-test-key", + user_id="test-user", + team_id="test-team", + ) + + # Patch at the source modules + with patch( + "litellm.proxy.agent_endpoints.a2a_endpoints._get_agent", + return_value=mock_agent, + ), patch( + "litellm.proxy.litellm_pre_call_utils.add_litellm_data_to_request", + side_effect=mock_add_litellm_data, + ) as mock_add_data, patch( + "litellm.a2a.create_a2a_client", + new_callable=AsyncMock, + ), patch( + "litellm.a2a.asend_message", + new_callable=AsyncMock, + return_value=mock_response, + ), patch( + "litellm.proxy.proxy_server.general_settings", + {}, + ), patch( + "litellm.proxy.proxy_server.proxy_config", + MagicMock(), + ), patch( + "litellm.proxy.proxy_server.version", + "1.0.0", + ): + from litellm.proxy.agent_endpoints.a2a_endpoints import invoke_agent_a2a + + mock_fastapi_response = MagicMock() + + result = await invoke_agent_a2a( + agent_id="test-agent", + request=mock_request, + fastapi_response=mock_fastapi_response, + user_api_key_dict=mock_user_api_key_dict, + ) + + # Verify add_litellm_data_to_request was called + mock_add_data.assert_called_once() + + # Verify model and custom_llm_provider were set + assert captured_data.get("model") == "a2a_agent/Test Agent" + assert captured_data.get("custom_llm_provider") == "a2a_agent" + + # Verify proxy_server_request was added + assert "proxy_server_request" in captured_data + assert captured_data["proxy_server_request"]["method"] == "POST" diff --git a/ui/litellm-dashboard/public/assets/logos/a2a_agent.png b/ui/litellm-dashboard/public/assets/logos/a2a_agent.png new file mode 100644 index 0000000000000000000000000000000000000000..305ae1acaf434f9b4151cdabdc86038abdd34f00 GIT binary patch literal 72568 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 literal 0 HcmV?d00001 diff --git a/ui/litellm-dashboard/src/components/provider_info_helpers.tsx b/ui/litellm-dashboard/src/components/provider_info_helpers.tsx index 110be8e376b..277786aa8ce 100644 --- a/ui/litellm-dashboard/src/components/provider_info_helpers.tsx +++ b/ui/litellm-dashboard/src/components/provider_info_helpers.tsx @@ -1,4 +1,6 @@ + export enum Providers { + A2A_Agent = "A2A Agent", AIML = "AI/ML API", Bedrock = "Amazon Bedrock", Anthropic = "Anthropic", @@ -43,6 +45,7 @@ export enum Providers { } export const provider_map: Record = { + A2A_Agent: "a2a_agent", AIML: "aiml", OpenAI: "openai", OpenAI_Text: "text-completion-openai", @@ -89,6 +92,7 @@ export const provider_map: Record = { const asset_logos_folder = "../ui/assets/logos/"; export const providerLogoMap: Record = { + [Providers.A2A_Agent]: `${asset_logos_folder}a2a_agent.png`, [Providers.AIML]: `${asset_logos_folder}aiml_api.svg`, [Providers.Anthropic]: `${asset_logos_folder}anthropic.svg`, [Providers.AssemblyAI]: `${asset_logos_folder}assemblyai_small.png`, From e4f954b354e637770e4dff261c91d9f66a601e54 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 3 Dec 2025 18:57:41 -0800 Subject: [PATCH 074/107] [Docs] Agent Gateway (#17454) * init litellm A2a client * simpler a2a client interface * test a2a * move a2a invoking tests * test fix * ensure a2a send message is tracked n logs * rename tags * add streaming handlng * add a2a invocation * add a2a invocation i cost calc * test_a2a_logging_payload * update invoke_agent_a2a * test_invoke_agent_a2a_adds_litellm_data * add A2a agent * fix endpoints on A2a * UI allow testing a2a endpoints * add agent imgs * add a2a as an endpoint * add a2a * docs a2a invoke * docs a2a * docs A2a invoke --- docs/my-website/docs/a2a.md | 198 +++++++++ docs/my-website/img/add_agent1.png | 0 docs/my-website/img/add_agent_1.png | Bin 0 -> 294807 bytes docs/my-website/img/agent2.png | Bin 0 -> 825788 bytes docs/my-website/sidebars.js | 1 + .../proxy/agent_endpoints/a2a_endpoints.py | 10 + provider_endpoints_support.json | 276 ++++++++---- .../playground/chat_ui/A2AMetrics.tsx | 231 ++++++++++ .../components/playground/chat_ui/ChatUI.tsx | 330 +++++++++----- .../playground/chat_ui/chatConstants.ts | 1 + .../chat_ui/mode_endpoint_mapping.tsx | 1 + .../components/playground/chat_ui/types.ts | 13 + .../playground/llm_calls/a2a_send_message.tsx | 413 ++++++++++++++++++ .../playground/llm_calls/fetch_agents.tsx | 52 +++ 14 files changed, 1330 insertions(+), 196 deletions(-) create mode 100644 docs/my-website/docs/a2a.md create mode 100644 docs/my-website/img/add_agent1.png create mode 100644 docs/my-website/img/add_agent_1.png create mode 100644 docs/my-website/img/agent2.png create mode 100644 ui/litellm-dashboard/src/components/playground/chat_ui/A2AMetrics.tsx create mode 100644 ui/litellm-dashboard/src/components/playground/llm_calls/a2a_send_message.tsx create mode 100644 ui/litellm-dashboard/src/components/playground/llm_calls/fetch_agents.tsx diff --git a/docs/my-website/docs/a2a.md b/docs/my-website/docs/a2a.md new file mode 100644 index 00000000000..7851f50d88e --- /dev/null +++ b/docs/my-website/docs/a2a.md @@ -0,0 +1,198 @@ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; +import Image from '@theme/IdealImage'; + +# /a2a - Agent Gateway (A2A Protocol) + +| Feature | Supported | +|---------|-----------| +| Logging | ✅ | +| Load Balancing | ✅ | +| Streaming | ✅ | + +:::tip + +LiteLLM follows the [A2A (Agent-to-Agent) Protocol](https://github.com/google/A2A) for invoking agents. + +::: + +## Adding your Agent + +You can add A2A-compatible agents through the LiteLLM Admin UI. + +1. Navigate to the **Agents** tab +2. Click **Add Agent** +3. Enter the agent name (e.g., `ij-local`) and the URL of your A2A agent + + + +The URL should be the invocation URL for your A2A agent (e.g., `http://localhost:10001`). + +## Invoking your Agents + +Use the [A2A Python SDK](https://pypi.org/project/a2a/) to invoke agents through LiteLLM: + +- `base_url`: Your LiteLLM proxy URL + `/a2a/{agent_name}` +- `headers`: Include your LiteLLM Virtual Key for authentication + +```python showLineNumbers title="invoke_a2a_agent.py" +from uuid import uuid4 +import httpx +import asyncio +from a2a.client import A2ACardResolver, A2AClient +from a2a.types import MessageSendParams, SendMessageRequest + +# === CONFIGURE THESE === +LITELLM_BASE_URL = "http://localhost:4000" # Your LiteLLM proxy URL +LITELLM_VIRTUAL_KEY = "sk-1234" # Your LiteLLM Virtual Key +LITELLM_AGENT_NAME = "ij-local" # Agent name registered in LiteLLM +# ======================= + +async def main(): + base_url = f"{LITELLM_BASE_URL}/a2a/{LITELLM_AGENT_NAME}" + headers = {"Authorization": f"Bearer {LITELLM_VIRTUAL_KEY}"} + + async with httpx.AsyncClient(headers=headers) as httpx_client: + # Resolve agent card and create client + resolver = A2ACardResolver(httpx_client=httpx_client, base_url=base_url) + agent_card = await resolver.get_agent_card() + client = A2AClient(httpx_client=httpx_client, agent_card=agent_card) + + # Send a message + request = SendMessageRequest( + id=str(uuid4()), + params=MessageSendParams( + message={ + "role": "user", + "parts": [{"kind": "text", "text": "Hello, what can you do?"}], + "messageId": uuid4().hex, + } + ), + ) + response = await client.send_message(request) + print(response.model_dump(mode="json", exclude_none=True)) + +if __name__ == "__main__": + asyncio.run(main()) +``` + +### Streaming Responses + +For streaming responses, use `send_message_streaming`: + +```python showLineNumbers title="invoke_a2a_agent_streaming.py" +from uuid import uuid4 +import httpx +import asyncio +from a2a.client import A2ACardResolver, A2AClient +from a2a.types import MessageSendParams, SendStreamingMessageRequest + +# === CONFIGURE THESE === +LITELLM_BASE_URL = "http://localhost:4000" # Your LiteLLM proxy URL +LITELLM_VIRTUAL_KEY = "sk-1234" # Your LiteLLM Virtual Key +LITELLM_AGENT_NAME = "ij-local" # Agent name registered in LiteLLM +# ======================= + +async def main(): + base_url = f"{LITELLM_BASE_URL}/a2a/{LITELLM_AGENT_NAME}" + headers = {"Authorization": f"Bearer {LITELLM_VIRTUAL_KEY}"} + + async with httpx.AsyncClient(headers=headers) as httpx_client: + # Resolve agent card and create client + resolver = A2ACardResolver(httpx_client=httpx_client, base_url=base_url) + agent_card = await resolver.get_agent_card() + client = A2AClient(httpx_client=httpx_client, agent_card=agent_card) + + # Send a streaming message + request = SendStreamingMessageRequest( + id=str(uuid4()), + params=MessageSendParams( + message={ + "role": "user", + "parts": [{"kind": "text", "text": "Hello, what can you do?"}], + "messageId": uuid4().hex, + } + ), + ) + + # Stream the response + async for chunk in client.send_message_streaming(request): + print(chunk.model_dump(mode="json", exclude_none=True)) + +if __name__ == "__main__": + asyncio.run(main()) +``` + +## Tracking Agent Logs + +After invoking an agent, you can view the request logs in the LiteLLM **Logs** tab. + +The logs show: +- **Request/Response content** sent to and received from the agent +- **User, Key, Team** information for tracking who made the request +- **Latency and cost** metrics + + + +## API Reference + +### Endpoint + +``` +POST /a2a/{agent_name}/message/send +``` + +### Authentication + +Include your LiteLLM Virtual Key in the `Authorization` header: + +``` +Authorization: Bearer sk-your-litellm-key +``` + +### Request Format + +LiteLLM follows the [A2A JSON-RPC 2.0 specification](https://github.com/google/A2A): + +```json title="Request Body" +{ + "jsonrpc": "2.0", + "id": "unique-request-id", + "method": "message/send", + "params": { + "message": { + "role": "user", + "parts": [{"kind": "text", "text": "Your message here"}], + "messageId": "unique-message-id" + } + } +} +``` + +### Response Format + +```json title="Response" +{ + "jsonrpc": "2.0", + "id": "unique-request-id", + "result": { + "kind": "task", + "id": "task-id", + "contextId": "context-id", + "status": {"state": "completed", "timestamp": "2025-01-01T00:00:00Z"}, + "artifacts": [ + { + "artifactId": "artifact-id", + "name": "response", + "parts": [{"kind": "text", "text": "Agent response here"}] + } + ] + } +} +``` diff --git a/docs/my-website/img/add_agent1.png b/docs/my-website/img/add_agent1.png new file mode 100644 index 00000000000..e69de29bb2d diff --git a/docs/my-website/img/add_agent_1.png b/docs/my-website/img/add_agent_1.png new file mode 100644 index 0000000000000000000000000000000000000000..e60435996a931ce61aeecba409322b0c0218bdea GIT binary patch literal 294807 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 McH33#xEy9w!C=Ck>5=%EM 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ν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

    t8_B|4Wo*GIq0XU?Ay3cQ=n1Z>7yiq1jK7YruW0 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.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 ^_sv%qAFz*{(I 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

    KMAA|3TSpdS6eMo_~E0B)0D43gbbs-G> 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

  • 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¥XtTiZf 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
      7aBo)?gjfCPdFiT)SK4e2aWJ0r1 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 +} + +interface A2AMetricsProps { + a2aMetadata?: A2ATaskMetadata; + timeToFirstToken?: number; + totalLatency?: number; +} + +const getStatusIcon = (state?: string) => { + switch (state) { + case "completed": + return ; + case "working": + case "submitted": + return ; + case "failed": + case "canceled": + return ; + default: + return ; + } +}; + +const getStatusColor = (state?: string) => { + switch (state) { + case "completed": + return "bg-green-100 text-green-700"; + case "working": + case "submitted": + return "bg-blue-100 text-blue-700"; + case "failed": + case "canceled": + return "bg-red-100 text-red-700"; + default: + return "bg-gray-100 text-gray-700"; + } +}; + +const formatTimestamp = (timestamp?: string) => { + if (!timestamp) return null; + try { + const date = new Date(timestamp); + return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); + } catch { + return timestamp; + } +}; + +const truncateId = (id?: string, length = 8) => { + if (!id) return null; + return id.length > length ? `${id.substring(0, length)}…` : id; +}; + +const copyToClipboard = (text: string) => { + navigator.clipboard.writeText(text); +}; + +const A2AMetrics: React.FC = ({ a2aMetadata, timeToFirstToken, totalLatency }) => { + const [showDetails, setShowDetails] = useState(false); + + if (!a2aMetadata && !timeToFirstToken && !totalLatency) return null; + + const { taskId, contextId, status, metadata } = a2aMetadata || {}; + const formattedTime = formatTimestamp(status?.timestamp); + + return ( +
      + {/* A2A Metadata Header */} +
      + + A2A Metadata +
      + + {/* Main metrics row */} +
      + {/* Status badge */} + {status?.state && ( + + {getStatusIcon(status.state)} + {status.state} + + )} + + {/* Timestamp */} + {formattedTime && ( + + + + {formattedTime} + + + )} + + {/* Latency */} + {totalLatency !== undefined && ( + + + + {(totalLatency / 1000).toFixed(2)}s + + + )} + + {/* Time to first token */} + {timeToFirstToken !== undefined && ( + + + TTFT: {(timeToFirstToken / 1000).toFixed(2)}s + + + )} +
      + + {/* IDs row */} +
      + {/* Task ID */} + {taskId && ( + + copyToClipboard(taskId)} + > + + Task: {truncateId(taskId)} + + + + )} + + {/* Context/Session ID */} + {contextId && ( + + copyToClipboard(contextId)} + > + + Session: {truncateId(contextId)} + + + + )} + + {/* Details toggle */} + {(metadata || status?.message) && ( + + )} +
      + + {/* Expandable details panel */} + {showDetails && ( +
      + {/* Status message */} + {status?.message && ( +
      + Status Message: + {status.message} +
      + )} + + {/* Full IDs */} + {taskId && ( +
      + Task ID: + {taskId} + copyToClipboard(taskId)} + /> +
      + )} + + {contextId && ( +
      + Session ID: + {contextId} + copyToClipboard(contextId)} + /> +
      + )} + + {/* Metadata fields */} + {metadata && Object.keys(metadata).length > 0 && ( +
      + Custom Metadata: +
      +                {JSON.stringify(metadata, null, 2)}
      +              
      +
      + )} +
      + )} +
      + ); +}; + +export default A2AMetrics; 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 c2924c048c3..18b5bb82dd4 100644 --- a/ui/litellm-dashboard/src/components/playground/chat_ui/ChatUI.tsx +++ b/ui/litellm-dashboard/src/components/playground/chat_ui/ChatUI.tsx @@ -51,6 +51,10 @@ import { fetchAvailableModels, ModelGroup } from "../llm_calls/fetch_models"; import { makeOpenAIImageEditsRequest } from "../llm_calls/image_edits"; import { makeOpenAIImageGenerationRequest } from "../llm_calls/image_generation"; import { makeOpenAIResponsesRequest } from "../llm_calls/responses_api"; +import { Agent, fetchAvailableAgents } from "../llm_calls/fetch_agents"; +import { makeA2AStreamMessageRequest } from "../llm_calls/a2a_send_message"; +import A2AMetrics from "./A2AMetrics"; +import { A2ATaskMetadata } from "./types"; import MCPEventsDisplay, { MCPEvent } from "./MCPEventsDisplay"; import { EndpointType, getEndpointType } from "./mode_endpoint_mapping"; import ReasoningContent from "./ReasoningContent"; @@ -124,6 +128,8 @@ const ChatUI: React.FC = ({ const [selectedModel, setSelectedModel] = useState(undefined); const [showCustomModelInput, setShowCustomModelInput] = useState(false); const [modelInfo, setModelInfo] = useState([]); + const [agentInfo, setAgentInfo] = useState([]); + const [selectedAgent, setSelectedAgent] = useState(undefined); const customModelTimeout = useRef(null); const [endpointType, setEndpointType] = useState( () => sessionStorage.getItem("endpointType") || EndpointType.CHAT, @@ -340,6 +346,29 @@ const ChatUI: React.FC = ({ loadMCPTools(); }, [accessToken, userID, userRole, apiKeySource, apiKey, token]); + // Fetch agents when A2A endpoint is selected + useEffect(() => { + const userApiKey = apiKeySource === "session" ? accessToken : apiKey; + if (!userApiKey || endpointType !== EndpointType.A2A_AGENTS) { + return; + } + + const loadAgents = async () => { + try { + const agents = await fetchAvailableAgents(userApiKey); + setAgentInfo(agents); + // Clear selection if current agent not in list + if (selectedAgent && !agents.some((a) => a.agent_name === selectedAgent)) { + setSelectedAgent(undefined); + } + } catch (error) { + console.error("Error fetching agents:", error); + } + }; + + loadAgents(); + }, [accessToken, apiKeySource, apiKey, endpointType]); + useEffect(() => { // Scroll to the bottom of the chat whenever chatHistory updates if (chatEndRef.current) { @@ -469,6 +498,23 @@ const ChatUI: React.FC = ({ }); }; + const updateA2AMetadata = (a2aMetadata: A2ATaskMetadata) => { + console.log("Received A2A metadata:", a2aMetadata); + setChatHistory((prevHistory) => { + const lastMessage = prevHistory[prevHistory.length - 1]; + + if (lastMessage && lastMessage.role === "assistant") { + const updatedMessage = { + ...lastMessage, + a2aMetadata, + }; + return [...prevHistory.slice(0, prevHistory.length - 1), updatedMessage]; + } + + return prevHistory; + }); + }; + const updateTotalLatency = (totalLatency: number) => { setChatHistory((prevHistory) => { const lastMessage = prevHistory[prevHistory.length - 1]; @@ -684,6 +730,12 @@ const ChatUI: React.FC = ({ return; } + // For A2A agents, require agent selection + if (endpointType === EndpointType.A2A_AGENTS && !selectedAgent) { + NotificationsManager.fromBackend("Please select an agent to send a message"); + return; + } + if (!token || !userRole || !userID) { return; } @@ -908,6 +960,20 @@ const ChatUI: React.FC = ({ } } } + + // Handle A2A agent calls (separate from model-based calls) - use streaming + if (endpointType === EndpointType.A2A_AGENTS && selectedAgent) { + await makeA2AStreamMessageRequest( + selectedAgent, + inputMessage, + (chunk, model) => updateTextUI("assistant", chunk, model), + effectiveApiKey, + signal, + updateTimingData, + updateTotalLatency, + updateA2AMetadata, + ); + } } catch (error) { if (signal.aborted) { console.log("Request was cancelled"); @@ -1038,11 +1104,13 @@ const ChatUI: React.FC = ({ endpointType={endpointType} onEndpointChange={(value) => { setEndpointType(value); - // Clear model selection when switching endpoint type + // Clear model/agent selection when switching endpoint type setSelectedModel(undefined); + setSelectedAgent(undefined); setShowCustomModelInput(false); try { sessionStorage.removeItem("selectedModel"); + sessionStorage.removeItem("selectedAgent"); } catch {} }} className="mb-4" @@ -1077,103 +1145,145 @@ const ChatUI: React.FC = ({ />
  • -
    - - - Select Model - - {isChatModel() ? ( - + + + Select Model + + {isChatModel() ? ( + + } + title="Model Settings" + trigger="click" + placement="right" + > +
    + {showCustomModelInput && ( + { + // Using setTimeout to create a simple debounce effect + if (customModelTimeout.current) { + clearTimeout(customModelTimeout.current); + } + + customModelTimeout.current = setTimeout(() => { + setSelectedModel(value); + }, 500); // 500ms delay after typing stops + }} + /> + )} +