mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-11 08:38:24 +00:00
fix(caching): answer 400 for a non-list /cache/delete keys and accept null Stability lists
Return pydantic errors without the caller's input on a bad /cache/delete body, let the Bedrock Stability image edit schema take null finish_reasons and images like the old reads did, declare user_id and user_role on the plugin session claim, wrap the retyped Vertex fields in ReadOnly, and drop unused Guardrails AI types
This commit is contained in:
parent
ea3d8fc78c
commit
202746255a
7 changed files with 54 additions and 29 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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]]]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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]]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue