mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-10 08:28:25 +00:00
fix(types): keep loose payloads working under the sweep's validators
An off-schema Anthropic stream event is now skipped instead of ending the guardrail's text and stop_reason scan, and MAI image usage accepts the loose numbers it did before. New dict and list adapters hide their input from validation errors, RerankResponse indexing keeps its field types, and the delete_user test mocks the int /user/delete returns.
This commit is contained in:
parent
659af8d73d
commit
5ea92486a6
14 changed files with 63 additions and 20 deletions
|
|
@ -44,7 +44,9 @@ from .s3_cache import S3Cache
|
|||
RESPONSE_CACHE_TARGET: Final = "llm_response"
|
||||
_ALWAYS_KEYED_PROVIDER_PARAMS: Final = frozenset({"continuation_token"})
|
||||
_DECODED_LITERAL: Final = TypeAdapter(object)
|
||||
_REDIS_STARTUP_NODES_ADAPTER: Final = TypeAdapter(list[dict[str, object]], config=ConfigDict(strict=True))
|
||||
_REDIS_STARTUP_NODES_ADAPTER: Final = TypeAdapter(
|
||||
list[dict[str, object]], config=ConfigDict(strict=True, hide_input_in_errors=True)
|
||||
)
|
||||
|
||||
|
||||
@contextmanager
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ if TYPE_CHECKING:
|
|||
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
|
||||
from litellm.types.proxy.auth.user_api_key_auth import UserAPIKeyAuth
|
||||
|
||||
_JSON_OBJECT: Final = TypeAdapter(dict[str, object], config=ConfigDict(strict=True))
|
||||
_JSON_OBJECT: Final = TypeAdapter(dict[str, object], config=ConfigDict(strict=True, hide_input_in_errors=True))
|
||||
|
||||
|
||||
class _A2ATextPart(TypedDict, total=False):
|
||||
|
|
|
|||
|
|
@ -1823,7 +1823,7 @@ class AnthropicMessagesHandler(BaseTranslation):
|
|||
delta: _SSETextDelta = data.get("delta", {})
|
||||
if delta.get("type") == "text_delta":
|
||||
text += delta.get("text", "")
|
||||
except json.JSONDecodeError:
|
||||
except (json.JSONDecodeError, ValidationError):
|
||||
verbose_proxy_logger.warning("Failed to parse JSON from SSE data: %s", data_line)
|
||||
|
||||
except Exception as e:
|
||||
|
|
@ -1887,7 +1887,7 @@ class AnthropicMessagesHandler(BaseTranslation):
|
|||
stop_reason: object = delta.get("stop_reason")
|
||||
if stop_reason is not None:
|
||||
return True
|
||||
except json.JSONDecodeError:
|
||||
except (json.JSONDecodeError, ValidationError):
|
||||
verbose_proxy_logger.warning("Failed to parse JSON from SSE data: %s", data_line)
|
||||
|
||||
except Exception as e:
|
||||
|
|
|
|||
|
|
@ -21,13 +21,13 @@ if TYPE_CHECKING:
|
|||
from litellm.litellm_core_utils.tokenizer import Encoding as Tokenizer
|
||||
|
||||
|
||||
@with_config(ConfigDict(extra="allow", strict=True))
|
||||
@with_config(ConfigDict(extra="allow"))
|
||||
class _MAIImageUsageDetails(TypedDict, total=False):
|
||||
text_tokens: ReadOnly[NotRequired[int | float | None]]
|
||||
image_tokens: ReadOnly[NotRequired[int | float | None]]
|
||||
|
||||
|
||||
@with_config(ConfigDict(extra="allow", strict=True))
|
||||
@with_config(ConfigDict(extra="allow"))
|
||||
class _MAIImageUsage(TypedDict, total=False):
|
||||
input_tokens_details: ReadOnly[NotRequired[object]]
|
||||
num_input_text_tokens: ReadOnly[NotRequired[int | float | None]]
|
||||
|
|
@ -39,7 +39,7 @@ class _MAIImageUsage(TypedDict, total=False):
|
|||
total_tokens: ReadOnly[NotRequired[int | float | None]]
|
||||
|
||||
|
||||
@with_config(ConfigDict(extra="allow", strict=True))
|
||||
@with_config(ConfigDict(extra="allow"))
|
||||
class _MAIImageResponse(TypedDict, total=False):
|
||||
usage: ReadOnly[NotRequired[object]]
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ from litellm.utils import convert_to_model_response_object
|
|||
|
||||
from ..common_utils import EdenAIException, authorized_headers, endpoint_url, reported_cost
|
||||
|
||||
_JSON_OBJECT: Final = TypeAdapter(dict[str, object], config=ConfigDict(strict=True))
|
||||
_JSON_OBJECT: Final = TypeAdapter(dict[str, object], config=ConfigDict(strict=True, hide_input_in_errors=True))
|
||||
|
||||
|
||||
def _form_fields(model: str, optional_params: Mapping[str, object]) -> dict[str, object]: # mutable-ok: httpx form data
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ from litellm.types.llms.openai import OpenAIImageGenerationOptionalParams
|
|||
from litellm.types.utils import ImageResponse
|
||||
from litellm.utils import convert_to_model_response_object
|
||||
|
||||
_JSON_OBJECT: Final = TypeAdapter(dict[str, object], config=ConfigDict(strict=True))
|
||||
_JSON_OBJECT: Final = TypeAdapter(dict[str, object], config=ConfigDict(strict=True, hide_input_in_errors=True))
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from litellm.litellm_core_utils.logging import Logging as LiteLLMLoggingObj
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ from litellm.types.llms.openai import OpenAIImageGenerationOptionalParams
|
|||
from litellm.types.utils import ImageResponse
|
||||
from litellm.utils import convert_to_model_response_object
|
||||
|
||||
_JSON_OBJECT: Final = TypeAdapter(dict[str, object], config=ConfigDict(strict=True))
|
||||
_JSON_OBJECT: Final = TypeAdapter(dict[str, object], config=ConfigDict(strict=True, hide_input_in_errors=True))
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from litellm.litellm_core_utils.logging import Logging as LiteLLMLoggingObj
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ from litellm.types.llms.openai import OpenAIImageGenerationOptionalParams
|
|||
from litellm.types.utils import ImageResponse
|
||||
from litellm.utils import convert_to_model_response_object
|
||||
|
||||
_JSON_OBJECT: Final = TypeAdapter(dict[str, object], config=ConfigDict(strict=True))
|
||||
_JSON_OBJECT: Final = TypeAdapter(dict[str, object], config=ConfigDict(strict=True, hide_input_in_errors=True))
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from litellm.litellm_core_utils.logging import Logging as LiteLLMLoggingObj
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import os
|
|||
from typing import Final, cast
|
||||
|
||||
import httpx
|
||||
from pydantic import TypeAdapter
|
||||
from pydantic import ConfigDict, TypeAdapter
|
||||
|
||||
from litellm._logging import verbose_proxy_logger
|
||||
from litellm.llms.custom_httpx.http_handler import (
|
||||
|
|
@ -12,7 +12,7 @@ from litellm.llms.custom_httpx.http_handler import (
|
|||
)
|
||||
from litellm.proxy._types import CommonProxyErrors, LitellmUserRoles, UserAPIKeyAuth
|
||||
|
||||
_TOKEN_INFO_RESPONSE: Final = TypeAdapter(dict[str, object])
|
||||
_TOKEN_INFO_RESPONSE: Final = TypeAdapter(dict[str, object], config=ConfigDict(strict=True, hide_input_in_errors=True))
|
||||
|
||||
|
||||
class Oauth2Handler:
|
||||
|
|
@ -182,7 +182,7 @@ class Oauth2Handler:
|
|||
response.raise_for_status()
|
||||
|
||||
# If we get here, the request was successful
|
||||
data = _TOKEN_INFO_RESPONSE.validate_python(response.json(), strict=True)
|
||||
data = _TOKEN_INFO_RESPONSE.validate_python(response.json())
|
||||
|
||||
verbose_proxy_logger.debug(
|
||||
"Oauth2 token validation for token=%s, response from endpoint=%s",
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ from pydantic import ConfigDict, TypeAdapter
|
|||
|
||||
from .exceptions import NotFoundError, UnauthorizedError
|
||||
|
||||
_JSON_VALUE: Final = TypeAdapter(object, config=ConfigDict(strict=True))
|
||||
_JSON_OBJECT: Final = TypeAdapter(dict[str, object], config=ConfigDict(strict=True))
|
||||
_JSON_VALUE: Final = TypeAdapter(object, config=ConfigDict(strict=True, hide_input_in_errors=True))
|
||||
_JSON_OBJECT: Final = TypeAdapter(dict[str, object], config=ConfigDict(strict=True, hide_input_in_errors=True))
|
||||
|
||||
|
||||
class UsersManagementClient:
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ https://docs.cohere.com/reference/rerank
|
|||
|
||||
"""
|
||||
|
||||
from typing import Literal
|
||||
from typing import Literal, overload
|
||||
|
||||
from pydantic import ConfigDict, PrivateAttr
|
||||
from typing_extensions import ReadOnly, Required, TypedDict
|
||||
|
|
@ -95,6 +95,18 @@ class RerankResponse(LiteLLMBaseModel):
|
|||
def hidden_params(self, hidden_params: dict[str, object]) -> None: # mutable-ok: API requires mutation
|
||||
self._hidden_params = hidden_params
|
||||
|
||||
@overload
|
||||
def __getitem__(self, key: Literal["id"]) -> str | None: ...
|
||||
|
||||
@overload
|
||||
def __getitem__(self, key: Literal["results"]) -> list[RerankResponseResult] | None: ...
|
||||
|
||||
@overload
|
||||
def __getitem__(self, key: Literal["meta"]) -> RerankResponseMeta | None: ...
|
||||
|
||||
@overload
|
||||
def __getitem__(self, key: str) -> object: ...
|
||||
|
||||
def __getitem__(self, key: str) -> object:
|
||||
return self.__dict__[key]
|
||||
|
||||
|
|
|
|||
|
|
@ -302,6 +302,28 @@ class TestAnthropicMessagesHandlerStreamingOutputProcessing:
|
|||
texts.append(data["delta"]["text"])
|
||||
return texts
|
||||
|
||||
def test_off_schema_delta_is_skipped_without_ending_the_text_scan(self):
|
||||
events = [
|
||||
{"type": "content_block_delta", "index": 0, "delta": {"type": "text_delta", "text": "abc"}},
|
||||
{"type": "content_block_delta", "index": 1, "delta": {"type": "thinking_delta", "text": None}},
|
||||
{"type": "content_block_delta", "index": 0, "delta": {"type": "text_delta", "text": "def"}},
|
||||
]
|
||||
chunk = "".join(f"event: content_block_delta\ndata: {json.dumps(event)}\n\n" for event in events).encode()
|
||||
|
||||
assert AnthropicMessagesHandler().get_streaming_string_so_far([chunk]) == "abcdef"
|
||||
|
||||
def test_off_schema_message_delta_does_not_hide_a_later_stop_reason(self):
|
||||
events = [
|
||||
{"type": "message_delta", "delta": None},
|
||||
{"type": "message_delta", "delta": {"stop_reason": "end_turn", "stop_sequence": None}},
|
||||
]
|
||||
chunk = "".join(f"event: message_delta\ndata: {json.dumps(event)}\n\n" for event in events).encode()
|
||||
|
||||
scan_key = AnthropicMessagesHandler().get_streaming_scan_key([chunk])
|
||||
|
||||
assert scan_key is not None
|
||||
assert scan_key.stream_ended is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_deliver_ended_stream_rewrites_writes_text_back_into_sse_chunks(self):
|
||||
handler = AnthropicMessagesHandler()
|
||||
|
|
|
|||
|
|
@ -386,6 +386,14 @@ class TestAzureMAIImageGeneration:
|
|||
assert normalized["input_tokens"] == 22
|
||||
assert normalized["total_tokens"] == 22
|
||||
|
||||
def test_normalize_mai_usage_accepts_a_bool_token_count(self):
|
||||
normalized = AzureFoundryMAIImageGenerationConfig().normalize_mai_image_usage(
|
||||
{"input_tokens": 22, "output_tokens": True}
|
||||
)
|
||||
|
||||
assert normalized["output_tokens"] == 1
|
||||
assert normalized["total_tokens"] == 23
|
||||
|
||||
def test_azure_sync_image_generation_uses_mai_response_transform(self):
|
||||
raw_response = MagicMock(spec=httpx.Response)
|
||||
raw_response.json.return_value = {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import pytest
|
|||
import requests
|
||||
|
||||
|
||||
|
||||
from litellm.proxy.client.users import (
|
||||
NotFoundError,
|
||||
UnauthorizedError,
|
||||
|
|
@ -72,9 +71,9 @@ def test_create_user_unauthorized(mock_post, client):
|
|||
@patch("requests.post")
|
||||
def test_delete_user_success(mock_post, client):
|
||||
mock_post.return_value.status_code = 200
|
||||
mock_post.return_value.json.return_value = {"deleted": 1}
|
||||
mock_post.return_value.json.return_value = 1
|
||||
result = client.delete_user(["u1"])
|
||||
assert result["deleted"] == 1
|
||||
assert result == 1
|
||||
mock_post.assert_called_once()
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue