mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-27 01:22:18 +00:00
fix(params): validate stream_chunk_size once, before any provider call (#43222)
* fix(params): validate stream_chunk_size once and carry it as typed control options Checks stream_chunk_size at the top of completion() and acompletion(), accepts digit strings, returns a 400 naming the param unless drop_params is set, and stores the checked value under _litellm_control. Bedrock Converse and Invoke read it from litellm_params; the Bedrock-only checker and the dead Invoke pops are gone. Owned-kwarg filtering now runs through one helper everywhere. Refs LIT-8317 * test(bedrock): drop tests for the removed stream_chunk_size_from helper Refs LIT-8317 * fix(params): check stream_chunk_size before the MCP gateway branch Refs LIT-8317 * fix(params): return assert_never in the exhaustive control-options match Refs LIT-8317 * fix(params): address council review of the control options change Read all_litellm_params live so names registered after import stay LiteLLM-owned, make litellm_params a required keyword on the stream wrapper hooks, give digit strings and ints the same 18-digit range, share the default-chunking test table, test the Responses bridge through litellm.responses, and revert formatting-only churn in existing tests. Refs LIT-8317 * fix(params): address the second council review of control options Keep the Responses bridge on its original all_litellm_params forwarding, narrow _int_from_decimal_string inline so it type-checks, bound nested huge ints in the error message, store _litellm_control only when a value is set, simplify the parser to its single field, drop the one-caller wrapper, and tighten the tests. Refs LIT-8317 * fix(params): keep the 18-digit length check on stream_chunk_size strings A 19-character string with leading zeros such as 0000000000000000001 would otherwise pass as 1, although the rule and the error message say at most 18 digits. Refs LIT-8317 * test(params): tidy control options tests after council sign-off Move the Responses bridge test into the existing bridge test file, drop the rebind test that pinned an implementation detail, assert through stored_control_options instead of the storage key, and cover drop_params="true" through Bedrock streaming. Refs LIT-8317 * test(params): wrap a chunking test row that went past 120 characters Refs LIT-8317
This commit is contained in:
parent
f12f7b5a03
commit
e494723105
34 changed files with 695 additions and 287 deletions
|
|
@ -25,7 +25,7 @@ from litellm._logging import verbose_logger
|
|||
from litellm.constants import CACHED_STREAMING_CHUNK_DELAY
|
||||
from litellm.litellm_core_utils.model_param_helper import ModelParamHelper
|
||||
from litellm.types.caching import *
|
||||
from litellm.types.utils import EmbeddingResponse, all_litellm_params
|
||||
from litellm.types.utils import EmbeddingResponse, is_litellm_owned_kwarg
|
||||
|
||||
from .azure_blob_cache import AzureBlobCache
|
||||
from .base_cache import BaseCache
|
||||
|
|
@ -377,7 +377,6 @@ class Cache:
|
|||
return preset_cache_key
|
||||
|
||||
combined_kwargs: Final = ModelParamHelper._get_all_llm_api_params()
|
||||
litellm_param_kwargs: Final = all_litellm_params
|
||||
is_semantic_cache: Final = self._is_semantic_cache()
|
||||
scope_excluded_params: Final = self._SEMANTIC_CACHE_SCOPE_EXCLUDED_PARAMS if is_semantic_cache else frozenset()
|
||||
for param in kwargs:
|
||||
|
|
@ -387,7 +386,7 @@ class Cache:
|
|||
param_value: str | None = self._get_param_value(param, kwargs)
|
||||
if param_value is not None:
|
||||
cache_key += f"{param}: {param_value}"
|
||||
elif param not in litellm_param_kwargs: # check if user passed in optional param - e.g. top_k
|
||||
elif not is_litellm_owned_kwarg(param):
|
||||
if litellm.enable_caching_on_provider_specific_optional_params is True: # feature flagged for now
|
||||
if kwargs[param] is None:
|
||||
continue # ignore None params
|
||||
|
|
|
|||
|
|
@ -1611,6 +1611,7 @@ ALLOWED_VERTEX_AI_PASSTHROUGH_HEADERS: Final = {
|
|||
# Works for all LLM pass-through endpoints (Vertex AI, Anthropic, Bedrock, etc.)
|
||||
PASS_THROUGH_HEADER_PREFIX: Final = "x-pass-"
|
||||
INTERNAL_KWARG_PREFIX: Final = "_litellm_"
|
||||
CONTROL_OPTIONS_KEY: Final = f"{INTERNAL_KWARG_PREFIX}control"
|
||||
|
||||
AZURE_SPEECH_CUSTOM_LLM_PROVIDER: Final = "azure_speech"
|
||||
AZURE_SPEECH_PASS_THROUGH_ROUTE_PREFIX: Final = "/azure_speech"
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ from litellm.llms.base_llm import BaseImageEditConfig, BaseImageGenerationConfig
|
|||
from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler
|
||||
from litellm.llms.custom_httpx.llm_http_handler import BaseLLMHTTPHandler
|
||||
from litellm.llms.custom_llm import CustomLLM
|
||||
from litellm.utils import exception_type, get_litellm_params
|
||||
from litellm.utils import exception_type, filter_out_litellm_params, get_litellm_params
|
||||
|
||||
#################### Initialize provider clients ####################
|
||||
llm_http_handler: BaseLLMHTTPHandler = BaseLLMHTTPHandler()
|
||||
|
|
@ -52,7 +52,6 @@ from litellm.types.router import GenericLiteLLMParams
|
|||
from litellm.types.utils import (
|
||||
LITELLM_IMAGE_VARIATION_PROVIDERS,
|
||||
LlmProviders,
|
||||
is_litellm_owned_kwarg,
|
||||
)
|
||||
from litellm.utils import (
|
||||
ImageResponse,
|
||||
|
|
@ -249,9 +248,7 @@ def image_generation(
|
|||
"size",
|
||||
"style",
|
||||
]
|
||||
non_default_params: Final = {
|
||||
k: v for k, v in kwargs.items() if k not in openai_params and not is_litellm_owned_kwarg(k)
|
||||
}
|
||||
non_default_params: Final = filter_out_litellm_params(kwargs, excluding=openai_params)
|
||||
|
||||
image_generation_config: BaseImageGenerationConfig | None = None
|
||||
if custom_llm_provider is not None and custom_llm_provider in LlmProviders._member_map_.values():
|
||||
|
|
@ -755,9 +752,7 @@ def image_edit(
|
|||
"style",
|
||||
"async_call",
|
||||
]
|
||||
non_default_params: Final = {
|
||||
k: v for k, v in kwargs.items() if k not in openai_params and not is_litellm_owned_kwarg(k)
|
||||
}
|
||||
non_default_params: Final = filter_out_litellm_params(kwargs, excluding=openai_params)
|
||||
litellm_logging_obj: Final[LiteLLMLoggingObj] = kwargs.get("litellm_logging_obj")
|
||||
litellm_call_id: Final[str | None] = kwargs.get("litellm_call_id", None)
|
||||
model_info: Final = kwargs.get("model_info", None)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,15 @@
|
|||
import reprlib
|
||||
from collections.abc import Mapping, MutableMapping
|
||||
from dataclasses import dataclass, fields
|
||||
from types import MappingProxyType
|
||||
from typing import Final
|
||||
|
||||
from pydantic import TypeAdapter, ValidationError
|
||||
|
||||
from litellm.constants import CONTROL_OPTIONS_KEY
|
||||
from litellm.litellm_core_utils.core_helpers import normalize_drop_params
|
||||
from litellm.llms.openai.data_residency import infer_openai_data_residency
|
||||
from litellm.types.litellm_params import MAX_CONTROL_INT_DIGITS, ControlOptions
|
||||
from litellm.types.router import CustomPricingLiteLLMParams
|
||||
|
||||
AWS_CREDENTIAL_KWARGS_KEYS: Final = frozenset(
|
||||
|
|
@ -70,6 +76,51 @@ OPTIONAL_KWARGS_KEYS: Final = (
|
|||
# Backward-compatible alias for existing imports/tests.
|
||||
_OPTIONAL_KWARGS_KEYS: Final = OPTIONAL_KWARGS_KEYS
|
||||
|
||||
_CONTROL_OPTIONS: Final = TypeAdapter(ControlOptions)
|
||||
_CONTROL_OPTION_NAMES: Final = tuple(field.name for field in fields(ControlOptions))
|
||||
_MAX_SHOWN_INT_BITS: Final = 64
|
||||
_EXPECTED: Final = f"expected a positive integer of at most {MAX_CONTROL_INT_DIGITS} digits"
|
||||
|
||||
|
||||
class _BoundedRepr(reprlib.Repr):
|
||||
def repr_int(self, x: int, level: int) -> str:
|
||||
if x.bit_length() > _MAX_SHOWN_INT_BITS:
|
||||
return f"<int of {x.bit_length()} bits>"
|
||||
return super().repr_int(x, level)
|
||||
|
||||
|
||||
_BOUNDED_REPR: Final = _BoundedRepr()
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class InvalidControlOption:
|
||||
param: str
|
||||
message: str
|
||||
|
||||
|
||||
def parse_control_options(kwargs: Mapping[str, object]) -> ControlOptions | InvalidControlOption:
|
||||
given: Final = { # mutable-ok: TypeAdapter.validate_python takes a dict
|
||||
name: kwargs[name] for name in _CONTROL_OPTION_NAMES if name in kwargs
|
||||
}
|
||||
try:
|
||||
return _CONTROL_OPTIONS.validate_python(given)
|
||||
except ValidationError as e:
|
||||
param: Final = str(e.errors(include_url=False)[0]["loc"][0])
|
||||
return InvalidControlOption(
|
||||
param=param, message=f"Invalid {param}={_BOUNDED_REPR.repr(given[param])}: {_EXPECTED}"
|
||||
)
|
||||
|
||||
|
||||
def stored_control_options(litellm_params: Mapping[str, object]) -> ControlOptions:
|
||||
control: Final = litellm_params.get(CONTROL_OPTIONS_KEY)
|
||||
return control if isinstance(control, ControlOptions) else ControlOptions()
|
||||
|
||||
|
||||
def with_control_options(litellm_params: Mapping[str, object], control: ControlOptions) -> dict[str, object]:
|
||||
if control == ControlOptions():
|
||||
return dict(litellm_params) # mutable-ok: completion() hands litellm_params to provider code typed as dict
|
||||
return {**litellm_params, CONTROL_OPTIONS_KEY: control} # mutable-ok: same dict contract as above
|
||||
|
||||
|
||||
def _get_base_model_from_litellm_call_metadata(
|
||||
metadata: dict | None,
|
||||
|
|
@ -130,7 +181,6 @@ def get_litellm_params(
|
|||
api_version: str | None = None,
|
||||
max_retries: int | None = None,
|
||||
litellm_request_debug: bool | None = None,
|
||||
stream_chunk_size: int | None = None,
|
||||
**kwargs,
|
||||
) -> dict:
|
||||
_litellm_metadata_dict: Final = litellm_metadata if isinstance(litellm_metadata, dict) else None
|
||||
|
|
@ -193,7 +243,6 @@ def get_litellm_params(
|
|||
"max_retries": max_retries,
|
||||
"use_litellm_proxy": use_litellm_proxy,
|
||||
"litellm_request_debug": litellm_request_debug,
|
||||
"stream_chunk_size": stream_chunk_size,
|
||||
}
|
||||
|
||||
# Sparse extraction: only add kwargs keys that are actually present
|
||||
|
|
|
|||
|
|
@ -393,6 +393,8 @@ class BaseConfig(ABC):
|
|||
client: AsyncHTTPHandler | None = None,
|
||||
json_mode: bool | None = None,
|
||||
signed_json_body: bytes | None = None,
|
||||
*,
|
||||
litellm_params: Mapping[str, object],
|
||||
) -> "CustomStreamWrapper":
|
||||
raise NotImplementedError
|
||||
|
||||
|
|
@ -408,6 +410,8 @@ class BaseConfig(ABC):
|
|||
client: HTTPHandler | AsyncHTTPHandler | None = None,
|
||||
json_mode: bool | None = None,
|
||||
signed_json_body: bytes | None = None,
|
||||
*,
|
||||
litellm_params: Mapping[str, object],
|
||||
) -> "CustomStreamWrapper":
|
||||
raise NotImplementedError
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ https://docs.aws.amazon.com/bedrock/latest/APIReference/API_agentcore_InvokeAgen
|
|||
"""
|
||||
|
||||
import json
|
||||
from collections.abc import AsyncGenerator
|
||||
from collections.abc import AsyncGenerator, Mapping
|
||||
from typing import TYPE_CHECKING, Any, Final, Optional, Union
|
||||
from urllib.parse import quote
|
||||
|
||||
|
|
@ -643,6 +643,8 @@ class AmazonAgentCoreConfig(BaseConfig, BaseAWSLLM):
|
|||
client: Union[HTTPHandler, "AsyncHTTPHandler"] | None = None,
|
||||
json_mode: bool | None = None,
|
||||
signed_json_body: bytes | None = None,
|
||||
*,
|
||||
litellm_params: Mapping[str, object],
|
||||
) -> "CustomStreamWrapper":
|
||||
"""
|
||||
Simplified sync streaming - returns a generator that yields ModelResponse chunks.
|
||||
|
|
@ -862,6 +864,8 @@ class AmazonAgentCoreConfig(BaseConfig, BaseAWSLLM):
|
|||
client: Optional["AsyncHTTPHandler"] = None,
|
||||
json_mode: bool | None = None,
|
||||
signed_json_body: bytes | None = None,
|
||||
*,
|
||||
litellm_params: Mapping[str, object],
|
||||
) -> "CustomStreamWrapper":
|
||||
"""
|
||||
Simplified async streaming - returns an async generator that yields ModelResponse chunks.
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import litellm
|
|||
from litellm.anthropic_beta_headers_manager import (
|
||||
update_headers_with_filtered_beta,
|
||||
)
|
||||
from litellm.litellm_core_utils.get_litellm_params import stored_control_options
|
||||
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObject
|
||||
from litellm.llms.custom_httpx.http_handler import (
|
||||
AsyncHTTPHandler,
|
||||
|
|
@ -18,7 +19,7 @@ from litellm.types.utils import ModelResponse
|
|||
from litellm.utils import CustomStreamWrapper
|
||||
|
||||
from ..base_aws_llm import BaseAWSLLM, Credentials, bedrock_bearer_token, pop_aws_auth_params, run_aws_signing
|
||||
from ..common_utils import BedrockError, _get_all_bedrock_regions, error_response_text, stream_chunk_size_from
|
||||
from ..common_utils import BedrockError, _get_all_bedrock_regions, error_response_text
|
||||
from .invoke_handler import AWSEventStreamDecoder, MockResponseIterator, make_call
|
||||
|
||||
|
||||
|
|
@ -280,7 +281,7 @@ class BedrockConverseLLM(BaseAWSLLM):
|
|||
):
|
||||
## SETUP ##
|
||||
stream: Final = optional_params.pop("stream", None)
|
||||
stream_chunk_size: Final = stream_chunk_size_from(litellm_params) if stream is True else None
|
||||
stream_chunk_size: Final = stored_control_options(litellm_params).stream_chunk_size if stream is True else None
|
||||
unencoded_model_id: Final = optional_params.pop("model_id", None)
|
||||
fake_stream = optional_params.pop("fake_stream", False)
|
||||
json_mode: Final = optional_params.get("json_mode", False)
|
||||
|
|
|
|||
|
|
@ -225,7 +225,6 @@ class AmazonAnthropicClaudeConfig(AmazonInvokeConfig, AnthropicConfig):
|
|||
|
||||
anthropic_request.pop("model", None)
|
||||
anthropic_request.pop("stream", None)
|
||||
anthropic_request.pop("stream_chunk_size", None)
|
||||
apply_bedrock_invoke_structured_output(
|
||||
model=model,
|
||||
request_body=anthropic_request,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import copy
|
||||
import json
|
||||
import time
|
||||
from collections.abc import Mapping
|
||||
from typing import TYPE_CHECKING, Any, Final, cast, get_args
|
||||
|
||||
import httpx
|
||||
|
|
@ -9,6 +10,7 @@ from pydantic import TypeAdapter, ValidationError
|
|||
import litellm
|
||||
from litellm._logging import verbose_logger
|
||||
from litellm.litellm_core_utils.core_helpers import map_finish_reason
|
||||
from litellm.litellm_core_utils.get_litellm_params import stored_control_options
|
||||
from litellm.litellm_core_utils.logging_utils import track_llm_api_timing
|
||||
from litellm.litellm_core_utils.prompt_templates.factory import (
|
||||
cohere_message_pt,
|
||||
|
|
@ -18,7 +20,7 @@ from litellm.litellm_core_utils.prompt_templates.factory import (
|
|||
)
|
||||
from litellm.llms.base_llm.chat.transformation import BaseConfig, BaseLLMException
|
||||
from litellm.llms.bedrock.chat.invoke_handler import make_call, make_sync_call
|
||||
from litellm.llms.bedrock.common_utils import BedrockError, stream_chunk_size_from
|
||||
from litellm.llms.bedrock.common_utils import BedrockError
|
||||
from litellm.llms.bedrock.request_metadata import (
|
||||
bedrock_request_metadata_headers,
|
||||
merge_bedrock_invoke_headers,
|
||||
|
|
@ -180,7 +182,6 @@ class AmazonInvokeConfig(BaseConfig, BaseAWSLLM):
|
|||
) -> dict:
|
||||
## SETUP ##
|
||||
stream: Final = optional_params.pop("stream", None)
|
||||
optional_params.pop("stream_chunk_size", None)
|
||||
custom_prompt_dict: Final[dict] = litellm_params.pop("custom_prompt_dict", None) or {}
|
||||
hf_model_name: Final = litellm_params.get("hf_model_name", None)
|
||||
|
||||
|
|
@ -452,8 +453,10 @@ class AmazonInvokeConfig(BaseConfig, BaseAWSLLM):
|
|||
client: AsyncHTTPHandler | None = None,
|
||||
json_mode: bool | None = None,
|
||||
signed_json_body: bytes | None = None,
|
||||
*,
|
||||
litellm_params: Mapping[str, object],
|
||||
) -> CustomStreamWrapper:
|
||||
chunk_size: Final = stream_chunk_size_from(logging_obj.litellm_params)
|
||||
chunk_size: Final = stored_control_options(litellm_params).stream_chunk_size
|
||||
completion_stream, response_headers = await make_call(
|
||||
client=client,
|
||||
api_base=api_base,
|
||||
|
|
@ -489,11 +492,13 @@ class AmazonInvokeConfig(BaseConfig, BaseAWSLLM):
|
|||
client: HTTPHandler | AsyncHTTPHandler | None = None,
|
||||
json_mode: bool | None = None,
|
||||
signed_json_body: bytes | None = None,
|
||||
*,
|
||||
litellm_params: Mapping[str, object],
|
||||
) -> CustomStreamWrapper:
|
||||
sync_client: Final = (
|
||||
_get_httpx_client(params={}) if client is None or isinstance(client, AsyncHTTPHandler) else client
|
||||
)
|
||||
chunk_size: Final = stream_chunk_size_from(logging_obj.litellm_params)
|
||||
chunk_size: Final = stored_control_options(litellm_params).stream_chunk_size
|
||||
completion_stream, response_headers = make_sync_call(
|
||||
client=sync_client,
|
||||
api_base=api_base,
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ if TYPE_CHECKING:
|
|||
from litellm.types.llms.bedrock import BedrockCreateBatchRequest
|
||||
|
||||
import httpx
|
||||
from pydantic import ConfigDict, TypeAdapter, ValidationError
|
||||
from pydantic import TypeAdapter, ValidationError
|
||||
|
||||
import litellm
|
||||
from litellm import verbose_logger
|
||||
|
|
@ -86,15 +86,6 @@ class BedrockError(BaseLLMException):
|
|||
|
||||
|
||||
_BEDROCK_AWS_AUTH_PARAMETER_KEYS: Final[tuple[str, ...]] = (*AWS_AUTH_PARAM_KEYS, "aws_region_name")
|
||||
_STREAM_CHUNK_SIZE_VALIDATOR: Final[TypeAdapter[int | None]] = TypeAdapter(int | None, config=ConfigDict(strict=True))
|
||||
|
||||
|
||||
def stream_chunk_size_from(litellm_params: Mapping[str, object]) -> int | None:
|
||||
raw: Final = litellm_params.get("stream_chunk_size")
|
||||
try:
|
||||
return _STREAM_CHUNK_SIZE_VALIDATOR.validate_python(raw)
|
||||
except ValidationError as e:
|
||||
raise BedrockError(status_code=400, message=f"Invalid stream_chunk_size={raw!r}. Expected int. Error: {e}")
|
||||
|
||||
|
||||
def merge_bedrock_aws_request_params(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import json
|
||||
import time
|
||||
import traceback
|
||||
from collections.abc import Mapping
|
||||
from typing import TYPE_CHECKING, Any, Final
|
||||
|
||||
import httpx
|
||||
|
|
@ -258,6 +259,8 @@ class BytezChatConfig(BaseConfig):
|
|||
client: HTTPHandler | AsyncHTTPHandler | None = None,
|
||||
json_mode: bool | None = None,
|
||||
signed_json_body: bytes | None = None,
|
||||
*,
|
||||
litellm_params: Mapping[str, object],
|
||||
) -> "BytezCustomStreamWrapper":
|
||||
if client is None or isinstance(client, AsyncHTTPHandler):
|
||||
client = _get_httpx_client(params={})
|
||||
|
|
@ -300,6 +303,8 @@ class BytezChatConfig(BaseConfig):
|
|||
client: HTTPHandler | AsyncHTTPHandler | None = None,
|
||||
json_mode: bool | None = None,
|
||||
signed_json_body: bytes | None = None,
|
||||
*,
|
||||
litellm_params: Mapping[str, object],
|
||||
) -> "BytezCustomStreamWrapper":
|
||||
if client is None or isinstance(client, HTTPHandler):
|
||||
client = get_async_httpx_client(llm_provider=LlmProviders.BYTEZ, params={})
|
||||
|
|
|
|||
|
|
@ -790,6 +790,7 @@ class BaseLLMHTTPHandler:
|
|||
messages=messages,
|
||||
client=client,
|
||||
json_mode=json_mode,
|
||||
litellm_params=litellm_params,
|
||||
)
|
||||
completion_stream, headers = self.make_sync_call(
|
||||
provider_config=provider_config,
|
||||
|
|
@ -953,6 +954,7 @@ class BaseLLMHTTPHandler:
|
|||
client=client,
|
||||
json_mode=json_mode,
|
||||
signed_json_body=signed_json_body,
|
||||
litellm_params=litellm_params,
|
||||
)
|
||||
|
||||
completion_stream, _response_headers = await self.make_async_call_stream_helper(
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ Non-streaming endpoint: POST /runs/wait
|
|||
"""
|
||||
|
||||
import json
|
||||
from collections.abc import Mapping
|
||||
from typing import TYPE_CHECKING, Any, Final, Optional, Union, cast
|
||||
|
||||
import httpx
|
||||
|
|
@ -285,6 +286,8 @@ class LangGraphConfig(BaseConfig):
|
|||
client: Union[HTTPHandler, "AsyncHTTPHandler"] | None = None,
|
||||
json_mode: bool | None = None,
|
||||
signed_json_body: bytes | None = None,
|
||||
*,
|
||||
litellm_params: Mapping[str, object],
|
||||
) -> CustomStreamWrapper:
|
||||
"""
|
||||
Get a CustomStreamWrapper for synchronous streaming.
|
||||
|
|
@ -344,6 +347,8 @@ class LangGraphConfig(BaseConfig):
|
|||
client: Optional["AsyncHTTPHandler"] = None,
|
||||
json_mode: bool | None = None,
|
||||
signed_json_body: bytes | None = None,
|
||||
*,
|
||||
litellm_params: Mapping[str, object],
|
||||
) -> CustomStreamWrapper:
|
||||
"""
|
||||
Get a CustomStreamWrapper for asynchronous streaming.
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ implement the LiteLLM BaseConfig interface. Heavy-lifting lives in:
|
|||
"""
|
||||
|
||||
import json
|
||||
from collections.abc import AsyncIterator, Callable, Iterator
|
||||
from collections.abc import AsyncIterator, Callable, Iterator, Mapping
|
||||
from typing import TYPE_CHECKING, Any, Final
|
||||
|
||||
import httpx
|
||||
|
|
@ -642,6 +642,8 @@ class OCIChatConfig(BaseConfig):
|
|||
client: HTTPHandler | AsyncHTTPHandler | None = None,
|
||||
json_mode: bool | None = None,
|
||||
signed_json_body: bytes | None = None,
|
||||
*,
|
||||
litellm_params: Mapping[str, object],
|
||||
) -> "OCIStreamWrapper":
|
||||
if client is None or isinstance(client, AsyncHTTPHandler):
|
||||
client = _get_httpx_client(params={})
|
||||
|
|
@ -681,6 +683,8 @@ class OCIChatConfig(BaseConfig):
|
|||
client: HTTPHandler | AsyncHTTPHandler | None = None,
|
||||
json_mode: bool | None = None,
|
||||
signed_json_body: bytes | None = None,
|
||||
*,
|
||||
litellm_params: Mapping[str, object],
|
||||
) -> "OCIStreamWrapper":
|
||||
if client is None or isinstance(client, HTTPHandler):
|
||||
client = get_async_httpx_client(llm_provider=LlmProviders.OCI, params={})
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ LiteLLM Docs: https://docs.litellm.ai/docs/providers/aws_sagemaker#sagemaker-mes
|
|||
Huggingface Docs: https://huggingface.co/docs/text-generation-inference/en/messages_api
|
||||
"""
|
||||
|
||||
from collections.abc import Mapping
|
||||
from typing import TYPE_CHECKING, Any, Final, cast
|
||||
|
||||
import httpx
|
||||
|
|
@ -149,6 +150,8 @@ class SagemakerChatConfig(OpenAIGPTConfig, BaseAWSLLM):
|
|||
client: HTTPHandler | AsyncHTTPHandler | None = None,
|
||||
json_mode: bool | None = None,
|
||||
signed_json_body: bytes | None = None,
|
||||
*,
|
||||
litellm_params: Mapping[str, object],
|
||||
) -> CustomStreamWrapper:
|
||||
if client is None or isinstance(client, AsyncHTTPHandler):
|
||||
client = _get_httpx_client(params={})
|
||||
|
|
@ -191,6 +194,8 @@ class SagemakerChatConfig(OpenAIGPTConfig, BaseAWSLLM):
|
|||
client: HTTPHandler | AsyncHTTPHandler | None = None,
|
||||
json_mode: bool | None = None,
|
||||
signed_json_body: bytes | None = None,
|
||||
*,
|
||||
litellm_params: Mapping[str, object],
|
||||
) -> CustomStreamWrapper:
|
||||
if client is None or isinstance(client, HTTPHandler):
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ API Reference:
|
|||
"""
|
||||
|
||||
import json
|
||||
from collections.abc import Mapping
|
||||
from typing import TYPE_CHECKING, Any, Final, Optional, Union, cast
|
||||
|
||||
import httpx
|
||||
|
|
@ -365,6 +366,8 @@ class VertexAgentEngineConfig(BaseConfig, VertexBase):
|
|||
client: Union[HTTPHandler, "AsyncHTTPHandler"] | None = None,
|
||||
json_mode: bool | None = None,
|
||||
signed_json_body: bytes | None = None,
|
||||
*,
|
||||
litellm_params: Mapping[str, object],
|
||||
) -> "CustomStreamWrapper":
|
||||
"""Get a CustomStreamWrapper for synchronous streaming."""
|
||||
from litellm.llms.custom_httpx.http_handler import (
|
||||
|
|
@ -423,6 +426,8 @@ class VertexAgentEngineConfig(BaseConfig, VertexBase):
|
|||
client: Optional["AsyncHTTPHandler"] = None,
|
||||
json_mode: bool | None = None,
|
||||
signed_json_body: bytes | None = None,
|
||||
*,
|
||||
litellm_params: Mapping[str, object],
|
||||
) -> "CustomStreamWrapper":
|
||||
"""Get a CustomStreamWrapper for asynchronous streaming."""
|
||||
from litellm.llms.custom_httpx.http_handler import (
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ import dotenv
|
|||
import httpx
|
||||
import openai
|
||||
from pydantic import BaseModel
|
||||
from typing_extensions import overload
|
||||
from typing_extensions import assert_never, overload
|
||||
|
||||
import litellm
|
||||
|
||||
|
|
@ -48,6 +48,7 @@ from litellm import client
|
|||
# Other utils are imported directly to avoid circular imports
|
||||
from litellm.utils import (
|
||||
exception_type,
|
||||
filter_out_litellm_params,
|
||||
get_litellm_params,
|
||||
get_optional_params,
|
||||
peek_reasoning_summary_aliases,
|
||||
|
|
@ -83,6 +84,9 @@ from litellm.litellm_core_utils.get_litellm_params import (
|
|||
AWS_CREDENTIAL_KWARGS_KEYS,
|
||||
OPTIONAL_KWARGS_KEYS,
|
||||
PROVIDER_AFFINITY_HEADER_KWARG_KEY,
|
||||
InvalidControlOption,
|
||||
parse_control_options,
|
||||
with_control_options,
|
||||
)
|
||||
from litellm.litellm_core_utils.get_provider_specific_headers import (
|
||||
ProviderSpecificHeaderUtils,
|
||||
|
|
@ -127,7 +131,7 @@ from litellm.types.completion import (
|
|||
_CompletionDispatchContext,
|
||||
_CompletionDispatchResult,
|
||||
)
|
||||
from litellm.types.litellm_params import RetryStrategy
|
||||
from litellm.types.litellm_params import ControlOptions, RetryStrategy
|
||||
from litellm.types.router import GenericLiteLLMParams
|
||||
from litellm.types.utils import (
|
||||
CustomPricingLiteLLMParams,
|
||||
|
|
@ -178,7 +182,7 @@ from litellm.utils import (
|
|||
|
||||
from ._logging import verbose_logger
|
||||
from .caching.caching import disable_cache, enable_cache, update_cache
|
||||
from .litellm_core_utils.core_helpers import safe_deep_copy
|
||||
from .litellm_core_utils.core_helpers import normalize_drop_params, safe_deep_copy
|
||||
from .litellm_core_utils.fallback_utils import (
|
||||
async_completion_with_fallbacks,
|
||||
completion_with_fallbacks,
|
||||
|
|
@ -284,7 +288,6 @@ from .types.utils import (
|
|||
LlmProviders,
|
||||
PromptTokensDetails,
|
||||
ProviderSpecificHeader,
|
||||
is_litellm_owned_kwarg,
|
||||
)
|
||||
|
||||
####### ENVIRONMENT VARIABLES ###################
|
||||
|
|
@ -335,6 +338,21 @@ ovhcloud_transformation: Final = OVHCloudChatConfig()
|
|||
lemonade_transformation: Final = LemonadeChatConfig()
|
||||
|
||||
MOCK_RESPONSE_TYPE = str | Exception | dict | ModelResponse | ModelResponseStream
|
||||
|
||||
|
||||
def _resolve_control_options(kwargs: Mapping[str, object], model: str) -> ControlOptions:
|
||||
control: Final = parse_control_options(kwargs)
|
||||
match control:
|
||||
case ControlOptions():
|
||||
return control
|
||||
case InvalidControlOption(param=param, message=message):
|
||||
if litellm.drop_params is True or normalize_drop_params(kwargs.get("drop_params")) is True:
|
||||
return ControlOptions()
|
||||
raise litellm.BadRequestError(message=message, model=model, llm_provider=None, body={"param": param})
|
||||
case _:
|
||||
return assert_never(control)
|
||||
|
||||
|
||||
####### COMPLETION ENDPOINTS ################
|
||||
|
||||
|
||||
|
|
@ -501,6 +519,7 @@ async def acompletion(
|
|||
|
||||
loop: Final = asyncio.get_event_loop()
|
||||
custom_llm_provider = kwargs.get("custom_llm_provider", None)
|
||||
_ = _resolve_control_options(kwargs, model)
|
||||
|
||||
## PROMPT MANAGEMENT HOOKS ##
|
||||
#########################################################
|
||||
|
|
@ -5230,6 +5249,7 @@ def completion(
|
|||
# Responses API config (get_provider_responses_api_config -> None).
|
||||
skip_responses_api_bridge: Final = kwargs.pop("_skip_responses_api_bridge", False)
|
||||
|
||||
control_options: Final = _resolve_control_options(kwargs, model)
|
||||
skip_mcp_handler: Final = kwargs.pop("_skip_mcp_handler", False)
|
||||
if not skip_mcp_handler and tools:
|
||||
from litellm.responses.mcp.chat_completions_handler import acompletion_with_mcp
|
||||
|
|
@ -5370,7 +5390,6 @@ def completion(
|
|||
)
|
||||
######## end of unpacking kwargs ###########
|
||||
non_default_params: Final = get_non_default_completion_params(kwargs=kwargs)
|
||||
litellm_params: dict[str, object] = {} # used to prevent unbound var errors
|
||||
## PROMPT MANAGEMENT HOOKS ##
|
||||
|
||||
from litellm.integrations.anthropic_cache_control_hook import (
|
||||
|
|
@ -5622,7 +5641,7 @@ def completion(
|
|||
messages = function_call_prompt(messages=messages, functions=functions_unsupported_model)
|
||||
|
||||
# For logging - save the values of the litellm-specific params passed in
|
||||
litellm_params = get_litellm_params(
|
||||
requested_litellm_params: Final = get_litellm_params(
|
||||
acompletion=acompletion,
|
||||
api_key=api_key,
|
||||
force_timeout=force_timeout,
|
||||
|
|
@ -5670,7 +5689,6 @@ def completion(
|
|||
max_retries=max_retries,
|
||||
timeout=timeout,
|
||||
litellm_request_debug=kwargs.get("litellm_request_debug", False),
|
||||
stream_chunk_size=kwargs.get("stream_chunk_size"),
|
||||
tpm=kwargs.get("tpm"),
|
||||
rpm=kwargs.get("rpm"),
|
||||
use_xai_oauth=kwargs.get("use_xai_oauth", False),
|
||||
|
|
@ -5683,6 +5701,7 @@ def completion(
|
|||
if key in kwargs
|
||||
},
|
||||
)
|
||||
litellm_params: Final = with_control_options(requested_litellm_params, control_options)
|
||||
if litellm_params.get("provider_affinity_header") is not None:
|
||||
try:
|
||||
headers = add_provider_affinity_header(
|
||||
|
|
@ -6352,9 +6371,7 @@ def embedding(
|
|||
"encoding_format",
|
||||
]
|
||||
default_params: Final = [*openai_params, "aembedding", "extra_headers"]
|
||||
non_default_params: Final = {
|
||||
k: v for k, v in kwargs.items() if k not in default_params and not is_litellm_owned_kwarg(k)
|
||||
}
|
||||
non_default_params: Final = filter_out_litellm_params(kwargs, excluding=default_params)
|
||||
|
||||
model, custom_llm_provider, dynamic_api_key, api_base = get_llm_provider(
|
||||
model=model,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,10 @@ from collections.abc import Callable, Iterator, Mapping, MutableMapping, Sequenc
|
|||
from dataclasses import dataclass, field, fields, is_dataclass
|
||||
from itertools import chain
|
||||
from types import MappingProxyType
|
||||
from typing import TYPE_CHECKING, Final, Literal, TypeAlias
|
||||
from typing import TYPE_CHECKING, Annotated, Final, Literal, TypeAlias
|
||||
|
||||
from pydantic import BeforeValidator, Field
|
||||
from pydantic.dataclasses import dataclass as pydantic_dataclass
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import httpx
|
||||
|
|
@ -234,11 +237,31 @@ class ResponseOptions:
|
|||
merge_reasoning_content_in_choices: bool | None = None
|
||||
enable_json_schema_validation: bool | None = None
|
||||
complete_response: bool | None = None
|
||||
stream_chunk_size: int | None = None
|
||||
keepalive_seconds: float | None = None
|
||||
allow_client_keepalive_override: bool | None = None
|
||||
|
||||
|
||||
MAX_CONTROL_INT_DIGITS: Final = 18
|
||||
|
||||
|
||||
def _int_from_decimal_string(value: object) -> object:
|
||||
if isinstance(value, str) and value.isascii() and value.isdecimal() and len(value) <= MAX_CONTROL_INT_DIGITS:
|
||||
return int(value)
|
||||
return value
|
||||
|
||||
|
||||
@pydantic_dataclass(frozen=True, slots=True, kw_only=True)
|
||||
class ControlOptions:
|
||||
stream_chunk_size: (
|
||||
Annotated[
|
||||
int,
|
||||
BeforeValidator(_int_from_decimal_string),
|
||||
Field(strict=True, gt=0, lt=10**MAX_CONTROL_INT_DIGITS),
|
||||
]
|
||||
| None
|
||||
) = None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True, kw_only=True)
|
||||
class MockOptions:
|
||||
mock_response: "MockResponse | None" = None
|
||||
|
|
@ -258,6 +281,7 @@ class LiteLLMOptions:
|
|||
guardrails: GuardrailOptions
|
||||
prompt: PromptOptions
|
||||
response: ResponseOptions
|
||||
control: ControlOptions
|
||||
mock: MockOptions
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -288,7 +288,7 @@ except (ImportError, AttributeError, TypeError):
|
|||
# Convert to str (if necessary)
|
||||
claude_json_str = json.dumps(json_data)
|
||||
import importlib.metadata
|
||||
from collections.abc import AsyncIterator, Callable, Iterable, Iterator, Mapping, Sequence
|
||||
from collections.abc import AsyncIterator, Callable, Collection, Iterable, Iterator, Mapping, Sequence
|
||||
from typing import TYPE_CHECKING, Any, Final, Literal, Protocol, cast, runtime_checkable
|
||||
|
||||
from typing_extensions import assert_never
|
||||
|
|
@ -4161,8 +4161,10 @@ def _remove_unsupported_params(non_default_params: dict, supported_openai_params
|
|||
return non_default_params
|
||||
|
||||
|
||||
def filter_out_litellm_params(kwargs: Mapping[str, object]) -> dict:
|
||||
return {key: value for key, value in kwargs.items() if not is_litellm_owned_kwarg(key)}
|
||||
def filter_out_litellm_params(
|
||||
kwargs: Mapping[str, object], excluding: Collection[str] = frozenset()
|
||||
) -> dict[str, object]:
|
||||
return {key: value for key, value in kwargs.items() if key not in excluding and not is_litellm_owned_kwarg(key)}
|
||||
|
||||
|
||||
def _provider_supports_vertex_params(custom_llm_provider: str) -> bool:
|
||||
|
|
@ -10132,13 +10134,8 @@ def get_standard_openai_params(params: Mapping[str, object]) -> dict:
|
|||
return {k: v for k, v in params.items() if k in litellm.OPENAI_CHAT_COMPLETION_PARAMS and v is not None}
|
||||
|
||||
|
||||
def get_non_default_completion_params(kwargs: Mapping[str, object]) -> dict:
|
||||
openai_params: Final = litellm.OPENAI_CHAT_COMPLETION_PARAMS
|
||||
non_default_params: Final = {
|
||||
k: v for k, v in kwargs.items() if k not in openai_params and not is_litellm_owned_kwarg(k)
|
||||
}
|
||||
|
||||
return non_default_params
|
||||
def get_non_default_completion_params(kwargs: Mapping[str, object]) -> dict[str, object]:
|
||||
return filter_out_litellm_params(kwargs, excluding=litellm.OPENAI_CHAT_COMPLETION_PARAMS)
|
||||
|
||||
|
||||
def peek_reasoning_summary_aliases(optional_params: dict) -> object | None:
|
||||
|
|
@ -10184,13 +10181,10 @@ def strip_reasoning_summary_aliases_from_optional_params(
|
|||
return op, rs_val
|
||||
|
||||
|
||||
def get_non_default_transcription_params(kwargs: Mapping[str, object]) -> dict:
|
||||
def get_non_default_transcription_params(kwargs: Mapping[str, object]) -> dict[str, object]:
|
||||
from litellm.constants import OPENAI_TRANSCRIPTION_PARAMS
|
||||
|
||||
non_default_params: Final = {
|
||||
k: v for k, v in kwargs.items() if k not in OPENAI_TRANSCRIPTION_PARAMS and not is_litellm_owned_kwarg(k)
|
||||
}
|
||||
return non_default_params
|
||||
return filter_out_litellm_params(kwargs, excluding=OPENAI_TRANSCRIPTION_PARAMS)
|
||||
|
||||
|
||||
def add_openai_metadata(
|
||||
|
|
|
|||
|
|
@ -1,26 +1,28 @@
|
|||
from collections.abc import Mapping
|
||||
from types import MappingProxyType
|
||||
from typing import Final
|
||||
|
||||
import litellm
|
||||
import pytest
|
||||
from litellm.integrations.custom_logger import CustomLogger
|
||||
|
||||
from litellm.constants import CONTROL_OPTIONS_KEY
|
||||
from litellm.types.litellm_params import ControlOptions
|
||||
|
||||
class LitellmParamsRecorder(CustomLogger):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.seen: tuple[Mapping[str, object], ...] = ()
|
||||
DEFAULT_CHUNKING_REQUESTS: Final = (
|
||||
pytest.param(MappingProxyType({}), id="unset"),
|
||||
pytest.param(MappingProxyType({"stream_chunk_size": "sixty-four", "drop_params": True}), id="dropped"),
|
||||
pytest.param(
|
||||
MappingProxyType({"stream_chunk_size": "sixty-four", "drop_params": "true"}), id="dropped_by_string_flag"
|
||||
),
|
||||
pytest.param(MappingProxyType({CONTROL_OPTIONS_KEY: ControlOptions(stream_chunk_size=1)}), id="forged_options"),
|
||||
pytest.param(MappingProxyType({CONTROL_OPTIONS_KEY: {"stream_chunk_size": 1}}), id="forged_mapping"),
|
||||
)
|
||||
|
||||
def log_pre_api_call(self, model: str, messages: object, kwargs: Mapping[str, object]) -> None:
|
||||
params: Final = kwargs["litellm_params"]
|
||||
assert isinstance(params, Mapping)
|
||||
self.seen = (*self.seen, params)
|
||||
|
||||
|
||||
def record_litellm_params(monkeypatch: pytest.MonkeyPatch) -> LitellmParamsRecorder:
|
||||
recorder: Final = LitellmParamsRecorder()
|
||||
monkeypatch.setattr(litellm, "input_callback", [recorder])
|
||||
return recorder
|
||||
ROUTER_CHUNK_SIZE_CASES: Final = (
|
||||
pytest.param(MappingProxyType({"stream_chunk_size": 64}), 64, id="int"),
|
||||
pytest.param(MappingProxyType({"stream_chunk_size": "64"}), 64, id="digit_string"),
|
||||
pytest.param(MappingProxyType({}), None, id="unset"),
|
||||
pytest.param(MappingProxyType({"stream_chunk_size": "sixty-four", "drop_params": True}), None, id="dropped"),
|
||||
)
|
||||
|
||||
|
||||
def keys_at_every_depth(value: object) -> frozenset[str]:
|
||||
|
|
|
|||
|
|
@ -8,11 +8,12 @@ from collections.abc import Callable, Mapping
|
|||
from pathlib import Path
|
||||
from typing import Final
|
||||
|
||||
import litellm
|
||||
import pytest
|
||||
from integration._support.upstream import INTERNAL_FIELDS
|
||||
from integration._support.wire import Reply, Request, wire_server
|
||||
from tests._support.stream_chunk_size import keys_at_every_depth, record_litellm_params
|
||||
|
||||
import litellm
|
||||
from tests._support.stream_chunk_size import keys_at_every_depth
|
||||
|
||||
TEXT: Final = "wire control"
|
||||
OPENAI_RESPONSE: Final = {
|
||||
|
|
@ -277,13 +278,11 @@ def provider_wire_environment(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -
|
|||
@pytest.mark.parametrize("asynchronous", [False, True])
|
||||
@pytest.mark.parametrize("stream", [False, True])
|
||||
async def test_internal_params_never_reach_provider_body(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
provider_wire_environment: None,
|
||||
provider: str,
|
||||
asynchronous: bool,
|
||||
stream: bool,
|
||||
) -> None:
|
||||
recorder: Final = record_litellm_params(monkeypatch)
|
||||
with wire_server(_peer(provider)) as wire:
|
||||
parameters: Final = {
|
||||
**_request_parameters(provider, wire.url),
|
||||
|
|
@ -308,8 +307,6 @@ async def test_internal_params_never_reach_provider_body(
|
|||
assert result.choices[0].message.content == TEXT
|
||||
requests: Final = wire.drain()
|
||||
assert len(requests) == 1
|
||||
assert len(recorder.seen) == 1
|
||||
assert recorder.seen[0]["stream_chunk_size"] == 64
|
||||
body: Final = json.loads(requests[0].body)
|
||||
keys: Final = keys_at_every_depth(body)
|
||||
assert "stream_chunk_size" not in keys
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
import asyncio
|
||||
import logging
|
||||
import re
|
||||
from typing import Final
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
import litellm
|
||||
import litellm.caching.redis_cache as redis_cache_module
|
||||
from litellm.caching.caching import Cache
|
||||
from litellm.caching.caching_handler import _PENDING_CACHE_WRITES
|
||||
|
|
@ -389,3 +391,15 @@ async def test_embedding_cache_serves_base64_string_embeddings_on_repeat(monkeyp
|
|||
|
||||
assert embedder.provider_calls == 1, "a string embedding written to the cache must be served on repeat"
|
||||
assert [item["embedding"] for item in second.data] == [item["embedding"] for item in first.data] == ["AACAPwAAAEA="]
|
||||
|
||||
|
||||
def test_provider_specific_cache_key_ignores_litellm_owned_kwargs(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(litellm, "enable_caching_on_provider_specific_optional_params", True)
|
||||
cache: Final = Cache(type=LiteLLMCacheType.LOCAL)
|
||||
request: Final = {"model": "gpt-4.1-mini", "messages": [{"role": "user", "content": "hi"}], "top_k": 5}
|
||||
|
||||
base_key: Final = cache.get_cache_key(**request)
|
||||
|
||||
assert cache.get_cache_key(**request, _litellm_control={"stream_chunk_size": 64}) == base_key
|
||||
assert cache.get_cache_key(**request, litellm_trace_id="trace-1") == base_key
|
||||
assert cache.get_cache_key(**{**request, "top_k": 6}) != base_key
|
||||
|
|
|
|||
|
|
@ -7,12 +7,18 @@ Ensures backward compatibility after sparse kwargs extraction optimization.
|
|||
from typing import Final
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from litellm.constants import CONTROL_OPTIONS_KEY
|
||||
from litellm.litellm_core_utils.get_litellm_params import (
|
||||
_OPTIONAL_KWARGS_KEYS,
|
||||
InvalidControlOption,
|
||||
_get_base_model_from_litellm_call_metadata,
|
||||
get_litellm_params,
|
||||
parse_control_options,
|
||||
stored_control_options,
|
||||
)
|
||||
from litellm.types.litellm_params import ControlOptions
|
||||
|
||||
NAMED_PRICE_PARAMS: Final = frozenset(
|
||||
{"input_cost_per_token", "output_cost_per_token", "input_cost_per_second", "output_cost_per_second"}
|
||||
|
|
@ -90,9 +96,8 @@ class TestGetLitellmParamsKwargsExtraction:
|
|||
assert "s3_endpoint_url" not in result_without_s3_kwargs
|
||||
assert "s3_region_name" not in result_without_s3_kwargs
|
||||
|
||||
def test_stream_chunk_size_is_carried_as_a_litellm_param(self) -> None:
|
||||
assert get_litellm_params(stream_chunk_size=64)["stream_chunk_size"] == 64
|
||||
assert get_litellm_params()["stream_chunk_size"] is None
|
||||
def test_a_caller_supplied_control_options_key_is_not_carried(self) -> None:
|
||||
assert CONTROL_OPTIONS_KEY not in get_litellm_params(**{CONTROL_OPTIONS_KEY: {"stream_chunk_size": 64}})
|
||||
|
||||
def test_s3_credential_kwargs_are_forwarded_for_s3_signing(self):
|
||||
result = get_litellm_params(s3_access_key_id="s3-key", s3_secret_access_key="s3-secret")
|
||||
|
|
@ -122,6 +127,79 @@ class TestGetLitellmParamsKwargsExtraction:
|
|||
assert result[key] == f"val_{key}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"kwargs,expected",
|
||||
[
|
||||
({"stream_chunk_size": 64, "temperature": 0.2}, ControlOptions(stream_chunk_size=64)),
|
||||
({"stream_chunk_size": "64"}, ControlOptions(stream_chunk_size=64)),
|
||||
({"stream_chunk_size": None}, ControlOptions()),
|
||||
({"temperature": 0.2}, ControlOptions()),
|
||||
],
|
||||
)
|
||||
def test_control_options_are_read_from_the_request_kwargs(kwargs: dict[str, object], expected: ControlOptions) -> None:
|
||||
assert parse_control_options(kwargs) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"raw,shown",
|
||||
[
|
||||
("sixty-four", "'sixty-four'"),
|
||||
(" 64", "' 64'"),
|
||||
("-1", "'-1'"),
|
||||
("\uff16\uff14", "'\uff16\uff14'"),
|
||||
("x" * 500, "'xxxxxxxxxxxx...xxxxxxxxxxxxx'"),
|
||||
pytest.param(-(10**5000), "<int of 16610 bits>", id="huge_negative_int"),
|
||||
pytest.param(-(2**64 - 1), "-18446744073709551615", id="64_bit_negative_int"),
|
||||
pytest.param(-(2**64), "<int of 65 bits>", id="65_bit_negative_int"),
|
||||
pytest.param([-(10**5000)], "[<int of 16610 bits>]", id="nested_huge_int"),
|
||||
pytest.param(10**18, "1000000000000000000", id="19_digit_int"),
|
||||
pytest.param("1" + "0" * 18, "'1000000000000000000'", id="19_digit_string"),
|
||||
pytest.param("9" * 5000, "'999999999999...9999999999999'", id="5000_digit_string"),
|
||||
pytest.param("0" * 18 + "1", "'0000000000000000001'", id="19_digit_string_with_leading_zeros"),
|
||||
(64.0, "64.0"),
|
||||
(True, "True"),
|
||||
(0, "0"),
|
||||
("0", "'0'"),
|
||||
(-1, "-1"),
|
||||
],
|
||||
)
|
||||
def test_control_options_reject_a_stream_chunk_size_that_is_not_a_positive_int(raw: object, shown: str) -> None:
|
||||
assert parse_control_options({"stream_chunk_size": raw}) == InvalidControlOption(
|
||||
param="stream_chunk_size",
|
||||
message=f"Invalid stream_chunk_size={shown}: expected a positive integer of at most 18 digits",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("raw", [10**18 - 1, "9" * 18], ids=["int", "digit_string"])
|
||||
def test_control_options_accept_the_largest_18_digit_value(raw: object) -> None:
|
||||
assert parse_control_options({"stream_chunk_size": raw}) == ControlOptions(stream_chunk_size=10**18 - 1)
|
||||
|
||||
|
||||
def test_control_options_accept_an_18_digit_string_with_leading_zeros() -> None:
|
||||
assert parse_control_options({"stream_chunk_size": "0" * 17 + "1"}) == ControlOptions(stream_chunk_size=1)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("raw", [0, -1, "sixty-four", 64.0, True])
|
||||
def test_control_options_enforce_their_rule_at_construction(raw: object) -> None:
|
||||
with pytest.raises(ValidationError):
|
||||
ControlOptions(stream_chunk_size=raw) # pyright: ignore[reportArgumentType] # the invalid type is the input
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"litellm_params,expected",
|
||||
[
|
||||
({CONTROL_OPTIONS_KEY: ControlOptions(stream_chunk_size=64)}, ControlOptions(stream_chunk_size=64)),
|
||||
({}, ControlOptions()),
|
||||
({CONTROL_OPTIONS_KEY: {"stream_chunk_size": 64}}, ControlOptions()),
|
||||
({"stream_chunk_size": 64}, ControlOptions()),
|
||||
],
|
||||
)
|
||||
def test_stored_control_options_reads_only_the_validated_options(
|
||||
litellm_params: dict[str, object], expected: ControlOptions
|
||||
) -> None:
|
||||
assert stored_control_options(litellm_params) == expected
|
||||
|
||||
|
||||
class TestGetLitellmParamsBaseModel:
|
||||
"""Verify base_model resolution precedence."""
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import json
|
||||
from collections.abc import Mapping
|
||||
from types import MappingProxyType
|
||||
from typing import Final
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
|
|
@ -6,44 +8,40 @@ import httpx
|
|||
import pytest
|
||||
|
||||
import litellm
|
||||
from litellm.llms.bedrock.chat.invoke_transformations.anthropic_claude3_transformation import (
|
||||
AmazonAnthropicClaudeConfig,
|
||||
)
|
||||
from litellm.llms.bedrock.chat.invoke_transformations.base_invoke_transformation import (
|
||||
AmazonInvokeConfig,
|
||||
)
|
||||
from litellm.llms.bedrock.common_utils import BedrockError
|
||||
from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler
|
||||
from tests._support.stream_chunk_size import (
|
||||
LitellmParamsRecorder,
|
||||
keys_at_every_depth,
|
||||
record_litellm_params,
|
||||
)
|
||||
from tests._support.stream_chunk_size import DEFAULT_CHUNKING_REQUESTS, ROUTER_CHUNK_SIZE_CASES, keys_at_every_depth
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"config,model",
|
||||
"model",
|
||||
[
|
||||
(AmazonInvokeConfig, "anthropic.claude-3-sonnet-20240229-v1:0"),
|
||||
(AmazonInvokeConfig, "amazon.titan-text-express-v1"),
|
||||
(AmazonInvokeConfig, "mistral.mistral-7b-instruct-v0:2"),
|
||||
(AmazonAnthropicClaudeConfig, "anthropic.claude-sonnet-4-6"),
|
||||
"anthropic.claude-sonnet-4-6",
|
||||
"amazon.titan-text-express-v1",
|
||||
"mistral.mistral-7b-instruct-v0:2",
|
||||
],
|
||||
)
|
||||
def test_transform_request_drops_stream_chunk_size(config, model):
|
||||
"""stream_chunk_size is a LiteLLM-internal knob for re-chunking the HTTP
|
||||
response stream. Leaking it into the provider request body makes Bedrock
|
||||
reject the whole request: ValidationException 'stream_chunk_size: Extra
|
||||
inputs are not permitted'."""
|
||||
request_body = config().transform_request(
|
||||
model=model,
|
||||
def test_completion_keeps_stream_chunk_size_out_of_invoke_bodies(model: str) -> None:
|
||||
send: Final = MagicMock(return_value=httpx.Response(200))
|
||||
client: Final = HTTPHandler(client=httpx.Client(transport=httpx.MockTransport(send)))
|
||||
|
||||
litellm.completion(
|
||||
model=f"bedrock/invoke/{model}",
|
||||
messages=[{"role": "user", "content": "hi"}],
|
||||
optional_params={"stream": True, "stream_chunk_size": 2048, "max_tokens": 10},
|
||||
litellm_params={},
|
||||
headers={},
|
||||
stream=True,
|
||||
max_tokens=10,
|
||||
client=client,
|
||||
aws_access_key_id="fake",
|
||||
aws_secret_access_key="fake",
|
||||
aws_region_name="us-east-1",
|
||||
stream_chunk_size=2048,
|
||||
)
|
||||
|
||||
assert "stream_chunk_size" not in json.dumps(request_body)
|
||||
request: Final = send.call_args.args[0]
|
||||
assert "stream_chunk_size" not in keys_at_every_depth(json.loads(request.content)), request.content
|
||||
|
||||
|
||||
def test_validate_environment_maps_guardrail_config_to_invoke_headers():
|
||||
|
|
@ -243,10 +241,7 @@ def test_transform_response_hands_json_mode_to_nova():
|
|||
assert json.loads(result.choices[0].message.content) == {"city": "Paris", "temperature": 21}
|
||||
|
||||
|
||||
def _stream_invoke_completion_with_spied_client(
|
||||
monkeypatch: pytest.MonkeyPatch, **kwargs
|
||||
) -> tuple[MagicMock, MagicMock, LitellmParamsRecorder]:
|
||||
recorder: Final = record_litellm_params(monkeypatch)
|
||||
def _stream_invoke_completion_with_spied_client(**kwargs: object) -> tuple[MagicMock, MagicMock]:
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.iter_bytes = MagicMock(return_value=iter([]))
|
||||
|
|
@ -263,39 +258,33 @@ def _stream_invoke_completion_with_spied_client(
|
|||
aws_region_name="us-east-1",
|
||||
**kwargs,
|
||||
)
|
||||
return mock_response.iter_bytes, client.post, recorder
|
||||
return mock_response.iter_bytes, client.post
|
||||
|
||||
|
||||
def test_completion_stream_chunk_size_reaches_iter_bytes_but_not_invoke_body(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
):
|
||||
iter_bytes_spy, post_spy, recorder = _stream_invoke_completion_with_spied_client(monkeypatch, stream_chunk_size=64)
|
||||
def test_completion_stream_chunk_size_reaches_iter_bytes_but_not_invoke_body() -> None:
|
||||
iter_bytes_spy, post_spy = _stream_invoke_completion_with_spied_client(stream_chunk_size=64)
|
||||
|
||||
iter_bytes_spy.assert_called_once_with(chunk_size=64)
|
||||
data: Final = post_spy.call_args.kwargs["data"]
|
||||
assert "stream_chunk_size" not in keys_at_every_depth(json.loads(data)), data
|
||||
assert len(recorder.seen) == 1
|
||||
assert recorder.seen[0]["stream_chunk_size"] == 64
|
||||
|
||||
|
||||
def test_completion_without_stream_chunk_size_uses_default_chunking(monkeypatch: pytest.MonkeyPatch):
|
||||
iter_bytes_spy, _, recorder = _stream_invoke_completion_with_spied_client(monkeypatch)
|
||||
@pytest.mark.parametrize("request_kwargs", DEFAULT_CHUNKING_REQUESTS)
|
||||
def test_completion_uses_default_chunking_unless_a_valid_size_is_requested(
|
||||
request_kwargs: Mapping[str, object],
|
||||
) -> None:
|
||||
iter_bytes_spy, _ = _stream_invoke_completion_with_spied_client(**request_kwargs)
|
||||
|
||||
iter_bytes_spy.assert_called_once_with(chunk_size=None)
|
||||
assert len(recorder.seen) == 1
|
||||
assert recorder.seen[0]["stream_chunk_size"] is None
|
||||
|
||||
|
||||
async def _astream_invoke_completion_with_spied_client(
|
||||
monkeypatch: pytest.MonkeyPatch, **kwargs
|
||||
) -> tuple[MagicMock, AsyncMock, LitellmParamsRecorder]:
|
||||
async def _astream_invoke_completion_with_spied_client(**kwargs: object) -> tuple[MagicMock, AsyncMock]:
|
||||
async def _no_bytes():
|
||||
return
|
||||
yield b""
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
recorder: Final = record_litellm_params(monkeypatch)
|
||||
mock_response.aiter_bytes = MagicMock(return_value=_no_bytes())
|
||||
aiter_bytes_spy = mock_response.aiter_bytes
|
||||
client = AsyncHTTPHandler()
|
||||
|
|
@ -311,57 +300,49 @@ async def _astream_invoke_completion_with_spied_client(
|
|||
aws_region_name="us-east-1",
|
||||
**kwargs,
|
||||
)
|
||||
return aiter_bytes_spy, client.post, recorder
|
||||
return aiter_bytes_spy, client.post
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_acompletion_stream_chunk_size_reaches_aiter_bytes_but_not_invoke_body(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
):
|
||||
aiter_bytes_spy, post_spy, recorder = await _astream_invoke_completion_with_spied_client(
|
||||
monkeypatch, stream_chunk_size=64
|
||||
)
|
||||
async def test_acompletion_stream_chunk_size_reaches_aiter_bytes_but_not_invoke_body() -> None:
|
||||
aiter_bytes_spy, post_spy = await _astream_invoke_completion_with_spied_client(stream_chunk_size=64)
|
||||
|
||||
aiter_bytes_spy.assert_called_once_with(chunk_size=64)
|
||||
data: Final = post_spy.call_args.kwargs["data"]
|
||||
assert "stream_chunk_size" not in keys_at_every_depth(json.loads(data)), data
|
||||
assert len(recorder.seen) == 1
|
||||
assert recorder.seen[0]["stream_chunk_size"] == 64
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_acompletion_without_stream_chunk_size_uses_default_chunking(monkeypatch: pytest.MonkeyPatch):
|
||||
aiter_bytes_spy, _, recorder = await _astream_invoke_completion_with_spied_client(monkeypatch)
|
||||
@pytest.mark.parametrize("request_kwargs", DEFAULT_CHUNKING_REQUESTS)
|
||||
async def test_acompletion_uses_default_chunking_unless_a_valid_size_is_requested(
|
||||
request_kwargs: Mapping[str, object],
|
||||
) -> None:
|
||||
aiter_bytes_spy, _ = await _astream_invoke_completion_with_spied_client(**request_kwargs)
|
||||
|
||||
aiter_bytes_spy.assert_called_once_with(chunk_size=None)
|
||||
assert len(recorder.seen) == 1
|
||||
assert recorder.seen[0]["stream_chunk_size"] is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("stream_chunk_size,expected_chunk_size", [(64, 64), (None, None)])
|
||||
def test_router_deployment_stream_chunk_size_reaches_iter_bytes(
|
||||
monkeypatch: pytest.MonkeyPatch, stream_chunk_size, expected_chunk_size
|
||||
):
|
||||
recorder: Final = record_litellm_params(monkeypatch)
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.iter_bytes = MagicMock(return_value=iter([]))
|
||||
client = HTTPHandler()
|
||||
client.post = MagicMock(return_value=mock_response)
|
||||
deployment_params = {
|
||||
INVOKE_DEPLOYMENT: Final = MappingProxyType(
|
||||
{
|
||||
"model": "bedrock/invoke/anthropic.claude-haiku-4-5-20251001-v1:0",
|
||||
"aws_access_key_id": "fake",
|
||||
"aws_secret_access_key": "fake",
|
||||
"aws_region_name": "us-east-1",
|
||||
}
|
||||
router = litellm.Router(
|
||||
model_list=[
|
||||
{
|
||||
"model_name": "invoke-chunked",
|
||||
"litellm_params": deployment_params
|
||||
| ({} if stream_chunk_size is None else {"stream_chunk_size": stream_chunk_size}),
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("deployment_extras,expected_chunk_size", ROUTER_CHUNK_SIZE_CASES)
|
||||
def test_router_deployment_stream_chunk_size_reaches_iter_bytes(
|
||||
deployment_extras: Mapping[str, object], expected_chunk_size: int | None
|
||||
) -> None:
|
||||
mock_response: Final = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.iter_bytes = MagicMock(return_value=iter([]))
|
||||
client: Final = HTTPHandler()
|
||||
client.post = MagicMock(return_value=mock_response)
|
||||
router: Final = litellm.Router(
|
||||
model_list=[{"model_name": "invoke-chunked", "litellm_params": {**INVOKE_DEPLOYMENT, **deployment_extras}}]
|
||||
)
|
||||
|
||||
router.completion(
|
||||
|
|
@ -374,17 +355,11 @@ def test_router_deployment_stream_chunk_size_reaches_iter_bytes(
|
|||
mock_response.iter_bytes.assert_called_once_with(chunk_size=expected_chunk_size)
|
||||
data: Final = client.post.call_args.kwargs["data"]
|
||||
assert "stream_chunk_size" not in keys_at_every_depth(json.loads(data)), data
|
||||
assert len(recorder.seen) == 1
|
||||
assert recorder.seen[0]["stream_chunk_size"] == stream_chunk_size
|
||||
|
||||
|
||||
def test_stream_wrapper_rejects_non_int_stream_chunk_size(monkeypatch: pytest.MonkeyPatch):
|
||||
record_litellm_params(monkeypatch)
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.iter_bytes = MagicMock(return_value=iter([]))
|
||||
client = HTTPHandler()
|
||||
client.post = MagicMock(return_value=mock_response)
|
||||
def test_invoke_stream_rejects_non_int_stream_chunk_size_before_calling_bedrock() -> None:
|
||||
send: Final = MagicMock(return_value=httpx.Response(200))
|
||||
client: Final = HTTPHandler(client=httpx.Client(transport=httpx.MockTransport(send)))
|
||||
|
||||
with pytest.raises(litellm.BadRequestError):
|
||||
litellm.completion(
|
||||
|
|
@ -398,4 +373,28 @@ def test_stream_wrapper_rejects_non_int_stream_chunk_size(monkeypatch: pytest.Mo
|
|||
stream_chunk_size="sixty-four",
|
||||
)
|
||||
|
||||
client.post.assert_not_called()
|
||||
send.assert_not_called()
|
||||
|
||||
|
||||
def test_router_deployment_with_a_non_numeric_stream_chunk_size_gets_a_400_before_calling_bedrock() -> None:
|
||||
send: Final = MagicMock(return_value=httpx.Response(200))
|
||||
client: Final = HTTPHandler(client=httpx.Client(transport=httpx.MockTransport(send)))
|
||||
router: Final = litellm.Router(
|
||||
model_list=[
|
||||
{
|
||||
"model_name": "invoke-chunked",
|
||||
"litellm_params": {**INVOKE_DEPLOYMENT, "stream_chunk_size": "sixty-four"},
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
with pytest.raises(litellm.BadRequestError) as exc_info:
|
||||
router.completion(
|
||||
model="invoke-chunked",
|
||||
messages=[{"role": "user", "content": "hi"}],
|
||||
stream=True,
|
||||
client=client,
|
||||
)
|
||||
|
||||
assert exc_info.value.status_code == 400
|
||||
send.assert_not_called()
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
import pytest
|
||||
|
||||
from litellm.llms.bedrock.common_utils import BedrockError, stream_chunk_size_from
|
||||
|
||||
|
||||
def test_stream_chunk_size_from_absent_is_none():
|
||||
assert stream_chunk_size_from({}) is None
|
||||
|
||||
|
||||
def test_stream_chunk_size_from_int_is_returned():
|
||||
assert stream_chunk_size_from({"stream_chunk_size": 64}) == 64
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bad_value", ["64", 6.4, True])
|
||||
def test_stream_chunk_size_from_rejects_non_int_with_400(bad_value):
|
||||
with pytest.raises(BedrockError) as excinfo:
|
||||
stream_chunk_size_from({"stream_chunk_size": bad_value})
|
||||
|
||||
assert excinfo.value.status_code == 400
|
||||
assert repr(bad_value) in excinfo.value.message
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import json
|
||||
from collections.abc import AsyncIterator
|
||||
from collections.abc import AsyncIterator, Mapping
|
||||
from types import MappingProxyType
|
||||
from typing import Final
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
|
|
@ -11,11 +12,7 @@ from litellm.llms.bedrock.chat import BedrockConverseLLM
|
|||
from litellm.llms.bedrock.chat.converse_handler import make_sync_call
|
||||
from litellm.llms.bedrock.common_utils import _get_all_bedrock_regions
|
||||
from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler
|
||||
from tests._support.stream_chunk_size import (
|
||||
LitellmParamsRecorder,
|
||||
keys_at_every_depth,
|
||||
record_litellm_params,
|
||||
)
|
||||
from tests._support.stream_chunk_size import DEFAULT_CHUNKING_REQUESTS, ROUTER_CHUNK_SIZE_CASES, keys_at_every_depth
|
||||
|
||||
|
||||
def test_encode_model_id_with_inference_profile():
|
||||
|
|
@ -319,10 +316,7 @@ def test_completion_plumbs_stream_chunk_size_through_converse() -> None:
|
|||
iter_bytes_spy.assert_called_once_with(chunk_size=2048)
|
||||
|
||||
|
||||
def _stream_converse_completion_with_spied_client(
|
||||
monkeypatch: pytest.MonkeyPatch, stream_chunk_size: int | None = None
|
||||
) -> tuple[MagicMock, MagicMock, LitellmParamsRecorder]:
|
||||
recorder: Final = record_litellm_params(monkeypatch)
|
||||
def _stream_converse_completion_with_spied_client(**request: object) -> tuple[MagicMock, MagicMock]:
|
||||
mock_response: Final = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.iter_bytes = MagicMock(return_value=iter([]))
|
||||
|
|
@ -337,43 +331,35 @@ def _stream_converse_completion_with_spied_client(
|
|||
aws_access_key_id="fake",
|
||||
aws_secret_access_key="fake",
|
||||
aws_region_name="us-east-1",
|
||||
stream_chunk_size=stream_chunk_size,
|
||||
**request,
|
||||
)
|
||||
return mock_response.iter_bytes, client.post, recorder
|
||||
return mock_response.iter_bytes, client.post
|
||||
|
||||
|
||||
def test_completion_stream_chunk_size_reaches_iter_bytes_but_not_converse_body(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
iter_bytes_spy, post_spy, recorder = _stream_converse_completion_with_spied_client(
|
||||
monkeypatch, stream_chunk_size=64
|
||||
)
|
||||
def test_completion_stream_chunk_size_reaches_iter_bytes_but_not_converse_body() -> None:
|
||||
iter_bytes_spy, post_spy = _stream_converse_completion_with_spied_client(stream_chunk_size=64)
|
||||
|
||||
iter_bytes_spy.assert_called_once_with(chunk_size=64)
|
||||
data: Final = post_spy.call_args.kwargs["data"]
|
||||
assert "stream_chunk_size" not in keys_at_every_depth(json.loads(data)), data
|
||||
assert len(recorder.seen) == 1
|
||||
assert recorder.seen[0]["stream_chunk_size"] == 64
|
||||
|
||||
|
||||
def test_completion_without_stream_chunk_size_uses_default_chunking(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
iter_bytes_spy, _, recorder = _stream_converse_completion_with_spied_client(monkeypatch)
|
||||
@pytest.mark.parametrize("request_kwargs", DEFAULT_CHUNKING_REQUESTS)
|
||||
def test_completion_uses_default_chunking_unless_a_valid_size_is_requested(
|
||||
request_kwargs: Mapping[str, object],
|
||||
) -> None:
|
||||
iter_bytes_spy, _ = _stream_converse_completion_with_spied_client(**request_kwargs)
|
||||
|
||||
iter_bytes_spy.assert_called_once_with(chunk_size=None)
|
||||
assert len(recorder.seen) == 1
|
||||
assert recorder.seen[0]["stream_chunk_size"] is None
|
||||
|
||||
|
||||
async def _astream_converse_completion_with_spied_client(
|
||||
monkeypatch: pytest.MonkeyPatch, stream_chunk_size: int | None = None
|
||||
) -> tuple[MagicMock, AsyncMock, LitellmParamsRecorder]:
|
||||
async def _astream_converse_completion_with_spied_client(**request: object) -> tuple[MagicMock, AsyncMock]:
|
||||
async def _no_bytes(chunk_size: int | None = None) -> AsyncIterator[bytes]:
|
||||
return
|
||||
yield b""
|
||||
|
||||
mock_response: Final = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
recorder: Final = record_litellm_params(monkeypatch)
|
||||
mock_response.aiter_bytes = MagicMock(return_value=_no_bytes())
|
||||
aiter_bytes_spy: Final = mock_response.aiter_bytes
|
||||
client: Final = AsyncHTTPHandler()
|
||||
|
|
@ -387,61 +373,51 @@ async def _astream_converse_completion_with_spied_client(
|
|||
aws_access_key_id="fake",
|
||||
aws_secret_access_key="fake",
|
||||
aws_region_name="us-east-1",
|
||||
stream_chunk_size=stream_chunk_size,
|
||||
**request,
|
||||
)
|
||||
return aiter_bytes_spy, client.post, recorder
|
||||
return aiter_bytes_spy, client.post
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_acompletion_stream_chunk_size_reaches_aiter_bytes_but_not_converse_body(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
aiter_bytes_spy, post_spy, recorder = await _astream_converse_completion_with_spied_client(
|
||||
monkeypatch, stream_chunk_size=64
|
||||
)
|
||||
async def test_acompletion_stream_chunk_size_reaches_aiter_bytes_but_not_converse_body() -> None:
|
||||
aiter_bytes_spy, post_spy = await _astream_converse_completion_with_spied_client(stream_chunk_size=64)
|
||||
|
||||
aiter_bytes_spy.assert_called_once_with(chunk_size=64)
|
||||
data: Final = post_spy.call_args.kwargs["data"]
|
||||
assert "stream_chunk_size" not in keys_at_every_depth(json.loads(data)), data
|
||||
assert len(recorder.seen) == 1
|
||||
assert recorder.seen[0]["stream_chunk_size"] == 64
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_acompletion_without_stream_chunk_size_uses_default_chunking(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
@pytest.mark.parametrize("request_kwargs", DEFAULT_CHUNKING_REQUESTS)
|
||||
async def test_acompletion_uses_default_chunking_unless_a_valid_size_is_requested(
|
||||
request_kwargs: Mapping[str, object],
|
||||
) -> None:
|
||||
aiter_bytes_spy, _, recorder = await _astream_converse_completion_with_spied_client(monkeypatch)
|
||||
aiter_bytes_spy, _ = await _astream_converse_completion_with_spied_client(**request_kwargs)
|
||||
|
||||
aiter_bytes_spy.assert_called_once_with(chunk_size=None)
|
||||
assert len(recorder.seen) == 1
|
||||
assert recorder.seen[0]["stream_chunk_size"] is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("stream_chunk_size,expected_chunk_size", [(64, 64), (None, None)])
|
||||
def test_router_deployment_stream_chunk_size_reaches_iter_bytes(
|
||||
monkeypatch: pytest.MonkeyPatch, stream_chunk_size: int | None, expected_chunk_size: int | None
|
||||
) -> None:
|
||||
recorder: Final = record_litellm_params(monkeypatch)
|
||||
mock_response: Final = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.iter_bytes = MagicMock(return_value=iter([]))
|
||||
client: Final = HTTPHandler()
|
||||
client.post = MagicMock(return_value=mock_response)
|
||||
deployment_params: Final = {
|
||||
CONVERSE_DEPLOYMENT: Final = MappingProxyType(
|
||||
{
|
||||
"model": "bedrock/converse/anthropic.claude-haiku-4-5-20251001-v1:0",
|
||||
"aws_access_key_id": "fake",
|
||||
"aws_secret_access_key": "fake",
|
||||
"aws_region_name": "us-east-1",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("deployment_extras,expected_chunk_size", ROUTER_CHUNK_SIZE_CASES)
|
||||
def test_router_deployment_stream_chunk_size_reaches_iter_bytes(
|
||||
deployment_extras: Mapping[str, object], expected_chunk_size: int | None
|
||||
) -> None:
|
||||
mock_response: Final = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.iter_bytes = MagicMock(return_value=iter([]))
|
||||
client: Final = HTTPHandler()
|
||||
client.post = MagicMock(return_value=mock_response)
|
||||
router: Final = litellm.Router(
|
||||
model_list=[
|
||||
{
|
||||
"model_name": "converse-chunked",
|
||||
"litellm_params": deployment_params
|
||||
| ({} if stream_chunk_size is None else {"stream_chunk_size": stream_chunk_size}),
|
||||
}
|
||||
]
|
||||
model_list=[{"model_name": "converse-chunked", "litellm_params": {**CONVERSE_DEPLOYMENT, **deployment_extras}}]
|
||||
)
|
||||
|
||||
router.completion(
|
||||
|
|
@ -454,20 +430,18 @@ def test_router_deployment_stream_chunk_size_reaches_iter_bytes(
|
|||
mock_response.iter_bytes.assert_called_once_with(chunk_size=expected_chunk_size)
|
||||
data: Final = client.post.call_args.kwargs["data"]
|
||||
assert "stream_chunk_size" not in keys_at_every_depth(json.loads(data)), data
|
||||
assert len(recorder.seen) == 1
|
||||
assert recorder.seen[0]["stream_chunk_size"] == stream_chunk_size
|
||||
|
||||
|
||||
def test_converse_stream_rejects_non_int_stream_chunk_size_before_calling_bedrock(monkeypatch: pytest.MonkeyPatch):
|
||||
record_litellm_params(monkeypatch)
|
||||
client = HTTPHandler()
|
||||
client.post = MagicMock()
|
||||
@pytest.mark.parametrize("stream", [True, False], ids=["stream", "non_stream"])
|
||||
def test_converse_rejects_non_int_stream_chunk_size_before_calling_bedrock(stream: bool) -> None:
|
||||
send: Final = MagicMock(return_value=httpx.Response(200))
|
||||
client: Final = HTTPHandler(client=httpx.Client(transport=httpx.MockTransport(send)))
|
||||
|
||||
with pytest.raises(litellm.BadRequestError):
|
||||
litellm.completion(
|
||||
model="bedrock/converse/anthropic.claude-haiku-4-5-20251001-v1:0",
|
||||
messages=[{"role": "user", "content": "hi"}],
|
||||
stream=True,
|
||||
stream=stream,
|
||||
client=client,
|
||||
aws_access_key_id="fake",
|
||||
aws_secret_access_key="fake",
|
||||
|
|
@ -475,30 +449,7 @@ def test_converse_stream_rejects_non_int_stream_chunk_size_before_calling_bedroc
|
|||
stream_chunk_size="sixty-four",
|
||||
)
|
||||
|
||||
client.post.assert_not_called()
|
||||
|
||||
|
||||
def test_converse_non_stream_ignores_invalid_stream_chunk_size():
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json = MagicMock(return_value=_converse_response_body())
|
||||
mock_response.text = json.dumps(_converse_response_body())
|
||||
mock_response.headers = httpx.Headers()
|
||||
client = HTTPHandler()
|
||||
client.post = MagicMock(return_value=mock_response)
|
||||
|
||||
response = litellm.completion(
|
||||
model="bedrock/converse/anthropic.claude-haiku-4-5-20251001-v1:0",
|
||||
messages=[{"role": "user", "content": "hi"}],
|
||||
client=client,
|
||||
aws_access_key_id="fake",
|
||||
aws_secret_access_key="fake",
|
||||
aws_region_name="us-east-1",
|
||||
stream_chunk_size="64",
|
||||
)
|
||||
|
||||
assert response.choices[0].message.content == "hi"
|
||||
client.post.assert_called_once()
|
||||
send.assert_not_called()
|
||||
|
||||
|
||||
def _bedrock_error_response(status_code: int, request_id: str) -> httpx.Response:
|
||||
|
|
|
|||
|
|
@ -1247,6 +1247,7 @@ class TestOCIStreamingSignedBody:
|
|||
mock_logging = MagicMock()
|
||||
|
||||
config.get_sync_custom_stream_wrapper(
|
||||
litellm_params={},
|
||||
api_base="https://example.com",
|
||||
headers={},
|
||||
data={"key": "value"},
|
||||
|
|
@ -1286,6 +1287,7 @@ class TestOCIStreamingSignedBody:
|
|||
payload = {"key": "value"}
|
||||
|
||||
config.get_sync_custom_stream_wrapper(
|
||||
litellm_params={},
|
||||
api_base="https://example.com",
|
||||
headers={},
|
||||
data=payload,
|
||||
|
|
|
|||
|
|
@ -1111,6 +1111,7 @@ def test_get_sync_custom_stream_wrapper_returns_wrapper():
|
|||
mock_client.post.return_value = mock_response
|
||||
|
||||
wrapper = config.get_sync_custom_stream_wrapper(
|
||||
litellm_params={},
|
||||
model=_GENERIC_MODEL,
|
||||
custom_llm_provider="oci",
|
||||
logging_obj=MagicMock(),
|
||||
|
|
@ -1143,6 +1144,7 @@ async def test_get_async_custom_stream_wrapper_returns_wrapper():
|
|||
mock_client.post = AsyncMock(return_value=mock_response)
|
||||
|
||||
wrapper = await config.get_async_custom_stream_wrapper(
|
||||
litellm_params={},
|
||||
model=_GENERIC_MODEL,
|
||||
custom_llm_provider="oci",
|
||||
logging_obj=MagicMock(),
|
||||
|
|
|
|||
|
|
@ -125,6 +125,7 @@ def test_sync_first_event_emitted_after_a_single_frame():
|
|||
response = httpx.Response(200, stream=stream)
|
||||
|
||||
wrapper = SagemakerChatConfig().get_sync_custom_stream_wrapper(
|
||||
litellm_params={},
|
||||
model="phi-4",
|
||||
custom_llm_provider="sagemaker_chat",
|
||||
logging_obj=MagicMock(),
|
||||
|
|
@ -147,6 +148,7 @@ def test_sync_events_emitted_incrementally_without_bursting():
|
|||
response = httpx.Response(200, stream=stream)
|
||||
|
||||
wrapper = SagemakerChatConfig().get_sync_custom_stream_wrapper(
|
||||
litellm_params={},
|
||||
model="phi-4",
|
||||
custom_llm_provider="sagemaker_chat",
|
||||
logging_obj=MagicMock(),
|
||||
|
|
@ -171,6 +173,7 @@ async def test_async_first_event_emitted_after_a_single_frame():
|
|||
response = httpx.Response(200, stream=stream)
|
||||
|
||||
wrapper = await SagemakerChatConfig().get_async_custom_stream_wrapper(
|
||||
litellm_params={},
|
||||
model="phi-4",
|
||||
custom_llm_provider="sagemaker_chat",
|
||||
logging_obj=MagicMock(),
|
||||
|
|
|
|||
|
|
@ -309,6 +309,7 @@ class TestSagemakerChatBackwardsCompatibility:
|
|||
) as mock_csw:
|
||||
mock_csw.return_value = MagicMock()
|
||||
self.config.get_sync_custom_stream_wrapper(
|
||||
litellm_params={},
|
||||
model="my-hf-endpoint",
|
||||
custom_llm_provider="sagemaker_chat",
|
||||
logging_obj=MagicMock(),
|
||||
|
|
@ -348,6 +349,7 @@ class TestSagemakerChatBackwardsCompatibility:
|
|||
mock_csw.return_value = MagicMock()
|
||||
asyncio.run(
|
||||
self.config.get_async_custom_stream_wrapper(
|
||||
litellm_params={},
|
||||
model="my-hf-endpoint",
|
||||
custom_llm_provider="sagemaker_chat",
|
||||
logging_obj=MagicMock(),
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ from typing import Final
|
|||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import httpx
|
||||
import openai
|
||||
import pytest
|
||||
import respx
|
||||
|
||||
|
|
@ -592,3 +593,20 @@ class TestUseResponsesApiBridgeFlag:
|
|||
|
||||
mock_native_handler.assert_called_once()
|
||||
assert result is not None
|
||||
|
||||
def test_bridge_still_rejects_an_invalid_stream_chunk_size(self) -> None:
|
||||
send: Final = MagicMock(return_value=httpx.Response(200))
|
||||
client: Final = openai.OpenAI(api_key="fake-key", http_client=httpx.Client(transport=httpx.MockTransport(send)))
|
||||
|
||||
with pytest.raises(litellm.BadRequestError) as exc_info:
|
||||
litellm.responses(
|
||||
model="openai/gpt-4.1-mini",
|
||||
input="hi",
|
||||
use_chat_completions_api=True,
|
||||
stream_chunk_size="sixty-four",
|
||||
client=client,
|
||||
num_retries=0,
|
||||
)
|
||||
|
||||
assert exc_info.value.param == "stream_chunk_size"
|
||||
send.assert_not_called()
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@
|
|||
Test filter_out_litellm_params helper function.
|
||||
"""
|
||||
|
||||
from typing import Final
|
||||
|
||||
|
||||
import litellm
|
||||
from litellm.utils import filter_out_litellm_params
|
||||
|
||||
|
||||
|
|
@ -34,3 +38,19 @@ def test_filter_out_litellm_params():
|
|||
assert "litellm_trace_id" not in filtered
|
||||
assert "proxy_server_request" not in filtered
|
||||
assert "secret_fields" not in filtered
|
||||
|
||||
|
||||
def test_filter_out_litellm_params_also_drops_the_excluded_names():
|
||||
kwargs = {"temperature": 0.2, "top_k": 5, "litellm_trace_id": "trace-1", "_litellm_control": object()}
|
||||
|
||||
assert filter_out_litellm_params(kwargs, excluding=("temperature",)) == {"top_k": 5}
|
||||
|
||||
|
||||
def test_filter_out_litellm_params_sees_a_name_appended_to_the_public_list_after_import():
|
||||
litellm.all_litellm_params.append("registered_later")
|
||||
try:
|
||||
filtered: Final = filter_out_litellm_params({"registered_later": 1, "top_k": 2})
|
||||
finally:
|
||||
litellm.all_litellm_params.remove("registered_later")
|
||||
|
||||
assert filtered == {"top_k": 2}
|
||||
|
|
|
|||
|
|
@ -22,10 +22,16 @@ from unittest.mock import MagicMock, patch
|
|||
|
||||
import litellm
|
||||
from litellm import main as litellm_main
|
||||
from litellm.constants import CONTROL_OPTIONS_KEY
|
||||
from litellm.integrations.custom_logger import CustomLogger
|
||||
from litellm.integrations.custom_prompt_management import CustomPromptManagement
|
||||
from litellm.litellm_core_utils.core_helpers import get_litellm_metadata_from_kwargs
|
||||
from litellm.litellm_core_utils.get_litellm_params import stored_control_options
|
||||
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLogging
|
||||
from litellm.types.utils import Delta, ModelResponseStream, StreamingChoices, Usage
|
||||
from litellm.types.litellm_params import ControlOptions
|
||||
from litellm.types.llms.openai import AllMessageValues
|
||||
from litellm.types.prompts.init_prompts import PromptSpec
|
||||
from litellm.types.utils import Delta, ModelResponseStream, StandardCallbackDynamicParams, StreamingChoices, Usage
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
|
|
@ -4273,3 +4279,228 @@ def test_completion_rejects_untranslatable_tool_choice_with_a_400(tool_choice):
|
|||
)
|
||||
assert exc_info.value.status_code == 400
|
||||
assert f"tool_choice={tool_choice}" in str(exc_info.value)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("raw", ["sixty-four", 0, -1])
|
||||
def test_completion_rejects_an_invalid_stream_chunk_size_with_a_400_naming_the_param(raw: object) -> None:
|
||||
with pytest.raises(litellm.BadRequestError) as exc_info:
|
||||
litellm.completion(
|
||||
model="openai/gpt-4.1-mini",
|
||||
messages=[{"role": "user", "content": "hi"}],
|
||||
stream_chunk_size=raw,
|
||||
mock_response="unused",
|
||||
)
|
||||
assert exc_info.value.status_code == 400
|
||||
assert exc_info.value.param == "stream_chunk_size"
|
||||
assert f"Invalid stream_chunk_size={raw!r}: expected a positive integer of at most 18 digits" in str(exc_info.value)
|
||||
|
||||
|
||||
class _PromptHookRecorder(CustomPromptManagement):
|
||||
def __init__(self, on_prompt: MagicMock) -> None:
|
||||
super().__init__()
|
||||
self.on_prompt: Final = on_prompt
|
||||
|
||||
def get_chat_completion_prompt(
|
||||
self,
|
||||
model: str,
|
||||
messages: list[AllMessageValues],
|
||||
non_default_params: dict,
|
||||
prompt_id: str | None,
|
||||
prompt_variables: dict | None,
|
||||
dynamic_callback_params: StandardCallbackDynamicParams,
|
||||
prompt_spec: PromptSpec | None = None,
|
||||
prompt_label: str | None = None,
|
||||
prompt_version: int | None = None,
|
||||
ignore_prompt_manager_model: bool | None = False,
|
||||
ignore_prompt_manager_optional_params: bool | None = False,
|
||||
) -> tuple[str, list[AllMessageValues], dict]:
|
||||
self.on_prompt("sync")
|
||||
return model, messages, non_default_params
|
||||
|
||||
async def async_get_chat_completion_prompt(
|
||||
self,
|
||||
model: str,
|
||||
messages: list[AllMessageValues],
|
||||
non_default_params: dict,
|
||||
prompt_id: str | None,
|
||||
prompt_variables: dict | None,
|
||||
dynamic_callback_params: StandardCallbackDynamicParams,
|
||||
litellm_logging_obj: LiteLLMLogging,
|
||||
prompt_spec: PromptSpec | None = None,
|
||||
tools: list[dict] | None = None,
|
||||
prompt_label: str | None = None,
|
||||
prompt_version: int | None = None,
|
||||
ignore_prompt_manager_model: bool | None = False,
|
||||
ignore_prompt_manager_optional_params: bool | None = False,
|
||||
) -> tuple[str, list[AllMessageValues], dict]:
|
||||
self.on_prompt("async")
|
||||
return model, messages, non_default_params
|
||||
|
||||
|
||||
async def _call_completion(is_async: bool, **kwargs: object) -> None:
|
||||
if is_async:
|
||||
await litellm.acompletion(**kwargs)
|
||||
else:
|
||||
litellm.completion(**kwargs)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("is_async,hook", [(False, "sync"), (True, "async")], ids=["completion", "acompletion"])
|
||||
async def test_the_prompt_hook_runs_when_stream_chunk_size_is_valid(
|
||||
monkeypatch: pytest.MonkeyPatch, is_async: bool, hook: str
|
||||
) -> None:
|
||||
on_prompt: Final = MagicMock()
|
||||
monkeypatch.setattr(litellm, "callbacks", [_PromptHookRecorder(on_prompt)])
|
||||
|
||||
await _call_completion(
|
||||
is_async,
|
||||
model="openai/gpt-4.1-mini",
|
||||
messages=[{"role": "user", "content": "hi"}],
|
||||
prompt_id="greeting",
|
||||
stream_chunk_size=64,
|
||||
mock_response="hi",
|
||||
)
|
||||
|
||||
on_prompt.assert_any_call(hook)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("is_async", [False, True], ids=["completion", "acompletion"])
|
||||
async def test_an_invalid_stream_chunk_size_is_rejected_before_any_prompt_hook_runs(
|
||||
monkeypatch: pytest.MonkeyPatch, is_async: bool
|
||||
) -> None:
|
||||
on_prompt: Final = MagicMock()
|
||||
monkeypatch.setattr(litellm, "callbacks", [_PromptHookRecorder(on_prompt)])
|
||||
|
||||
with pytest.raises(litellm.BadRequestError):
|
||||
await _call_completion(
|
||||
is_async,
|
||||
model="openai/gpt-4.1-mini",
|
||||
messages=[{"role": "user", "content": "hi"}],
|
||||
prompt_id="greeting",
|
||||
stream_chunk_size="sixty-four",
|
||||
mock_response="hi",
|
||||
)
|
||||
|
||||
on_prompt.assert_not_called()
|
||||
|
||||
|
||||
def _completion_logging_obj(call_id: str) -> LiteLLMLogging:
|
||||
return LiteLLMLogging(
|
||||
model="gpt-4.1-mini",
|
||||
messages=[{"role": "user", "content": "hi"}],
|
||||
stream=False,
|
||||
call_type="completion",
|
||||
start_time=datetime(2026, 1, 1),
|
||||
litellm_call_id=call_id,
|
||||
function_id=f"{call_id}-function",
|
||||
)
|
||||
|
||||
|
||||
def test_completion_carries_the_control_options_into_the_logged_litellm_params() -> None:
|
||||
logging_obj: Final = _completion_logging_obj("control-params")
|
||||
litellm.completion(
|
||||
model="openai/gpt-4.1-mini",
|
||||
messages=[{"role": "user", "content": "hi"}],
|
||||
stream_chunk_size=64,
|
||||
mock_response="hi",
|
||||
litellm_logging_obj=logging_obj,
|
||||
)
|
||||
assert stored_control_options(logging_obj.litellm_params) == ControlOptions(stream_chunk_size=64)
|
||||
|
||||
|
||||
def test_completion_ignores_a_caller_supplied_control_options_key() -> None:
|
||||
logging_obj: Final = _completion_logging_obj("control-params-injection")
|
||||
litellm.completion(
|
||||
model="openai/gpt-4.1-mini",
|
||||
messages=[{"role": "user", "content": "hi"}],
|
||||
mock_response="hi",
|
||||
litellm_logging_obj=logging_obj,
|
||||
**{CONTROL_OPTIONS_KEY: {"stream_chunk_size": 1}},
|
||||
)
|
||||
assert stored_control_options(logging_obj.litellm_params) == ControlOptions()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("drop_params", [True, "true"])
|
||||
def test_drop_params_drops_an_invalid_stream_chunk_size_instead_of_rejecting_it(drop_params: object) -> None:
|
||||
logging_obj: Final = _completion_logging_obj(f"drop-params-{drop_params}")
|
||||
litellm.completion(
|
||||
model="openai/gpt-4.1-mini",
|
||||
messages=[{"role": "user", "content": "hi"}],
|
||||
stream_chunk_size="sixty-four",
|
||||
drop_params=drop_params,
|
||||
mock_response="hi",
|
||||
litellm_logging_obj=logging_obj,
|
||||
)
|
||||
assert stored_control_options(logging_obj.litellm_params) == ControlOptions()
|
||||
|
||||
|
||||
def test_drop_params_keeps_a_dropped_stream_chunk_size_out_of_the_provider_request(
|
||||
respx_mock: respx.MockRouter,
|
||||
) -> None:
|
||||
api_base: Final = "http://localhost:12346/v1"
|
||||
mock_route: Final = respx_mock.post(url__regex=rf"{api_base}/chat/completions.*").mock(
|
||||
return_value=httpx.Response(
|
||||
status_code=200,
|
||||
json={
|
||||
"id": "chatcmpl-drop",
|
||||
"object": "chat.completion",
|
||||
"created": 1712697600,
|
||||
"model": "gpt-4.1-mini",
|
||||
"choices": [{"index": 0, "message": {"role": "assistant", "content": "hi"}, "finish_reason": "stop"}],
|
||||
"usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2},
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
litellm.completion(
|
||||
model="openai/gpt-4.1-mini",
|
||||
messages=[{"role": "user", "content": "hi"}],
|
||||
api_base=api_base,
|
||||
api_key="fake_openai_api_key",
|
||||
stream_chunk_size="sixty-four",
|
||||
drop_params=True,
|
||||
)
|
||||
|
||||
assert mock_route.called
|
||||
sent: Final = json.loads(respx_mock.calls[0].request.content)
|
||||
assert "stream_chunk_size" not in sent, sent
|
||||
assert sent["model"] == "gpt-4.1-mini"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_global_drop_params_drops_an_invalid_stream_chunk_size_on_acompletion(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.setattr(litellm, "drop_params", True)
|
||||
logging_obj: Final = _completion_logging_obj("global-drop-params")
|
||||
await litellm.acompletion(
|
||||
model="openai/gpt-4.1-mini",
|
||||
messages=[{"role": "user", "content": "hi"}],
|
||||
stream_chunk_size=0,
|
||||
mock_response="hi",
|
||||
litellm_logging_obj=logging_obj,
|
||||
)
|
||||
assert stored_control_options(logging_obj.litellm_params) == ControlOptions()
|
||||
|
||||
|
||||
def test_completion_rejects_an_invalid_stream_chunk_size_before_the_mcp_gateway() -> None:
|
||||
with pytest.raises(litellm.BadRequestError) as exc_info:
|
||||
litellm.completion(
|
||||
model="openai/gpt-4.1-mini",
|
||||
messages=[{"role": "user", "content": "hi"}],
|
||||
tools=[{"type": "mcp", "server_label": "gateway", "server_url": "litellm_proxy"}],
|
||||
stream_chunk_size="sixty-four",
|
||||
)
|
||||
assert exc_info.value.param == "stream_chunk_size"
|
||||
|
||||
|
||||
def test_drop_params_false_still_rejects_an_invalid_stream_chunk_size() -> None:
|
||||
with pytest.raises(litellm.BadRequestError):
|
||||
litellm.completion(
|
||||
model="openai/gpt-4.1-mini",
|
||||
messages=[{"role": "user", "content": "hi"}],
|
||||
stream_chunk_size="sixty-four",
|
||||
drop_params=False,
|
||||
mock_response="hi",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -504,7 +504,8 @@ LEAF_SAMPLES: Final[Mapping[type, Mapping[str, object]]] = {
|
|||
litellm_params.AgenticLoopOptions: {"max_agentic_loops": 2},
|
||||
litellm_params.GuardrailOptions: {"guardrails": ("default",)},
|
||||
litellm_params.PromptOptions: {"prompt_id": "prompt", "prompt_variables": {"name": "value"}},
|
||||
litellm_params.ResponseOptions: {"stream_chunk_size": 64},
|
||||
litellm_params.ResponseOptions: {"keepalive_seconds": 1.5},
|
||||
litellm_params.ControlOptions: {"stream_chunk_size": 64},
|
||||
litellm_params.MockOptions: {"mock_timeout": True},
|
||||
litellm_params.CallState: {
|
||||
"completion_call_id": "call",
|
||||
|
|
@ -533,7 +534,8 @@ LEAF_BAD_SAMPLES: Final[Mapping[type, Mapping[str, object]]] = {
|
|||
litellm_params.AgenticLoopOptions: {"max_agentic_loops": "2"},
|
||||
litellm_params.GuardrailOptions: {"guardrails": (1,)},
|
||||
litellm_params.PromptOptions: {"prompt_id": 1},
|
||||
litellm_params.ResponseOptions: {"stream_chunk_size": "64"},
|
||||
litellm_params.ResponseOptions: {"keepalive_seconds": "1.5"},
|
||||
litellm_params.ControlOptions: {"stream_chunk_size": "sixty-four"},
|
||||
litellm_params.MockOptions: {"mock_timeout": "true"},
|
||||
litellm_params.CallState: {"completion_call_id": 1},
|
||||
litellm_params.AgenticLoopState: {"depth": "1"},
|
||||
|
|
@ -583,10 +585,8 @@ def test_every_owned_leaf_accepts_a_strict_reader_shaped_sample(leaf: type, samp
|
|||
|
||||
@pytest.mark.parametrize("leaf,sample", LEAF_BAD_SAMPLES.items(), ids=_leaf_id)
|
||||
def test_every_owned_leaf_rejects_a_strict_wrong_typed_sample(leaf: type, sample: Mapping[str, object]) -> None:
|
||||
instance: Final = _leaf_instance(leaf, sample)
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
_strict_leaf_validation(leaf, instance)
|
||||
_strict_leaf_validation(leaf, _leaf_instance(leaf, sample))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("leaf,sample", INVALID_LITERAL_SAMPLES, ids=_leaf_id)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue