mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-07 02:59:05 +00:00
Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_databricks_claude_cache_pricing
This commit is contained in:
commit
5d8e42a759
1045 changed files with 3657 additions and 3959 deletions
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
|
|
@ -4,3 +4,4 @@
|
|||
/model_prices_and_context_window.json @mateo-berri
|
||||
/litellm/model_prices_and_context_window_backup.json @mateo-berri
|
||||
/litellm-proxy-extras/litellm_proxy_extras/migrations/ @yuneng-berri @ryan-crabbe-berri
|
||||
/.github/CODEOWNERS @yuneng-berri
|
||||
|
|
|
|||
2
.github/workflows/image-scan.yml
vendored
2
.github/workflows/image-scan.yml
vendored
|
|
@ -17,6 +17,8 @@ on:
|
|||
- backend/Dockerfile
|
||||
- backend/main.py
|
||||
- docker/component_entrypoint.sh
|
||||
- docker/entrypoint.sh
|
||||
- litellm/proxy/prisma_migration.py
|
||||
- litellm-proxy-extras/**
|
||||
- tests/proxy_migration_tests/**
|
||||
- uv.lock
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
# syntax=docker/dockerfile:1.7
|
||||
|
||||
# Base image for building
|
||||
ARG LITELLM_BUILD_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:42df77a9974d6ec8b17a5ee8bc23b532600a44d705acef2409e0933c1251b45f
|
||||
ARG LITELLM_BUILD_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:a31344ab2cb8618db84f535eec56f76f6178b142cb92cb2e48676cc2dcebea72
|
||||
|
||||
# Runtime image
|
||||
ARG LITELLM_RUNTIME_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:42df77a9974d6ec8b17a5ee8bc23b532600a44d705acef2409e0933c1251b45f
|
||||
ARG LITELLM_RUNTIME_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:a31344ab2cb8618db84f535eec56f76f6178b142cb92cb2e48676cc2dcebea72
|
||||
ARG UV_IMAGE=ghcr.io/astral-sh/uv:0.11.7@sha256:240fb85ab0f263ef12f492d8476aa3a2e4e1e333f7d67fbdd923d00a506a516a
|
||||
# Pinned by digest like the other base images; bump explicitly on Node upgrades.
|
||||
ARG UI_BUILD_IMAGE=node:24.19-alpine3.24@sha256:d32cdf619f63fe0471182d08996dd516c6275bb5fd31ae06e55a570bd9e1ad43
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
ARG LITELLM_BUILD_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:42df77a9974d6ec8b17a5ee8bc23b532600a44d705acef2409e0933c1251b45f
|
||||
ARG LITELLM_RUNTIME_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:42df77a9974d6ec8b17a5ee8bc23b532600a44d705acef2409e0933c1251b45f
|
||||
ARG LITELLM_BUILD_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:a31344ab2cb8618db84f535eec56f76f6178b142cb92cb2e48676cc2dcebea72
|
||||
ARG LITELLM_RUNTIME_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:a31344ab2cb8618db84f535eec56f76f6178b142cb92cb2e48676cc2dcebea72
|
||||
ARG UV_IMAGE=ghcr.io/astral-sh/uv:0.11.7@sha256:240fb85ab0f263ef12f492d8476aa3a2e4e1e333f7d67fbdd923d00a506a516a
|
||||
|
||||
FROM $UV_IMAGE AS uvbin
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
# syntax=docker/dockerfile:1.7
|
||||
|
||||
# Base image for building
|
||||
ARG LITELLM_BUILD_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:42df77a9974d6ec8b17a5ee8bc23b532600a44d705acef2409e0933c1251b45f
|
||||
ARG LITELLM_BUILD_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:a31344ab2cb8618db84f535eec56f76f6178b142cb92cb2e48676cc2dcebea72
|
||||
|
||||
# Runtime image
|
||||
ARG LITELLM_RUNTIME_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:42df77a9974d6ec8b17a5ee8bc23b532600a44d705acef2409e0933c1251b45f
|
||||
ARG LITELLM_RUNTIME_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:a31344ab2cb8618db84f535eec56f76f6178b142cb92cb2e48676cc2dcebea72
|
||||
ARG UV_IMAGE=ghcr.io/astral-sh/uv:0.11.7@sha256:240fb85ab0f263ef12f492d8476aa3a2e4e1e333f7d67fbdd923d00a506a516a
|
||||
# Pinned by digest like the other base images; bump explicitly on Node upgrades.
|
||||
ARG UI_BUILD_IMAGE=node:24.19-alpine3.24@sha256:d32cdf619f63fe0471182d08996dd516c6275bb5fd31ae06e55a570bd9e1ad43
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
# syntax=docker/dockerfile:1.7
|
||||
|
||||
# Base images
|
||||
ARG LITELLM_BUILD_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:42df77a9974d6ec8b17a5ee8bc23b532600a44d705acef2409e0933c1251b45f
|
||||
ARG LITELLM_RUNTIME_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:42df77a9974d6ec8b17a5ee8bc23b532600a44d705acef2409e0933c1251b45f
|
||||
ARG LITELLM_BUILD_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:a31344ab2cb8618db84f535eec56f76f6178b142cb92cb2e48676cc2dcebea72
|
||||
ARG LITELLM_RUNTIME_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:a31344ab2cb8618db84f535eec56f76f6178b142cb92cb2e48676cc2dcebea72
|
||||
ARG PROXY_EXTRAS_SOURCE=published
|
||||
ARG UV_IMAGE=ghcr.io/astral-sh/uv:0.11.7@sha256:240fb85ab0f263ef12f492d8476aa3a2e4e1e333f7d67fbdd923d00a506a516a
|
||||
# Pinned by digest like the other base images; bump explicitly on Node upgrades.
|
||||
|
|
|
|||
|
|
@ -45,6 +45,8 @@ from litellm.proxy._types import (
|
|||
UserAPIKeyAuth,
|
||||
)
|
||||
from litellm.proxy.openai_files_endpoints.common_utils import (
|
||||
FILE_LIST_CONTINUATION_CHUNK_SIZE,
|
||||
MAX_FILE_LIST_LIMIT,
|
||||
_is_base64_encoded_unified_file_id,
|
||||
apply_unified_file_ids,
|
||||
ensure_batch_response_managed_file_ids,
|
||||
|
|
@ -54,6 +56,8 @@ from litellm.proxy.openai_files_endpoints.common_utils import (
|
|||
map_raw_file_ids_to_unified,
|
||||
normalize_mime_type_for_provider,
|
||||
resolve_managed_output_file_model_name,
|
||||
validate_file_list_limit,
|
||||
validate_file_list_purpose,
|
||||
)
|
||||
from litellm.proxy.pass_through_endpoints.llm_provider_handlers.batch_attribution import (
|
||||
request_tags_from_metadata,
|
||||
|
|
@ -63,9 +67,9 @@ from litellm.types.llms.openai import ( # pyright: ignore[reportAttributeAccess
|
|||
AsyncCursorPage,
|
||||
ChatCompletionFileObject,
|
||||
CreateFileRequest,
|
||||
FileListPage,
|
||||
FileObject,
|
||||
OpenAIFileObject,
|
||||
OpenAIFilesPurpose,
|
||||
ResponsesAPIResponse,
|
||||
)
|
||||
from litellm.types.utils import (
|
||||
|
|
@ -144,7 +148,14 @@ class _ManagedFileRow(Protocol):
|
|||
class _ManagedFileTableActions(Protocol):
|
||||
async def find_first(self, where: Mapping[str, object]) -> Optional[_ManagedFileRow]: ...
|
||||
|
||||
async def find_many(self, where: Mapping[str, object]) -> Sequence[_ManagedFileRow]: ...
|
||||
async def find_many(
|
||||
self,
|
||||
where: Mapping[str, object],
|
||||
take: int = ...,
|
||||
order: Union[Mapping[str, str], Sequence[Mapping[str, str]]] = ...,
|
||||
cursor: Mapping[str, str] = ...,
|
||||
skip: int = ...,
|
||||
) -> Sequence[_ManagedFileRow]: ...
|
||||
|
||||
async def upsert(self, where: Mapping[str, str], data: Mapping[str, Mapping[str, object]]) -> _ManagedFileRow: ...
|
||||
|
||||
|
|
@ -1365,12 +1376,76 @@ class _PROXY_LiteLLMManagedFiles(CustomLogger, BaseFileEndpoints):
|
|||
|
||||
async def afile_list(
|
||||
self,
|
||||
purpose: Optional[OpenAIFilesPurpose],
|
||||
purpose: Optional[str],
|
||||
litellm_parent_otel_span: Optional[Span],
|
||||
user_api_key_dict: UserAPIKeyAuth,
|
||||
limit: Optional[int] = None,
|
||||
after: Optional[str] = None,
|
||||
**data: Dict,
|
||||
) -> List[OpenAIFileObject]:
|
||||
"""Handled in files_endpoints.py"""
|
||||
return []
|
||||
) -> FileListPage:
|
||||
"""List the managed files the caller owns, newest first.
|
||||
|
||||
Pagination is keyset based on ``unified_file_id`` so a key that owns
|
||||
every file on the proxy still reads one bounded page at a time.
|
||||
``purpose`` is applied after parsing, because the managed file table
|
||||
keeps it inside the ``file_object`` blob instead of a column, and rows
|
||||
whose blob will not parse drop out there too, so a chunk of rows can
|
||||
yield fewer matches than the page holds. Successive chunks are read
|
||||
until the page is full or the caller's rows run out, which keeps
|
||||
``data`` non-empty while matches remain and its last id usable as the
|
||||
next cursor. A first chunk that fills the page costs one query; once a
|
||||
scan has to continue past it, the chunk widens to
|
||||
``FILE_LIST_CONTINUATION_CHUNK_SIZE``, so the walk costs one query per
|
||||
that many rows instead of one per page. That bound is per query, not
|
||||
per request: the work is still linear in the rows the caller owns, and
|
||||
a filter matching nothing reads every one of them, with no index
|
||||
covering either the owner filter or the sort.
|
||||
"""
|
||||
validate_file_list_limit(limit)
|
||||
validate_file_list_purpose(purpose)
|
||||
|
||||
owner_filter: Final = build_owner_filter(user_api_key_dict)
|
||||
if owner_filter is None:
|
||||
return FileListPage(**build_list_page([]))
|
||||
|
||||
if after:
|
||||
cursor_row = await _managed_file_table(self.prisma_client).find_first(
|
||||
where={**owner_filter, "unified_file_id": after}
|
||||
)
|
||||
if cursor_row is None:
|
||||
raise ProxyException(
|
||||
message=f"Invalid 'after' cursor: no file found with id '{after}'.",
|
||||
type="invalid_request_error",
|
||||
param="after",
|
||||
code=400,
|
||||
openai_code="invalid_value",
|
||||
)
|
||||
|
||||
page_size: Final = min(limit or MAX_FILE_LIST_LIMIT, MAX_FILE_LIST_LIMIT)
|
||||
matches: Final[List[OpenAIFileObject]] = []
|
||||
cursor_id = after
|
||||
chunk_size = page_size + 1
|
||||
|
||||
while len(matches) <= page_size:
|
||||
cursor_args: _CursorPageArgs = {"cursor": {"unified_file_id": cursor_id}, "skip": 1} if cursor_id else {}
|
||||
chunk = await _managed_file_table(self.prisma_client).find_many(
|
||||
where=owner_filter,
|
||||
take=chunk_size,
|
||||
order=[{"created_at": "desc"}, {"unified_file_id": "desc"}],
|
||||
**cursor_args,
|
||||
)
|
||||
matches.extend(
|
||||
parsed_file_object.model_copy(update={"id": row.unified_file_id})
|
||||
for row in chunk
|
||||
if (parsed_file_object := _parse_managed_file_object(row.file_object, row.unified_file_id)) is not None
|
||||
and (purpose is None or parsed_file_object.purpose == purpose)
|
||||
)
|
||||
if len(chunk) < chunk_size:
|
||||
break
|
||||
cursor_id = chunk[-1].unified_file_id
|
||||
chunk_size = max(chunk_size, FILE_LIST_CONTINUATION_CHUNK_SIZE)
|
||||
|
||||
return FileListPage(**build_list_page(matches[:page_size], has_more=len(matches) > page_size))
|
||||
|
||||
def _is_batch_polling_enabled(self) -> bool:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[project]
|
||||
name = "litellm-enterprise"
|
||||
version = "0.1.58"
|
||||
version = "0.1.59"
|
||||
description = "Package for LiteLLM Enterprise features"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.9"
|
||||
|
|
@ -26,7 +26,7 @@ required-version = ">=0.10.9"
|
|||
module-root = ""
|
||||
|
||||
[tool.commitizen]
|
||||
version = "0.1.58"
|
||||
version = "0.1.59"
|
||||
version_files = [
|
||||
"pyproject.toml:^version",
|
||||
"../pyproject.toml:litellm-enterprise==",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
ARG LITELLM_BUILD_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:42df77a9974d6ec8b17a5ee8bc23b532600a44d705acef2409e0933c1251b45f
|
||||
ARG LITELLM_RUNTIME_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:42df77a9974d6ec8b17a5ee8bc23b532600a44d705acef2409e0933c1251b45f
|
||||
ARG LITELLM_BUILD_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:a31344ab2cb8618db84f535eec56f76f6178b142cb92cb2e48676cc2dcebea72
|
||||
ARG LITELLM_RUNTIME_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:a31344ab2cb8618db84f535eec56f76f6178b142cb92cb2e48676cc2dcebea72
|
||||
ARG UV_IMAGE=ghcr.io/astral-sh/uv:0.11.7@sha256:240fb85ab0f263ef12f492d8476aa3a2e4e1e333f7d67fbdd923d00a506a516a
|
||||
|
||||
FROM $UV_IMAGE AS uvbin
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[project]
|
||||
name = "litellm-proxy-extras"
|
||||
version = "0.4.88"
|
||||
version = "0.4.89"
|
||||
description = "Additional files for the LiteLLM Proxy. Reduces the size of the main litellm package."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.9"
|
||||
|
|
@ -26,7 +26,7 @@ required-version = ">=0.10.9"
|
|||
module-root = ""
|
||||
|
||||
[tool.commitizen]
|
||||
version = "0.4.88"
|
||||
version = "0.4.89"
|
||||
version_files = [
|
||||
"pyproject.toml:^version",
|
||||
"../pyproject.toml:litellm-proxy-extras==",
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ from litellm.integrations.otel.model.semconv import (
|
|||
Error,
|
||||
GenAI,
|
||||
GenAIOperation,
|
||||
GenAIOutputType,
|
||||
GenAIProvider,
|
||||
JsonRpc,
|
||||
LiteLLM,
|
||||
|
|
@ -60,6 +61,7 @@ from litellm.integrations.otel.model.semconv import (
|
|||
RpcSystem,
|
||||
Server,
|
||||
resolve_operation,
|
||||
resolve_output_type,
|
||||
resolve_provider,
|
||||
)
|
||||
from litellm.integrations.otel.model.spans import (
|
||||
|
|
@ -84,6 +86,7 @@ __all__ = [
|
|||
"Error",
|
||||
"GenAI",
|
||||
"GenAIOperation",
|
||||
"GenAIOutputType",
|
||||
"GenAIProvider",
|
||||
"GuardrailSpanData",
|
||||
"JsonRpc",
|
||||
|
|
@ -116,6 +119,7 @@ __all__ = [
|
|||
"is_otel_v2_enabled",
|
||||
"promoted_baggage",
|
||||
"resolve_operation",
|
||||
"resolve_output_type",
|
||||
"resolve_provider",
|
||||
"span_role_for_service",
|
||||
"validate_registry",
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ class GenAIMapper:
|
|||
_LLM_CALL_ATTRS: dict[str, Callable[[LLMCallSpanData], AttrValue | None]] = {
|
||||
GenAI.OPERATION_NAME: lambda d: d.operation.value,
|
||||
GenAI.PROVIDER_NAME: lambda d: d.provider or None,
|
||||
GenAI.OUTPUT_TYPE: lambda d: d.output_type.value if d.output_type else None,
|
||||
GenAI.REQUEST_MODEL: lambda d: d.request_model or None,
|
||||
GenAI.REQUEST_TEMPERATURE: lambda d: d.request_params.temperature,
|
||||
GenAI.REQUEST_TOP_P: lambda d: d.request_params.top_p,
|
||||
|
|
@ -65,6 +66,7 @@ class GenAIMapper:
|
|||
Server.ADDRESS: lambda d: d.server.address if d.server else None,
|
||||
Server.PORT: lambda d: d.server.port if d.server else None,
|
||||
LiteLLM.CALL_ID: lambda d: d.identity.call_id or None,
|
||||
LiteLLM.CALL_TYPE: lambda d: d.call_type,
|
||||
# The provider/underlying model is only known once routing has picked a
|
||||
# deployment, so it can't ride identity Baggage (seeded at auth, before
|
||||
# routing) onto the boundary-born LLM span — stamp it directly here.
|
||||
|
|
|
|||
|
|
@ -15,8 +15,10 @@ from litellm.integrations.otel.model.metadata import (
|
|||
)
|
||||
from litellm.integrations.otel.model.semconv import (
|
||||
GenAIOperation,
|
||||
GenAIOutputType,
|
||||
MCPMethod,
|
||||
resolve_operation,
|
||||
resolve_output_type,
|
||||
resolve_provider,
|
||||
)
|
||||
from litellm.integrations.otel.model.utils import (
|
||||
|
|
@ -310,6 +312,11 @@ class LLMCallSpanData:
|
|||
choices_out: tuple[Mapping[str, object], ...] = ()
|
||||
system_fingerprint: str | None = None
|
||||
time_to_first_chunk_seconds: float | None = None
|
||||
# The requested output modality, set only on the routes that pin one (image
|
||||
# generation, speech, transcription, OCR), and the litellm route itself, which
|
||||
# keeps routes the convention folds into one operation distinguishable.
|
||||
output_type: GenAIOutputType | None = None
|
||||
call_type: str | None = None
|
||||
|
||||
@classmethod
|
||||
def from_standard_logging_payload(
|
||||
|
|
@ -334,8 +341,9 @@ class LLMCallSpanData:
|
|||
# otherwise the content-bearing mappers receive empty sequences and emit
|
||||
# no prompt/response text.
|
||||
finish_reasons: Final = _finish_reasons(choices_out)
|
||||
call_type: Final = as_str(payload.get("call_type"))
|
||||
return cls(
|
||||
operation=resolve_operation(as_str(payload.get("call_type"))),
|
||||
operation=resolve_operation(call_type),
|
||||
provider=resolve_provider(as_str(payload.get("custom_llm_provider"))),
|
||||
request_model=context.request_model,
|
||||
response_model=context.response_model,
|
||||
|
|
@ -358,6 +366,8 @@ class LLMCallSpanData:
|
|||
choices_out=choices_out if capture_content else (),
|
||||
system_fingerprint=as_str(response.get("system_fingerprint")),
|
||||
time_to_first_chunk_seconds=time_to_first_chunk_seconds,
|
||||
output_type=resolve_output_type(call_type),
|
||||
call_type=call_type or None,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@ Keys follow the OpenTelemetry GenAI semantic conventions (experimental). Anythin
|
|||
without a semconv equivalent lives under the ``litellm.*`` vendor namespace.
|
||||
"""
|
||||
|
||||
from collections.abc import Mapping
|
||||
from enum import Enum
|
||||
from types import MappingProxyType
|
||||
from typing import Final
|
||||
|
||||
from litellm._logging import verbose_logger
|
||||
|
|
@ -30,6 +32,21 @@ class GenAIOperation(str, Enum):
|
|||
EXECUTE_TOOL = "execute_tool" # MCP tool-call spans
|
||||
LITELLM_VECTOR_STORE_MANAGEMENT = "litellm.vector_store_management"
|
||||
LITELLM_VECTOR_STORE_FILE_MANAGEMENT = "litellm.vector_store_file_management"
|
||||
LITELLM_MODERATION = "litellm.moderation"
|
||||
|
||||
|
||||
class GenAIOutputType(str, Enum):
|
||||
"""Values for ``gen_ai.output.type``, the modality the client asked for.
|
||||
|
||||
It is what separates the inference routes that share ``generate_content``:
|
||||
image generation requests ``image``, speech requests ``speech``, and
|
||||
transcription and OCR both request ``text``.
|
||||
"""
|
||||
|
||||
TEXT = "text"
|
||||
JSON = "json"
|
||||
IMAGE = "image"
|
||||
SPEECH = "speech"
|
||||
|
||||
|
||||
class GenAIProvider(str, Enum):
|
||||
|
|
@ -258,6 +275,11 @@ class LiteLLM:
|
|||
"""Vendor-extension keys (no semconv equivalent). Always ``litellm.*``."""
|
||||
|
||||
CALL_ID: Final = "litellm.call_id"
|
||||
# The litellm route that produced the call. Needed because the convention maps
|
||||
# several routes onto one operation: transcription and OCR are both
|
||||
# ``generate_content`` with a ``text`` output type, so this is the only thing
|
||||
# that tells them apart.
|
||||
CALL_TYPE: Final = "litellm.call_type"
|
||||
COST_PREFIX: Final = "litellm.cost."
|
||||
METADATA_PREFIX: Final = "litellm.metadata."
|
||||
TEAM_ID: Final = "litellm.team.id"
|
||||
|
|
@ -352,6 +374,16 @@ _OPERATION_BY_CALL_TYPE: Final[dict[str, GenAIOperation]] = {
|
|||
"aembedding": GenAIOperation.EMBEDDINGS,
|
||||
"responses": GenAIOperation.CHAT,
|
||||
"aresponses": GenAIOperation.CHAT,
|
||||
"image_generation": GenAIOperation.GENERATE_CONTENT,
|
||||
"aimage_generation": GenAIOperation.GENERATE_CONTENT,
|
||||
"moderation": GenAIOperation.LITELLM_MODERATION,
|
||||
"amoderation": GenAIOperation.LITELLM_MODERATION,
|
||||
"ocr": GenAIOperation.GENERATE_CONTENT,
|
||||
"aocr": GenAIOperation.GENERATE_CONTENT,
|
||||
"speech": GenAIOperation.GENERATE_CONTENT,
|
||||
"aspeech": GenAIOperation.GENERATE_CONTENT,
|
||||
"transcription": GenAIOperation.GENERATE_CONTENT,
|
||||
"atranscription": GenAIOperation.GENERATE_CONTENT,
|
||||
"call_mcp_tool": GenAIOperation.EXECUTE_TOOL,
|
||||
"vector_store_search": GenAIOperation.RETRIEVAL,
|
||||
"avector_store_search": GenAIOperation.RETRIEVAL,
|
||||
|
|
@ -385,6 +417,23 @@ _OPERATION_BY_CALL_TYPE: Final[dict[str, GenAIOperation]] = {
|
|||
}
|
||||
|
||||
|
||||
# litellm ``call_type`` -> ``gen_ai.output.type``. Only the call types whose route
|
||||
# fixes the requested modality are listed; the attribute is conditionally required
|
||||
# on a request that asks for an output format, so anything else is left unstamped.
|
||||
_OUTPUT_TYPE_BY_CALL_TYPE: Final[Mapping[str, GenAIOutputType]] = MappingProxyType(
|
||||
{
|
||||
"image_generation": GenAIOutputType.IMAGE,
|
||||
"aimage_generation": GenAIOutputType.IMAGE,
|
||||
"speech": GenAIOutputType.SPEECH,
|
||||
"aspeech": GenAIOutputType.SPEECH,
|
||||
"transcription": GenAIOutputType.TEXT,
|
||||
"atranscription": GenAIOutputType.TEXT,
|
||||
"ocr": GenAIOutputType.TEXT,
|
||||
"aocr": GenAIOutputType.TEXT,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def resolve_provider(custom_llm_provider: str | None) -> str:
|
||||
"""Map a litellm provider string to a ``gen_ai.provider.name`` value.
|
||||
|
||||
|
|
@ -416,3 +465,11 @@ def resolve_operation(call_type: str | None) -> GenAIOperation:
|
|||
GenAIOperation.CHAT.value,
|
||||
)
|
||||
return GenAIOperation.CHAT
|
||||
|
||||
|
||||
def resolve_output_type(call_type: str | None) -> GenAIOutputType | None:
|
||||
"""Map a litellm ``call_type`` to a ``gen_ai.output.type`` value, or ``None``
|
||||
for a route that doesn't pin the output modality."""
|
||||
if not call_type:
|
||||
return None
|
||||
return _OUTPUT_TYPE_BY_CALL_TYPE.get(call_type.lower())
|
||||
|
|
|
|||
|
|
@ -207,6 +207,59 @@ response = await litellm.messages.acreate(
|
|||
|
||||
---
|
||||
|
||||
## Loop Ceiling
|
||||
|
||||
One intercepted request can chain several follow-up model calls, since the model often searches again after
|
||||
reading the first set of results. `max_agentic_loops` caps how many of those follow-ups run, and it defaults
|
||||
to 3. LiteLLM also breaks the loop early when the model asks for the exact same tool call twice in a row.
|
||||
|
||||
Set the ceiling on the feature, which the interceptor applies to `/v1/messages` requests:
|
||||
|
||||
```yaml
|
||||
litellm_settings:
|
||||
websearch_interception_params:
|
||||
enabled_providers: ["bedrock"]
|
||||
max_agentic_loops: 5
|
||||
```
|
||||
|
||||
Or per deployment, which wins over the feature-level setting:
|
||||
|
||||
```yaml
|
||||
model_list:
|
||||
- model_name: claude-sonnet-4-5
|
||||
litellm_params:
|
||||
model: bedrock/us.anthropic.claude-sonnet-4-5-20250929-v1:0
|
||||
max_agentic_loops: 5
|
||||
```
|
||||
|
||||
Clients cannot set it. `max_agentic_loops` is on the proxy's untrusted-field list, so a request body that
|
||||
carries it is ignored and one request can never drive an unbounded number of upstream model calls.
|
||||
|
||||
Both places are validated at config load, and a value that is not an integer of at least 1 stops the proxy
|
||||
from starting rather than surfacing later. The per-deployment one is checked while the model list is read,
|
||||
not on `LiteLLM_Params`, because the proxy builds its router with `ignore_invalid_deployments=True` and a
|
||||
validator down there would drop the deployment silently instead of refusing to start.
|
||||
|
||||
When the ceiling is reached on a non-streaming `/v1/messages` request, the turn ends there and the client gets
|
||||
the last response back with the internal `litellm_web_search` tool call removed and `stop_reason: end_turn`.
|
||||
The client never declared that tool, so leaving the block in would hand it a tool call it has no way to answer.
|
||||
The answer can be less complete than it would have been with more loops, which is the tradeoff the ceiling
|
||||
buys. Where the refused call was the only block left, the turn comes back with no text in it at all.
|
||||
|
||||
Non-streaming is not a limitation on the client here, because a client that asked for a stream gets the same
|
||||
treatment. Interception converts an intercepted `stream=True` request to non-streaming before the loop runs and
|
||||
rebuilds the SSE stream from the finalized turn afterwards, so the ceiling is always reached on a response the
|
||||
client has not seen yet. `AgenticStreamingIterator` is the one caller that reaches the loop with its events
|
||||
already on the wire, and it keeps raising, because a finalized turn would arrive there as a second message
|
||||
rather than as a replacement.
|
||||
|
||||
Two other surfaces do not get that treatment yet. `/v1/responses` returns its own shape that the finalizer does
|
||||
not rewrite, so it still hands back the internal call. And `/v1/chat/completions` runs its own copy of these
|
||||
rails in `litellm_core_utils/chat_completion_agentic_loop.py`, which still raises rather than ending the turn.
|
||||
Both are tracked separately
|
||||
|
||||
---
|
||||
|
||||
## Streaming Support
|
||||
|
||||
WebSearch interception works transparently with both streaming and non-streaming requests.
|
||||
|
|
|
|||
|
|
@ -31,6 +31,9 @@ from litellm.integrations.websearch_interception.tools import (
|
|||
from litellm.integrations.websearch_interception.transformation import (
|
||||
WebSearchTransformation,
|
||||
)
|
||||
from litellm.litellm_core_utils.agentic_loop_settings import (
|
||||
validated_max_agentic_loops,
|
||||
)
|
||||
from litellm.llms.base_llm.search.transformation import SearchResponse
|
||||
from litellm.types.integrations.custom_logger import (
|
||||
CHAT_COMPLETION_AGENTIC_SURFACE,
|
||||
|
|
@ -122,6 +125,7 @@ class WebSearchInterceptionLogger(CustomLogger):
|
|||
self,
|
||||
enabled_providers: list[LlmProviders | str] | None = None,
|
||||
search_tool_name: str | None = None,
|
||||
max_agentic_loops: int | None = None,
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
|
|
@ -131,6 +135,9 @@ class WebSearchInterceptionLogger(CustomLogger):
|
|||
Default: None (all providers enabled)
|
||||
search_tool_name: Name of search tool configured in router's search_tools.
|
||||
If None, will attempt to use first available search tool.
|
||||
max_agentic_loops: How many follow-up model calls one intercepted request
|
||||
may chain before the loop is refused and the turn ends.
|
||||
If None, LiteLLM's default of 3 applies.
|
||||
"""
|
||||
super().__init__()
|
||||
# Convert enum values to strings for comparison
|
||||
|
|
@ -139,8 +146,16 @@ class WebSearchInterceptionLogger(CustomLogger):
|
|||
else:
|
||||
self.enabled_providers = [p.value if isinstance(p, LlmProviders) else p for p in enabled_providers]
|
||||
self.search_tool_name = search_tool_name
|
||||
self.max_agentic_loops = self._validated_max_agentic_loops(max_agentic_loops)
|
||||
self._request_has_websearch = False # Track if current request has web search
|
||||
|
||||
@staticmethod
|
||||
def _validated_max_agentic_loops(max_agentic_loops: object) -> int | None:
|
||||
"""
|
||||
Reject loop ceilings the agentic loop cannot honor, at config load time.
|
||||
"""
|
||||
return validated_max_agentic_loops(max_agentic_loops, field="websearch_interception_params.max_agentic_loops")
|
||||
|
||||
async def try_short_circuit_search(
|
||||
self,
|
||||
model: str,
|
||||
|
|
@ -398,6 +413,7 @@ class WebSearchInterceptionLogger(CustomLogger):
|
|||
websearch_interception_params:
|
||||
enabled_providers: ["bedrock"]
|
||||
search_tool_name: "my-perplexity-search"
|
||||
max_agentic_loops: 5
|
||||
|
||||
Usage:
|
||||
config = litellm_settings.get("websearch_interception_params", {})
|
||||
|
|
@ -406,6 +422,7 @@ class WebSearchInterceptionLogger(CustomLogger):
|
|||
# Extract parameters from config
|
||||
enabled_providers_str: Final = config.get("enabled_providers", None)
|
||||
search_tool_name: Final = config.get("search_tool_name", None)
|
||||
max_agentic_loops: Final = config.get("max_agentic_loops", None)
|
||||
|
||||
# Convert string provider names to LlmProviders enum values
|
||||
enabled_providers: list[LlmProviders | str] | None = None
|
||||
|
|
@ -423,6 +440,7 @@ class WebSearchInterceptionLogger(CustomLogger):
|
|||
return cls(
|
||||
enabled_providers=enabled_providers,
|
||||
search_tool_name=search_tool_name,
|
||||
max_agentic_loops=max_agentic_loops,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
|
|
@ -493,6 +511,10 @@ class WebSearchInterceptionLogger(CustomLogger):
|
|||
|
||||
verbose_logger.debug("WebSearchInterception: Pre-request hook triggered for provider=%s", custom_llm_provider)
|
||||
|
||||
deployment_max_agentic_loops: Final = kwargs.get("max_agentic_loops")
|
||||
if self.max_agentic_loops is not None and deployment_max_agentic_loops is None:
|
||||
kwargs["max_agentic_loops"] = self.max_agentic_loops # rebind-ok: this hook returns the kwargs it edits
|
||||
|
||||
# If the client sent an Anthropic-native web_search_* tool, mark the
|
||||
# request so the agentic loop emits native web_search_tool_result
|
||||
# blocks in the final response (for citations panels, etc.). The flag
|
||||
|
|
|
|||
59
litellm/litellm_core_utils/agentic_loop_settings.py
Normal file
59
litellm/litellm_core_utils/agentic_loop_settings.py
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
"""
|
||||
Shared validation for the agentic loop ceiling.
|
||||
|
||||
``max_agentic_loops`` can be set in two places, and the two disagreed about
|
||||
what a bad value means. The feature-level
|
||||
``litellm_settings.websearch_interception_params.max_agentic_loops`` was
|
||||
checked at config load, while a per-deployment
|
||||
``model_list[].litellm_params.max_agentic_loops`` was passed straight through
|
||||
to ``int(... or 3)``. That let a per-deployment ``0`` read as the default 3,
|
||||
turning the tightest ceiling into the loosest one, and let a per-deployment
|
||||
``"three"`` boot the proxy and then fail every request to that model.
|
||||
|
||||
Both settings now go through :func:`validated_max_agentic_loops`, which names
|
||||
the field it rejected so the error says which line of the config to fix.
|
||||
|
||||
Anything that spells a whole number is still accepted, because the old
|
||||
``int(... or 3)`` accepted those and a ceiling is routinely parameterized as
|
||||
``max_agentic_loops: os.environ/MAX_AGENTIC_LOOPS``, which resolves to a
|
||||
string. Rejecting ``"5"`` would stop such a proxy from booting on upgrade.
|
||||
"""
|
||||
|
||||
from typing import Final
|
||||
|
||||
DEFAULT_MAX_AGENTIC_LOOPS: Final = 3
|
||||
|
||||
|
||||
def _as_whole_number(value: object) -> int | None:
|
||||
"""
|
||||
Return ``value`` as an int when it spells a whole number, else ``None``.
|
||||
|
||||
``bool`` is excluded explicitly because it is an ``int`` subclass, so
|
||||
``max_agentic_loops: true`` would otherwise be read as a ceiling of 1.
|
||||
"""
|
||||
if isinstance(value, bool):
|
||||
return None
|
||||
if isinstance(value, int):
|
||||
return value
|
||||
if isinstance(value, float):
|
||||
return int(value) if value.is_integer() else None
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
return int(value.strip())
|
||||
except ValueError:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def validated_max_agentic_loops(max_agentic_loops: object, field: str) -> int | None:
|
||||
"""
|
||||
Return ``max_agentic_loops`` as an int, or raise naming ``field``.
|
||||
"""
|
||||
if max_agentic_loops is None:
|
||||
return None
|
||||
ceiling: Final = _as_whole_number(max_agentic_loops)
|
||||
if ceiling is None:
|
||||
raise TypeError(f"{field} must be an integer, got {max_agentic_loops!r}")
|
||||
if ceiling < 1:
|
||||
raise ValueError(f"{field} must be at least 1, got {ceiling}")
|
||||
return ceiling
|
||||
|
|
@ -5,6 +5,10 @@ from typing import Final, cast
|
|||
|
||||
from litellm._logging import verbose_logger
|
||||
from litellm.integrations.custom_logger import CustomLogger
|
||||
from litellm.litellm_core_utils.agentic_loop_settings import (
|
||||
DEFAULT_MAX_AGENTIC_LOOPS,
|
||||
validated_max_agentic_loops,
|
||||
)
|
||||
from litellm.types.integrations.custom_logger import (
|
||||
CHAT_COMPLETION_AGENTIC_SURFACE,
|
||||
NON_CODE_INTERPRETER_INTERCEPTION_INTERNAL_PREFIXES,
|
||||
|
|
@ -52,7 +56,10 @@ def _coerce_int(value: object, default: int) -> int:
|
|||
|
||||
def _agentic_loop_settings(kwargs: dict[str, object]) -> tuple[int, int, list[str]]:
|
||||
depth: Final = _coerce_int(kwargs.get("_agentic_loop_depth"), 0)
|
||||
max_loops: Final = max(_coerce_int(kwargs.get("max_agentic_loops"), 3), 1)
|
||||
configured: Final = validated_max_agentic_loops(
|
||||
kwargs.get("max_agentic_loops"), field="litellm_params.max_agentic_loops"
|
||||
)
|
||||
max_loops: Final = DEFAULT_MAX_AGENTIC_LOOPS if configured is None else configured
|
||||
raw_fingerprints: Final = kwargs.get("_agentic_loop_fingerprints")
|
||||
fingerprints: Final = [str(fp) for fp in raw_fingerprints] if isinstance(raw_fingerprints, list) else []
|
||||
return depth, max_loops, fingerprints
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
from collections.abc import Sequence
|
||||
from typing import Final
|
||||
|
||||
from litellm.types.utils import ProviderSpecificHeader
|
||||
|
|
@ -6,13 +7,17 @@ from litellm.types.utils import ProviderSpecificHeader
|
|||
class ProviderSpecificHeaderUtils:
|
||||
@staticmethod
|
||||
def get_provider_specific_headers(
|
||||
provider_specific_header: ProviderSpecificHeader | None,
|
||||
provider_specific_header: ProviderSpecificHeader | Sequence[ProviderSpecificHeader] | None,
|
||||
custom_llm_provider: str | None,
|
||||
) -> dict:
|
||||
"""
|
||||
Get the provider specific headers for the given custom llm provider.
|
||||
|
||||
Supports comma-separated provider lists for headers that work across multiple providers.
|
||||
Accepts either a single ProviderSpecificHeader or a sequence of them. Each entry
|
||||
carries its own comma-separated provider list, so headers that are safe for several
|
||||
providers and headers that are safe for exactly one can travel on the same request
|
||||
without sharing a scope. Entries whose provider list does not contain
|
||||
`custom_llm_provider` contribute nothing.
|
||||
|
||||
Returns:
|
||||
Dict: The provider specific headers for the given custom llm provider
|
||||
|
|
@ -20,10 +25,15 @@ class ProviderSpecificHeaderUtils:
|
|||
if provider_specific_header is None or custom_llm_provider is None:
|
||||
return {}
|
||||
|
||||
stored_providers: Final = provider_specific_header.get("custom_llm_provider", "")
|
||||
provider_list: Final = [p.strip() for p in stored_providers.split(",")]
|
||||
scoped_headers: Final = (
|
||||
(provider_specific_header,) if isinstance(provider_specific_header, dict) else provider_specific_header
|
||||
)
|
||||
|
||||
if custom_llm_provider in provider_list:
|
||||
return provider_specific_header.get("extra_headers", {})
|
||||
matched_headers: Final = {}
|
||||
for scoped_header in scoped_headers:
|
||||
stored_providers = scoped_header.get("custom_llm_provider", "")
|
||||
provider_list = [p.strip() for p in stored_providers.split(",")]
|
||||
if custom_llm_provider in provider_list:
|
||||
matched_headers.update(scoped_header.get("extra_headers", {}))
|
||||
|
||||
return {}
|
||||
return matched_headers
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import functools
|
|||
import inspect
|
||||
import re
|
||||
import time
|
||||
from collections.abc import Mapping
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Any, Final
|
||||
|
||||
|
|
@ -268,6 +269,16 @@ def _set_duration_in_model_call_details(
|
|||
verbose_logger.warning("Error setting `llm_api_duration_ms`: %s", e)
|
||||
|
||||
|
||||
def speech_request_body(model: str, voice: str, optional_params: Mapping[str, object]) -> Mapping[str, object]:
|
||||
"""Speech request body for telemetry, without the caller headers the provider SDKs
|
||||
take as request kwargs rather than body fields."""
|
||||
return { # mutable-ok: loggers isinstance-check the request body as a dict
|
||||
"model": model,
|
||||
"voice": voice,
|
||||
**{key: value for key, value in optional_params.items() if key != "extra_headers"},
|
||||
}
|
||||
|
||||
|
||||
def track_llm_api_timing():
|
||||
"""
|
||||
Decorator to track LLM API call timing for both sync and async functions.
|
||||
|
|
|
|||
|
|
@ -124,6 +124,14 @@ def ptu_identity_error(
|
|||
return None
|
||||
|
||||
|
||||
PTU_MODEL_INFO_FIELDS: Final = ("ptu_count", "cost_per_ptu_per_hour", "ptu_effective_from", "ptu_effective_to")
|
||||
|
||||
|
||||
def declares_ptu(model_info: Mapping[str, object]) -> bool:
|
||||
"""Whether any PTU field is set here, including one too malformed to charge."""
|
||||
return any(model_info.get(field) is not None for field in PTU_MODEL_INFO_FIELDS)
|
||||
|
||||
|
||||
def ptu_config_error(model_info: Mapping[str, object], *, model_name: str | None = None) -> str | None:
|
||||
"""Why this PTU configuration cannot be honoured, else None.
|
||||
|
||||
|
|
|
|||
|
|
@ -113,6 +113,14 @@ class FakeAnthropicMessagesStreamIterator:
|
|||
}
|
||||
chunks.append(f"event: content_block_delta\ndata: {json.dumps(content_block_delta)}\n\n".encode())
|
||||
|
||||
else:
|
||||
passthrough_start: Final = {
|
||||
"type": "content_block_start",
|
||||
"index": index,
|
||||
"content_block": block_dict,
|
||||
}
|
||||
chunks.append(f"event: content_block_start\ndata: {json.dumps(passthrough_start)}\n\n".encode())
|
||||
|
||||
content_block_stop: Final = {"type": "content_block_stop", "index": index}
|
||||
chunks.append(f"event: content_block_stop\ndata: {json.dumps(content_block_stop)}\n\n".encode())
|
||||
return chunks
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ from openai import (
|
|||
import litellm
|
||||
from litellm.constants import AZURE_OPERATION_POLLING_TIMEOUT, DEFAULT_MAX_RETRIES
|
||||
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
|
||||
from litellm.litellm_core_utils.logging_utils import track_llm_api_timing
|
||||
from litellm.litellm_core_utils.logging_utils import speech_request_body, track_llm_api_timing
|
||||
from litellm.litellm_core_utils.url_utils import SSRFError, assert_same_origin
|
||||
from litellm.llms.custom_httpx.http_handler import (
|
||||
AsyncHTTPHandler,
|
||||
|
|
@ -1352,6 +1352,7 @@ class AzureChatCompletion(BaseAzureLLM, BaseLLM):
|
|||
organization: str | None,
|
||||
max_retries: int,
|
||||
timeout: float | httpx.Timeout,
|
||||
logging_obj: LiteLLMLoggingObj,
|
||||
azure_ad_token: str | None = None,
|
||||
azure_ad_token_provider: Callable | None = None,
|
||||
aspeech: bool | None = None,
|
||||
|
|
@ -1373,6 +1374,7 @@ class AzureChatCompletion(BaseAzureLLM, BaseLLM):
|
|||
azure_ad_token_provider=azure_ad_token_provider,
|
||||
max_retries=max_retries,
|
||||
timeout=timeout,
|
||||
logging_obj=logging_obj,
|
||||
client=client,
|
||||
litellm_params=litellm_params,
|
||||
)
|
||||
|
|
@ -1387,6 +1389,15 @@ class AzureChatCompletion(BaseAzureLLM, BaseLLM):
|
|||
litellm_params=litellm_params,
|
||||
)
|
||||
|
||||
logging_obj.pre_call(
|
||||
input=input,
|
||||
api_key=api_key,
|
||||
additional_args={ # mutable-ok: loggers isinstance-check this payload as a dict
|
||||
"complete_input_dict": speech_request_body(model, voice, optional_params),
|
||||
"api_base": str(azure_client.base_url),
|
||||
},
|
||||
)
|
||||
|
||||
response: Final = azure_client.audio.speech.create(
|
||||
model=model,
|
||||
voice=voice,
|
||||
|
|
@ -1408,6 +1419,7 @@ class AzureChatCompletion(BaseAzureLLM, BaseLLM):
|
|||
azure_ad_token_provider: Callable | None,
|
||||
max_retries: int,
|
||||
timeout: float | httpx.Timeout,
|
||||
logging_obj: LiteLLMLoggingObj,
|
||||
client=None,
|
||||
litellm_params: dict | None = None,
|
||||
) -> HttpxBinaryResponseContent:
|
||||
|
|
@ -1421,6 +1433,15 @@ class AzureChatCompletion(BaseAzureLLM, BaseLLM):
|
|||
litellm_params=litellm_params,
|
||||
)
|
||||
|
||||
logging_obj.pre_call(
|
||||
input=input,
|
||||
api_key=api_key,
|
||||
additional_args={ # mutable-ok: loggers isinstance-check this payload as a dict
|
||||
"complete_input_dict": speech_request_body(model, voice, optional_params),
|
||||
"api_base": str(azure_client.base_url),
|
||||
},
|
||||
)
|
||||
|
||||
azure_response: Final = await azure_client.audio.speech.create(
|
||||
model=model,
|
||||
voice=voice,
|
||||
|
|
|
|||
|
|
@ -11,9 +11,9 @@ from litellm.types.llms.openai import (
|
|||
AllMessageValues,
|
||||
CreateFileRequest,
|
||||
FileContentRequest,
|
||||
FileListPage,
|
||||
OpenAICreateFileRequestOptionalParams,
|
||||
OpenAIFileObject,
|
||||
OpenAIFilesPurpose,
|
||||
)
|
||||
from litellm.types.utils import LlmProviders, ModelResponse
|
||||
|
||||
|
|
@ -240,10 +240,13 @@ class BaseFileEndpoints(ABC):
|
|||
@abstractmethod
|
||||
async def afile_list(
|
||||
self,
|
||||
purpose: OpenAIFilesPurpose | None,
|
||||
purpose: str | None,
|
||||
litellm_parent_otel_span: Span | None,
|
||||
user_api_key_dict: UserAPIKeyAuth,
|
||||
limit: int | None = None,
|
||||
after: str | None = None,
|
||||
**data: dict,
|
||||
) -> list[OpenAIFileObject]:
|
||||
) -> FileListPage:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
|
|
|
|||
|
|
@ -19,6 +19,10 @@ import litellm.types.utils
|
|||
from litellm._logging import _redact_string, verbose_logger
|
||||
from litellm.anthropic_beta_headers_manager import update_headers_with_filtered_beta
|
||||
from litellm.constants import REALTIME_WEBSOCKET_MAX_MESSAGE_SIZE_BYTES
|
||||
from litellm.litellm_core_utils.agentic_loop_settings import (
|
||||
DEFAULT_MAX_AGENTIC_LOOPS,
|
||||
validated_max_agentic_loops,
|
||||
)
|
||||
from litellm.litellm_core_utils.asyncify import run_async_function
|
||||
from litellm.litellm_core_utils.realtime_errors import realtime_error_event, websocket_close_reason
|
||||
from litellm.litellm_core_utils.realtime_streaming import RealTimeStreaming
|
||||
|
|
@ -89,6 +93,7 @@ from litellm.types.files import StreamingMediaUploadConfig, TwoStepFileUploadCon
|
|||
from litellm.types.integrations.custom_logger import (
|
||||
AgenticLoopPlan,
|
||||
AgenticLoopRequestPatch,
|
||||
AgenticLoopSafetyError,
|
||||
)
|
||||
from litellm.types.llms.anthropic_messages.anthropic_response import (
|
||||
AnthropicMessagesResponse,
|
||||
|
|
@ -2056,7 +2061,7 @@ class BaseLLMHTTPHandler:
|
|||
# Prepare headers
|
||||
kwargs = kwargs or {}
|
||||
provider_specific_header: Final = cast(
|
||||
litellm.types.utils.ProviderSpecificHeader | None,
|
||||
litellm.types.utils.ProviderSpecificHeader | Sequence[litellm.types.utils.ProviderSpecificHeader] | None,
|
||||
kwargs.get("provider_specific_header", None),
|
||||
)
|
||||
provider_specific_headers: Final = ProviderSpecificHeaderUtils.get_provider_specific_headers(
|
||||
|
|
@ -5077,9 +5082,12 @@ class BaseLLMHTTPHandler:
|
|||
@staticmethod
|
||||
def _get_agentic_loop_settings(kwargs: dict) -> tuple[int, int, list[str]]:
|
||||
depth: Final = int(kwargs.get("_agentic_loop_depth", 0) or 0)
|
||||
max_loops: Final = int(kwargs.get("max_agentic_loops", 3) or 3)
|
||||
configured: Final = validated_max_agentic_loops(
|
||||
kwargs.get("max_agentic_loops"), field="litellm_params.max_agentic_loops"
|
||||
)
|
||||
max_loops: Final = DEFAULT_MAX_AGENTIC_LOOPS if configured is None else configured
|
||||
fingerprints: Final = list(kwargs.get("_agentic_loop_fingerprints", []) or [])
|
||||
return depth, max(max_loops, 1), fingerprints
|
||||
return depth, max_loops, fingerprints
|
||||
|
||||
@staticmethod
|
||||
def _has_agentic_completion_hook(logging_obj: LiteLLMLoggingObj) -> bool:
|
||||
|
|
@ -5122,7 +5130,8 @@ class BaseLLMHTTPHandler:
|
|||
"""
|
||||
Evaluate agentic-loop safety guards (fingerprint cycle / max depth).
|
||||
|
||||
Raises ValueError on abort. Returns the current fingerprint on success.
|
||||
Raises AgenticLoopSafetyError on abort. Returns the current fingerprint
|
||||
on success.
|
||||
|
||||
These checks must not be swallowed by the per-callback ``except Exception``
|
||||
block that wraps callback dispatch — they are bounded-loop / cycle-break
|
||||
|
|
@ -5130,9 +5139,9 @@ class BaseLLMHTTPHandler:
|
|||
"""
|
||||
fingerprint: Final = BaseLLMHTTPHandler._fingerprint_agentic_tools(tool_calls)
|
||||
if fingerprint in fingerprints:
|
||||
raise ValueError("Agentic loop detected repeated tool-call fingerprint; aborting rerun")
|
||||
raise AgenticLoopSafetyError("Agentic loop detected repeated tool-call fingerprint; aborting rerun")
|
||||
if depth >= max_loops:
|
||||
raise ValueError(f"Exceeded max_agentic_loops={max_loops} for model={model}")
|
||||
raise AgenticLoopSafetyError(f"Exceeded max_agentic_loops={max_loops} for model={model}")
|
||||
return fingerprint
|
||||
|
||||
@staticmethod
|
||||
|
|
@ -5142,6 +5151,97 @@ class BaseLLMHTTPHandler:
|
|||
except Exception:
|
||||
return str(tools)
|
||||
|
||||
@staticmethod
|
||||
def _refused_agentic_tool_identifiers(tool_calls: object) -> tuple[frozenset[str], frozenset[str]]:
|
||||
"""
|
||||
Collect the ids and names of the tool calls a safety rail just refused.
|
||||
|
||||
Callbacks hand back either a bare list of tool calls or a dict wrapping
|
||||
that list under ``tool_calls``, and both the anthropic and responses
|
||||
shapes carry an ``id`` (or ``call_id``) plus a ``name``.
|
||||
"""
|
||||
calls: Final = tool_calls.get("tool_calls") if isinstance(tool_calls, dict) else tool_calls
|
||||
if not isinstance(calls, list):
|
||||
return frozenset(), frozenset()
|
||||
dict_calls: Final = (call for call in calls if isinstance(call, dict))
|
||||
fields: Final = tuple((call.get("id"), call.get("call_id"), call.get("name")) for call in dict_calls)
|
||||
ids: Final = frozenset(
|
||||
value for call_id, caller_id, _ in fields for value in (call_id, caller_id) if isinstance(value, str)
|
||||
)
|
||||
names: Final = frozenset(name for _, _, name in fields if isinstance(name, str))
|
||||
return ids, names
|
||||
|
||||
@staticmethod
|
||||
def _is_refused_tool_use_block(block: object, refused_ids: frozenset[str], refused_names: frozenset[str]) -> bool:
|
||||
"""
|
||||
Whether this response block belongs to a tool call the rail refused.
|
||||
|
||||
An id settles it on its own, so a block carrying one is matched on the id
|
||||
alone and a client's own tool call survives even where it happens to
|
||||
share a name with a refused one. The name is only consulted for tool call
|
||||
shapes that arrive without an id.
|
||||
"""
|
||||
if not isinstance(block, dict) or block.get("type") != "tool_use":
|
||||
return False
|
||||
block_id: Final = block.get("id")
|
||||
if isinstance(block_id, str) and refused_ids:
|
||||
return block_id in refused_ids
|
||||
return block.get("name") in refused_names
|
||||
|
||||
@staticmethod
|
||||
def _can_replace_turn_with_terminal_response(stream: bool, api_surface: str) -> bool:
|
||||
"""
|
||||
Whether a refused rerun can still be answered with a finalized turn.
|
||||
|
||||
Only the anthropic messages surface can. The responses surface carries a
|
||||
pydantic model the finalizer does not rewrite, so it keeps raising, which
|
||||
is what every surface did before this path learned to end the turn.
|
||||
|
||||
The messages and responses call sites pass ``stream=False``, because
|
||||
interception converts an intercepted stream to non-streaming before the
|
||||
loop runs and rebuilds the SSE stream from the finalized turn
|
||||
afterwards. ``AgenticStreamingIterator`` passes ``stream=True``, and
|
||||
that path keeps raising: its events are already on the wire, so a
|
||||
finalized turn would reach the client as a second message rather than
|
||||
as a replacement.
|
||||
"""
|
||||
return not stream and api_surface == "anthropic_messages"
|
||||
|
||||
@staticmethod
|
||||
def _finalize_refused_agentic_response(response: object, tool_calls: object) -> object:
|
||||
"""
|
||||
Turn the response into a terminal turn after a safety rail refused the rerun.
|
||||
|
||||
The refused tool calls target tools LiteLLM injected on the client's
|
||||
behalf, so a client that never declared them cannot send back a matching
|
||||
``tool_result``. Their blocks are dropped and a ``tool_use`` stop reason
|
||||
is closed out as ``end_turn``, which is what a provider-native web search
|
||||
turn returns once it stops calling tools.
|
||||
|
||||
A ``tool_use`` block the client itself declared is left alone, and while
|
||||
one is still in the response the stop reason stays ``tool_use`` so the
|
||||
client knows to answer it.
|
||||
"""
|
||||
if not isinstance(response, dict):
|
||||
return response
|
||||
|
||||
refused_ids, refused_names = BaseLLMHTTPHandler._refused_agentic_tool_identifiers(tool_calls)
|
||||
finalized: Final = dict(response)
|
||||
content: Final = finalized.get("content")
|
||||
if isinstance(content, list):
|
||||
kept_blocks: Final = [
|
||||
block
|
||||
for block in content
|
||||
if not BaseLLMHTTPHandler._is_refused_tool_use_block(block, refused_ids, refused_names)
|
||||
]
|
||||
finalized["content"] = kept_blocks
|
||||
client_tool_use_remains: Final = any(
|
||||
isinstance(block, dict) and block.get("type") == "tool_use" for block in kept_blocks
|
||||
)
|
||||
if not client_tool_use_remains and finalized.get("stop_reason") == "tool_use":
|
||||
finalized["stop_reason"] = "end_turn"
|
||||
return finalized
|
||||
|
||||
async def _execute_anthropic_agentic_plan(
|
||||
self,
|
||||
plan: AgenticLoopPlan,
|
||||
|
|
@ -5507,14 +5607,30 @@ class BaseLLMHTTPHandler:
|
|||
continue
|
||||
|
||||
# Safety guards must run OUTSIDE the callback try/except — they are
|
||||
# bounded-loop / cycle-break rails that must propagate to the caller.
|
||||
fingerprint = self._check_agentic_loop_safety(
|
||||
tool_calls=tool_calls,
|
||||
fingerprints=fingerprints,
|
||||
depth=depth,
|
||||
max_loops=max_loops,
|
||||
model=model,
|
||||
)
|
||||
# bounded-loop / cycle-break rails, not callback bugs.
|
||||
try:
|
||||
fingerprint = self._check_agentic_loop_safety(
|
||||
tool_calls=tool_calls,
|
||||
fingerprints=fingerprints,
|
||||
depth=depth,
|
||||
max_loops=max_loops,
|
||||
model=model,
|
||||
)
|
||||
except AgenticLoopSafetyError as e:
|
||||
if not self._can_replace_turn_with_terminal_response(stream, api_surface):
|
||||
raise
|
||||
_call_id = getattr(logging_obj, "litellm_call_id", "unknown")
|
||||
verbose_logger.warning(
|
||||
"LiteLLM.AgenticLoopRefused: ending turn [call_id=%s model=%s]: %s",
|
||||
_call_id,
|
||||
model,
|
||||
str(e),
|
||||
)
|
||||
return self._maybe_wrap_in_fake_stream(
|
||||
self._finalize_refused_agentic_response(response=response, tool_calls=tool_calls),
|
||||
logging_obj,
|
||||
api_surface,
|
||||
)
|
||||
|
||||
try:
|
||||
kwargs_with_provider = hook_kwargs.copy()
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ from litellm._logging import verbose_logger
|
|||
from litellm.constants import DEFAULT_MAX_RETRIES
|
||||
from litellm.files.types import FileContentStreamingResult
|
||||
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
|
||||
from litellm.litellm_core_utils.logging_utils import track_llm_api_timing
|
||||
from litellm.litellm_core_utils.logging_utils import speech_request_body, track_llm_api_timing
|
||||
from litellm.llms.base_llm.base_model_iterator import BaseModelResponseIterator
|
||||
from litellm.llms.base_llm.chat.transformation import BaseConfig, BaseLLMException
|
||||
from litellm.llms.bedrock.chat.invoke_handler import MockResponseIterator
|
||||
|
|
@ -1365,9 +1365,21 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM):
|
|||
client=client,
|
||||
)
|
||||
|
||||
if headers:
|
||||
data["extra_headers"] = headers
|
||||
response = await openai_aclient.images.generate(**data, timeout=timeout)
|
||||
logging_obj.pre_call(
|
||||
input=prompt,
|
||||
api_key=openai_aclient.api_key,
|
||||
additional_args={ # mutable-ok: loggers isinstance-check this payload as a dict
|
||||
"headers": {"Authorization": f"Bearer {openai_aclient.api_key}"}, # mutable-ok: logged header map
|
||||
"api_base": str(openai_aclient.base_url),
|
||||
"acompletion": True,
|
||||
"complete_input_dict": data,
|
||||
},
|
||||
)
|
||||
|
||||
request_data: Final = ( # mutable-ok: the OpenAI SDK takes the request body as a dict
|
||||
{**data, "extra_headers": headers} if headers else data
|
||||
)
|
||||
response = await openai_aclient.images.generate(**request_data, timeout=timeout)
|
||||
stringified_response: Final = response.model_dump()
|
||||
## LOGGING
|
||||
logging_obj.post_call(
|
||||
|
|
@ -1450,9 +1462,10 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM):
|
|||
)
|
||||
|
||||
## COMPLETION CALL
|
||||
if headers:
|
||||
data["extra_headers"] = headers
|
||||
_response: Final = openai_client.images.generate(**data, timeout=timeout)
|
||||
request_data: Final = ( # mutable-ok: the OpenAI SDK takes the request body as a dict
|
||||
{**data, "extra_headers": headers} if headers else data
|
||||
)
|
||||
_response: Final = openai_client.images.generate(**request_data, timeout=timeout)
|
||||
|
||||
response: Final = _response.model_dump()
|
||||
## LOGGING
|
||||
|
|
@ -1501,6 +1514,7 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM):
|
|||
project: str | None,
|
||||
max_retries: int,
|
||||
timeout: float | httpx.Timeout,
|
||||
logging_obj: LiteLLMLoggingObj,
|
||||
aspeech: bool | None = None,
|
||||
client=None,
|
||||
shared_session: Optional["ClientSession"] = None,
|
||||
|
|
@ -1517,6 +1531,7 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM):
|
|||
project=project,
|
||||
max_retries=max_retries,
|
||||
timeout=timeout,
|
||||
logging_obj=logging_obj,
|
||||
client=client,
|
||||
shared_session=shared_session,
|
||||
)
|
||||
|
|
@ -1531,7 +1546,17 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM):
|
|||
shared_session=shared_session,
|
||||
)
|
||||
|
||||
response: Final = cast(OpenAI, openai_client).audio.speech.create(
|
||||
sync_client: Final = cast(OpenAI, openai_client)
|
||||
logging_obj.pre_call(
|
||||
input=input,
|
||||
api_key=api_key,
|
||||
additional_args={ # mutable-ok: loggers isinstance-check this payload as a dict
|
||||
"complete_input_dict": speech_request_body(model, voice, optional_params),
|
||||
"api_base": str(sync_client.base_url),
|
||||
},
|
||||
)
|
||||
|
||||
response: Final = sync_client.audio.speech.create(
|
||||
model=model,
|
||||
voice=voice,
|
||||
input=input,
|
||||
|
|
@ -1551,6 +1576,7 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM):
|
|||
project: str | None,
|
||||
max_retries: int,
|
||||
timeout: float | httpx.Timeout,
|
||||
logging_obj: LiteLLMLoggingObj,
|
||||
client=None,
|
||||
shared_session: Optional["ClientSession"] = None,
|
||||
) -> HttpxBinaryResponseContent:
|
||||
|
|
@ -1567,6 +1593,15 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM):
|
|||
),
|
||||
)
|
||||
|
||||
logging_obj.pre_call(
|
||||
input=input,
|
||||
api_key=api_key,
|
||||
additional_args={ # mutable-ok: loggers isinstance-check this payload as a dict
|
||||
"complete_input_dict": speech_request_body(model, voice, optional_params),
|
||||
"api_base": str(openai_client.base_url),
|
||||
},
|
||||
)
|
||||
|
||||
response: Final = await openai_client.audio.speech.create(
|
||||
model=model,
|
||||
voice=voice,
|
||||
|
|
|
|||
|
|
@ -5091,14 +5091,16 @@ def completion(
|
|||
model_info: Final = kwargs.get("model_info", None)
|
||||
proxy_server_request: Final = kwargs.get("proxy_server_request", None)
|
||||
fallbacks = kwargs.get("fallbacks", None)
|
||||
provider_specific_header: Final = cast(ProviderSpecificHeader | None, kwargs.get("provider_specific_header", None))
|
||||
provider_specific_header: Final = cast(
|
||||
ProviderSpecificHeader | Sequence[ProviderSpecificHeader] | None,
|
||||
kwargs.get("provider_specific_header", None),
|
||||
)
|
||||
headers = kwargs.get("headers", None) or extra_headers
|
||||
|
||||
ensure_alternating_roles: Final[bool | None] = kwargs.get("ensure_alternating_roles", None)
|
||||
user_continue_message: Final[ChatCompletionUserMessage | None] = kwargs.get("user_continue_message", None)
|
||||
assistant_continue_message: ChatCompletionAssistantMessage | None = kwargs.get("assistant_continue_message", None)
|
||||
if headers is None:
|
||||
headers = {}
|
||||
headers = {} if headers is None else dict(headers)
|
||||
if extra_headers is not None:
|
||||
headers.update(extra_headers)
|
||||
# Inject proxy auth headers if configured
|
||||
|
|
@ -7535,6 +7537,15 @@ async def amoderation(
|
|||
},
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
)
|
||||
moderation_request: Final = {"input": input, "model": model} # mutable-ok: logged as the raw request body
|
||||
litellm_logging_obj.pre_call(
|
||||
input=input,
|
||||
api_key=api_key,
|
||||
additional_args={ # mutable-ok: loggers isinstance-check this payload as a dict
|
||||
"complete_input_dict": moderation_request,
|
||||
"api_base": str(_openai_client.base_url),
|
||||
},
|
||||
)
|
||||
|
||||
if model is not None:
|
||||
response = await _openai_client.moderations.create(input=input, model=model)
|
||||
|
|
@ -8040,6 +8051,7 @@ def speech(
|
|||
project=project,
|
||||
max_retries=max_retries,
|
||||
timeout=timeout,
|
||||
logging_obj=logging_obj,
|
||||
client=client, # pass AsyncOpenAI, OpenAI client
|
||||
aspeech=aspeech,
|
||||
shared_session=shared_session,
|
||||
|
|
@ -8118,6 +8130,7 @@ def speech(
|
|||
organization=organization,
|
||||
max_retries=max_retries,
|
||||
timeout=timeout,
|
||||
logging_obj=logging_obj,
|
||||
client=client, # pass AsyncOpenAI, OpenAI client
|
||||
aspeech=aspeech,
|
||||
litellm_params=litellm_params_dict,
|
||||
|
|
|
|||
|
|
@ -3044,36 +3044,36 @@ async def add_guardrails_from_policy_engine(
|
|||
)
|
||||
|
||||
|
||||
_ANTHROPIC_API_HEADER_PROVIDERS: Final = ",".join(
|
||||
(LlmProviders.ANTHROPIC.value, LlmProviders.BEDROCK.value, LlmProviders.VERTEX_AI.value)
|
||||
)
|
||||
_ANTHROPIC_OAUTH_CREDENTIAL_PROVIDERS: Final = LlmProviders.ANTHROPIC.value
|
||||
|
||||
|
||||
def add_provider_specific_headers_to_request(
|
||||
data: dict,
|
||||
headers: dict,
|
||||
):
|
||||
from litellm.llms.anthropic.common_utils import is_anthropic_oauth_key
|
||||
|
||||
anthropic_headers: Final = {}
|
||||
# boolean to indicate if a header was added
|
||||
added_header = False
|
||||
for header in ANTHROPIC_API_HEADERS:
|
||||
if header in headers:
|
||||
header_value = headers[header]
|
||||
anthropic_headers[header] = header_value
|
||||
added_header = True
|
||||
anthropic_api_headers: Final = {header: headers[header] for header in ANTHROPIC_API_HEADERS if header in headers}
|
||||
anthropic_oauth_credential_headers: Final = {
|
||||
header: value
|
||||
for header, value in headers.items()
|
||||
if header.lower() == "authorization" and is_anthropic_oauth_key(value)
|
||||
}
|
||||
|
||||
# Check for Authorization header with Anthropic OAuth token (sk-ant-oat*)
|
||||
# This needs to be handled via provider-specific headers to ensure it only
|
||||
# goes to Anthropic-compatible providers, not all providers in the router
|
||||
for header, value in headers.items():
|
||||
if header.lower() == "authorization" and is_anthropic_oauth_key(value):
|
||||
anthropic_headers[header] = value
|
||||
added_header = True
|
||||
break
|
||||
if added_header is True:
|
||||
# Anthropic headers work across multiple providers
|
||||
# Store as comma-separated list so retrieval can match any of them
|
||||
data["provider_specific_header"] = ProviderSpecificHeader(
|
||||
custom_llm_provider=f"{LlmProviders.ANTHROPIC.value},{LlmProviders.BEDROCK.value},{LlmProviders.VERTEX_AI.value}",
|
||||
extra_headers=anthropic_headers,
|
||||
scoped_headers: Final = [
|
||||
ProviderSpecificHeader(custom_llm_provider=providers, extra_headers=extra_headers)
|
||||
for providers, extra_headers in (
|
||||
(_ANTHROPIC_API_HEADER_PROVIDERS, anthropic_api_headers),
|
||||
(_ANTHROPIC_OAUTH_CREDENTIAL_PROVIDERS, anthropic_oauth_credential_headers),
|
||||
)
|
||||
if extra_headers
|
||||
]
|
||||
|
||||
if scoped_headers:
|
||||
data["provider_specific_header"] = scoped_headers[0] if len(scoped_headers) == 1 else scoped_headers
|
||||
|
||||
|
||||
def _add_otel_traceparent_to_data(data: dict, request: Request):
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ from litellm.constants import LITELLM_PROXY_ADMIN_NAME
|
|||
from litellm.litellm_core_utils.ptu_pricing import (
|
||||
CUSTOM_PRICING_FIELDS,
|
||||
PTU_EMPTIED_PRICING_FIELDS,
|
||||
PTU_MODEL_INFO_FIELDS,
|
||||
PTU_ZEROED_PRICING_FIELDS,
|
||||
PTU_ZEROED_TABLE_FIELDS,
|
||||
SEARCH_CONTEXT_SIZES,
|
||||
|
|
@ -247,7 +248,6 @@ def _raise_on_strategy_router_write_violation(
|
|||
)
|
||||
|
||||
|
||||
_PTU_MODEL_INFO_FIELDS: Final = ("ptu_count", "cost_per_ptu_per_hour", "ptu_effective_from", "ptu_effective_to")
|
||||
_PTU_PRICED_PAIR: Final = frozenset({"ptu_count", "cost_per_ptu_per_hour"})
|
||||
|
||||
|
||||
|
|
@ -261,7 +261,7 @@ def _explicitly_cleared_ptu_fields(model_info: ModelInfo | None) -> frozenset[st
|
|||
return frozenset()
|
||||
return frozenset(
|
||||
field
|
||||
for field in _PTU_MODEL_INFO_FIELDS
|
||||
for field in PTU_MODEL_INFO_FIELDS
|
||||
if field in model_info.model_fields_set and getattr(model_info, field) is None
|
||||
)
|
||||
|
||||
|
|
@ -294,7 +294,7 @@ def _raise_if_ptu_cost_attribution_disabled(incoming_model_info: Mapping[str, ob
|
|||
"""
|
||||
if is_ptu_cost_attribution_enabled():
|
||||
return
|
||||
supplied: Final = tuple(field for field in _PTU_MODEL_INFO_FIELDS if incoming_model_info.get(field) is not None)
|
||||
supplied: Final = tuple(field for field in PTU_MODEL_INFO_FIELDS if incoming_model_info.get(field) is not None)
|
||||
if not supplied:
|
||||
return
|
||||
raise HTTPException(
|
||||
|
|
|
|||
|
|
@ -4,12 +4,14 @@ import re
|
|||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass, field
|
||||
from types import MappingProxyType
|
||||
from typing import TYPE_CHECKING, Final, Literal, Optional, Protocol, runtime_checkable
|
||||
from typing import TYPE_CHECKING, Final, Literal, Optional, Protocol, get_args, runtime_checkable
|
||||
|
||||
from litellm.proxy._types import ProxyException
|
||||
from litellm.repositories.table_repositories import (
|
||||
ManagedFileRepository,
|
||||
ManagedObjectRepository,
|
||||
)
|
||||
from litellm.types.llms.openai import OpenAIFilesPurpose
|
||||
from litellm.types.utils import SpecialEnums
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -22,6 +24,50 @@ if TYPE_CHECKING:
|
|||
from litellm.types.utils import LiteLLMBatch
|
||||
|
||||
|
||||
MAX_FILE_LIST_LIMIT: Final = 10000
|
||||
|
||||
FILE_LIST_CONTINUATION_CHUNK_SIZE: Final = 500
|
||||
|
||||
|
||||
def validate_file_list_limit(limit: int | None) -> None:
|
||||
"""Reject a ``limit`` outside the range OpenAI documents for GET /v1/files."""
|
||||
if limit is None or 1 <= limit <= MAX_FILE_LIST_LIMIT:
|
||||
return
|
||||
bound, expected, openai_code = (
|
||||
("below minimum", ">= 1", "integer_below_min_value")
|
||||
if limit < 1
|
||||
else ("above maximum", f"<= {MAX_FILE_LIST_LIMIT}", "integer_above_max_value")
|
||||
)
|
||||
raise ProxyException(
|
||||
message=f"Invalid 'limit': integer {bound} value. Expected a value {expected}, but got {limit} instead.",
|
||||
type="invalid_request_error",
|
||||
param="limit",
|
||||
code=400,
|
||||
openai_code=openai_code,
|
||||
)
|
||||
|
||||
|
||||
def validate_file_list_purpose(purpose: str | None) -> None:
|
||||
"""Reject a ``purpose`` filter no upload to this proxy could have stored.
|
||||
|
||||
An unknown purpose matches no file, so filtering on it would report an
|
||||
empty page for what is really a bad request. Rejecting it keeps a managed
|
||||
listing consistent with the upload route, which refuses the same values
|
||||
against this same set. The provider-backed listings do not: they pass
|
||||
``purpose`` upstream, so a purpose OpenAI accepts before it is added here
|
||||
is rejected on the managed path while still working on those.
|
||||
"""
|
||||
valid_purposes: Final = get_args(OpenAIFilesPurpose)
|
||||
if purpose is None or purpose in valid_purposes:
|
||||
return
|
||||
raise ProxyException(
|
||||
message=f"Invalid purpose: {purpose}. Must be one of: {valid_purposes}",
|
||||
type="invalid_request_error",
|
||||
param="purpose",
|
||||
code=400,
|
||||
)
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class ManagedResourceAccessChecker(Protocol):
|
||||
async def can_user_call_unified_file_id(
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ from litellm.proxy.openai_files_endpoints.common_utils import (
|
|||
get_credentials_for_model,
|
||||
handle_model_based_routing,
|
||||
prepare_data_with_credentials,
|
||||
validate_file_list_limit,
|
||||
validate_managed_files_requirement,
|
||||
validate_managed_id_requirement,
|
||||
)
|
||||
|
|
@ -1410,6 +1411,8 @@ async def list_files(
|
|||
provider: str | None = None,
|
||||
target_model_names: str | None = None,
|
||||
purpose: str | None = None,
|
||||
limit: int | None = None,
|
||||
after: str | None = None,
|
||||
):
|
||||
"""
|
||||
Returns information about a specific file. that can be used across - Assistants API, Batch API
|
||||
|
|
@ -1434,6 +1437,8 @@ async def list_files(
|
|||
|
||||
data: dict = {}
|
||||
try:
|
||||
validate_file_list_limit(limit)
|
||||
|
||||
# Include original request and headers in the data
|
||||
base_llm_response_processor: Final = ProxyBaseLLMRequestProcessing(data=data)
|
||||
(
|
||||
|
|
@ -1500,24 +1505,30 @@ async def list_files(
|
|||
or get_custom_llm_provider_from_request_headers(request=request)
|
||||
or get_custom_llm_provider_from_request_query(request=request)
|
||||
or await get_custom_llm_provider_from_request_body(request=request)
|
||||
or "openai"
|
||||
)
|
||||
managed_files_obj: Final = proxy_logging_obj.get_proxy_hook("managed_files")
|
||||
if custom_llm_provider is None and isinstance(managed_files_obj, BaseFileEndpoints):
|
||||
response = await managed_files_obj.afile_list(
|
||||
purpose=purpose,
|
||||
litellm_parent_otel_span=user_api_key_dict.parent_otel_span,
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
limit=limit,
|
||||
after=after,
|
||||
)
|
||||
else:
|
||||
resolved_custom_llm_provider: Final = custom_llm_provider or "openai"
|
||||
apply_team_provider_credentials(
|
||||
data=data,
|
||||
llm_router=llm_router,
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
custom_llm_provider=resolved_custom_llm_provider,
|
||||
)
|
||||
|
||||
# No model/target_model_names pinned: resolve upstream credentials from
|
||||
# the team's deployment for this provider so the call is authenticated
|
||||
# against the team's own account (e.g. the team's openai deployment).
|
||||
apply_team_provider_credentials(
|
||||
data=data,
|
||||
llm_router=llm_router,
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
)
|
||||
|
||||
response = await litellm.afile_list(
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
purpose=purpose,
|
||||
**data,
|
||||
)
|
||||
response = await litellm.afile_list(
|
||||
custom_llm_provider=resolved_custom_llm_provider,
|
||||
purpose=purpose,
|
||||
**data,
|
||||
)
|
||||
|
||||
if response is None:
|
||||
raise HTTPException(
|
||||
|
|
@ -1561,6 +1572,8 @@ async def list_files(
|
|||
)
|
||||
verbose_proxy_logger.error("litellm.proxy.proxy_server.list_files(): Exception occured - %s", e)
|
||||
verbose_proxy_logger.debug(traceback.format_exc())
|
||||
if isinstance(e, ProxyException):
|
||||
raise
|
||||
if isinstance(e, HTTPException):
|
||||
raise ProxyException(
|
||||
message=getattr(e, "message", str(e.detail)),
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
"""Standalone entrypoint for applying database migrations and generating the Prisma client.
|
||||
|
||||
The entrypoint enforces migration failures by default. Set
|
||||
ENFORCE_PRISMA_MIGRATION_CHECK=false to preserve log-only behavior for migration and
|
||||
Prisma generate failures.
|
||||
Migration failures fail the entrypoint by default; set ENFORCE_PRISMA_MIGRATION_CHECK=false
|
||||
for log-only behavior. A failed 'prisma generate' is always log-only: every shipped image
|
||||
bakes the client at build time, and refreshing it writes into site-packages, which an
|
||||
arbitrary non-root uid or a read-only root filesystem cannot do.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
|
@ -30,13 +31,13 @@ def main() -> int:
|
|||
verbose_proxy_logger.info("Running 'prisma generate'...")
|
||||
result: Final = subprocess.run(("prisma", "generate"), capture_output=True, text=True)
|
||||
verbose_proxy_logger.info("'prisma generate' stdout: %s", result.stdout)
|
||||
exit_code: Final = result.returncode
|
||||
|
||||
if exit_code != 0:
|
||||
verbose_proxy_logger.info("'prisma generate' failed with exit code %s.", exit_code)
|
||||
verbose_proxy_logger.error("'prisma generate' stderr: %s", result.stderr)
|
||||
if enforce_prisma_migration_check:
|
||||
return exit_code
|
||||
if result.returncode != 0:
|
||||
verbose_proxy_logger.warning(
|
||||
"'prisma generate' exited %s; continuing with the client baked at image build time. stderr: %s",
|
||||
result.returncode,
|
||||
result.stderr,
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -254,6 +254,9 @@ from litellm.exceptions import RejectedRequestError
|
|||
from litellm.integrations.custom_guardrail import ModifyResponseException
|
||||
from litellm.integrations.custom_logger import CustomLogger
|
||||
from litellm.integrations.SlackAlerting.slack_alerting import SlackAlerting
|
||||
from litellm.litellm_core_utils.agentic_loop_settings import (
|
||||
validated_max_agentic_loops,
|
||||
)
|
||||
from litellm.litellm_core_utils.asyncify import asyncify
|
||||
from litellm.litellm_core_utils.core_helpers import (
|
||||
_get_parent_otel_span_from_kwargs,
|
||||
|
|
@ -4081,6 +4084,27 @@ def resolve_complexity_router_plugins(
|
|||
complexity_router_config["classifier_plugin"] = resolved_classifier # rebind-ok: out-param, resolved in place
|
||||
|
||||
|
||||
def validate_deployment_max_agentic_loops(model: Mapping[str, Any]) -> None:
|
||||
"""
|
||||
Reject a per-deployment `max_agentic_loops` the agentic loop cannot honor.
|
||||
|
||||
Checked here rather than on `LiteLLM_Params` because the proxy builds its
|
||||
router with `ignore_invalid_deployments=True`, so a validator down there
|
||||
turns a bad value into a silently missing model instead of a refusal to
|
||||
start. Left unchecked entirely, a `0` used to read as the default ceiling
|
||||
of 3 and a non-integer failed every request to that model instead.
|
||||
"""
|
||||
litellm_params: Final = model.get("litellm_params") or {}
|
||||
if "max_agentic_loops" not in litellm_params:
|
||||
return
|
||||
|
||||
model_name: Final = model.get("model_name", "")
|
||||
validated_max_agentic_loops(
|
||||
litellm_params["max_agentic_loops"],
|
||||
field=f"litellm_params.max_agentic_loops on model {model_name!r}",
|
||||
)
|
||||
|
||||
|
||||
def pin_complexity_router_model_id(model: dict) -> None: # mutable-ok: out-param, model_info is stamped in place
|
||||
"""
|
||||
Stamps `model_info.id` from the raw litellm_params before plugin resolution swaps
|
||||
|
|
@ -5416,6 +5440,7 @@ class ProxyConfig:
|
|||
for k, v in model["litellm_params"].items():
|
||||
if isinstance(v, str) and v.startswith("os.environ/"):
|
||||
model["litellm_params"][k] = get_secret(v)
|
||||
validate_deployment_max_agentic_loops(model)
|
||||
pin_complexity_router_model_id(model)
|
||||
complexity_router_config = model["litellm_params"].get("complexity_router_config")
|
||||
if isinstance(complexity_router_config, dict):
|
||||
|
|
|
|||
|
|
@ -66,6 +66,8 @@ from litellm.litellm_core_utils.credential_accessor import CredentialAccessor
|
|||
from litellm.litellm_core_utils.dd_tracing import tracer
|
||||
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLogging
|
||||
from litellm.litellm_core_utils.ptu_pricing import (
|
||||
PTU_COST_ATTRIBUTION_ENV_VAR,
|
||||
declares_ptu,
|
||||
is_ptu_cost_attribution_enabled,
|
||||
ptu_config_error,
|
||||
ptu_identity_error,
|
||||
|
|
@ -8234,6 +8236,21 @@ class Router:
|
|||
)
|
||||
duplicate_ids: Final = frozenset(model_id for model_id in declared_ids if declared_ids.count(model_id) > 1)
|
||||
|
||||
ptu_declared: Final = tuple(
|
||||
str(entry.get("model_name"))
|
||||
for entry in original_model_list
|
||||
if isinstance(entry.get("model_info"), dict)
|
||||
and entry["model_info"].get("db_model") is not True
|
||||
and declares_ptu(entry["model_info"])
|
||||
)
|
||||
if ptu_declared and not is_ptu_cost_attribution_enabled():
|
||||
verbose_router_logger.warning(
|
||||
"PTU fields are set on config.yaml deployment(s) %s, but PTU cost attribution is disabled, so no "
|
||||
"flat cost accrues and this traffic is billed per token. Set %s=True to enable it",
|
||||
", ".join(ptu_declared),
|
||||
PTU_COST_ATTRIBUTION_ENV_VAR,
|
||||
)
|
||||
|
||||
for model in original_model_list:
|
||||
_model_name = model.pop("model_name")
|
||||
_litellm_params = model.pop("litellm_params")
|
||||
|
|
|
|||
|
|
@ -23,6 +23,21 @@ def is_interception_internal_key(
|
|||
return any(key.startswith(prefix) for prefix in prefixes)
|
||||
|
||||
|
||||
class AgenticLoopSafetyError(ValueError):
|
||||
"""
|
||||
Raised when an agentic-loop safety rail refuses a rerun.
|
||||
|
||||
Covers both rails: the bounded-loop cap (``max_agentic_loops``) and the
|
||||
repeated tool-call fingerprint cycle break. Subclasses ``ValueError`` so
|
||||
callers that already catch the broader type keep working.
|
||||
|
||||
Only the anthropic messages loop raises this today. The chat completions
|
||||
loop in ``litellm_core_utils/chat_completion_agentic_loop.py`` still raises
|
||||
a plain ``ValueError`` from its own copy of the same rails, so catching
|
||||
this type alone will not cover that surface until it is moved over.
|
||||
"""
|
||||
|
||||
|
||||
class StandardCustomLoggerInitParams(BaseModel):
|
||||
"""
|
||||
Params for initializing a CustomLogger.
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ Type definitions for WebSearch Interception integration.
|
|||
from typing import Literal, TypedDict
|
||||
|
||||
from pydantic import BaseModel
|
||||
from typing_extensions import ReadOnly
|
||||
|
||||
|
||||
class AnthropicSearchQuery(BaseModel):
|
||||
|
|
@ -35,6 +36,7 @@ class WebSearchInterceptionConfig(TypedDict, total=False):
|
|||
websearch_interception_params:
|
||||
enabled_providers: ["bedrock"]
|
||||
search_tool_name: "my-perplexity-search"
|
||||
max_agentic_loops: 5
|
||||
"""
|
||||
|
||||
enabled_providers: list[str]
|
||||
|
|
@ -42,3 +44,6 @@ class WebSearchInterceptionConfig(TypedDict, total=False):
|
|||
|
||||
search_tool_name: str | None
|
||||
"""Name of search tool configured in router's search_tools. If None, uses first available."""
|
||||
|
||||
max_agentic_loops: ReadOnly[int | None]
|
||||
"""How many follow-up model calls one intercepted request may chain. If None, LiteLLM's default of 3 applies."""
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ from pydantic import (
|
|||
BaseModel,
|
||||
ConfigDict,
|
||||
Discriminator,
|
||||
Field,
|
||||
PrivateAttr,
|
||||
field_serializer,
|
||||
field_validator,
|
||||
|
|
@ -275,6 +276,7 @@ OpenAIFilesPurpose = Literal[
|
|||
"fine-tune-results",
|
||||
"vision",
|
||||
"user_data",
|
||||
"evals",
|
||||
"messages",
|
||||
]
|
||||
|
||||
|
|
@ -381,6 +383,21 @@ class OpenAIFileObject(BaseModel):
|
|||
return self.dict()
|
||||
|
||||
|
||||
class FileListPage(BaseModel):
|
||||
"""A page of files, as `GET /v1/files` returns it.
|
||||
|
||||
Post-call hooks and logging callbacks are handed the listing response, and
|
||||
the provider SDKs hand them a page object rather than a mapping, so this
|
||||
exposes the same ``.data`` attribute while serializing to an identical body.
|
||||
"""
|
||||
|
||||
object: Literal["list"] = "list"
|
||||
data: list[OpenAIFileObject] = Field(default_factory=list)
|
||||
first_id: str | None = None
|
||||
last_id: str | None = None
|
||||
has_more: bool = False
|
||||
|
||||
|
||||
CREATE_FILE_REQUESTS_PURPOSE = Literal["assistants", "batch", "fine-tune", "messages"]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2846,7 +2846,7 @@ class StandardLoggingRoutingDecision(TypedDict, total=False):
|
|||
classifier_cost: float
|
||||
escalated: bool
|
||||
tier_boundaries: StandardLoggingRoutingDecisionTierBoundaries
|
||||
reasoning_override_min_score: ReadOnly[float]
|
||||
reasoning_override_min_score: float # writable-ok: Pydantic warns on ReadOnly TypedDict fields
|
||||
conversation_continuing: bool
|
||||
savings_baseline_model: str
|
||||
savings_baseline_deployment_id: str
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
ARG LITELLM_BUILD_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:42df77a9974d6ec8b17a5ee8bc23b532600a44d705acef2409e0933c1251b45f
|
||||
ARG LITELLM_RUNTIME_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:42df77a9974d6ec8b17a5ee8bc23b532600a44d705acef2409e0933c1251b45f
|
||||
ARG LITELLM_BUILD_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:a31344ab2cb8618db84f535eec56f76f6178b142cb92cb2e48676cc2dcebea72
|
||||
ARG LITELLM_RUNTIME_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:a31344ab2cb8618db84f535eec56f76f6178b142cb92cb2e48676cc2dcebea72
|
||||
ARG UV_IMAGE=ghcr.io/astral-sh/uv:0.11.7@sha256:240fb85ab0f263ef12f492d8476aa3a2e4e1e333f7d67fbdd923d00a506a516a
|
||||
|
||||
FROM $UV_IMAGE AS uvbin
|
||||
|
|
|
|||
|
|
@ -67,8 +67,8 @@ proxy = [
|
|||
"azure-identity>=1.25.2,<2.0",
|
||||
"azure-storage-blob>=12.28.0,<13.0",
|
||||
"mcp>=1.28.1,<2.0",
|
||||
"litellm-proxy-extras==0.4.88",
|
||||
"litellm-enterprise==0.1.58",
|
||||
"litellm-proxy-extras==0.4.89",
|
||||
"litellm-enterprise==0.1.59",
|
||||
"RestrictedPython>=8.1,<9.0",
|
||||
"rich>=13.9.4,<14.0",
|
||||
"InquirerPy>=0.3.4,<1.0",
|
||||
|
|
|
|||
|
|
@ -36,6 +36,10 @@
|
|||
# `re.search`, so a `.` copied out of an error message is a wildcard and the block
|
||||
# accepts messages the author never meant to accept. Mark a real regex raw, wrap a
|
||||
# literal message in `re.escape`, and the pattern says which one it is
|
||||
# F823 a module-level name read inside a function that also binds it lower down. The
|
||||
# later binding makes the name local for the whole body, so the read raises
|
||||
# UnboundLocalError, and in an autouse fixture that takes every test in the
|
||||
# directory down with it
|
||||
#
|
||||
# No target-version here on purpose: it resolves from requires-python (>=3.10), so
|
||||
# 3.11-only builtins like BaseExceptionGroup are correctly flagged in a tree that
|
||||
|
|
@ -58,4 +62,5 @@ lint.select = [
|
|||
"PLR0133",
|
||||
"PLW0127",
|
||||
"RUF043",
|
||||
"F823",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -6,13 +6,13 @@
|
|||
"limit": 742
|
||||
},
|
||||
"TQ003": {
|
||||
"limit": 1068
|
||||
"limit": 62
|
||||
},
|
||||
"TQ004": {
|
||||
"limit": 469
|
||||
},
|
||||
"TQ005": {
|
||||
"limit": 2459
|
||||
"limit": 2405
|
||||
},
|
||||
"TQ006": {
|
||||
"limit": 34
|
||||
|
|
|
|||
|
|
@ -6,8 +6,6 @@ Run with:
|
|||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
from typing import Optional
|
||||
from uuid import uuid4
|
||||
|
|
@ -18,9 +16,6 @@ import litellm
|
|||
from litellm.integrations.custom_logger import CustomLogger
|
||||
from litellm.types.utils import StandardLoggingPayload
|
||||
|
||||
sys.path.insert(
|
||||
0, os.path.abspath("../..")
|
||||
) # Adds the parent directory to the system path
|
||||
from a2a.types import MessageSendParams, SendMessageRequest
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -10,13 +10,10 @@ Prerequisites:
|
|||
- LangGraph server running on localhost:2024
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.abspath("../.."))
|
||||
|
||||
import litellm
|
||||
from a2a.types import MessageSendParams, SendMessageRequest, SendStreamingMessageRequest
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.abspath("../.."))
|
||||
|
||||
from tests._vcr_conftest_common import ( # noqa: E402,F401
|
||||
VerboseReporterState,
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
import asyncio
|
||||
import os
|
||||
import random
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
from litellm._uuid import uuid
|
||||
|
|
@ -13,9 +12,6 @@ from dotenv import load_dotenv
|
|||
|
||||
load_dotenv()
|
||||
|
||||
sys.path.insert(
|
||||
0, os.path.abspath("../..")
|
||||
) # Adds the parent directory to the system path
|
||||
from pathlib import Path
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
from typing import Optional
|
||||
|
|
@ -41,9 +40,6 @@ def _audio_file2():
|
|||
|
||||
load_dotenv()
|
||||
|
||||
sys.path.insert(
|
||||
0, os.path.abspath("../")
|
||||
) # Adds the parent directory to the system path
|
||||
from litellm import Router
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,7 @@
|
|||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(
|
||||
0, os.path.abspath("../..")
|
||||
) # Adds the parent directory to the system path
|
||||
import litellm # noqa: E402,F401
|
||||
|
||||
from tests._vcr_conftest_common import ( # noqa: E402,F401
|
||||
|
|
|
|||
|
|
@ -5,14 +5,10 @@ Integration Tests for Batch Rate Limits
|
|||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
|
||||
sys.path.insert(
|
||||
0, os.path.abspath("../..")
|
||||
) # Adds the parent directory to the system path
|
||||
|
||||
import litellm
|
||||
from litellm import DualCache
|
||||
|
|
|
|||
|
|
@ -1,15 +1,10 @@
|
|||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
sys.path.insert(
|
||||
0, os.path.abspath("../..")
|
||||
) # Adds the parent directory to the system-path
|
||||
import logging
|
||||
import time
|
||||
|
||||
|
|
|
|||
|
|
@ -3,15 +3,11 @@
|
|||
import asyncio
|
||||
import json as json_module
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
import tempfile
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
sys.path.insert(
|
||||
0, os.path.abspath("../..")
|
||||
) # Adds the parent directory to the system-path
|
||||
|
||||
|
||||
import pytest
|
||||
|
|
|
|||
|
|
@ -1,12 +1,7 @@
|
|||
import os
|
||||
import sys
|
||||
import traceback
|
||||
import json
|
||||
import pytest
|
||||
|
||||
sys.path.insert(
|
||||
0, os.path.abspath("../..")
|
||||
) # Adds the parent directory to the system path
|
||||
from openai import APITimeoutError as Timeout
|
||||
|
||||
import litellm
|
||||
|
|
|
|||
|
|
@ -3,14 +3,10 @@
|
|||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
sys.path.insert(
|
||||
0, os.path.abspath("../..")
|
||||
) # Adds the parent directory to the system-path
|
||||
|
||||
import logging
|
||||
import time
|
||||
|
|
@ -103,6 +99,25 @@ def load_vertex_ai_credentials():
|
|||
print("created gcs path service account=", os.environ["GCS_PATH_SERVICE_ACCOUNT"])
|
||||
|
||||
|
||||
async def cancel_batch_unless_already_terminal(batch_id: str, provider: str) -> None:
|
||||
try:
|
||||
cancel_batch_response = await litellm.acancel_batch(batch_id=batch_id, custom_llm_provider=provider)
|
||||
except openai.ConflictError as e:
|
||||
if "Cannot cancel a batch with status 'completed'" in str(e):
|
||||
print(f"Batch already completed, cannot cancel: {e}")
|
||||
return
|
||||
if "Cannot cancel a batch with status 'failed'" not in str(e):
|
||||
raise
|
||||
failed_batch = await litellm.aretrieve_batch(batch_id=batch_id, custom_llm_provider=provider)
|
||||
print(f"Batch failed before cancel, errors={failed_batch.errors}")
|
||||
failure_codes = {err.code for err in (failed_batch.errors.data if failed_batch.errors else None) or []}
|
||||
assert failure_codes == {"token_limit_exceeded"}, (
|
||||
f"batch failed for a reason other than the org's enqueued token limit: {failed_batch.errors}"
|
||||
)
|
||||
return
|
||||
print("cancel_batch_response=", cancel_batch_response)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("provider", ["openai"]) # , "azure"
|
||||
@pytest.mark.asyncio
|
||||
@skip_if_no_openai_network
|
||||
|
|
@ -176,24 +191,7 @@ async def test_create_batch(provider, tmp_path):
|
|||
result_file_path = tmp_path / "batch_job_results_furniture.jsonl"
|
||||
result_file_path.write_bytes(result)
|
||||
|
||||
# Cancel Batch - handle race condition where batch may already be completed
|
||||
try:
|
||||
cancel_batch_response = await litellm.acancel_batch(
|
||||
batch_id=create_batch_response.id,
|
||||
custom_llm_provider=provider,
|
||||
)
|
||||
print("cancel_batch_response=", cancel_batch_response)
|
||||
except openai.ConflictError as e:
|
||||
# Only allow to pass if it's specifically the "batch already completed" error
|
||||
if "Cannot cancel a batch with status 'completed'" in str(e):
|
||||
print(f"Batch already completed, cannot cancel: {e}")
|
||||
else:
|
||||
# Re-raise other ConflictError types
|
||||
raise
|
||||
except Exception as e:
|
||||
# Re-raise any other unexpected errors
|
||||
print(f"Unexpected error during batch cancellation: {e}")
|
||||
raise
|
||||
await cancel_batch_unless_already_terminal(batch_id=create_batch_response.id, provider=provider)
|
||||
|
||||
pass
|
||||
|
||||
|
|
@ -395,24 +393,7 @@ async def test_async_create_batch(provider, tmp_path):
|
|||
result_file_path = tmp_path / "batch_job_results_furniture.jsonl"
|
||||
result_file_path.write_bytes(file_content.content)
|
||||
|
||||
# Cancel Batch - handle race condition where batch may already be completed
|
||||
try:
|
||||
cancel_batch_response = await litellm.acancel_batch(
|
||||
batch_id=create_batch_response.id,
|
||||
custom_llm_provider=provider,
|
||||
)
|
||||
print("cancel_batch_response=", cancel_batch_response)
|
||||
except openai.ConflictError as e:
|
||||
# Only allow to pass if it's specifically the "batch already completed" error
|
||||
if "Cannot cancel a batch with status 'completed'" in str(e):
|
||||
print(f"Batch already completed, cannot cancel: {e}")
|
||||
else:
|
||||
# Re-raise other ConflictError types
|
||||
raise
|
||||
except Exception as e:
|
||||
# Re-raise any other unexpected errors
|
||||
print(f"Unexpected error during batch cancellation: {e}")
|
||||
raise
|
||||
await cancel_batch_unless_already_terminal(batch_id=create_batch_response.id, provider=provider)
|
||||
|
||||
|
||||
mock_file_response = {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.abspath("../.."))
|
||||
import litellm
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
|
|
|
|||
|
|
@ -27,10 +27,8 @@ import ast
|
|||
import os
|
||||
import re
|
||||
from typing import List, Tuple
|
||||
import sys
|
||||
|
||||
# Add parent directory to path so we can import litellm
|
||||
sys.path.insert(0, os.path.abspath("../.."))
|
||||
import litellm
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
import ast
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.abspath("../.."))
|
||||
|
||||
import litellm
|
||||
|
||||
|
|
|
|||
|
|
@ -4,14 +4,9 @@ Test that all cache calls in async functions in router_strategy/ are async
|
|||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import Dict, List, Tuple
|
||||
import ast
|
||||
|
||||
sys.path.insert(
|
||||
0, os.path.abspath("../..")
|
||||
) # Adds the parent directory to the system path
|
||||
import os
|
||||
|
||||
|
||||
class AsyncCacheCallVisitor(ast.NodeVisitor):
|
||||
|
|
|
|||
|
|
@ -4,11 +4,7 @@ import os
|
|||
from dataclasses import dataclass
|
||||
import argparse
|
||||
import re
|
||||
import sys
|
||||
|
||||
sys.path.insert(
|
||||
0, os.path.abspath("../..")
|
||||
) # Adds the parent directory to the system path
|
||||
import litellm
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -11,9 +11,6 @@ import re
|
|||
# Backup the original sys.path
|
||||
original_sys_path = sys.path.copy()
|
||||
|
||||
sys.path.insert(
|
||||
0, os.path.abspath("../..")
|
||||
) # Adds the parent directory to the system path
|
||||
import litellm
|
||||
|
||||
public_exceptions = litellm.LITELLM_EXCEPTION_TYPES
|
||||
|
|
|
|||
|
|
@ -2,11 +2,7 @@ import os
|
|||
import re
|
||||
import inspect
|
||||
from typing import Type
|
||||
import sys
|
||||
|
||||
sys.path.insert(
|
||||
0, os.path.abspath("../..")
|
||||
) # Adds the parent directory to the system path
|
||||
import litellm
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,7 @@
|
|||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
from typing import get_type_hints
|
||||
|
||||
sys.path.insert(
|
||||
0, os.path.abspath("../..")
|
||||
) # Adds the parent directory to the system path
|
||||
|
||||
from litellm.types.utils import StandardLoggingPayload
|
||||
|
||||
|
|
|
|||
|
|
@ -3,13 +3,9 @@
|
|||
import asyncio
|
||||
import importlib
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(
|
||||
0, os.path.abspath("../..")
|
||||
) # Adds the parent directory to the system path
|
||||
import litellm
|
||||
|
||||
|
||||
|
|
@ -31,9 +27,6 @@ def setup_and_teardown():
|
|||
This fixture reloads litellm before every function. To speed up testing by removing callbacks being chained.
|
||||
"""
|
||||
curr_dir = os.getcwd() # Get the current working directory
|
||||
sys.path.insert(
|
||||
0, os.path.abspath("../..")
|
||||
) # Adds the project directory to the system path
|
||||
|
||||
from litellm import Router
|
||||
|
||||
|
|
@ -41,8 +34,6 @@ def setup_and_teardown():
|
|||
|
||||
try:
|
||||
if hasattr(litellm, "proxy") and hasattr(litellm.proxy, "proxy_server"):
|
||||
import litellm.proxy.proxy_server
|
||||
|
||||
importlib.reload(litellm.proxy.proxy_server)
|
||||
except Exception as e:
|
||||
print(f"Error reloading litellm.proxy.proxy_server: {e}")
|
||||
|
|
|
|||
|
|
@ -1,7 +1,4 @@
|
|||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.abspath("../.."))
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
|
|
|
|||
|
|
@ -1,9 +1,4 @@
|
|||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(
|
||||
0, os.path.abspath("../..")
|
||||
) # Adds the parent directory to the system-path
|
||||
from litellm.integrations.custom_guardrail import CustomGuardrail
|
||||
from litellm.types.guardrails import GuardrailEventHooks, Mode
|
||||
|
||||
|
|
|
|||
|
|
@ -3,15 +3,10 @@ Mock prometheus unit tests, these don't rely on LLM API calls
|
|||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
sys.path.insert(
|
||||
0, os.path.abspath("../../..")
|
||||
) # Adds the parent directory to the system path
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
|
|
|
|||
|
|
@ -9,16 +9,12 @@ except Exception:
|
|||
PrometheusLogger = None
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
import os
|
||||
|
||||
sys.path.insert(
|
||||
0, os.path.abspath("../..")
|
||||
) # Adds the parent directory to the system-path
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
|
|
|||
|
|
@ -1,10 +1,6 @@
|
|||
import os
|
||||
import sys
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
sys.path.insert(
|
||||
0, os.path.abspath("../../../..")
|
||||
) # Adds the parent directory to the system path
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
|
|
|
|||
|
|
@ -2,13 +2,10 @@
|
|||
Test the /guardrails/apply_guardrail endpoint
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.abspath("../../../../.."))
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
||||
|
|
|
|||
|
|
@ -2,13 +2,10 @@
|
|||
Test the Bedrock guardrail apply_guardrail functionality
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.abspath("../../../../.."))
|
||||
|
||||
|
||||
from litellm.proxy._types import UserAPIKeyAuth
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import os
|
||||
import sys
|
||||
import traceback
|
||||
from litellm._uuid import uuid
|
||||
from unittest import mock
|
||||
|
|
@ -10,7 +9,6 @@ from fastapi import Request
|
|||
load_dotenv()
|
||||
import time
|
||||
|
||||
sys.path.insert(0, os.path.abspath("../.."))
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
|
|
|
|||
|
|
@ -7,13 +7,9 @@
|
|||
|
||||
import importlib
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(
|
||||
0, os.path.abspath("../..")
|
||||
) # Adds the parent directory to the system path
|
||||
import litellm
|
||||
|
||||
from tests._vcr_conftest_common import ( # noqa: E402,F401
|
||||
|
|
@ -122,7 +118,6 @@ def setup_and_teardown():
|
|||
Module-scoped setup. Reloads litellm only in single-process mode
|
||||
(skipped under xdist to avoid cross-worker interference).
|
||||
"""
|
||||
sys.path.insert(0, os.path.abspath("../.."))
|
||||
|
||||
import litellm
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
import sys
|
||||
import os
|
||||
import io, asyncio
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.abspath("../.."))
|
||||
import litellm
|
||||
from litellm.proxy.guardrails.guardrail_hooks.bedrock_guardrails import (
|
||||
BedrockGuardrail,
|
||||
|
|
|
|||
|
|
@ -3,11 +3,8 @@ Test custom guardrail + unit tests for guardrails
|
|||
"""
|
||||
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
sys.path.insert(0, os.path.abspath("../.."))
|
||||
|
||||
import asyncio
|
||||
import gzip
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import os
|
||||
import sys
|
||||
from unittest.mock import patch, AsyncMock
|
||||
|
||||
from httpx import Response, Request
|
||||
|
|
@ -13,9 +12,6 @@ from litellm.proxy.guardrails.guardrail_hooks.deepkeep.deepkeep import (
|
|||
)
|
||||
from litellm.exceptions import GuardrailRaisedException
|
||||
|
||||
sys.path.insert(
|
||||
0, os.path.abspath("../..")
|
||||
) # Adds the parent directory to the system path
|
||||
import litellm
|
||||
from litellm.proxy.guardrails.init_guardrails import init_guardrails_v2
|
||||
|
||||
|
|
|
|||
|
|
@ -2,11 +2,8 @@
|
|||
Test DynamoAI Guardrails integration
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.abspath("../.."))
|
||||
|
||||
from litellm.proxy.guardrails.guardrail_hooks.dynamoai import DynamoAIGuardrails
|
||||
from litellm.proxy._types import UserAPIKeyAuth
|
||||
|
|
|
|||
|
|
@ -8,11 +8,9 @@ Tests 40 different sentences to validate the conditional matching logic:
|
|||
- identifier or block word alone should ALLOW
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.abspath("../.."))
|
||||
import litellm
|
||||
from litellm.proxy.guardrails.guardrail_hooks.litellm_content_filter.content_filter import (
|
||||
ContentFilterGuardrail,
|
||||
|
|
@ -162,7 +160,6 @@ def content_filter_guardrail():
|
|||
"""Initialize content filter guardrail with EU AI Act Article 5 template."""
|
||||
|
||||
# Get absolute path to the policy template
|
||||
import os
|
||||
|
||||
content_filter_dir = os.path.join(
|
||||
os.path.dirname(__file__),
|
||||
|
|
|
|||
|
|
@ -7,11 +7,9 @@ Tests the exact 3 scenarios requested:
|
|||
3. Request 3: Safe query in French that should pass (allowed)
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.abspath("../.."))
|
||||
import litellm
|
||||
from litellm.proxy.guardrails.guardrail_hooks.litellm_content_filter.content_filter import (
|
||||
ContentFilterGuardrail,
|
||||
|
|
|
|||
|
|
@ -2,11 +2,8 @@
|
|||
Test guardrail load balancing through the Router and ProxyLogging.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from unittest.mock import MagicMock, patch, AsyncMock
|
||||
|
||||
sys.path.insert(0, os.path.abspath("../.."))
|
||||
|
||||
import litellm
|
||||
import pytest
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@
|
|||
## Unit Tests for guardrails config
|
||||
import asyncio
|
||||
import inspect
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
from litellm._uuid import uuid
|
||||
|
|
@ -15,7 +13,6 @@ from pydantic import BaseModel
|
|||
import litellm.litellm_core_utils
|
||||
import litellm.litellm_core_utils.litellm_logging
|
||||
|
||||
sys.path.insert(0, os.path.abspath("../.."))
|
||||
from typing import Any, List, Literal, Optional, Tuple, Union
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
import sys
|
||||
import os
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, patch
|
||||
from fastapi import HTTPException
|
||||
|
||||
sys.path.insert(0, os.path.abspath("../.."))
|
||||
from litellm.proxy.guardrails.guardrail_hooks.javelin import JavelinGuardrail
|
||||
import litellm
|
||||
from litellm.proxy._types import UserAPIKeyAuth
|
||||
|
|
|
|||
|
|
@ -1,12 +1,9 @@
|
|||
import sys
|
||||
import os
|
||||
import io, asyncio
|
||||
import pytest
|
||||
import time
|
||||
from litellm import mock_completion
|
||||
from unittest.mock import MagicMock, AsyncMock, patch
|
||||
|
||||
sys.path.insert(0, os.path.abspath("../.."))
|
||||
import litellm
|
||||
from litellm.proxy.guardrails.guardrail_hooks.lakera_ai_v2 import LakeraAIGuardrail
|
||||
from litellm.types.guardrails import PiiEntityType, PiiAction
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import os
|
||||
import sys
|
||||
from fastapi.exceptions import HTTPException
|
||||
from unittest.mock import patch
|
||||
from httpx import Response, Request
|
||||
|
|
@ -14,9 +13,6 @@ from litellm.proxy.guardrails.guardrail_hooks.lasso.lasso import (
|
|||
LassoGuardrailAPIError,
|
||||
)
|
||||
|
||||
sys.path.insert(
|
||||
0, os.path.abspath("../..")
|
||||
) # Adds the parent directory to the system path
|
||||
import litellm
|
||||
from litellm.proxy.guardrails.init_guardrails import init_guardrails_v2
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
import sys
|
||||
import os
|
||||
import pytest
|
||||
from litellm import mock_completion
|
||||
from unittest.mock import patch
|
||||
|
||||
sys.path.insert(0, os.path.abspath("../.."))
|
||||
import litellm
|
||||
from litellm.proxy.guardrails.guardrail_hooks.presidio import (
|
||||
_OPTIONAL_PresidioPIIMasking,
|
||||
|
|
|
|||
|
|
@ -3,9 +3,7 @@ Tests for the Semantic Guard guardrail — embedding-based prompt injection dete
|
|||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.abspath("../.."))
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
|
|
|
|||
|
|
@ -10,11 +10,9 @@ for Singapore financial institutions:
|
|||
5. sg_mas_model_security — Adversarial attacks on financial AI
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.abspath("../.."))
|
||||
import litellm
|
||||
from litellm.proxy.guardrails.guardrail_hooks.litellm_content_filter.content_filter import (
|
||||
ContentFilterGuardrail,
|
||||
|
|
|
|||
|
|
@ -15,11 +15,9 @@ Each sub-guardrail validates:
|
|||
- identifier or block word alone → ALLOW (no match)
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.abspath("../.."))
|
||||
import litellm
|
||||
from litellm.proxy.guardrails.guardrail_hooks.litellm_content_filter.content_filter import (
|
||||
ContentFilterGuardrail,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import sys
|
||||
import os
|
||||
import io, asyncio
|
||||
import json
|
||||
|
|
@ -7,7 +6,6 @@ import time
|
|||
from litellm import mock_completion
|
||||
from unittest.mock import MagicMock, AsyncMock, patch
|
||||
|
||||
sys.path.insert(0, os.path.abspath("../.."))
|
||||
import litellm
|
||||
from litellm.proxy.guardrails.guardrail_hooks.presidio import (
|
||||
_OPTIONAL_PresidioPIIMasking,
|
||||
|
|
|
|||
|
|
@ -2,14 +2,9 @@ import asyncio
|
|||
import httpx
|
||||
import json
|
||||
import pytest
|
||||
import sys
|
||||
from typing import Any, Dict, List, Optional
|
||||
from unittest.mock import MagicMock, Mock, patch
|
||||
import os
|
||||
|
||||
sys.path.insert(
|
||||
0, os.path.abspath("../..")
|
||||
) # Adds the parent directory to the system path
|
||||
import litellm
|
||||
from litellm.exceptions import BadRequestError
|
||||
from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler
|
||||
|
|
|
|||
|
|
@ -1,12 +1,7 @@
|
|||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(
|
||||
0, os.path.abspath("../..")
|
||||
) # Adds the parent directory to the system path
|
||||
import litellm # noqa: E402,F401
|
||||
|
||||
from tests._vcr_conftest_common import ( # noqa: E402,F401
|
||||
|
|
|
|||
|
|
@ -1,14 +1,9 @@
|
|||
import logging
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from openai.types.image import Image
|
||||
|
||||
sys.path.insert(
|
||||
0, os.path.abspath("../..")
|
||||
) # Adds the parent directory to the system path
|
||||
|
||||
from litellm.llms.bedrock.image_generation.amazon_nova_canvas_transformation import (
|
||||
AmazonNovaCanvasConfig,
|
||||
|
|
@ -18,13 +13,9 @@ logging.basicConfig(level=logging.DEBUG)
|
|||
load_dotenv()
|
||||
import asyncio
|
||||
|
||||
sys.path.insert(
|
||||
0, os.path.abspath("../..")
|
||||
) # Adds the parent directory to the system path
|
||||
import pytest
|
||||
from litellm.llms.bedrock.image_generation.cost_calculator import cost_calculator
|
||||
from litellm.types.utils import ImageResponse, ImageObject
|
||||
import os
|
||||
|
||||
import litellm
|
||||
from litellm.llms.bedrock.image_generation.amazon_stability3_transformation import (
|
||||
|
|
|
|||
|
|
@ -1,11 +1,8 @@
|
|||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.abspath("../.."))
|
||||
|
||||
import litellm
|
||||
from litellm import aimage_generation
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import logging
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
import asyncio
|
||||
from typing import Optional
|
||||
|
|
@ -11,9 +10,6 @@ from unittest.mock import patch, AsyncMock
|
|||
import json
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
sys.path.insert(
|
||||
0, os.path.abspath("../..")
|
||||
) # Adds the parent directory to the system path
|
||||
|
||||
import litellm
|
||||
from litellm.utils import ImageResponse
|
||||
|
|
|
|||
|
|
@ -3,14 +3,10 @@
|
|||
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
|
||||
sys.path.insert(
|
||||
0, os.path.abspath("../..")
|
||||
) # Adds the parent directory to the system path
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from openai.types.image import Image
|
||||
|
|
@ -19,7 +15,6 @@ from litellm.caching import InMemoryCache
|
|||
logging.basicConfig(level=logging.DEBUG)
|
||||
load_dotenv()
|
||||
import asyncio
|
||||
import os
|
||||
import pytest
|
||||
|
||||
import litellm
|
||||
|
|
|
|||
|
|
@ -2,14 +2,9 @@
|
|||
## This tests the litellm support for the openai /generations endpoint
|
||||
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
|
||||
sys.path.insert(
|
||||
0, os.path.abspath("../..")
|
||||
) # Adds the parent directory to the system path
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from openai.types.image import Image
|
||||
|
|
@ -18,7 +13,6 @@ from litellm.caching import InMemoryCache
|
|||
logging.basicConfig(level=logging.DEBUG)
|
||||
load_dotenv()
|
||||
import asyncio
|
||||
import os
|
||||
import pytest
|
||||
|
||||
import litellm
|
||||
|
|
|
|||
|
|
@ -1,14 +1,9 @@
|
|||
import logging
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
import pytest
|
||||
import json
|
||||
from unittest.mock import Mock, patch, AsyncMock
|
||||
|
||||
sys.path.insert(
|
||||
0, os.path.abspath("../..")
|
||||
) # Adds the parent directory to the system path
|
||||
|
||||
import litellm
|
||||
from litellm.types.utils import ImageObject
|
||||
|
|
|
|||
|
|
@ -20,12 +20,10 @@ Run only these tests:
|
|||
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
from typing import NamedTuple, Optional
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.abspath("../.."))
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fixtures / helpers
|
||||
|
|
|
|||
|
|
@ -10,16 +10,11 @@ Usage:
|
|||
the abstract methods to provide provider-specific configuration.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(
|
||||
0, os.path.abspath("../..")
|
||||
) # Adds the parent directory to the system path
|
||||
|
||||
from litellm.llms.base_llm.base_utils import BaseTokenCounter
|
||||
from litellm.types.utils import TokenCountResponse
|
||||
|
|
|
|||
|
|
@ -2,14 +2,9 @@
|
|||
|
||||
import asyncio
|
||||
import importlib
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(
|
||||
0, os.path.abspath("../..")
|
||||
) # Adds the parent directory to the system path
|
||||
import litellm # noqa: E402,F401
|
||||
|
||||
from tests._vcr_conftest_common import ( # noqa: E402,F401
|
||||
|
|
@ -38,9 +33,6 @@ def setup_and_teardown():
|
|||
"""
|
||||
This fixture reloads litellm before every function. To speed up testing by removing callbacks being chained.
|
||||
"""
|
||||
sys.path.insert(
|
||||
0, os.path.abspath("../..")
|
||||
) # Adds the project directory to the system path
|
||||
|
||||
|
||||
importlib.reload(litellm)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import asyncio
|
||||
import copy
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime
|
||||
from unittest import mock
|
||||
|
|
@ -10,11 +9,7 @@ from dotenv import load_dotenv
|
|||
from litellm.types.utils import StandardCallbackDynamicParams
|
||||
|
||||
load_dotenv()
|
||||
import os
|
||||
|
||||
sys.path.insert(
|
||||
0, os.path.abspath("../..")
|
||||
) # Adds the parent directory to the system-path
|
||||
import pytest
|
||||
|
||||
import litellm
|
||||
|
|
|
|||
|
|
@ -5,14 +5,10 @@ Tests for the Anthropic token counter implementation using the base test suite.
|
|||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(
|
||||
0, os.path.abspath("../..")
|
||||
) # Adds the parent directory to the system path
|
||||
|
||||
from litellm.llms.anthropic.count_tokens import AnthropicTokenCounter
|
||||
from litellm.llms.base_llm.base_utils import BaseTokenCounter
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue