Merge branch 'main' into feat/improve-opik

This commit is contained in:
mrFranklin 2025-09-25 11:05:40 +08:00
commit cdfb32fd16
39 changed files with 1229 additions and 74 deletions

View file

@ -144,7 +144,7 @@ jobs:
name: Linting Testing
command: |
cd litellm
pip install "cryptography<40.0.0"
pip install "cryptography>=43.0.1"
python -m pip install types-requests types-setuptools types-redis types-PyYAML
if ! python -m mypy . \
--config-file mypy.ini \

View file

@ -350,13 +350,21 @@ curl 'http://0.0.0.0:4000/key/generate' \
[**Read the Docs**](https://docs.litellm.ai/docs/)
## Contributing
## Run in Developer mode
### Services
1. Setup .env file in root
2. Run dependant services `docker-compose up db prometheus`
Interested in contributing? Contributions to LiteLLM Python SDK, Proxy Server, and LLM integrations are both accepted and highly encouraged!
### Backend
1. (In root) create virtual environment `python -m venv .venv`
2. Activate virtual environment `source .venv/bin/activate`
3. Install dependencies `pip install -e ".[all]"`
4. Start proxy backend `python litellm/proxy_cli.py`
**Quick start:** `git clone``make install-dev``make format``make lint``make test-unit`
See our comprehensive [Contributing Guide (CONTRIBUTING.md)](CONTRIBUTING.md) for detailed instructions.
### Frontend
1. Navigate to `ui/litellm-dashboard`
2. Install dependencies `npm install`
3. Run `npm run dev` to start the dashboard
# Enterprise
For companies that need better security, user management and professional support
@ -434,18 +442,3 @@ All these checks must pass before your PR can be merged.
</a>
## Run in Developer mode
### Services
1. Setup .env file in root
2. Run dependant services `docker-compose up db prometheus`
### Backend
1. (In root) create virtual environment `python -m venv .venv`
2. Activate virtual environment `source .venv/bin/activate`
3. Install dependencies `pip install -e ".[all]"`
4. Start proxy backend `python3 /path/to/litellm/proxy_cli.py`
### Frontend
1. Navigate to `ui/litellm-dashboard`
2. Install dependencies `npm install`
3. Run `npm run dev` to start the dashboard

View file

@ -17120,9 +17120,10 @@
}
},
"node_modules/prebuild-install/node_modules/tar-fs": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz",
"integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==",
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
"integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
"license": "MIT",
"dependencies": {
"chownr": "^1.1.1",
"mkdirp-classic": "^0.5.2",
@ -19295,9 +19296,10 @@
}
},
"node_modules/tar-fs": {
"version": "3.0.10",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.10.tgz",
"integrity": "sha512-C1SwlQGNLe/jPNqapK8epDsXME7CAJR5RL3GcE6KWx1d9OUByzoHVcbu1VPI8tevg9H8Alae0AApHHFGzrD5zA==",
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz",
"integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==",
"license": "MIT",
"dependencies": {
"pump": "^3.0.0",
"tar-stream": "^3.1.5"

View file

@ -36,7 +36,7 @@ class InMemoryCache(BaseCache):
max_size_in_memory [int]: Maximum number of items in cache. done to prevent memory leaks. Use 200 items as a default
"""
self.max_size_in_memory = (
max_size_in_memory or 200
max_size_in_memory if max_size_in_memory is not None else 200
) # set an upper bound of 200 items in-memory
self.default_ttl = default_ttl or 600
self.max_size_per_item = (
@ -103,20 +103,32 @@ class InMemoryCache(BaseCache):
def evict_cache(self):
"""
Eviction policy:
- check if any items in ttl_dict are expired -> remove them from ttl_dict and cache_dict
1. First, remove expired items from ttl_dict and cache_dict
2. If cache is still at or above max_size_in_memory, evict items with earliest expiration times
This guarantees the following:
- 1. When item ttl not set: At minimumm each item will remain in memory for 5 minutes
- 2. When ttl is set: the item will remain in memory for at least that amount of time
- 1. When item ttl not set: At minimum each item will remain in memory for the default ttl
- 2. When ttl is set: the item will remain in memory for at least that amount of time, unless cache size requires eviction
- 3. the size of in-memory cache is bounded
"""
current_time = time.time()
# Step 1: Remove expired items
expired_keys = [key for key, ttl in self.ttl_dict.items() if current_time > ttl]
for key in expired_keys:
self._remove_key(key)
# Step 2: If cache is still full, evict items with earliest expiration times
if len(self.cache_dict) >= self.max_size_in_memory:
# Sort by expiration time (earliest first) and evict until we're under the limit
items_by_expiration = sorted(self.ttl_dict.items(), key=lambda x: x[1])
keys_to_evict = items_by_expiration[:len(self.cache_dict) - self.max_size_in_memory + 1]
for key, _ in keys_to_evict:
self._remove_key(key)
# de-reference the removed item
# https://www.geeksforgeeks.org/diagnosing-and-fixing-memory-leaks-in-python/
# One of the most common causes of memory leaks in Python is the retention of objects that are no longer being used.
@ -135,6 +147,10 @@ class InMemoryCache(BaseCache):
return False
def set_cache(self, key, value, **kwargs):
# Handle the edge case where max_size_in_memory is 0
if self.max_size_in_memory == 0:
return # Don't cache anything if max size is 0
if len(self.cache_dict) >= self.max_size_in_memory:
# only evict when cache is full
self.evict_cache()

View file

@ -812,6 +812,11 @@ BEDROCK_EMBEDDING_PROVIDERS_LITERAL = Literal[
]
BEDROCK_CONVERSE_MODELS = [
"qwen.qwen3-coder-480b-a35b-v1:0",
"qwen.qwen3-235b-a22b-2507-v1:0",
"qwen.qwen3-coder-30b-a3b-v1:0",
"qwen.qwen3-32b-v1:0",
"deepseek.v3-v1:0",
"openai.gpt-oss-20b-1:0",
"openai.gpt-oss-120b-1:0",
"anthropic.claude-opus-4-1-20250805-v1:0",

View file

@ -671,6 +671,7 @@ class LangFuseLogger:
generation_id = None
usage = None
usage_details = None
if response_obj is not None:
if (
hasattr(response_obj, "id")
@ -687,6 +688,11 @@ class LangFuseLogger:
"completion_tokens": _usage_obj.completion_tokens,
"total_cost": cost if self._supports_costs() else None,
}
usage_details = LangfuseUsageDetails(input=_usage_obj.prompt_tokens,
output=_usage_obj.completion_tokens,
cache_creation_input_tokens=_usage_obj.get('cache_creation_input_tokens', 0),
cache_read_input_tokens=_usage_obj.get('cache_read_input_tokens', 0))
generation_name = clean_metadata.pop("generation_name", None)
if generation_name is None:
# if `generation_name` is None, use sensible default values
@ -719,6 +725,7 @@ class LangFuseLogger:
"input": input if not mask_input else "redacted-by-litellm",
"output": output if not mask_output else "redacted-by-litellm",
"usage": usage,
"usage_details": usage_details,
"metadata": log_requester_metadata(clean_metadata),
"level": level,
"version": clean_metadata.pop("version", None),

View file

@ -16,11 +16,35 @@ Memory Management Strategy:
from typing import Any, Callable, Optional, Type, TypeVar
from pond import Pond, PooledObject, PooledObjectFactory
try:
from pond import Pond, PooledObject, PooledObjectFactory # type: ignore
POND_AVAILABLE = True
except ImportError: # pragma: no cover
POND_AVAILABLE = False
class Pond: # type: ignore
def __init__(self, *args: Any, **kwargs: Any) -> None:
pass
def register(self, *args: Any, **kwargs: Any) -> None:
pass
def borrow(self, *args: Any, **kwargs: Any) -> Any:
pass
def recycle(self, *args: Any, **kwargs: Any) -> None:
pass
class PooledObject: # type: ignore
def __init__(self, keeped_object: Any = None) -> None:
self.keeped_object = keeped_object
class PooledObjectFactory: # type: ignore
def __init__(self, *args: Any, **kwargs: Any) -> None:
pass
T = TypeVar('T')
class GenericPooledObjectFactory(PooledObjectFactory):
class GenericPooledObjectFactory(PooledObjectFactory): # type: ignore[misc]
"""Generic factory class for creating pooled objects of any type."""
def __init__(
@ -79,7 +103,7 @@ def get_object_pool(
time_between_eviction_runs: int = 300, # Less frequent eviction to maintain high reuse ratio
eviction_weight: float = 0.3, # Less aggressive eviction for better reuse
prewarm_count: int = 5 # Lower pre-warm count to reduce initial memory usage
) -> Pond:
) -> Pond | None:
"""Get or create a global object pool instance with balanced eviction-based memory control.
Memory is controlled through moderate eviction to balance reuse ratio and memory usage:
@ -98,9 +122,13 @@ def get_object_pool(
prewarm_count: Number of objects to pre-warm the pool with (default: 5)
Returns:
Pond instance for the specified object type
Pond instance for the specified object type or None if pond is not available
"""
# If pond is not available, disable pooling gracefully
if not POND_AVAILABLE:
return None
if pool_name in _pools:
return _pools[pool_name]
@ -134,4 +162,4 @@ def _prewarm_pool(pond: Pond, pool_name: str, prewarm_count: int = 20) -> None:
pond.recycle(pooled_obj, name=f"{pool_name}Factory")
except Exception:
# If pre-warming fails, just continue
break
break

View file

@ -2032,6 +2032,36 @@
"supports_tool_choice": false,
"supports_vision": true
},
"azure/gpt-5-codex": {
"cache_read_input_token_cost": 1.25e-07,
"input_cost_per_token": 1.25e-06,
"litellm_provider": "azure",
"max_input_tokens": 272000,
"max_output_tokens": 128000,
"max_tokens": 128000,
"mode": "responses",
"output_cost_per_token": 1e-05,
"supported_endpoints": [
"/v1/responses"
],
"supported_modalities": [
"text",
"image"
],
"supported_output_modalities": [
"text"
],
"supports_function_calling": true,
"supports_native_streaming": true,
"supports_parallel_function_calling": true,
"supports_pdf_input": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_system_messages": true,
"supports_tool_choice": true,
"supports_vision": true
},
"azure/gpt-5-mini": {
"cache_read_input_token_cost": 2.5e-08,
"input_cost_per_token": 2.5e-07,
@ -5282,6 +5312,49 @@
"supports_tool_choice": true,
"supports_vision": true
},
"deepseek-chat": {
"cache_read_input_token_cost": 6e-08,
"input_cost_per_token": 6e-07,
"litellm_provider": "deepseek",
"max_input_tokens": 131072,
"max_output_tokens": 8192,
"max_tokens": 131072,
"mode": "chat",
"output_cost_per_token": 1.7e-06,
"source": "https://api-docs.deepseek.com/quick_start/pricing",
"supported_endpoints": [
"/v1/chat/completions"
],
"supports_function_calling": true,
"supports_native_streaming": true,
"supports_parallel_function_calling": true,
"supports_prompt_caching": true,
"supports_response_schema": true,
"supports_system_messages": true,
"supports_tool_choice": true
},
"deepseek-reasoner": {
"cache_read_input_token_cost": 6e-08,
"input_cost_per_token": 6e-07,
"litellm_provider": "deepseek",
"max_input_tokens": 131072,
"max_output_tokens": 65536,
"max_tokens": 131072,
"mode": "chat",
"output_cost_per_token": 1.7e-06,
"source": "https://api-docs.deepseek.com/quick_start/pricing",
"supported_endpoints": [
"/v1/chat/completions"
],
"supports_function_calling": false,
"supports_native_streaming": true,
"supports_parallel_function_calling": false,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_system_messages": true,
"supports_tool_choice": false
},
"dashscope/qwen-coder": {
"input_cost_per_token": 3e-07,
"litellm_provider": "dashscope",
@ -7203,6 +7276,18 @@
"supports_prompt_caching": true,
"supports_tool_choice": true
},
"deepseek.v3-v1:0": {
"input_cost_per_token": 5.8e-07,
"litellm_provider": "bedrock_converse",
"max_input_tokens": 163840,
"max_output_tokens": 81920,
"max_tokens": 163840,
"mode": "chat",
"output_cost_per_token": 1.68e-06,
"supports_function_calling": true,
"supports_reasoning": true,
"supports_tool_choice": true
},
"dolphin": {
"input_cost_per_token": 5e-07,
"litellm_provider": "nlp_cloud",
@ -12415,6 +12500,36 @@
"supports_tool_choice": false,
"supports_vision": true
},
"gpt-5-codex": {
"cache_read_input_token_cost": 1.25e-07,
"input_cost_per_token": 1.25e-06,
"litellm_provider": "openai",
"max_input_tokens": 272000,
"max_output_tokens": 128000,
"max_tokens": 128000,
"mode": "responses",
"output_cost_per_token": 1e-05,
"supported_endpoints": [
"/v1/responses"
],
"supported_modalities": [
"text",
"image"
],
"supported_output_modalities": [
"text"
],
"supports_function_calling": true,
"supports_native_streaming": false,
"supports_parallel_function_calling": true,
"supports_pdf_input": false,
"supports_prompt_caching": true,
"supports_reasoning": false,
"supports_response_schema": true,
"supports_system_messages": false,
"supports_tool_choice": true,
"supports_vision": true
},
"gpt-5-mini": {
"cache_read_input_token_cost": 2.5e-08,
"cache_read_input_token_cost_flex": 1.25e-08,
@ -17520,6 +17635,54 @@
"mode": "chat",
"output_cost_per_token": 2.8e-07
},
"qwen.qwen3-coder-480b-a35b-v1:0": {
"input_cost_per_token": 2.2e-07,
"litellm_provider": "bedrock_converse",
"max_input_tokens": 262000,
"max_output_tokens": 65536,
"max_tokens": 262144,
"mode": "chat",
"output_cost_per_token": 1.8e-06,
"supports_function_calling": true,
"supports_reasoning": true,
"supports_tool_choice": true
},
"qwen.qwen3-235b-a22b-2507-v1:0": {
"input_cost_per_token": 2.2e-07,
"litellm_provider": "bedrock_converse",
"max_input_tokens": 262144,
"max_output_tokens": 131072,
"max_tokens": 262144,
"mode": "chat",
"output_cost_per_token": 8.8e-07,
"supports_function_calling": true,
"supports_reasoning": true,
"supports_tool_choice": true
},
"qwen.qwen3-coder-30b-a3b-v1:0": {
"input_cost_per_token": 1.5e-07,
"litellm_provider": "bedrock_converse",
"max_input_tokens": 262144,
"max_output_tokens": 131072,
"max_tokens": 262144,
"mode": "chat",
"output_cost_per_token": 6.0e-07,
"supports_function_calling": true,
"supports_reasoning": true,
"supports_tool_choice": true
},
"qwen.qwen3-32b-v1:0": {
"input_cost_per_token": 1.5e-07,
"litellm_provider": "bedrock_converse",
"max_input_tokens": 131072,
"max_output_tokens": 16384,
"max_tokens": 131072,
"mode": "chat",
"output_cost_per_token": 6.0e-07,
"supports_function_calling": true,
"supports_reasoning": true,
"supports_tool_choice": true
},
"recraft/recraftv2": {
"litellm_provider": "recraft",
"mode": "image_generation",
@ -17946,6 +18109,32 @@
"supports_reasoning": true,
"supports_tool_choice": true
},
"sambanova/DeepSeek-V3.1": {
"max_tokens": 32768,
"max_input_tokens": 32768,
"max_output_tokens": 32768,
"input_cost_per_token": 3e-06,
"output_cost_per_token": 4.5e-06,
"litellm_provider": "sambanova",
"mode": "chat",
"supports_function_calling": true,
"supports_tool_choice": true,
"supports_reasoning": true,
"source": "https://cloud.sambanova.ai/plans/pricing"
},
"sambanova/gpt-oss-120b": {
"max_tokens": 131072,
"max_input_tokens": 131072,
"max_output_tokens": 131072,
"input_cost_per_token": 3e-06,
"output_cost_per_token": 4.5e-06,
"litellm_provider": "sambanova",
"mode": "chat",
"supports_function_calling": true,
"supports_tool_choice": true,
"supports_reasoning": true,
"source": "https://cloud.sambanova.ai/plans/pricing"
},
"sample_spec": {
"code_interpreter_cost_per_session": 0.0,
"computer_use_input_cost_per_1k_tokens": 0.0,
@ -20467,6 +20656,24 @@
"supports_function_calling": true,
"supports_tool_choice": true
},
"vertex_ai/deepseek-ai/deepseek-v3.1-maas": {
"input_cost_per_token": 1.35e-06,
"litellm_provider": "vertex_ai-deepseek_models",
"max_input_tokens": 163840,
"max_output_tokens": 32768,
"max_tokens": 163840,
"mode": "chat",
"output_cost_per_token": 5.4e-06,
"source": "https://cloud.google.com/vertex-ai/generative-ai/pricing#partner-models",
"supported_regions": [
"us-west2"
],
"supports_assistant_prefill": true,
"supports_function_calling": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_tool_choice": true
},
"vertex_ai/deepseek-ai/deepseek-r1-0528-maas": {
"input_cost_per_token": 1.35e-06,
"litellm_provider": "vertex_ai-deepseek_models",
@ -20880,6 +21087,30 @@
"supports_function_calling": true,
"supports_tool_choice": true
},
"vertex_ai/qwen/qwen3-next-80b-a3b-instruct-maas": {
"input_cost_per_token": 1.5e-07,
"litellm_provider": "vertex_ai-qwen_models",
"max_input_tokens": 262144,
"max_output_tokens": 262144,
"max_tokens": 262144,
"mode": "chat",
"output_cost_per_token": 1.2e-06,
"source": "https://cloud.google.com/vertex-ai/generative-ai/pricing",
"supports_function_calling": true,
"supports_tool_choice": true
},
"vertex_ai/qwen/qwen3-next-80b-a3b-thinking-maas": {
"input_cost_per_token": 1.5e-07,
"litellm_provider": "vertex_ai-qwen_models",
"max_input_tokens": 262144,
"max_output_tokens": 262144,
"max_tokens": 262144,
"mode": "chat",
"output_cost_per_token": 1.2e-06,
"source": "https://cloud.google.com/vertex-ai/generative-ai/pricing",
"supports_function_calling": true,
"supports_tool_choice": true
},
"vertex_ai/veo-2.0-generate-001": {
"litellm_provider": "vertex_ai-video-models",
"max_input_tokens": 1024,

View file

@ -422,6 +422,7 @@ class LiteLLMRoutes(enum.Enum):
"/user/update",
"/user/delete",
"/user/info",
"/user/list",
# team
"/team/new",
"/team/update",

View file

@ -3,9 +3,10 @@ import os
from datetime import datetime
from typing import Dict, List, Literal, Optional, Tuple, Union
from fastapi import HTTPException
import litellm
from litellm._logging import verbose_proxy_logger
from litellm.exceptions import GuardrailRaisedException
from litellm.integrations.custom_guardrail import CustomGuardrail
from litellm.llms.custom_httpx.http_handler import (
get_async_httpx_client,
@ -237,9 +238,8 @@ class LakeraAIGuardrail(CustomGuardrail):
)
else:
# If there are other violations or not set to mask PII, raise exception
raise GuardrailRaisedException(
guardrail_name=self.guardrail_name,
message="Lakera AI flagged this request. Please review the request and try again.",
raise self._get_http_exception_for_blocked_guardrail(
lakera_guardrail_response
)
#########################################################
@ -304,9 +304,8 @@ class LakeraAIGuardrail(CustomGuardrail):
)
else:
# If there are other violations or not set to mask PII, raise exception
raise GuardrailRaisedException(
guardrail_name=self.guardrail_name,
message="Lakera AI flagged this request. Please review the request and try again.",
raise self._get_http_exception_for_blocked_guardrail(
lakera_guardrail_response
)
#########################################################
@ -327,8 +326,32 @@ class LakeraAIGuardrail(CustomGuardrail):
if not lakera_response:
return False
for item in lakera_response.get("payload", []) or []:
detector_type = item.get("detector_type", "") or ""
if not detector_type.startswith("pii/"):
return False
return True
# Check breakdown field for detected violations
breakdown = lakera_response.get("breakdown", []) or []
if not breakdown:
return False
has_violations = False
for item in breakdown:
if item.get("detected", False):
has_violations = True
detector_type = item.get("detector_type", "") or ""
if not detector_type.startswith("pii/"):
return False
# Return True only if there are violations and they are all PII
return has_violations
def _get_http_exception_for_blocked_guardrail(
self, lakera_response: Optional[LakeraAIResponse]
) -> HTTPException:
"""
Get the HTTP exception for a blocked guardrail, similar to Bedrock's implementation.
"""
return HTTPException(
status_code=400,
detail={
"error": "Violated guardrail policy",
"lakera_guardrail_response": lakera_response,
},
)

View file

@ -23,3 +23,16 @@ model_list:
litellm_params:
model: gemini/*
api_key: os.environ/GEMINI_API_KEY
guardrails:
- guardrail_name: lakera
litellm_params:
guardrail: lakera_v2
mode: pre_call
api_key: os.environ/LAKERA_API_KEY
default_on: false
project_id: project-9770817088
breakdown: true
payload: true
dev_info: true

View file

@ -7,3 +7,10 @@ class LangfuseLoggingConfig(TypedDict):
langfuse_secret: Optional[str]
langfuse_public_key: Optional[str]
langfuse_host: Optional[str]
class LangfuseUsageDetails(TypedDict):
input: Optional[int]
output: Optional[int]
cache_creation_input_tokens: Optional[int]
cache_read_input_tokens: Optional[int]

View file

@ -2032,6 +2032,36 @@
"supports_tool_choice": false,
"supports_vision": true
},
"azure/gpt-5-codex": {
"cache_read_input_token_cost": 1.25e-07,
"input_cost_per_token": 1.25e-06,
"litellm_provider": "azure",
"max_input_tokens": 272000,
"max_output_tokens": 128000,
"max_tokens": 128000,
"mode": "responses",
"output_cost_per_token": 1e-05,
"supported_endpoints": [
"/v1/responses"
],
"supported_modalities": [
"text",
"image"
],
"supported_output_modalities": [
"text"
],
"supports_function_calling": true,
"supports_native_streaming": true,
"supports_parallel_function_calling": true,
"supports_pdf_input": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_system_messages": true,
"supports_tool_choice": true,
"supports_vision": true
},
"azure/gpt-5-mini": {
"cache_read_input_token_cost": 2.5e-08,
"input_cost_per_token": 2.5e-07,
@ -5282,6 +5312,49 @@
"supports_tool_choice": true,
"supports_vision": true
},
"deepseek-chat": {
"cache_read_input_token_cost": 6e-08,
"input_cost_per_token": 6e-07,
"litellm_provider": "deepseek",
"max_input_tokens": 131072,
"max_output_tokens": 8192,
"max_tokens": 131072,
"mode": "chat",
"output_cost_per_token": 1.7e-06,
"source": "https://api-docs.deepseek.com/quick_start/pricing",
"supported_endpoints": [
"/v1/chat/completions"
],
"supports_function_calling": true,
"supports_native_streaming": true,
"supports_parallel_function_calling": true,
"supports_prompt_caching": true,
"supports_response_schema": true,
"supports_system_messages": true,
"supports_tool_choice": true
},
"deepseek-reasoner": {
"cache_read_input_token_cost": 6e-08,
"input_cost_per_token": 6e-07,
"litellm_provider": "deepseek",
"max_input_tokens": 131072,
"max_output_tokens": 65536,
"max_tokens": 131072,
"mode": "chat",
"output_cost_per_token": 1.7e-06,
"source": "https://api-docs.deepseek.com/quick_start/pricing",
"supported_endpoints": [
"/v1/chat/completions"
],
"supports_function_calling": false,
"supports_native_streaming": true,
"supports_parallel_function_calling": false,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_system_messages": true,
"supports_tool_choice": false
},
"dashscope/qwen-coder": {
"input_cost_per_token": 3e-07,
"litellm_provider": "dashscope",
@ -7203,6 +7276,18 @@
"supports_prompt_caching": true,
"supports_tool_choice": true
},
"deepseek.v3-v1:0": {
"input_cost_per_token": 5.8e-07,
"litellm_provider": "bedrock_converse",
"max_input_tokens": 163840,
"max_output_tokens": 81920,
"max_tokens": 163840,
"mode": "chat",
"output_cost_per_token": 1.68e-06,
"supports_function_calling": true,
"supports_reasoning": true,
"supports_tool_choice": true
},
"dolphin": {
"input_cost_per_token": 5e-07,
"litellm_provider": "nlp_cloud",
@ -12415,6 +12500,36 @@
"supports_tool_choice": false,
"supports_vision": true
},
"gpt-5-codex": {
"cache_read_input_token_cost": 1.25e-07,
"input_cost_per_token": 1.25e-06,
"litellm_provider": "openai",
"max_input_tokens": 272000,
"max_output_tokens": 128000,
"max_tokens": 128000,
"mode": "responses",
"output_cost_per_token": 1e-05,
"supported_endpoints": [
"/v1/responses"
],
"supported_modalities": [
"text",
"image"
],
"supported_output_modalities": [
"text"
],
"supports_function_calling": true,
"supports_native_streaming": false,
"supports_parallel_function_calling": true,
"supports_pdf_input": false,
"supports_prompt_caching": true,
"supports_reasoning": false,
"supports_response_schema": true,
"supports_system_messages": false,
"supports_tool_choice": true,
"supports_vision": true
},
"gpt-5-mini": {
"cache_read_input_token_cost": 2.5e-08,
"cache_read_input_token_cost_flex": 1.25e-08,
@ -17520,6 +17635,54 @@
"mode": "chat",
"output_cost_per_token": 2.8e-07
},
"qwen.qwen3-coder-480b-a35b-v1:0": {
"input_cost_per_token": 2.2e-07,
"litellm_provider": "bedrock_converse",
"max_input_tokens": 262000,
"max_output_tokens": 65536,
"max_tokens": 262144,
"mode": "chat",
"output_cost_per_token": 1.8e-06,
"supports_function_calling": true,
"supports_reasoning": true,
"supports_tool_choice": true
},
"qwen.qwen3-235b-a22b-2507-v1:0": {
"input_cost_per_token": 2.2e-07,
"litellm_provider": "bedrock_converse",
"max_input_tokens": 262144,
"max_output_tokens": 131072,
"max_tokens": 262144,
"mode": "chat",
"output_cost_per_token": 8.8e-07,
"supports_function_calling": true,
"supports_reasoning": true,
"supports_tool_choice": true
},
"qwen.qwen3-coder-30b-a3b-v1:0": {
"input_cost_per_token": 1.5e-07,
"litellm_provider": "bedrock_converse",
"max_input_tokens": 262144,
"max_output_tokens": 131072,
"max_tokens": 262144,
"mode": "chat",
"output_cost_per_token": 6.0e-07,
"supports_function_calling": true,
"supports_reasoning": true,
"supports_tool_choice": true
},
"qwen.qwen3-32b-v1:0": {
"input_cost_per_token": 1.5e-07,
"litellm_provider": "bedrock_converse",
"max_input_tokens": 131072,
"max_output_tokens": 16384,
"max_tokens": 131072,
"mode": "chat",
"output_cost_per_token": 6.0e-07,
"supports_function_calling": true,
"supports_reasoning": true,
"supports_tool_choice": true
},
"recraft/recraftv2": {
"litellm_provider": "recraft",
"mode": "image_generation",
@ -17946,6 +18109,32 @@
"supports_reasoning": true,
"supports_tool_choice": true
},
"sambanova/DeepSeek-V3.1": {
"max_tokens": 32768,
"max_input_tokens": 32768,
"max_output_tokens": 32768,
"input_cost_per_token": 3e-06,
"output_cost_per_token": 4.5e-06,
"litellm_provider": "sambanova",
"mode": "chat",
"supports_function_calling": true,
"supports_tool_choice": true,
"supports_reasoning": true,
"source": "https://cloud.sambanova.ai/plans/pricing"
},
"sambanova/gpt-oss-120b": {
"max_tokens": 131072,
"max_input_tokens": 131072,
"max_output_tokens": 131072,
"input_cost_per_token": 3e-06,
"output_cost_per_token": 4.5e-06,
"litellm_provider": "sambanova",
"mode": "chat",
"supports_function_calling": true,
"supports_tool_choice": true,
"supports_reasoning": true,
"source": "https://cloud.sambanova.ai/plans/pricing"
},
"sample_spec": {
"code_interpreter_cost_per_session": 0.0,
"computer_use_input_cost_per_1k_tokens": 0.0,
@ -20467,6 +20656,24 @@
"supports_function_calling": true,
"supports_tool_choice": true
},
"vertex_ai/deepseek-ai/deepseek-v3.1-maas": {
"input_cost_per_token": 1.35e-06,
"litellm_provider": "vertex_ai-deepseek_models",
"max_input_tokens": 163840,
"max_output_tokens": 32768,
"max_tokens": 163840,
"mode": "chat",
"output_cost_per_token": 5.4e-06,
"source": "https://cloud.google.com/vertex-ai/generative-ai/pricing#partner-models",
"supported_regions": [
"us-west2"
],
"supports_assistant_prefill": true,
"supports_function_calling": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_tool_choice": true
},
"vertex_ai/deepseek-ai/deepseek-r1-0528-maas": {
"input_cost_per_token": 1.35e-06,
"litellm_provider": "vertex_ai-deepseek_models",
@ -20880,6 +21087,30 @@
"supports_function_calling": true,
"supports_tool_choice": true
},
"vertex_ai/qwen/qwen3-next-80b-a3b-instruct-maas": {
"input_cost_per_token": 1.5e-07,
"litellm_provider": "vertex_ai-qwen_models",
"max_input_tokens": 262144,
"max_output_tokens": 262144,
"max_tokens": 262144,
"mode": "chat",
"output_cost_per_token": 1.2e-06,
"source": "https://cloud.google.com/vertex-ai/generative-ai/pricing",
"supports_function_calling": true,
"supports_tool_choice": true
},
"vertex_ai/qwen/qwen3-next-80b-a3b-thinking-maas": {
"input_cost_per_token": 1.5e-07,
"litellm_provider": "vertex_ai-qwen_models",
"max_input_tokens": 262144,
"max_output_tokens": 262144,
"max_tokens": 262144,
"mode": "chat",
"output_cost_per_token": 1.2e-06,
"source": "https://cloud.google.com/vertex-ai/generative-ai/pricing",
"supports_function_calling": true,
"supports_tool_choice": true
},
"vertex_ai/veo-2.0-generate-001": {
"litellm_provider": "vertex_ai-video-models",
"max_input_tokens": 1024,

10
poetry.lock generated
View file

@ -2730,9 +2730,10 @@ files = [
name = "madoka"
version = "0.7.1"
description = "Memory-efficient CountMin Sketch key-value structure (based on Madoka C++ library)"
optional = false
optional = true
python-versions = "*"
groups = ["main"]
markers = "extra == \"proxy\""
files = [
{file = "madoka-0.7.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:7521eee9ace30b376bb54fdcb2cb42bf6b7a0346b0d0b612f25f3299aa4a95af"},
{file = "madoka-0.7.1.tar.gz", hash = "sha256:e258baa84fc0a3764365993b8bf5e1b065383a6ca8c9f862fb3e3e709843fae7"},
@ -4044,9 +4045,10 @@ xlsxwriter = ["xlsxwriter"]
name = "pondpond"
version = "1.4.1"
description = "Pond is a high performance object-pooling library for Python."
optional = false
optional = true
python-versions = ">=3.8"
groups = ["main"]
markers = "extra == \"proxy\""
files = [
{file = "pondpond-1.4.1-py3-none-any.whl", hash = "sha256:641028ead4e8018ca6de1220c660ddd6d6fbf62a60e72f410655dd0451d82880"},
{file = "pondpond-1.4.1.tar.gz", hash = "sha256:8afa34b869d1434d21dd2ec12644abc3b1733fcda8fcf355300338a13a79bb7b"},
@ -6747,11 +6749,11 @@ type = ["pytest-mypy"]
caching = ["diskcache"]
extra-proxy = ["azure-identity", "azure-keyvault-secrets", "google-cloud-iam", "google-cloud-kms", "prisma", "redisvl", "resend"]
mlflow = ["mlflow"]
proxy = ["PyJWT", "apscheduler", "azure-identity", "azure-storage-blob", "backoff", "boto3", "cryptography", "fastapi", "fastapi-sso", "fastuuid", "gunicorn", "litellm-enterprise", "litellm-proxy-extras", "mcp", "orjson", "polars", "pynacl", "python-multipart", "pyyaml", "rich", "rq", "uvicorn", "uvloop", "websockets"]
proxy = ["PyJWT", "apscheduler", "azure-identity", "azure-storage-blob", "backoff", "boto3", "cryptography", "fastapi", "fastapi-sso", "fastuuid", "gunicorn", "litellm-enterprise", "litellm-proxy-extras", "mcp", "orjson", "polars", "pondpond", "pynacl", "python-multipart", "pyyaml", "rich", "rq", "uvicorn", "uvloop", "websockets"]
semantic-router = ["semantic-router"]
utils = ["numpydoc"]
[metadata]
lock-version = "2.1"
python-versions = ">=3.8.1,<4.0, !=3.9.7"
content-hash = "75004c6a23b70be86622fa417fd0d62fa3843e6e61c8dff8507ae5c967b7205d"
content-hash = "16fdc1044b4bb316803cbf1825bc970895526949a8bef93eab741777b7210ca8"

View file

@ -31,7 +31,7 @@ jinja2 = "^3.1.2"
aiohttp = ">=3.10"
pydantic = "^2.5.0"
jsonschema = "^4.22.0"
pondpond = "^1.4.1"
pondpond = {version = "^1.4.1", optional = true}
numpydoc = {version = "*", optional = true} # used in utils.py
fastuuid = {version = ">=0.12.0", optional = true}
@ -94,6 +94,7 @@ proxy = [
"rich",
"polars",
"fastuuid",
"pondpond",
]
extra_proxy = [

View file

@ -11,7 +11,8 @@ from litellm.proxy.guardrails.guardrail_hooks.lakera_ai_v2 import LakeraAIGuardr
from litellm.types.guardrails import PiiEntityType, PiiAction
from litellm.proxy._types import UserAPIKeyAuth
from litellm.caching.caching import DualCache
from litellm.exceptions import BlockedPiiEntityError
from litellm.exceptions import BlockedPiiEntityError, GuardrailRaisedException
from fastapi import HTTPException
from litellm.types.utils import CallTypes as LitellmCallTypes
@ -54,3 +55,179 @@ async def test_lakera_pre_call_hook_for_pii_masking():
assert "4111-1111-1111-1111" not in user_message
assert "test@example.com" not in user_message
@pytest.mark.asyncio
async def test_lakera_blocks_non_pii_violations():
"""Test that Lakera guardrail blocks requests with non-PII violations like hate speech, violence, etc."""
lakera_guardrail = LakeraAIGuardrail(
api_key="test_key",
)
# Mock the call_v2_guard method to return a response similar to the user's example
mock_response = {
'payload': [],
'flagged': True,
'dev_info': {'git_revision': 'f0bc093a', 'git_timestamp': '2025-09-23T15:28:06+00:00', 'model_version': 'lakera-guard-1', 'version': '2.0.281'},
'metadata': {'request_uuid': 'b7cd4c8a-28aa-4285-a245-2befee514dbf'},
'breakdown': [
{'project_id': 'project-9770817088', 'policy_id': 'policy-lakera-default', 'detector_id': 'detector-lakera-default-moderated-content', 'detector_type': 'moderated_content/crime', 'detected': True, 'message_id': 0},
{'project_id': 'project-9770817088', 'policy_id': 'policy-lakera-default', 'detector_id': 'detector-lakera-default-moderated-content', 'detector_type': 'moderated_content/hate', 'detected': True, 'message_id': 0},
{'project_id': 'project-9770817088', 'policy_id': 'policy-lakera-default', 'detector_id': 'detector-lakera-default-moderated-content', 'detector_type': 'moderated_content/violence', 'detected': True, 'message_id': 0},
{'project_id': 'project-9770817088', 'policy_id': 'policy-lakera-default', 'detector_id': 'detector-lakera-default-prompt-attack', 'detector_type': 'prompt_attack', 'detected': True, 'message_id': 0},
{'project_id': 'project-9770817088', 'policy_id': 'policy-lakera-default', 'detector_id': 'detector-lakera-default-pii', 'detector_type': 'pii/email', 'detected': False, 'message_id': 0},
]
}
with patch.object(lakera_guardrail, 'call_v2_guard', new_callable=AsyncMock) as mock_call:
mock_call.return_value = (mock_response, {})
# Create a sample request that would trigger violations
data = {
"messages": [
{"role": "user", "content": "Some harmful content that triggers violations"}
],
"model": "gpt-3.5-turbo",
"metadata": {}
}
# Mock objects needed for the pre-call hook
user_api_key_dict = UserAPIKeyAuth(api_key="test_key")
cache = DualCache()
# The guardrail should raise an HTTPException for non-PII violations
with pytest.raises(HTTPException) as exc_info:
await lakera_guardrail.async_pre_call_hook(
user_api_key_dict=user_api_key_dict,
cache=cache,
data=data,
call_type="completion"
)
# Verify the exception details include the Lakera response
assert exc_info.value.status_code == 400
assert "Violated guardrail policy" in str(exc_info.value.detail)
assert "lakera_guardrail_response" in exc_info.value.detail
@pytest.mark.asyncio
async def test_lakera_only_pii_violations_are_masked():
"""Test that Lakera guardrail only masks PII violations and doesn't block the request."""
lakera_guardrail = LakeraAIGuardrail(
api_key="test_key",
)
# Mock response with only PII violations
mock_response = {
'payload': [
{'detector_type': 'pii/email', 'start': 10, 'end': 25, 'message_id': 0}
],
'flagged': True,
'breakdown': [
{'project_id': 'project-9770817088', 'detector_type': 'pii/email', 'detected': True, 'message_id': 0},
{'project_id': 'project-9770817088', 'detector_type': 'moderated_content/hate', 'detected': False, 'message_id': 0},
{'project_id': 'project-9770817088', 'detector_type': 'prompt_attack', 'detected': False, 'message_id': 0},
]
}
with patch.object(lakera_guardrail, 'call_v2_guard', new_callable=AsyncMock) as mock_call:
mock_call.return_value = (mock_response, {})
data = {
"messages": [
{"role": "user", "content": "My email test@example.com here"}
],
"model": "gpt-3.5-turbo",
"metadata": {}
}
user_api_key_dict = UserAPIKeyAuth(api_key="test_key")
cache = DualCache()
# Should not raise an exception, just mask the PII
result = await lakera_guardrail.async_pre_call_hook(
user_api_key_dict=user_api_key_dict,
cache=cache,
data=data,
call_type="completion"
)
# Verify the request was not blocked
assert result is not None
assert "messages" in result
@pytest.mark.asyncio
async def test_lakera_blocks_flagged_content_with_user_scenario():
"""
Test the exact user scenario where Lakera flagged content but request went through.
This should now be blocked with the fix to check breakdown field instead of payload.
"""
lakera_guardrail = LakeraAIGuardrail(
api_key="test_key",
)
# Mock response matching the exact user scenario
mock_response = {
'payload': [], # Empty payload like in user's case
'flagged': True,
'dev_info': {'git_revision': 'f0bc093a', 'git_timestamp': '2025-09-23T15:28:06+00:00', 'model_version': 'lakera-guard-1', 'version': '2.0.281'},
'metadata': {'request_uuid': 'b7cd4c8a-28aa-4285-a245-2befee514dbf'},
'breakdown': [
{'project_id': 'project-9770817088', 'policy_id': 'policy-lakera-default', 'detector_id': 'detector-lakera-default-moderated-content', 'detector_type': 'moderated_content/crime', 'detected': True, 'message_id': 0},
{'project_id': 'project-9770817088', 'policy_id': 'policy-lakera-default', 'detector_id': 'detector-lakera-default-moderated-content', 'detector_type': 'moderated_content/hate', 'detected': True, 'message_id': 0},
{'project_id': 'project-9770817088', 'policy_id': 'policy-lakera-default', 'detector_id': 'detector-lakera-default-moderated-content', 'detector_type': 'moderated_content/profanity', 'detected': False, 'message_id': 0},
{'project_id': 'project-9770817088', 'policy_id': 'policy-lakera-default', 'detector_id': 'detector-lakera-default-moderated-content', 'detector_type': 'moderated_content/sexual', 'detected': False, 'message_id': 0},
{'project_id': 'project-9770817088', 'policy_id': 'policy-lakera-default', 'detector_id': 'detector-lakera-default-moderated-content', 'detector_type': 'moderated_content/violence', 'detected': True, 'message_id': 0},
{'project_id': 'project-9770817088', 'policy_id': 'policy-lakera-default', 'detector_id': 'detector-lakera-default-moderated-content', 'detector_type': 'moderated_content/weapons', 'detected': True, 'message_id': 0},
{'project_id': 'project-9770817088', 'policy_id': 'policy-lakera-default', 'detector_id': 'detector-lakera-default-pii', 'detector_type': 'pii/address', 'detected': False, 'message_id': 0},
{'project_id': 'project-9770817088', 'policy_id': 'policy-lakera-default', 'detector_id': 'detector-lakera-default-pii', 'detector_type': 'pii/credit_card', 'detected': False, 'message_id': 0},
{'project_id': 'project-9770817088', 'policy_id': 'policy-lakera-default', 'detector_id': 'detector-lakera-default-pii', 'detector_type': 'pii/email', 'detected': False, 'message_id': 0},
{'project_id': 'project-9770817088', 'policy_id': 'policy-lakera-default', 'detector_id': 'detector-lakera-default-pii', 'detector_type': 'pii/iban_code', 'detected': False, 'message_id': 0},
{'project_id': 'project-9770817088', 'policy_id': 'policy-lakera-default', 'detector_id': 'detector-lakera-default-pii', 'detector_type': 'pii/ip_address', 'detected': False, 'message_id': 0},
{'project_id': 'project-9770817088', 'policy_id': 'policy-lakera-default', 'detector_id': 'detector-lakera-default-pii', 'detector_type': 'pii/name', 'detected': False, 'message_id': 0},
{'project_id': 'project-9770817088', 'policy_id': 'policy-lakera-default', 'detector_id': 'detector-lakera-default-pii', 'detector_type': 'pii/phone_number', 'detected': False, 'message_id': 0},
{'project_id': 'project-9770817088', 'policy_id': 'policy-lakera-default', 'detector_id': 'detector-lakera-default-pii', 'detector_type': 'pii/us_social_security_number', 'detected': False, 'message_id': 0},
{'project_id': 'project-9770817088', 'policy_id': 'policy-lakera-default', 'detector_id': 'detector-lakera-default-prompt-attack', 'detector_type': 'prompt_attack', 'detected': True, 'message_id': 0},
{'project_id': 'project-9770817088', 'policy_id': 'policy-lakera-default', 'detector_id': 'detector-lakera-default-unknown-links', 'detector_type': 'unknown_links', 'detected': False, 'message_id': 0}
]
}
with patch.object(lakera_guardrail, 'call_v2_guard', new_callable=AsyncMock) as mock_call:
mock_call.return_value = (mock_response, {})
# Create a sample request that would trigger violations
data = {
"messages": [
{"role": "user", "content": "Some harmful content that should be blocked"}
],
"model": "gpt-3.5-turbo",
"metadata": {}
}
# Mock objects needed for the pre-call hook
user_api_key_dict = UserAPIKeyAuth(api_key="test_key")
cache = DualCache()
# With the fix, this should now raise an HTTPException instead of letting the request through
with pytest.raises(HTTPException) as exc_info:
await lakera_guardrail.async_pre_call_hook(
user_api_key_dict=user_api_key_dict,
cache=cache,
data=data,
call_type="completion"
)
# Verify the exception details
assert exc_info.value.status_code == 400
assert "Violated guardrail policy" in str(exc_info.value.detail)
assert "lakera_guardrail_response" in exc_info.value.detail
# Verify the full response is included in the exception
lakera_response = exc_info.value.detail["lakera_guardrail_response"]
assert lakera_response["flagged"] is True
assert lakera_response["metadata"]["request_uuid"] == "b7cd4c8a-28aa-4285-a245-2befee514dbf"
assert len(lakera_response["breakdown"]) == 16 # All the breakdown items from the user's scenario

View file

@ -3,6 +3,7 @@ Simplified tests for object pooling utilities in litellm.
"""
import pytest
pytest.importorskip("pond")
from litellm.litellm_core_utils.object_pooling import (
get_object_pool,
@ -103,4 +104,4 @@ class TestObjectPooling:
if __name__ == "__main__":
pytest.main([__file__])
pytest.main([__file__])

View file

@ -27,6 +27,7 @@ import litellm
from litellm import (
ModelResponse,
RateLimitError,
ServiceUnavailableError,
Timeout,
completion,
completion_cost,
@ -2020,10 +2021,16 @@ def test_bedrock_context_window_error():
def test_bedrock_converse_route():
litellm.set_verbose = True
litellm.completion(
model="bedrock/converse/us.amazon.nova-pro-v1:0",
messages=[{"role": "user", "content": "Hello, world!"}],
)
try:
litellm.completion(
model="bedrock/converse/us.amazon.nova-pro-v1:0",
messages=[{"role": "user", "content": "Hello, world!"}],
)
except ServiceUnavailableError as e:
if "Too many requests" in str(e):
pytest.skip("Skipping test due to AWS Bedrock rate limiting")
else:
raise
def test_bedrock_mapped_converse_models():

View file

@ -68,7 +68,14 @@
"output": 20,
"unit": "TOKENS",
"totalCost": 5.4999999999999995e-05
}
},
"usageDetails": {
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 0,
"input": 10,
"output": 20
},
"traceId": "litellm-test-6a51ae70-a4e7-499e-afcd-dce2a3b31850"
},
"timestamp": "2025-01-16T19:28:55.125258Z"
}

View file

@ -63,7 +63,13 @@
"output": 10,
"unit": "TOKENS",
"totalCost": 0.00018
}
},
"usageDetails": {
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 0,
"input": 10,
"output": 10
}
},
"timestamp": "2025-05-26T21:13:16.797156Z"
}

View file

@ -109,6 +109,12 @@
"output": 20,
"unit": "TOKENS",
"totalCost": 5.4999999999999995e-05
},
"usageDetails": {
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 0,
"input": 10,
"output": 20
}
},
"timestamp": "2025-01-22T17:27:51.703046Z"

View file

@ -87,6 +87,12 @@
"output": 20,
"unit": "TOKENS",
"totalCost": 5.4999999999999995e-05
},
"usageDetails": {
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 0,
"input": 10,
"output": 20
}
},
"timestamp": "2025-01-22T17:19:11.235541Z"

View file

@ -58,6 +58,12 @@
"output": 10,
"unit": "TOKENS",
"totalCost": 3.5e-05
},
"usageDetails": {
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 0,
"input": 10,
"output": 10
}
},
"timestamp": "2025-02-07T00:23:27.670175Z"

View file

@ -73,6 +73,12 @@
"output": 10,
"unit": "TOKENS",
"totalCost": 3.5e-05
},
"usageDetails": {
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 0,
"input": 10,
"output": 10
}
},
"timestamp": "2025-05-24T17:01:19.408586Z"

View file

@ -77,6 +77,12 @@
"output": 20,
"unit": "TOKENS",
"totalCost": 5.4999999999999995e-05
},
"usageDetails": {
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 0,
"input": 10,
"output": 20
}
},
"timestamp": "2025-01-22T15:31:28.964179Z"

View file

@ -77,6 +77,12 @@
"output": 20,
"unit": "TOKENS",
"totalCost": 5.4999999999999995e-05
},
"usageDetails": {
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 0,
"input": 10,
"output": 20
}
},
"timestamp": "2025-01-22T16:38:26.017252Z"

View file

@ -61,6 +61,12 @@
"output": 10,
"unit": "TOKENS",
"totalCost": 7.5e-06
},
"usageDetails": {
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 0,
"input": 10,
"output": 10
}
},
"timestamp": "2025-05-26T21:15:40.610953Z"

View file

@ -84,6 +84,12 @@
"output": 20,
"unit": "TOKENS",
"totalCost": 5.4999999999999995e-05
},
"usageDetails": {
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 0,
"input": 10,
"output": 20
}
},
"timestamp": "2025-01-22T17:59:39.368310Z"

View file

@ -76,6 +76,12 @@
"output": 20,
"unit": "TOKENS",
"totalCost": 5.4999999999999995e-05
},
"usageDetails": {
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 0,
"input": 10,
"output": 20
}
},
"timestamp": "2025-01-22T18:06:50.959850Z"

View file

@ -70,6 +70,12 @@
"output": 20,
"unit": "TOKENS",
"totalCost": 5.4999999999999995e-05
},
"usageDetails": {
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 0,
"input": 10,
"output": 20
}
},
"timestamp": "2025-01-22T17:59:32.889548Z"

View file

@ -70,6 +70,12 @@
"output": 20,
"unit": "TOKENS",
"totalCost": 5.4999999999999995e-05
},
"usageDetails": {
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 0,
"input": 10,
"output": 20
}
},
"timestamp": "2025-01-22T17:59:36.162997Z"

View file

@ -70,6 +70,12 @@
"output": 20,
"unit": "TOKENS",
"totalCost": 5.4999999999999995e-05
},
"usageDetails": {
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 0,
"input": 10,
"output": 20
}
},
"timestamp": "2025-01-22T17:59:32.889548Z"

View file

@ -76,6 +76,12 @@
"output": 20,
"unit": "TOKENS",
"totalCost": 5.4999999999999995e-05
},
"usageDetails": {
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 0,
"input": 10,
"output": 20
}
},
"timestamp": "2025-01-22T17:55:28.855732Z"

View file

@ -76,6 +76,12 @@
"output": 20,
"unit": "TOKENS",
"totalCost": 5.4999999999999995e-05
},
"usageDetails": {
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 0,
"input": 10,
"output": 20
}
},
"timestamp": "2025-01-22T17:53:53.754511Z"

View file

@ -80,6 +80,12 @@
"output": 20,
"unit": "TOKENS",
"totalCost": 5.4999999999999995e-05
},
"usageDetails": {
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 0,
"input": 10,
"output": 20
}
},
"timestamp": "2025-01-22T17:56:35.478171Z"

View file

@ -84,6 +84,12 @@
"output": 20,
"unit": "TOKENS",
"totalCost": 5.4999999999999995e-05
},
"usageDetails": {
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 0,
"input": 10,
"output": 20
}
},
"timestamp": "2025-01-22T17:56:38.787196Z"

View file

@ -43,8 +43,9 @@ async def test_anthropic_basic_completion_with_headers():
json.dumps(response_json, indent=4, default=str),
)
reported_usage = response_json.get("usage", None)
anthropic_api_input_tokens = reported_usage.get("input_tokens", None)
anthropic_api_output_tokens = reported_usage.get("output_tokens", None)
# fix null checks for reported_usage
anthropic_api_input_tokens = reported_usage.get("input_tokens", None) if reported_usage else None
anthropic_api_output_tokens = reported_usage.get("output_tokens", None) if reported_usage else None
litellm_call_id = response_headers.get("x-litellm-call-id")
print(f"LiteLLM Call ID: {litellm_call_id}")

View file

@ -88,3 +88,101 @@ def test_in_memory_cache_ttl_allow_override():
new_ttl_time = in_memory_cache.ttl_dict["new-fake-key"]
assert new_ttl_time is not None
assert new_ttl_time != initial_ttl_time
def test_in_memory_cache_max_size_with_ttl():
"""
Test that max_size_in_memory is respected even when all items have long TTLs.
This tests the fix for the unbounded growth issue.
"""
in_memory_cache = InMemoryCache(max_size_in_memory=3)
long_ttl = 86400 # 1 day
# Fill the cache to max capacity
for i in range(3):
in_memory_cache.set_cache(key=f"key_{i}", value=f"value_{i}", ttl=long_ttl)
time.sleep(0.01) # Small delay to ensure different timestamps
assert len(in_memory_cache.cache_dict) == 3
assert len(in_memory_cache.ttl_dict) == 3
# Add another item - should evict the earliest item
in_memory_cache.set_cache(key="key_3", value="value_3", ttl=long_ttl)
# Cache should still be at max size, not larger
assert len(in_memory_cache.cache_dict) == 3
assert len(in_memory_cache.ttl_dict) == 3
# key_0 should have been evicted (it was added first)
assert "key_0" not in in_memory_cache.cache_dict
assert "key_0" not in in_memory_cache.ttl_dict
# Other keys should still be present
assert "key_1" in in_memory_cache.cache_dict
assert "key_2" in in_memory_cache.cache_dict
assert "key_3" in in_memory_cache.cache_dict
def test_in_memory_cache_expired_items_evicted_first():
"""
Test that expired items are evicted before non-expired items when cache is full.
"""
in_memory_cache = InMemoryCache(max_size_in_memory=3)
# Add items with short TTL that will expire
in_memory_cache.set_cache(key="expired_1", value="value_1", ttl=1)
in_memory_cache.set_cache(key="expired_2", value="value_2", ttl=1)
# Add item with long TTL
in_memory_cache.set_cache(key="long_lived", value="value_long", ttl=86400)
assert len(in_memory_cache.cache_dict) == 3
# Wait for short TTL items to expire
time.sleep(2)
# Add new item - should evict expired items first, not the long-lived one
in_memory_cache.set_cache(key="new_item", value="new_value", ttl=86400)
# Long-lived item should still be present
assert "long_lived" in in_memory_cache.cache_dict
assert "new_item" in in_memory_cache.cache_dict
# Expired items should be gone
assert "expired_1" not in in_memory_cache.cache_dict
assert "expired_2" not in in_memory_cache.cache_dict
assert "expired_1" not in in_memory_cache.ttl_dict
assert "expired_2" not in in_memory_cache.ttl_dict
def test_in_memory_cache_eviction_order():
"""
Test that when non-expired items need to be evicted, those with earliest expiration times are evicted first.
"""
in_memory_cache = InMemoryCache(max_size_in_memory=2)
# Add items with different TTLs
now = time.time()
in_memory_cache.set_cache(key="early_expire", value="value_1", ttl=100) # expires in 100 seconds
time.sleep(0.01)
in_memory_cache.set_cache(key="late_expire", value="value_2", ttl=200) # expires in 200 seconds
# Verify TTL order
early_ttl = in_memory_cache.ttl_dict["early_expire"]
late_ttl = in_memory_cache.ttl_dict["late_expire"]
assert early_ttl < late_ttl, "early_expire should have earlier expiration time"
assert len(in_memory_cache.cache_dict) == 2
# Add third item - should evict the one with earliest expiration time
in_memory_cache.set_cache(key="new_item", value="value_3", ttl=300)
assert len(in_memory_cache.cache_dict) == 2
# Item with earliest expiration should be evicted
assert "early_expire" not in in_memory_cache.cache_dict
assert "early_expire" not in in_memory_cache.ttl_dict
# Items with later expiration should remain
assert "late_expire" in in_memory_cache.cache_dict
assert "new_item" in in_memory_cache.cache_dict

View file

@ -1,20 +1,222 @@
import json
import os
import sys
from typing import Optional
# Adds the grandparent directory to sys.path to allow importing project modules
sys.path.insert(0, os.path.abspath("../.."))
import unittest
import asyncio
from unittest.mock import patch
from unittest.mock import patch, MagicMock
from typing import Optional
import sys
import os
import datetime
import json
import pytest
import litellm
from litellm.integrations.langfuse import langfuse as langfuse_module
from litellm.integrations.langfuse.langfuse import LangFuseLogger
sys.path.insert(0, os.path.abspath("../.."))
from litellm.integrations.langfuse.langfuse import LangFuseLogger
# Import LangfuseUsageDetails directly from the module where it's defined
from litellm.types.integrations.langfuse import *
class TestLangfuseUsageDetails(unittest.TestCase):
def setUp(self):
# Set up environment variables for testing
self.env_patcher = patch.dict('os.environ', {
'LANGFUSE_SECRET_KEY': 'test-secret-key',
'LANGFUSE_PUBLIC_KEY': 'test-public-key',
'LANGFUSE_HOST': 'https://test.langfuse.com'
})
self.env_patcher.start()
# Create mock objects
self.mock_langfuse_client = MagicMock()
self.mock_langfuse_trace = MagicMock()
self.mock_langfuse_generation = MagicMock()
# Setup the trace and generation chain
self.mock_langfuse_trace.generation.return_value = self.mock_langfuse_generation
self.mock_langfuse_client.trace.return_value = self.mock_langfuse_trace
# Mock the langfuse module that's imported locally in methods
self.langfuse_module_patcher = patch.dict('sys.modules', {'langfuse': MagicMock()})
self.mock_langfuse_module = self.langfuse_module_patcher.start()
# Create a mock for the langfuse module with version
self.mock_langfuse = MagicMock()
self.mock_langfuse.version = MagicMock()
self.mock_langfuse.version.__version__ = "3.0.0" # Set a version that supports all features
# Mock the Langfuse class
self.mock_langfuse_class = MagicMock()
self.mock_langfuse_class.return_value = self.mock_langfuse_client
# Set up the sys.modules['langfuse'] mock
sys.modules['langfuse'] = self.mock_langfuse
sys.modules['langfuse'].Langfuse = self.mock_langfuse_class
# Mock the Langfuse client
self.mock_langfuse_client = MagicMock()
self.mock_langfuse_trace = MagicMock()
self.mock_langfuse_generation = MagicMock()
# Setup the trace and generation chain
self.mock_langfuse_trace.generation.return_value = self.mock_langfuse_generation
self.mock_langfuse_client.trace.return_value = self.mock_langfuse_trace
# Mock the Langfuse class
self.mock_langfuse_class = MagicMock()
self.mock_langfuse_class.return_value = self.mock_langfuse_client
self.mock_langfuse.Langfuse = self.mock_langfuse_class
# Create the logger
self.logger = LangFuseLogger()
# Add the log_event_on_langfuse method to the instance
def log_event_on_langfuse(self, kwargs, response_obj, start_time=None, end_time=None, user_id=None, level="DEFAULT", status_message=None):
# This implementation calls _log_langfuse_v2 directly
return self._log_langfuse_v2(
user_id=user_id,
metadata=kwargs.get("litellm_params", {}).get("metadata", {}),
litellm_params=kwargs.get("litellm_params", {}),
output=None,
start_time=start_time,
end_time=end_time,
kwargs=kwargs,
optional_params=kwargs.get("optional_params", {}),
input=None,
response_obj=response_obj,
level=level,
litellm_call_id=kwargs.get("litellm_call_id", None),
print_verbose=True # Add the missing parameter
)
# Bind the method to the instance
import types
self.logger.log_event_on_langfuse = types.MethodType(log_event_on_langfuse, self.logger)
# Make sure _is_langfuse_v2 returns True
def mock_is_langfuse_v2(self):
return True
self.logger._is_langfuse_v2 = types.MethodType(mock_is_langfuse_v2, self.logger)
def tearDown(self):
self.env_patcher.stop()
self.langfuse_module_patcher.stop()
def test_langfuse_usage_details_type(self):
"""Test that LangfuseUsageDetails TypedDict is properly defined with the correct fields"""
# Create an instance of LangfuseUsageDetails
usage_details: LangfuseUsageDetails = {
"input": 10,
"output": 20,
"cache_creation_input_tokens": 5,
"cache_read_input_tokens": 3
}
# Verify all fields are present
self.assertEqual(usage_details["input"], 10)
self.assertEqual(usage_details["output"], 20)
self.assertEqual(usage_details["cache_creation_input_tokens"], 5)
self.assertEqual(usage_details["cache_read_input_tokens"], 3)
# Test with all fields (all fields are required in TypedDict by default)
minimal_usage_details: LangfuseUsageDetails = {
"input": 10,
"output": 20,
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 0
}
self.assertEqual(minimal_usage_details["input"], 10)
self.assertEqual(minimal_usage_details["output"], 20)
def test_log_langfuse_v2_usage_details(self):
"""Test that usage_details in _log_langfuse_v2 is correctly typed and assigned"""
# Create a mock response object with usage information
response_obj = MagicMock()
response_obj.usage = MagicMock()
response_obj.usage.prompt_tokens = 15
response_obj.usage.completion_tokens = 25
# Add the cache token attributes using get method
def mock_get(key, default=None):
if key == 'cache_creation_input_tokens':
return 7
elif key == 'cache_read_input_tokens':
return 4
return default
response_obj.usage.get = mock_get
# Create kwargs for the log_event method
kwargs = {
"model": "gpt-4",
"messages": [{"role": "user", "content": "Hello"}],
"litellm_params": {"metadata": {}}
}
# Create start and end times
start_time = datetime.datetime.now()
end_time = start_time + datetime.timedelta(seconds=1)
# Call the log_event method
with patch.object(self.logger, '_log_langfuse_v2') as mock_log_langfuse_v2:
self.logger.log_event_on_langfuse(
kwargs=kwargs,
response_obj=response_obj,
start_time=start_time,
end_time=end_time
)
# Check if _log_langfuse_v2 was called
mock_log_langfuse_v2.assert_called_once()
# Get the arguments passed to _log_langfuse_v2
call_args = mock_log_langfuse_v2.call_args[1]
# Verify response_obj was passed correctly
self.assertEqual(call_args["response_obj"], response_obj)
def test_langfuse_usage_details_optional_fields(self):
"""Test that LangfuseUsageDetails fields are properly defined as Optional"""
# Create an instance with None values for optional fields
usage_details: LangfuseUsageDetails = {
"input": 10,
"output": 20,
"cache_creation_input_tokens": None,
"cache_read_input_tokens": None
}
# Verify fields can be None
self.assertEqual(usage_details["input"], 10)
self.assertEqual(usage_details["output"], 20)
self.assertIsNone(usage_details["cache_creation_input_tokens"])
self.assertIsNone(usage_details["cache_read_input_tokens"])
def test_langfuse_usage_details_structure(self):
"""Test that LangfuseUsageDetails has the correct structure as defined in the commit"""
# This test directly verifies the structure of the TypedDict
# without relying on the LangFuseLogger class
# Create a dictionary that matches the LangfuseUsageDetails structure
usage_details = {
"input": 15,
"output": 25,
"cache_creation_input_tokens": 7,
"cache_read_input_tokens": 4
}
# Verify the structure matches what we expect
self.assertIn("input", usage_details)
self.assertIn("output", usage_details)
self.assertIn("cache_creation_input_tokens", usage_details)
self.assertIn("cache_read_input_tokens", usage_details)
# Verify the values
self.assertEqual(usage_details["input"], 15)
self.assertEqual(usage_details["output"], 25)
self.assertEqual(usage_details["cache_creation_input_tokens"], 7)
self.assertEqual(usage_details["cache_read_input_tokens"], 4)
def test_max_langfuse_clients_limit():
"""