diff --git a/litellm/llms/bedrock/image_edit/stability_transformation.py b/litellm/llms/bedrock/image_edit/stability_transformation.py index 266c67140c0..fd2fff8fc0b 100644 --- a/litellm/llms/bedrock/image_edit/stability_transformation.py +++ b/litellm/llms/bedrock/image_edit/stability_transformation.py @@ -50,8 +50,8 @@ else: @with_config(ConfigDict(extra="allow", strict=True)) class _StabilityResponse(TypedDict): - finish_reasons: ReadOnly[NotRequired[Sequence[object]]] - images: ReadOnly[NotRequired[Sequence[str | None]]] + finish_reasons: ReadOnly[NotRequired[Sequence[object] | None]] + images: ReadOnly[NotRequired[Sequence[str | None] | None]] _STABILITY_RESPONSE: Final = TypeAdapter(_StabilityResponse) diff --git a/litellm/proxy/caching_routes.py b/litellm/proxy/caching_routes.py index 247c31d207f..fe71ee121b1 100644 --- a/litellm/proxy/caching_routes.py +++ b/litellm/proxy/caching_routes.py @@ -2,7 +2,7 @@ from collections.abc import Mapping from typing import Final from fastapi import APIRouter, Depends, HTTPException, Request -from pydantic import ConfigDict, TypeAdapter, with_config +from pydantic import ConfigDict, TypeAdapter, ValidationError, with_config from typing_extensions import NotRequired, ReadOnly, TypedDict import litellm @@ -172,6 +172,8 @@ async def cache_delete(request: Request): status_code=500, detail=f"Cache type {litellm.cache.type} does not support deleting a key. only `redis` is supported", ) + except ValidationError as e: + raise HTTPException(status_code=400, detail=e.errors(include_url=False, include_input=False)) except Exception as e: raise HTTPException( status_code=500, diff --git a/litellm/proxy/guardrails/guardrail_hooks/guardrails_ai/guardrails_ai.py b/litellm/proxy/guardrails/guardrail_hooks/guardrails_ai/guardrails_ai.py index f740882528c..4877c166026 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/guardrails_ai/guardrails_ai.py +++ b/litellm/proxy/guardrails/guardrail_hooks/guardrails_ai/guardrails_ai.py @@ -8,12 +8,11 @@ import json import os from collections.abc import Sequence -from typing import TYPE_CHECKING, Final, Literal, TypedDict +from typing import TYPE_CHECKING, Final, Literal from fastapi import HTTPException from pydantic import ConfigDict, TypeAdapter, with_config -from typing_extensions import NotRequired, ReadOnly -from typing_extensions import TypedDict as _ValidatedTypedDict +from typing_extensions import NotRequired, ReadOnly, TypedDict import litellm from litellm._logging import verbose_proxy_logger @@ -38,26 +37,13 @@ class GuardrailsAIResponse(TypedDict): validationPassed: bool -class InferenceData(TypedDict): - name: str - shape: list[int] - data: list - datatype: str - - -class GuardrailsAIResponsePreCall(TypedDict): - modelname: str - modelversion: str - outputs: list[InferenceData] - - @with_config(ConfigDict(extra="allow", strict=True)) -class _PreCallOutput(_ValidatedTypedDict): +class _PreCallOutput(TypedDict): data: ReadOnly[NotRequired[Sequence[str]]] @with_config(ConfigDict(extra="allow", strict=True)) -class _PreCallResponse(_ValidatedTypedDict): +class _PreCallResponse(TypedDict): outputs: ReadOnly[NotRequired[Sequence[_PreCallOutput]]] diff --git a/litellm/proxy/plugin_routes.py b/litellm/proxy/plugin_routes.py index e1b645384e9..8a9c1edd9da 100644 --- a/litellm/proxy/plugin_routes.py +++ b/litellm/proxy/plugin_routes.py @@ -45,6 +45,8 @@ router: Final = APIRouter() @with_config(ConfigDict(extra="allow", strict=True)) class _PluginSessionClaim(TypedDict): plugin: NotRequired[ReadOnly[object]] + user_id: NotRequired[ReadOnly[str]] + user_role: NotRequired[ReadOnly[str]] exp: NotRequired[ReadOnly[int | float | str]] diff --git a/litellm/types/llms/vertex_ai.py b/litellm/types/llms/vertex_ai.py index 596fd0b4571..3ef5845c904 100644 --- a/litellm/types/llms/vertex_ai.py +++ b/litellm/types/llms/vertex_ai.py @@ -140,7 +140,7 @@ class Schema(TypedDict, total=False): title: str description: str nullable: bool - default: object + default: ReadOnly[object] items: "Schema" minItems: str maxItems: str @@ -155,7 +155,7 @@ class Schema(TypedDict, total=False): minLength: str maxLength: str pattern: str - example: object + example: ReadOnly[object] anyOf: list["Schema"] @@ -794,9 +794,9 @@ class VertexVideoGenerationResponse(TypedDict, total=False): name: str done: bool - response: dict[str, object] - metadata: dict[str, object] - error: dict[str, object] + response: ReadOnly[dict[str, object]] + metadata: ReadOnly[dict[str, object]] + error: ReadOnly[dict[str, object]] VERTEX_CREDENTIALS_TYPES = str | dict[str, str] diff --git a/tests/unit/llms/bedrock/image_edit/test_stability_transformation.py b/tests/unit/llms/bedrock/image_edit/test_stability_transformation.py index 6ca70324162..d761deabbe4 100644 --- a/tests/unit/llms/bedrock/image_edit/test_stability_transformation.py +++ b/tests/unit/llms/bedrock/image_edit/test_stability_transformation.py @@ -1,3 +1,8 @@ +from unittest.mock import MagicMock + +import httpx +import pytest + from litellm.llms.bedrock.image_edit.stability_transformation import BedrockStabilityImageEditConfig @@ -12,3 +17,31 @@ def test_transform_image_edit_request_returns_the_json_body_and_no_files(): ) assert request == ({"output_format": "png", "prompt": "add a red hat", "image": "iVBORy1ieXRlcw=="}, {}) + + +@pytest.mark.parametrize( + ("body", "expected_images"), + [ + ({"images": ["aW1hZ2U="], "finish_reasons": [None], "seeds": [42]}, ["aW1hZ2U="]), + ({"images": ["aW1hZ2U="], "finish_reasons": None}, ["aW1hZ2U="]), + ({"images": None, "finish_reasons": None}, []), + ({}, []), + ], +) +def test_transform_image_edit_response_returns_each_image_and_tolerates_null_lists(body, expected_images): + response = BedrockStabilityImageEditConfig().transform_image_edit_response( + model="stability.stable-image-inpaint-v1:0", + raw_response=httpx.Response(200, json=body), + logging_obj=MagicMock(), + ) + + assert [image.b64_json for image in response.data or []] == expected_images + + +def test_transform_image_edit_response_raises_the_first_finish_reason(): + with pytest.raises(Exception, match="Bedrock Stability error: Filter reason: prompt"): + BedrockStabilityImageEditConfig().transform_image_edit_response( + model="stability.stable-image-inpaint-v1:0", + raw_response=httpx.Response(200, json={"images": [None], "finish_reasons": ["Filter reason: prompt"]}), + logging_obj=MagicMock(), + ) diff --git a/tests/unit/proxy/test_caching_routes.py b/tests/unit/proxy/test_caching_routes.py index ac3cfdaf8fa..0190b9390e8 100644 --- a/tests/unit/proxy/test_caching_routes.py +++ b/tests/unit/proxy/test_caching_routes.py @@ -70,10 +70,12 @@ def test_cache_delete_passes_string_and_numeric_keys_to_the_cache(mock_redis_del mock_redis_delete.delete_cache_keys.assert_awaited_once_with(keys=keys) -def test_cache_delete_rejects_keys_that_are_not_a_list(mock_redis_delete): - response = client.post("/cache/delete", headers={"Authorization": "Bearer sk-9876"}, json={"keys": "abc"}) +def test_cache_delete_rejects_keys_that_are_not_a_list_with_a_400_that_hides_them(mock_redis_delete): + response = client.post("/cache/delete", headers={"Authorization": "Bearer sk-9876"}, json={"keys": "sk-secret-key"}) - assert response.status_code == 500 + assert response.status_code == 400 + assert response.json()["detail"][0]["loc"] == ["keys"] + assert "sk-secret-key" not in response.text mock_redis_delete.delete_cache_keys.assert_not_awaited()