Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/test-unit-proxy-db.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ jobs:
test-path: >-
tests/proxy_unit_tests/test_auth_checks.py
tests/proxy_unit_tests/test_user_api_key_auth.py
tests/proxy_unit_tests/test_deprecated_key_grace_period.py
workers: 4
dist: loadscope
timeout: 15
Expand Down
16 changes: 14 additions & 2 deletions litellm/llms/anthropic/chat/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,10 @@ def __init__(
# Accumulate compaction blocks for multi-turn reconstruction
self.compaction_blocks: List[Dict[str, Any]] = []

# Accumulate streamed thinking text so final usage can split reasoning
# tokens from regular output tokens.
self.reasoning_content_chunks: List[str] = []

# Track server tool use inputs and results for code_interpreter_results
self._server_tool_inputs: Dict[str, Any] = {}
self.tool_results: List[Dict[str, Any]] = []
Expand Down Expand Up @@ -587,9 +591,14 @@ def check_empty_tool_call_args(self) -> bool:
return False

def _handle_usage(self, anthropic_usage_chunk: Union[dict, UsageDelta]) -> Usage:
reasoning_content = (
"".join(self.reasoning_content_chunks)
if self.reasoning_content_chunks
else None
)
return AnthropicConfig().calculate_usage(
usage_object=cast(dict, anthropic_usage_chunk),
reasoning_content=None,
reasoning_content=reasoning_content,
speed=self.speed,
)

Expand Down Expand Up @@ -636,10 +645,13 @@ def _content_block_delta_helper(self, chunk: dict) -> Tuple[
"thinking" in content_block["delta"]
or "signature" in content_block["delta"]
):
thinking_content = content_block["delta"].get("thinking")
if isinstance(thinking_content, str) and thinking_content:
self.reasoning_content_chunks.append(thinking_content)
thinking_blocks = [
ChatCompletionThinkingBlock(
type="thinking",
thinking=content_block["delta"].get("thinking") or "",
thinking=thinking_content or "",
signature=str(content_block["delta"].get("signature") or ""),
)
]
Expand Down
15 changes: 12 additions & 3 deletions litellm/llms/anthropic/chat/transformation.py
Original file line number Diff line number Diff line change
Expand Up @@ -1856,8 +1856,16 @@ def calculate_usage(
speed: Optional[str] = None,
) -> Usage:
# NOTE: Sometimes the usage object has None set explicitly for token counts, meaning .get() & key access returns None, and we need to account for this
prompt_tokens = usage_object.get("input_tokens", 0) or 0
completion_tokens = usage_object.get("output_tokens", 0) or 0
raw_prompt_tokens = usage_object.get("input_tokens", 0) or 0
prompt_tokens: int = (
int(raw_prompt_tokens) if isinstance(raw_prompt_tokens, (int, float)) else 0
)
raw_completion_tokens = usage_object.get("output_tokens", 0) or 0
completion_tokens: int = (
int(raw_completion_tokens)
if isinstance(raw_completion_tokens, (int, float))
else 0
)
_usage = usage_object
cache_creation_input_tokens: int = 0
cache_read_input_tokens: int = 0
Expand Down Expand Up @@ -1926,11 +1934,12 @@ def calculate_usage(
text_tokens=raw_input_tokens,
)
# Always populate completion_token_details, not just when there's reasoning_content
reasoning_tokens = (
estimated_reasoning_tokens = (
token_counter(text=reasoning_content, count_response_tokens=True)
if reasoning_content
else 0
)
reasoning_tokens = min(estimated_reasoning_tokens, completion_tokens)
completion_token_details = CompletionTokensDetailsWrapper(
reasoning_tokens=reasoning_tokens if reasoning_tokens > 0 else 0,
text_tokens=(
Expand Down
41 changes: 39 additions & 2 deletions litellm/llms/azure/containers/transformation.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
from typing import Optional
from urllib.parse import parse_qs, urlparse, urlunparse

from litellm.llms.azure.common_utils import BaseAzureLLM
from litellm.llms.openai.containers.transformation import OpenAIContainerConfig
from litellm.types.router import GenericLiteLLMParams

# Endpoint-specific path suffixes that may appear in a deployment's api_base
# (e.g. the responses endpoint URL is stored as api_base for Azure models).
# Strip these before building the containers URL so we always start from the
# resource root (https://resource.cognitiveservices.azure.com).
_AZURE_ENDPOINT_PATHS = ("/openai/responses",)


class AzureContainerConfig(OpenAIContainerConfig):
"""
Expand All @@ -27,6 +34,27 @@ def validate_environment(
litellm_params=GenericLiteLLMParams(api_key=api_key),
)

@staticmethod
def _normalize_api_base(api_base: Optional[str]) -> Optional[str]:
"""Strip endpoint-specific path suffixes from api_base to get the resource root."""
if not api_base:
return api_base
parsed = urlparse(api_base)
path = parsed.path.rstrip("/")
for ep in _AZURE_ENDPOINT_PATHS:
if path.endswith(ep):
return urlunparse(
(parsed.scheme, parsed.netloc, path[: -len(ep)], "", "", "")
)
return api_base

@staticmethod
def _extract_api_version(api_base: Optional[str]) -> Optional[str]:
"""Return the api-version query param from api_base if present."""
if not api_base:
return None
return parse_qs(urlparse(api_base).query).get("api-version", [None])[0]

def get_complete_url(
self,
api_base: Optional[str],
Expand All @@ -39,10 +67,19 @@ def get_complete_url(
{endpoint}/openai/v1/containers
when api_version is 'v1', 'latest', or 'preview'; otherwise:
{endpoint}/openai/containers

The deployment's api_base may be the responses endpoint URL
(e.g. .../openai/responses?api-version=2025-04-01-preview). We
prefer the api-version embedded there over the deployment's
api_version field, which may point to an older chat API version.
"""
effective_params = dict(litellm_params)
api_version_from_base = self._extract_api_version(api_base)
if api_version_from_base:
effective_params["api_version"] = api_version_from_base
return BaseAzureLLM._get_base_azure_url(
api_base=api_base,
litellm_params=litellm_params,
api_base=self._normalize_api_base(api_base),
litellm_params=effective_params,
route="/openai/containers",
default_api_version="v1",
)
26 changes: 18 additions & 8 deletions litellm/llms/custom_httpx/container_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,26 +257,31 @@ def _sync_handle(
returns_binary = endpoint_config.get("returns_binary", False)
is_multipart = endpoint_config.get("is_multipart", False)

# An empty dict passed as `params` to httpx strips any existing query
# string from the URL (e.g. ?api-version=...). Use None instead so
# httpx leaves the URL's own query string intact.
effective_params = query_params or None

try:
if method == "GET":
response = http_client.get(
url=url, headers=headers, params=query_params
url=url, headers=headers, params=effective_params
)
elif method == "DELETE":
response = http_client.delete(
url=url, headers=headers, params=query_params
url=url, headers=headers, params=effective_params
)
elif method == "POST":
if is_multipart and "file" in kwargs:
files, headers = _prepare_multipart_file_upload(
kwargs["file"], headers
)
response = http_client.post(
url=url, headers=headers, params=query_params, files=files
url=url, headers=headers, params=effective_params, files=files
)
else:
response = http_client.post(
url=url, headers=headers, params=query_params
url=url, headers=headers, params=effective_params
)
else:
raise ValueError(f"Unsupported HTTP method: {method}")
Expand Down Expand Up @@ -376,26 +381,31 @@ async def _async_handle(
returns_binary = endpoint_config.get("returns_binary", False)
is_multipart = endpoint_config.get("is_multipart", False)

# An empty dict passed as `params` to httpx strips any existing query
# string from the URL (e.g. ?api-version=...). Use None instead so
# httpx leaves the URL's own query string intact.
effective_params = query_params or None

try:
if method == "GET":
response = await http_client.get(
url=url, headers=headers, params=query_params
url=url, headers=headers, params=effective_params
)
elif method == "DELETE":
response = await http_client.delete(
url=url, headers=headers, params=query_params
url=url, headers=headers, params=effective_params
)
elif method == "POST":
if is_multipart and "file" in kwargs:
files, headers = _prepare_multipart_file_upload(
kwargs["file"], headers
)
response = await http_client.post(
url=url, headers=headers, params=query_params, files=files
url=url, headers=headers, params=effective_params, files=files
)
else:
response = await http_client.post(
url=url, headers=headers, params=query_params
url=url, headers=headers, params=effective_params
)
else:
raise ValueError(f"Unsupported HTTP method: {method}")
Expand Down
20 changes: 10 additions & 10 deletions litellm/llms/custom_httpx/llm_http_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -7816,7 +7816,7 @@ def container_list_handler(
response = sync_httpx_client.get(
url=url,
headers=headers,
params=params,
params=params or None,
)

return container_provider_config.transform_container_list_response(
Expand Down Expand Up @@ -7893,7 +7893,7 @@ async def async_container_list_handler(
response = await async_httpx_client.get(
url=url,
headers=headers,
params=params,
params=params or None,
)

return container_provider_config.transform_container_list_response(
Expand Down Expand Up @@ -7983,7 +7983,7 @@ def container_retrieve_handler(
response = sync_httpx_client.get(
url=url,
headers=headers,
params=params,
params=params or None,
)

return container_provider_config.transform_container_retrieve_response(
Expand Down Expand Up @@ -8060,7 +8060,7 @@ async def async_container_retrieve_handler(
response = await async_httpx_client.get(
url=url,
headers=headers,
params=params,
params=params or None,
)

return container_provider_config.transform_container_retrieve_response(
Expand Down Expand Up @@ -8150,7 +8150,7 @@ def container_delete_handler(
response = sync_httpx_client.delete(
url=url,
headers=headers,
params=params,
params=params or None,
)

return container_provider_config.transform_container_delete_response(
Expand Down Expand Up @@ -8227,7 +8227,7 @@ async def async_container_delete_handler(
response = await async_httpx_client.delete(
url=url,
headers=headers,
params=params,
params=params or None,
)

return container_provider_config.transform_container_delete_response(
Expand Down Expand Up @@ -8323,7 +8323,7 @@ def container_file_list_handler(
response = sync_httpx_client.get(
url=url,
headers=headers,
params=params,
params=params or None,
)

return container_provider_config.transform_container_file_list_response(
Expand Down Expand Up @@ -8402,7 +8402,7 @@ async def async_container_file_list_handler(
response = await async_httpx_client.get(
url=url,
headers=headers,
params=params,
params=params or None,
)

return container_provider_config.transform_container_file_list_response(
Expand Down Expand Up @@ -8490,7 +8490,7 @@ def container_file_content_handler(
response = sync_httpx_client.get(
url=url,
headers=headers,
params=params,
params=params or None,
)

return container_provider_config.transform_container_file_content_response(
Expand Down Expand Up @@ -8566,7 +8566,7 @@ async def async_container_file_content_handler(
response = await async_httpx_client.get(
url=url,
headers=headers,
params=params,
params=params or None,
)

return container_provider_config.transform_container_file_content_response(
Expand Down
35 changes: 35 additions & 0 deletions litellm/proxy/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -2327,6 +2327,41 @@ class ConfigGeneralSettings(LiteLLMPydanticObjectBase):
database_connection_timeout: Optional[float] = Field(
60, description="default timeout for a connection to the database"
)
database_connect_timeout: Optional[float] = Field(
None,
description=(
"Prisma `connect_timeout` URL param (seconds). Bounds how long the "
"engine waits to establish a new connection before failing. Defaults "
"to Prisma's built-in value when unset."
),
)
database_socket_timeout: Optional[float] = Field(
None,
description=(
"Prisma `socket_timeout` URL param (seconds). When set, an idle/slow "
"connection that has not produced data within this window is closed. "
"This is the main knob for capping idle DB connections from LiteLLM."
),
)
database_extra_connection_params: Optional[Dict[str, Any]] = Field(
None,
description=(
"Escape hatch: extra key/value pairs appended verbatim to the Prisma "
"DATABASE_URL / DIRECT_URL query string (e.g. `sslmode`, `pgbouncer`, "
"`statement_cache_size`). Keys here override any default LiteLLM sets."
),
)
database_disable_prepared_statements: Optional[bool] = Field(
None,
description=(
"Disable server-side prepared statements by setting Prisma's "
"`pgbouncer=true` URL param. Use this for pgbouncer transaction-pooling "
"deployments, or to prevent the 'cached plan must not change result "
"type' error that pooled connections hit during rolling schema "
"migrations. An explicit `pgbouncer` in `database_extra_connection_params` "
"takes precedence."
),
)
database_type: Optional[Literal["dynamo_db"]] = Field(
None, description="to use dynamodb instead of postgres db"
)
Expand Down
10 changes: 10 additions & 0 deletions litellm/proxy/auth/auth_exception_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,16 @@ async def _handle_authentication_error(
)
elif isinstance(e, ProxyException):
raise e
if PrismaDBExceptionHandler.is_database_service_unavailable_error(e):
raise ProxyException(
message=(
"Service Unavailable, the authentication database is "
"temporarily unreachable. Please retry shortly."
),
type=ProxyErrorTypes.no_db_connection,
param="None",
code=status.HTTP_503_SERVICE_UNAVAILABLE,
)
raise ProxyException(
message="Authentication Error, " + str(e),
type=ProxyErrorTypes.auth_error,
Expand Down
4 changes: 2 additions & 2 deletions litellm/proxy/container_endpoints/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,7 @@ async def retrieve_container(
custom_llm_provider=custom_llm_provider,
)
data.update(
get_container_forwarding_params(
await get_container_forwarding_params(
container_id,
original_container_id,
custom_llm_provider,
Expand Down Expand Up @@ -433,7 +433,7 @@ async def delete_container(
custom_llm_provider=custom_llm_provider,
)
data.update(
get_container_forwarding_params(
await get_container_forwarding_params(
container_id,
original_container_id,
custom_llm_provider,
Expand Down
Loading
Loading