mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-07 08:26:10 +00:00
fix(tests): wrap callbacks cleanup in try/finally and resolve merge conflict
- test_litellm_pre_call_utils.py: wrap test body in try/finally so
litellm.callbacks is always restored even when an assertion fails,
addressing greptile review comment
- test_langfuse_otel.py: resolve trivial merge conflict in comment
("unpatched" vs "unpatch-ed"), keeping correct spelling
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
7b6ffbb52a
158 changed files with 14066 additions and 1378 deletions
4
.github/workflows/test-litellm-matrix.yml
vendored
4
.github/workflows/test-litellm-matrix.yml
vendored
|
|
@ -102,6 +102,10 @@ jobs:
|
|||
run: |
|
||||
cd enterprise && poetry run pip install -e . && cd ..
|
||||
|
||||
- name: Generate Prisma client
|
||||
run: |
|
||||
poetry run prisma generate --schema litellm/proxy/schema.prisma
|
||||
|
||||
- name: Run tests - ${{ matrix.test-group.name }}
|
||||
run: |
|
||||
poetry run pytest ${{ matrix.test-group.path }} \
|
||||
|
|
|
|||
293
cookbook/mock_prompt_management_server/README.md
Normal file
293
cookbook/mock_prompt_management_server/README.md
Normal file
|
|
@ -0,0 +1,293 @@
|
|||
# Mock Prompt Management Server
|
||||
|
||||
A reference implementation of the [LiteLLM Generic Prompt Management API](https://docs.litellm.ai/docs/adding_provider/generic_prompt_management_api).
|
||||
|
||||
This FastAPI server demonstrates how to build a prompt management API that integrates with LiteLLM without requiring a PR to the LiteLLM repository.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Install Dependencies
|
||||
|
||||
```bash
|
||||
pip install fastapi uvicorn pydantic
|
||||
```
|
||||
|
||||
### 2. Start the Server
|
||||
|
||||
```bash
|
||||
python mock_prompt_management_server.py
|
||||
```
|
||||
|
||||
The server will start on `http://localhost:8080`
|
||||
|
||||
### 3. Test the Endpoint
|
||||
|
||||
```bash
|
||||
# Get a prompt
|
||||
curl "http://localhost:8080/beta/litellm_prompt_management?prompt_id=hello-world-prompt"
|
||||
|
||||
# Get a prompt with authentication
|
||||
curl "http://localhost:8080/beta/litellm_prompt_management?prompt_id=hello-world-prompt" \
|
||||
-H "Authorization: Bearer test-token-12345"
|
||||
|
||||
# List all prompts
|
||||
curl "http://localhost:8080/prompts"
|
||||
|
||||
# Get prompt variables
|
||||
curl "http://localhost:8080/prompts/hello-world-prompt/variables"
|
||||
```
|
||||
|
||||
## Using with LiteLLM
|
||||
|
||||
### Configuration
|
||||
|
||||
Create a `config.yaml` file:
|
||||
|
||||
```yaml
|
||||
model_list:
|
||||
- model_name: gpt-3.5-turbo
|
||||
litellm_params:
|
||||
model: openai/gpt-3.5-turbo
|
||||
api_key: os.environ/OPENAI_API_KEY
|
||||
|
||||
prompts:
|
||||
- prompt_id: "hello-world-prompt"
|
||||
litellm_params:
|
||||
prompt_integration: "generic_prompt_management"
|
||||
api_base: http://localhost:8080
|
||||
api_key: test-token-12345
|
||||
```
|
||||
|
||||
### Start LiteLLM Proxy
|
||||
|
||||
```bash
|
||||
litellm --config config.yaml
|
||||
```
|
||||
|
||||
### Make a Request
|
||||
|
||||
```bash
|
||||
curl http://0.0.0.0:4000/v1/chat/completions \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer sk-1234" \
|
||||
-d '{
|
||||
"model": "gpt-3.5-turbo",
|
||||
"prompt_id": "hello-world-prompt",
|
||||
"prompt_variables": {
|
||||
"domain": "data science",
|
||||
"task": "analyzing customer behavior"
|
||||
},
|
||||
"messages": [
|
||||
{"role": "user", "content": "Please help me get started"}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
## Available Prompts
|
||||
|
||||
The server includes several example prompts:
|
||||
|
||||
| Prompt ID | Description | Variables |
|
||||
|-----------|-------------|-----------|
|
||||
| `hello-world-prompt` | Basic helpful assistant | `domain`, `task` |
|
||||
| `code-review-prompt` | Code review assistant | `years_experience`, `language`, `code` |
|
||||
| `customer-support-prompt` | Customer support agent | `company_name`, `customer_message` |
|
||||
| `data-analysis-prompt` | Data analysis expert | `analysis_type`, `dataset_name`, `data` |
|
||||
| `creative-writing-prompt` | Creative writing assistant | `genre`, `length`, `topic` |
|
||||
|
||||
## Authentication
|
||||
|
||||
The server supports optional Bearer token authentication. Valid tokens for testing:
|
||||
|
||||
- `test-token-12345`
|
||||
- `dev-token-67890`
|
||||
- `prod-token-abcdef`
|
||||
|
||||
If no `Authorization` header is provided, requests are allowed (for testing purposes).
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### LiteLLM Spec Endpoints
|
||||
|
||||
#### `GET /beta/litellm_prompt_management`
|
||||
|
||||
Get a prompt by ID (required by LiteLLM).
|
||||
|
||||
**Query Parameters:**
|
||||
- `prompt_id` (required): The prompt ID
|
||||
- `project_name` (optional): Project filter
|
||||
- `slug` (optional): Slug filter
|
||||
- `version` (optional): Version filter
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"prompt_id": "hello-world-prompt",
|
||||
"prompt_template": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are a helpful assistant specialized in {domain}."
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Help me with: {task}"
|
||||
}
|
||||
],
|
||||
"prompt_template_model": "gpt-4",
|
||||
"prompt_template_optional_params": {
|
||||
"temperature": 0.7,
|
||||
"max_tokens": 500
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Convenience Endpoints (Not in LiteLLM Spec)
|
||||
|
||||
#### `GET /health`
|
||||
|
||||
Health check endpoint.
|
||||
|
||||
#### `GET /prompts`
|
||||
|
||||
List all available prompts.
|
||||
|
||||
#### `GET /prompts/{prompt_id}/variables`
|
||||
|
||||
Get all variables used in a prompt template.
|
||||
|
||||
#### `POST /prompts`
|
||||
|
||||
Create a new prompt (in-memory only, for testing).
|
||||
|
||||
## Example: Full Integration Test
|
||||
|
||||
### 1. Start the Mock Server
|
||||
|
||||
```bash
|
||||
python mock_prompt_management_server.py
|
||||
```
|
||||
|
||||
### 2. Test with Python
|
||||
|
||||
```python
|
||||
from litellm import completion
|
||||
|
||||
# The completion will:
|
||||
# 1. Fetch the prompt from your API
|
||||
# 2. Replace {domain} with "machine learning"
|
||||
# 3. Replace {task} with "building a recommendation system"
|
||||
# 4. Merge with your messages
|
||||
# 5. Use the model and params from the prompt
|
||||
|
||||
response = completion(
|
||||
model="gpt-4",
|
||||
prompt_id="hello-world-prompt",
|
||||
prompt_variables={
|
||||
"domain": "machine learning",
|
||||
"task": "building a recommendation system"
|
||||
},
|
||||
messages=[
|
||||
{"role": "user", "content": "I have user behavior data from the past year."}
|
||||
],
|
||||
# Configure the generic prompt manager
|
||||
generic_prompt_config={
|
||||
"api_base": "http://localhost:8080",
|
||||
"api_key": "test-token-12345",
|
||||
}
|
||||
)
|
||||
|
||||
print(response.choices[0].message.content)
|
||||
```
|
||||
|
||||
## Customization
|
||||
|
||||
### Adding New Prompts
|
||||
|
||||
Edit the `PROMPTS_DB` dictionary in `mock_prompt_management_server.py`:
|
||||
|
||||
```python
|
||||
PROMPTS_DB = {
|
||||
"my-custom-prompt": {
|
||||
"prompt_id": "my-custom-prompt",
|
||||
"prompt_template": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are a {role}."
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "{user_input}"
|
||||
}
|
||||
],
|
||||
"prompt_template_model": "gpt-4",
|
||||
"prompt_template_optional_params": {
|
||||
"temperature": 0.8,
|
||||
"max_tokens": 1000
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Using a Database
|
||||
|
||||
Replace the `PROMPTS_DB` dictionary with database queries:
|
||||
|
||||
```python
|
||||
@app.get("/beta/litellm_prompt_management")
|
||||
async def get_prompt(prompt_id: str):
|
||||
# Fetch from database
|
||||
prompt = await db.prompts.find_one({"prompt_id": prompt_id})
|
||||
|
||||
if not prompt:
|
||||
raise HTTPException(status_code=404, detail="Prompt not found")
|
||||
|
||||
return PromptResponse(**prompt)
|
||||
```
|
||||
|
||||
### Adding Access Control
|
||||
|
||||
Use the custom query parameters for access control:
|
||||
|
||||
```python
|
||||
@app.get("/beta/litellm_prompt_management")
|
||||
async def get_prompt(
|
||||
prompt_id: str,
|
||||
project_name: Optional[str] = None,
|
||||
user_id: Optional[str] = None,
|
||||
authorization: Optional[str] = Header(None)
|
||||
):
|
||||
token = verify_api_key(authorization)
|
||||
|
||||
# Check if user has access to this project
|
||||
if not has_project_access(token, project_name):
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
|
||||
# Fetch and return prompt
|
||||
...
|
||||
```
|
||||
|
||||
## Production Considerations
|
||||
|
||||
Before deploying to production:
|
||||
|
||||
1. **Use a real database** instead of in-memory storage
|
||||
2. **Implement proper authentication** with JWT tokens or API keys
|
||||
3. **Add rate limiting** to prevent abuse
|
||||
4. **Use HTTPS** for encrypted communication
|
||||
5. **Add logging and monitoring** for observability
|
||||
6. **Implement caching** for frequently accessed prompts
|
||||
7. **Add versioning** for prompt management
|
||||
8. **Implement access control** based on teams/users
|
||||
9. **Add input validation** for all parameters
|
||||
10. **Use environment variables** for configuration
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Generic Prompt Management API Documentation](https://docs.litellm.ai/docs/adding_provider/generic_prompt_management_api)
|
||||
- [LiteLLM Prompt Management](https://docs.litellm.ai/docs/proxy/prompt_management)
|
||||
- [Generic Guardrail API](https://docs.litellm.ai/docs/adding_provider/generic_guardrail_api)
|
||||
|
||||
## Questions?
|
||||
|
||||
This is a reference implementation for the LiteLLM Generic Prompt Management API. For questions or issues, please open an issue on the [LiteLLM GitHub repository](https://github.com/BerriAI/litellm).
|
||||
|
||||
|
|
@ -0,0 +1,390 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Mock Prompt Management API Server
|
||||
|
||||
This is a FastAPI server that implements the LiteLLM Generic Prompt Management API
|
||||
for testing and demonstration purposes.
|
||||
|
||||
Usage:
|
||||
python mock_prompt_management_server.py
|
||||
|
||||
The server will start on http://localhost:8080
|
||||
|
||||
Test the endpoint:
|
||||
curl "http://localhost:8080/beta/litellm_prompt_management?prompt_id=hello-world-prompt"
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from fastapi import FastAPI, HTTPException, Header, Query, status
|
||||
from fastapi.responses import JSONResponse
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
# ============================================================================
|
||||
# Response Models
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class MessageContent(BaseModel):
|
||||
"""A single message in the prompt template"""
|
||||
|
||||
role: str = Field(..., description="Message role (system, user, assistant)")
|
||||
content: str = Field(
|
||||
..., description="Message content with optional {variable} placeholders"
|
||||
)
|
||||
|
||||
|
||||
class PromptResponse(BaseModel):
|
||||
"""Response format for the prompt management API"""
|
||||
|
||||
prompt_id: str = Field(..., description="The ID of the prompt")
|
||||
prompt_template: List[MessageContent] = Field(
|
||||
..., description="Array of messages in OpenAI format"
|
||||
)
|
||||
prompt_template_model: Optional[str] = Field(
|
||||
None, description="Optional model to use for this prompt"
|
||||
)
|
||||
prompt_template_optional_params: Optional[Dict[str, Any]] = Field(
|
||||
None, description="Optional parameters like temperature, max_tokens, etc."
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Mock Prompt Database
|
||||
# ============================================================================
|
||||
|
||||
PROMPTS_DB = {
|
||||
"hello-world-prompt": {
|
||||
"prompt_id": "hello-world-prompt",
|
||||
"prompt_template": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are a helpful assistant specialized in {domain}.",
|
||||
},
|
||||
{"role": "user", "content": "Help me with: {task}"},
|
||||
],
|
||||
"prompt_template_model": "gpt-4",
|
||||
"prompt_template_optional_params": {"temperature": 0.7, "max_tokens": 500},
|
||||
},
|
||||
"code-review-prompt": {
|
||||
"prompt_id": "code-review-prompt",
|
||||
"prompt_template": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are an expert code reviewer with {years_experience} years of experience in {language}.",
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Please review the following code for bugs, security issues, and best practices:\n\n{code}",
|
||||
},
|
||||
],
|
||||
"prompt_template_model": "gpt-4-turbo",
|
||||
"prompt_template_optional_params": {
|
||||
"temperature": 0.3,
|
||||
"max_tokens": 1500,
|
||||
},
|
||||
},
|
||||
"customer-support-prompt": {
|
||||
"prompt_id": "customer-support-prompt",
|
||||
"prompt_template": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are a friendly customer support agent for {company_name}. Always be professional, empathetic, and solution-oriented.",
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Customer inquiry: {customer_message}",
|
||||
},
|
||||
],
|
||||
"prompt_template_model": "gpt-3.5-turbo",
|
||||
"prompt_template_optional_params": {
|
||||
"temperature": 0.8,
|
||||
"max_tokens": 800,
|
||||
"top_p": 0.9,
|
||||
},
|
||||
},
|
||||
"data-analysis-prompt": {
|
||||
"prompt_id": "data-analysis-prompt",
|
||||
"prompt_template": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are a data scientist expert in {analysis_type} analysis.",
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Analyze the following data and provide insights:\n\nDataset: {dataset_name}\nData: {data}",
|
||||
},
|
||||
],
|
||||
"prompt_template_model": "gpt-4",
|
||||
"prompt_template_optional_params": {
|
||||
"temperature": 0.5,
|
||||
"max_tokens": 2000,
|
||||
},
|
||||
},
|
||||
"creative-writing-prompt": {
|
||||
"prompt_id": "creative-writing-prompt",
|
||||
"prompt_template": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are a creative writer specializing in {genre} fiction.",
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Write a {length} story about: {topic}",
|
||||
},
|
||||
],
|
||||
"prompt_template_model": "gpt-4",
|
||||
"prompt_template_optional_params": {
|
||||
"temperature": 0.9,
|
||||
"max_tokens": 3000,
|
||||
"top_p": 0.95,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
# Valid API tokens for authentication (in production, use a secure token store)
|
||||
VALID_API_TOKENS = {
|
||||
"test-token-12345",
|
||||
"dev-token-67890",
|
||||
"prod-token-abcdef",
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# FastAPI App
|
||||
# ============================================================================
|
||||
|
||||
app = FastAPI(
|
||||
title="Mock Prompt Management API",
|
||||
description="A mock server implementing the LiteLLM Generic Prompt Management API",
|
||||
version="1.0.0",
|
||||
)
|
||||
|
||||
|
||||
def verify_api_key(authorization: Optional[str] = Header(None)) -> bool:
|
||||
"""
|
||||
Verify the API key from the Authorization header.
|
||||
|
||||
Args:
|
||||
authorization: Authorization header (Bearer token)
|
||||
|
||||
Returns:
|
||||
True if valid, raises HTTPException if invalid
|
||||
"""
|
||||
if authorization is None:
|
||||
# Allow requests without authentication for testing
|
||||
return True
|
||||
|
||||
# Extract token from "Bearer <token>"
|
||||
if not authorization.startswith("Bearer "):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid authorization header format. Expected 'Bearer <token>'",
|
||||
)
|
||||
|
||||
token = authorization.replace("Bearer ", "").strip()
|
||||
|
||||
if token not in VALID_API_TOKENS:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid API key",
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@app.get("/beta/litellm_prompt_management", response_model=PromptResponse)
|
||||
async def get_prompt(
|
||||
prompt_id: str = Query(..., description="The ID of the prompt to fetch"),
|
||||
project_name: Optional[str] = Query(
|
||||
None, description="Optional project name filter"
|
||||
),
|
||||
slug: Optional[str] = Query(None, description="Optional slug filter"),
|
||||
version: Optional[str] = Query(None, description="Optional version filter"),
|
||||
authorization: Optional[str] = Header(None),
|
||||
) -> PromptResponse:
|
||||
"""
|
||||
Get a prompt by ID with optional filtering.
|
||||
|
||||
This endpoint implements the LiteLLM Generic Prompt Management API specification.
|
||||
|
||||
Args:
|
||||
prompt_id: The ID of the prompt to fetch
|
||||
project_name: Optional project name for filtering
|
||||
slug: Optional slug for filtering
|
||||
version: Optional version for filtering
|
||||
authorization: Optional Bearer token for authentication
|
||||
|
||||
Returns:
|
||||
PromptResponse with the prompt template and configuration
|
||||
|
||||
Raises:
|
||||
HTTPException: 401 if authentication fails, 404 if prompt not found
|
||||
"""
|
||||
# Verify authentication
|
||||
verify_api_key(authorization)
|
||||
|
||||
# Log the request parameters (useful for debugging)
|
||||
print(f"Fetching prompt: {prompt_id}")
|
||||
if project_name:
|
||||
print(f" Project: {project_name}")
|
||||
if slug:
|
||||
print(f" Slug: {slug}")
|
||||
if version:
|
||||
print(f" Version: {version}")
|
||||
|
||||
# Check if prompt exists
|
||||
if prompt_id not in PROMPTS_DB:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Prompt '{prompt_id}' not found. Available prompts: {list(PROMPTS_DB.keys())}",
|
||||
)
|
||||
|
||||
# Get the prompt from the database
|
||||
prompt_data = PROMPTS_DB[prompt_id]
|
||||
|
||||
# Optional: Apply filtering based on project_name, slug, or version
|
||||
# In a real implementation, you might use these to filter prompts by access control
|
||||
# or to fetch specific versions from your database
|
||||
|
||||
return PromptResponse(**prompt_data)
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
"""Health check endpoint"""
|
||||
return {
|
||||
"status": "healthy",
|
||||
"service": "mock-prompt-management-api",
|
||||
"version": "1.0.0",
|
||||
}
|
||||
|
||||
|
||||
@app.get("/prompts")
|
||||
async def list_prompts(authorization: Optional[str] = Header(None)):
|
||||
"""
|
||||
List all available prompts.
|
||||
|
||||
This is a convenience endpoint (not part of the LiteLLM spec) for
|
||||
discovering available prompts.
|
||||
"""
|
||||
# Verify authentication
|
||||
verify_api_key(authorization)
|
||||
|
||||
prompts_list = [
|
||||
{
|
||||
"prompt_id": pid,
|
||||
"model": p.get("prompt_template_model"),
|
||||
"has_variables": any(
|
||||
"{" in msg.get("content", "") for msg in p.get("prompt_template", [])
|
||||
),
|
||||
}
|
||||
for pid, p in PROMPTS_DB.items()
|
||||
]
|
||||
|
||||
return {"prompts": prompts_list, "total": len(prompts_list)}
|
||||
|
||||
|
||||
@app.get("/prompts/{prompt_id}/variables")
|
||||
async def get_prompt_variables(
|
||||
prompt_id: str, authorization: Optional[str] = Header(None)
|
||||
):
|
||||
"""
|
||||
Get all variables in a prompt template.
|
||||
|
||||
This is a convenience endpoint (not part of the LiteLLM spec) for
|
||||
discovering what variables a prompt expects.
|
||||
"""
|
||||
# Verify authentication
|
||||
verify_api_key(authorization)
|
||||
|
||||
if prompt_id not in PROMPTS_DB:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Prompt '{prompt_id}' not found",
|
||||
)
|
||||
|
||||
prompt_data = PROMPTS_DB[prompt_id]
|
||||
variables = set()
|
||||
|
||||
# Extract variables from the prompt template
|
||||
import re
|
||||
|
||||
for message in prompt_data["prompt_template"]:
|
||||
content = message.get("content", "")
|
||||
# Find all {variable} patterns
|
||||
found_vars = re.findall(r"\{(\w+)\}", content)
|
||||
variables.update(found_vars)
|
||||
|
||||
return {
|
||||
"prompt_id": prompt_id,
|
||||
"variables": sorted(list(variables)),
|
||||
"example_usage": {
|
||||
"prompt_id": prompt_id,
|
||||
"prompt_variables": {var: f"<{var}_value>" for var in variables},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@app.post("/prompts")
|
||||
async def create_prompt(
|
||||
prompt: PromptResponse, authorization: Optional[str] = Header(None)
|
||||
):
|
||||
"""
|
||||
Create a new prompt (convenience endpoint for testing).
|
||||
|
||||
This is NOT part of the LiteLLM spec - it's just for testing purposes.
|
||||
"""
|
||||
# Verify authentication
|
||||
verify_api_key(authorization)
|
||||
|
||||
if prompt.prompt_id in PROMPTS_DB:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail=f"Prompt '{prompt.prompt_id}' already exists",
|
||||
)
|
||||
|
||||
PROMPTS_DB[prompt.prompt_id] = prompt.dict()
|
||||
|
||||
return {
|
||||
"status": "created",
|
||||
"prompt_id": prompt.prompt_id,
|
||||
"message": "Prompt created successfully (in-memory only)",
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Main
|
||||
# ============================================================================
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
print("=" * 70)
|
||||
print("Mock Prompt Management API Server")
|
||||
print("=" * 70)
|
||||
print(f"\nStarting server on http://localhost:8080")
|
||||
print(f"\nAvailable prompts: {len(PROMPTS_DB)}")
|
||||
for prompt_id in PROMPTS_DB.keys():
|
||||
print(f" - {prompt_id}")
|
||||
print(f"\nValid API tokens: {len(VALID_API_TOKENS)}")
|
||||
print(" - test-token-12345")
|
||||
print(" - dev-token-67890")
|
||||
print(" - prod-token-abcdef")
|
||||
print("\nEndpoints:")
|
||||
print(" GET /beta/litellm_prompt_management?prompt_id=<id> (LiteLLM spec)")
|
||||
print(" GET /health (health check)")
|
||||
print(" GET /prompts (list all prompts)")
|
||||
print(
|
||||
" GET /prompts/{id}/variables (get prompt variables)"
|
||||
)
|
||||
print(" POST /prompts (create prompt)")
|
||||
print("\nExample usage:")
|
||||
print(
|
||||
' curl "http://localhost:8080/beta/litellm_prompt_management?prompt_id=hello-world-prompt"'
|
||||
)
|
||||
print("\nPress CTRL+C to stop the server")
|
||||
print("=" * 70)
|
||||
|
||||
uvicorn.run(app, host="0.0.0.0", port=8080, log_level="info")
|
||||
|
|
@ -24,6 +24,8 @@ hide_table_of_contents: false
|
|||
**Severity:** High
|
||||
**Status:** Resolved
|
||||
|
||||
> **Note:** This fix will be available starting from `v1.81.13-nightly` or higher of LiteLLM.
|
||||
|
||||
## Summary
|
||||
|
||||
Claude Code began sending unsupported Anthropic beta headers to non-Anthropic providers (Bedrock, Azure AI, Vertex AI), causing `invalid beta flag` errors. LiteLLM was forwarding all beta headers without provider-specific validation. Users experienced request failures when routing Claude Code requests through LiteLLM to these providers.
|
||||
|
|
|
|||
117
docs/my-website/blog/vllm_embeddings_incident/index.md
Normal file
117
docs/my-website/blog/vllm_embeddings_incident/index.md
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
---
|
||||
slug: vllm-embeddings-incident
|
||||
title: "Incident Report: vLLM Embeddings Broken by encoding_format Parameter"
|
||||
date: 2026-02-18T10:00:00
|
||||
authors:
|
||||
- name: Sameer Kankute
|
||||
title: SWE @ LiteLLM (LLM Translation)
|
||||
url: https://www.linkedin.com/in/sameer-kankute/
|
||||
image_url: https://pbs.twimg.com/profile_images/2001352686994907136/ONgNuSk5_400x400.jpg
|
||||
- name: Krrish Dholakia
|
||||
title: "CEO, LiteLLM"
|
||||
url: https://www.linkedin.com/in/krish-d/
|
||||
image_url: https://pbs.twimg.com/profile_images/1298587542745358340/DZv3Oj-h_400x400.jpg
|
||||
- name: Ishaan Jaff
|
||||
title: "CTO, LiteLLM"
|
||||
url: https://www.linkedin.com/in/reffajnaahsi/
|
||||
image_url: https://pbs.twimg.com/profile_images/1613813310264340481/lz54oEiB_400x400.jpg
|
||||
tags: [incident-report, embeddings, vllm]
|
||||
hide_table_of_contents: false
|
||||
---
|
||||
|
||||
**Date:** Feb 16, 2026
|
||||
**Duration:** ~3 hours
|
||||
**Severity:** High (for vLLM embedding users)
|
||||
**Status:** Resolved
|
||||
|
||||
## Summary
|
||||
|
||||
A commit ([`dbcae4a`](https://github.com/BerriAI/litellm/commit/dbcae4aca5836770d0e9cd43abab0333c3d61ab2)) intended to fix OpenAI SDK behavior broke vLLM embeddings by explicitly passing `encoding_format=None` in API requests. vLLM rejects this with error: `"unknown variant \`\`, expected float or base64"`.
|
||||
|
||||
- **vLLM embedding calls:** Complete failure - all requests rejected
|
||||
- **Other providers:** No impact - OpenAI and other providers functioned normally
|
||||
- **Other vLLM functionality:** No impact - only embeddings were affected
|
||||
|
||||
{/* truncate */}
|
||||
|
||||
---
|
||||
|
||||
## Background
|
||||
|
||||
The `encoding_format` parameter for embeddings specifies whether vectors should be returned as `float` arrays or `base64` encoded strings. Different providers have different expectations:
|
||||
|
||||
- **OpenAI SDK:** If `encoding_format` is omitted, the SDK adds a default value of `"float"`
|
||||
- **vLLM:** Strictly validates `encoding_format` - only accepts `"float"`, `"base64"`, or complete omission. Rejects `None` or empty string values.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["1. User calls litellm.embedding()
|
||||
litellm/main.py"] --> B["2. Transform request for provider
|
||||
litellm/llms/openai_like/embedding/handler.py"]
|
||||
B --> C["3. Send request to vLLM endpoint"]
|
||||
C -->|"encoding_format omitted"| D["4a. ✅ vLLM processes request"]
|
||||
C -->|"encoding_format='float' or 'base64'"| D
|
||||
C -->|"encoding_format=None or ''"| E["4b. ❌ vLLM rejects with error:
|
||||
'unknown variant, expected float or base64'"]
|
||||
|
||||
style D fill:#d4edda,stroke:#28a745
|
||||
style E fill:#f8d7da,stroke:#dc3545
|
||||
style B fill:#fff3cd,stroke:#ffc107
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Root cause
|
||||
|
||||
A well-intentioned fix for OpenAI SDK behavior inadvertently broke vLLM embeddings:
|
||||
|
||||
**The Breaking Change ([`dbcae4a`](https://github.com/BerriAI/litellm/commit/dbcae4aca5836770d0e9cd43abab0333c3d61ab2)):**
|
||||
|
||||
In `litellm/main.py`, the code was changed to explicitly set `encoding_format=None` instead of omitting it:
|
||||
|
||||
```python
|
||||
# Added in dbcae4a
|
||||
if encoding_format is not None:
|
||||
optional_params["encoding_format"] = encoding_format
|
||||
else:
|
||||
# Omitting causes openai sdk to add default value of "float"
|
||||
optional_params["encoding_format"] = None
|
||||
```
|
||||
|
||||
This fix worked correctly for OpenAI - explicitly passing `None` prevented the SDK from adding its default value. However, vLLM's strict parameter validation rejected `None` values, causing all embedding requests to fail.
|
||||
|
||||
---
|
||||
|
||||
## The Fix
|
||||
|
||||
Fix deployed ([`55348dd`](https://github.com/BerriAI/litellm/commit/55348dd9c51b5b028f676d25ad023b8f052fc071)). The solution filters out `None` and empty string values from `optional_params` before sending requests to OpenAI-like providers (including vLLM).
|
||||
|
||||
**In `litellm/llms/openai_like/embedding/handler.py`:**
|
||||
|
||||
```python
|
||||
# Before (broken)
|
||||
data = {"model": model, "input": input, **optional_params}
|
||||
|
||||
# After (fixed)
|
||||
filtered_optional_params = {k: v for k, v in optional_params.items() if v not in (None, '')}
|
||||
data = {"model": model, "input": input, **filtered_optional_params}
|
||||
```
|
||||
|
||||
This ensures:
|
||||
- Valid values (`"float"`, `"base64"`) are preserved and sent
|
||||
- `None` and empty string values are filtered out (parameter omitted entirely)
|
||||
- OpenAI SDK no longer adds defaults because liteLLM handles the parameter upstream
|
||||
|
||||
---
|
||||
|
||||
## Remediation
|
||||
|
||||
| # | Action | Status | Code |
|
||||
|---|---|---|---|
|
||||
| 1 | Filter `None` and empty string values in OpenAI-like embedding handler | ✅ Done | [`handler.py#L108`](https://github.com/BerriAI/litellm/blob/main/litellm/llms/openai_like/embedding/handler.py#L108) |
|
||||
| 2 | Unit tests for parameter filtering (None, empty string, valid values) | ✅ Done | [`test_openai_like_embedding.py`](https://github.com/BerriAI/litellm/blob/main/tests/test_litellm/llms/openai_like/embedding/test_openai_like_embedding.py) |
|
||||
| 3 | Transformation tests for hosted_vllm embedding config | ✅ Done | [`test_hosted_vllm_embedding_transformation.py`](https://github.com/BerriAI/litellm/blob/main/tests/test_litellm/llms/hosted_vllm/embedding/test_hosted_vllm_embedding_transformation.py) |
|
||||
| 4 | E2E tests with actual vLLM endpoint | ✅ Done | [`test_hosted_vllm_embedding_e2e.py`](https://github.com/BerriAI/litellm/blob/main/tests/test_litellm/llms/hosted_vllm/embedding/test_hosted_vllm_embedding_e2e.py) |
|
||||
| 5 | Validate JSON payload structure matches vLLM expectations | ✅ Done | Tests verify exact JSON sent to endpoint |
|
||||
|
||||
---
|
||||
|
|
@ -237,6 +237,7 @@ litellm_settings:
|
|||
mode: pre_call # or post_call, during_call
|
||||
api_base: https://your-guardrail-api.com
|
||||
api_key: os.environ/YOUR_GUARDRAIL_API_KEY # optional
|
||||
unreachable_fallback: fail_closed # default: fail_closed. Set to fail_open to proceed if the guardrail endpoint is unreachable (network errors, or HTTP 502/503/504 from an upstream proxy/LB).
|
||||
additional_provider_specific_params:
|
||||
# your custom parameters
|
||||
threshold: 0.8
|
||||
|
|
|
|||
|
|
@ -0,0 +1,576 @@
|
|||
# [BETA] Generic Prompt Management API - Integrate Without a PR
|
||||
|
||||
## The Problem
|
||||
|
||||
As a prompt management provider, integrating with LiteLLM traditionally requires:
|
||||
- Making a PR to the LiteLLM repository
|
||||
- Waiting for review and merge
|
||||
- Maintaining provider-specific code in LiteLLM's codebase
|
||||
- Updating the integration for changes to your API
|
||||
|
||||
## The Solution
|
||||
|
||||
The **Generic Prompt Management API** lets you integrate with LiteLLM **instantly** by implementing a simple API endpoint. No PR required.
|
||||
|
||||
### Key Benefits
|
||||
|
||||
1. **No PR Needed** - Deploy and integrate immediately
|
||||
3. **Simple Contract** - One GET endpoint, standard JSON response
|
||||
4. **Variable Substitution** - Support for prompt variables with `{variable}` syntax
|
||||
5. **Custom Parameters** - Pass provider-specific query params via config
|
||||
6. **Full Control** - You own and maintain your prompt management API
|
||||
7. **Model & Parameters Override** - Optionally override model and parameters from your prompts
|
||||
|
||||
## Get Started in 3 Steps
|
||||
|
||||
### Step 1: Configure LiteLLM
|
||||
|
||||
Add to your `config.yaml`:
|
||||
|
||||
```yaml
|
||||
prompts:
|
||||
- prompt_id: "simple_prompt"
|
||||
litellm_params:
|
||||
prompt_integration: "generic_prompt_management"
|
||||
api_base: http://localhost:8080
|
||||
api_key: os.environ/YOUR_API_KEY
|
||||
```
|
||||
|
||||
### Step 2: Implement Your API Endpoint
|
||||
|
||||
```python
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
@app.get("/beta/litellm_prompt_management")
|
||||
async def get_prompt(prompt_id: str):
|
||||
return {
|
||||
"prompt_id": prompt_id,
|
||||
"prompt_template": [
|
||||
{"role": "system", "content": "You are a helpful assistant."},
|
||||
{"role": "user", "content": "Help me with {task}"}
|
||||
],
|
||||
"prompt_template_model": "gpt-4",
|
||||
"prompt_template_optional_params": {"temperature": 0.7}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3: Use in Your App
|
||||
|
||||
```python
|
||||
from litellm import completion
|
||||
|
||||
response = completion(
|
||||
model="gpt-4",
|
||||
prompt_id="simple_prompt",
|
||||
prompt_variables={"task": "data analysis"},
|
||||
messages=[{"role": "user", "content": "I have sales data"}]
|
||||
)
|
||||
```
|
||||
|
||||
That's it! LiteLLM fetches your prompt, applies variables, and makes the request
|
||||
|
||||
## API Contract
|
||||
|
||||
### Endpoint
|
||||
|
||||
Implement `GET /beta/litellm_prompt_management`
|
||||
|
||||
### Request Format
|
||||
|
||||
Your endpoint will receive a GET request with query parameters:
|
||||
|
||||
```
|
||||
GET /beta/litellm_prompt_management?prompt_id={prompt_id}&{custom_params}
|
||||
```
|
||||
|
||||
**Query Parameters:**
|
||||
- `prompt_id` (required): The ID of the prompt to fetch
|
||||
- Custom parameters: Any additional parameters you configured in `provider_specific_query_params`
|
||||
|
||||
**Example:**
|
||||
```
|
||||
GET /beta/litellm_prompt_management?prompt_id=hello-world-prompt-2bac&project_name=litellm&slug=hello-world-prompt-2bac
|
||||
```
|
||||
|
||||
### Response Format
|
||||
|
||||
```json
|
||||
{
|
||||
"prompt_id": "hello-world-prompt-2bac",
|
||||
"prompt_template": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are a helpful assistant specialized in {domain}."
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Help me with {task}"
|
||||
}
|
||||
],
|
||||
"prompt_template_model": "gpt-4",
|
||||
"prompt_template_optional_params": {
|
||||
"temperature": 0.7,
|
||||
"max_tokens": 500,
|
||||
"top_p": 0.9
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response Fields:**
|
||||
- `prompt_id` (string, required): The ID of the prompt
|
||||
- `prompt_template` (array, required): Array of OpenAI-format messages with optional `{variable}` placeholders
|
||||
- `prompt_template_model` (string, optional): Model to use for this prompt (overrides client model unless `ignore_prompt_manager_model: true`)
|
||||
- `prompt_template_optional_params` (object, optional): Additional parameters like temperature, max_tokens, etc. (merged with client params unless `ignore_prompt_manager_optional_params: true`)
|
||||
|
||||
## LiteLLM Configuration
|
||||
|
||||
Add to `config.yaml`:
|
||||
|
||||
```yaml
|
||||
model_list:
|
||||
- model_name: gpt-3.5-turbo
|
||||
litellm_params:
|
||||
model: openai/gpt-3.5-turbo
|
||||
api_key: os.environ/OPENAI_API_KEY
|
||||
|
||||
prompts:
|
||||
- prompt_id: "simple_prompt"
|
||||
litellm_params:
|
||||
prompt_integration: "generic_prompt_management"
|
||||
provider_specific_query_params:
|
||||
project_name: litellm
|
||||
slug: hello-world-prompt-2bac
|
||||
api_base: http://localhost:8080
|
||||
api_key: os.environ/YOUR_PROMPT_API_KEY # optional
|
||||
ignore_prompt_manager_model: true # optional, keep client's model
|
||||
ignore_prompt_manager_optional_params: true # optional, don't merge prompt manager's params (e.g. temperature, max_tokens, etc.)
|
||||
```
|
||||
|
||||
### Configuration Parameters
|
||||
|
||||
- `prompt_integration`: Must be `"generic_prompt_management"`
|
||||
- `provider_specific_query_params`: Custom query parameters sent to your API (optional)
|
||||
- `api_base`: Base URL of your prompt management API
|
||||
- `api_key`: Optional API key for authentication (sent as `Bearer` token)
|
||||
- `ignore_prompt_manager_model`: If `true`, use the model specified by client instead of prompt's model (default: `false`)
|
||||
- `ignore_prompt_manager_optional_params`: If `true`, don't merge prompt's optional params with client params (default: `false`)
|
||||
|
||||
## Usage
|
||||
|
||||
### Using with LiteLLM SDK
|
||||
|
||||
**Basic usage with prompt ID:**
|
||||
|
||||
```python
|
||||
from litellm import completion
|
||||
|
||||
response = completion(
|
||||
model="gpt-4",
|
||||
prompt_id="simple_prompt",
|
||||
messages=[{"role": "user", "content": "Additional message"}]
|
||||
)
|
||||
```
|
||||
|
||||
**With prompt variables:**
|
||||
|
||||
```python
|
||||
response = completion(
|
||||
model="gpt-4",
|
||||
prompt_id="simple_prompt",
|
||||
prompt_variables={
|
||||
"domain": "data science",
|
||||
"task": "analyzing customer churn"
|
||||
},
|
||||
messages=[{"role": "user", "content": "Please provide a detailed analysis"}]
|
||||
)
|
||||
```
|
||||
|
||||
The prompt template will have `{domain}` replaced with "data science" and `{task}` replaced with "analyzing customer churn".
|
||||
|
||||
### Using with LiteLLM Proxy
|
||||
|
||||
**1. Start the proxy with your config:**
|
||||
|
||||
```bash
|
||||
litellm --config /path/to/config.yaml
|
||||
```
|
||||
|
||||
**2. Make requests with prompt_id:**
|
||||
|
||||
```bash
|
||||
curl http://0.0.0.0:4000/v1/chat/completions \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer sk-1234" \
|
||||
-d '{
|
||||
"model": "gpt-4",
|
||||
"prompt_id": "simple_prompt",
|
||||
"prompt_variables": {
|
||||
"domain": "healthcare",
|
||||
"task": "patient risk assessment"
|
||||
},
|
||||
"messages": [
|
||||
{"role": "user", "content": "Analyze the following data..."}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
**3. Using with OpenAI SDK:**
|
||||
|
||||
```python
|
||||
from openai import OpenAI
|
||||
|
||||
client = OpenAI(
|
||||
base_url="http://0.0.0.0:4000",
|
||||
api_key="sk-1234"
|
||||
)
|
||||
|
||||
response = client.chat.completions.create(
|
||||
model="gpt-4",
|
||||
messages=[
|
||||
{"role": "user", "content": "Analyze the data"}
|
||||
],
|
||||
extra_body={
|
||||
"prompt_id": "simple_prompt",
|
||||
"prompt_variables": {
|
||||
"domain": "finance",
|
||||
"task": "fraud detection"
|
||||
}
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
## Implementation Example
|
||||
|
||||
See [mock_prompt_management_server.py](https://github.com/BerriAI/litellm/blob/main/cookbook/mock_prompt_management_server/mock_prompt_management_server.py) for a complete reference implementation with multiple example prompts, authentication, and convenience endpoints.
|
||||
|
||||
**Minimal FastAPI example:**
|
||||
|
||||
```python
|
||||
from fastapi import FastAPI, HTTPException, Header
|
||||
from typing import Optional, Dict, Any, List
|
||||
from pydantic import BaseModel
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# In-memory prompt storage (replace with your database)
|
||||
PROMPTS = {
|
||||
"hello-world-prompt": {
|
||||
"prompt_id": "hello-world-prompt",
|
||||
"prompt_template": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are a helpful assistant specialized in {domain}."
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Help me with: {task}"
|
||||
}
|
||||
],
|
||||
"prompt_template_model": "gpt-4",
|
||||
"prompt_template_optional_params": {
|
||||
"temperature": 0.7,
|
||||
"max_tokens": 500
|
||||
}
|
||||
},
|
||||
"code-review-prompt": {
|
||||
"prompt_id": "code-review-prompt",
|
||||
"prompt_template": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are an expert code reviewer. Review code for {language}."
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Review the following code:\n\n{code}"
|
||||
}
|
||||
],
|
||||
"prompt_template_model": "gpt-4-turbo",
|
||||
"prompt_template_optional_params": {
|
||||
"temperature": 0.3,
|
||||
"max_tokens": 1000
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PromptResponse(BaseModel):
|
||||
prompt_id: str
|
||||
prompt_template: List[Dict[str, str]]
|
||||
prompt_template_model: Optional[str] = None
|
||||
prompt_template_optional_params: Optional[Dict[str, Any]] = None
|
||||
|
||||
@app.get("/beta/litellm_prompt_management", response_model=PromptResponse)
|
||||
async def get_prompt(
|
||||
prompt_id: str,
|
||||
authorization: Optional[str] = Header(None),
|
||||
project_name: Optional[str] = None,
|
||||
slug: Optional[str] = None,
|
||||
):
|
||||
"""
|
||||
Get a prompt by ID with optional filtering by project_name and slug.
|
||||
|
||||
Args:
|
||||
prompt_id: The ID of the prompt to fetch
|
||||
authorization: Optional Bearer token for authentication
|
||||
project_name: Optional project name filter
|
||||
slug: Optional slug filter
|
||||
"""
|
||||
|
||||
# Optional: Validate authorization
|
||||
if authorization:
|
||||
token = authorization.replace("Bearer ", "")
|
||||
# Validate your token here
|
||||
if not is_valid_token(token):
|
||||
raise HTTPException(status_code=401, detail="Invalid API key")
|
||||
|
||||
# Optional: Apply additional filtering based on custom params
|
||||
if project_name or slug:
|
||||
# You can use these parameters to filter or validate access
|
||||
# For example, check if the user has access to this project
|
||||
pass
|
||||
|
||||
# Fetch the prompt from your storage
|
||||
if prompt_id not in PROMPTS:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Prompt '{prompt_id}' not found"
|
||||
)
|
||||
|
||||
prompt_data = PROMPTS[prompt_id]
|
||||
|
||||
return PromptResponse(**prompt_data)
|
||||
|
||||
def is_valid_token(token: str) -> bool:
|
||||
"""Validate API token - implement your logic here"""
|
||||
# Example: Check against your database or secret store
|
||||
valid_tokens = ["your-secret-token", "another-valid-token"]
|
||||
return token in valid_tokens
|
||||
|
||||
# Optional: Health check endpoint
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
return {"status": "healthy"}
|
||||
|
||||
# Optional: List all prompts endpoint
|
||||
@app.get("/prompts")
|
||||
async def list_prompts(authorization: Optional[str] = Header(None)):
|
||||
"""List all available prompts"""
|
||||
if authorization:
|
||||
token = authorization.replace("Bearer ", "")
|
||||
if not is_valid_token(token):
|
||||
raise HTTPException(status_code=401, detail="Invalid API key")
|
||||
|
||||
return {
|
||||
"prompts": [
|
||||
{"prompt_id": pid, "model": p.get("prompt_template_model")}
|
||||
for pid, p in PROMPTS.items()
|
||||
]
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=8080)
|
||||
```
|
||||
|
||||
### Running the Example Server
|
||||
|
||||
1. Install dependencies:
|
||||
```bash
|
||||
pip install fastapi uvicorn
|
||||
```
|
||||
|
||||
2. Save the code above to `prompt_server.py`
|
||||
|
||||
3. Run the server:
|
||||
```bash
|
||||
python prompt_server.py
|
||||
```
|
||||
|
||||
4. Test the endpoint:
|
||||
```bash
|
||||
curl "http://localhost:8080/beta/litellm_prompt_management?prompt_id=hello-world-prompt&project_name=litellm&slug=hello-world-prompt-2bac"
|
||||
```
|
||||
|
||||
Expected response:
|
||||
```json
|
||||
{
|
||||
"prompt_id": "hello-world-prompt",
|
||||
"prompt_template": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are a helpful assistant specialized in {domain}."
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Help me with: {task}"
|
||||
}
|
||||
],
|
||||
"prompt_template_model": "gpt-4",
|
||||
"prompt_template_optional_params": {
|
||||
"temperature": 0.7,
|
||||
"max_tokens": 500
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### Variable Substitution
|
||||
|
||||
LiteLLM automatically substitutes variables in your prompt templates using the `{variable}` syntax. Both `{variable}` and `{{variable}}` formats are supported.
|
||||
|
||||
**Example prompt template:**
|
||||
```json
|
||||
{
|
||||
"prompt_template": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are an expert in {domain} with {years} years of experience."
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Client request:**
|
||||
```python
|
||||
completion(
|
||||
model="gpt-4",
|
||||
prompt_id="expert_prompt",
|
||||
prompt_variables={
|
||||
"domain": "machine learning",
|
||||
"years": "10"
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
**Result:**
|
||||
```
|
||||
"You are an expert in machine learning with 10 years of experience."
|
||||
```
|
||||
|
||||
### Caching
|
||||
|
||||
LiteLLM automatically caches fetched prompts in memory. The cache key includes:
|
||||
- `prompt_id`
|
||||
- `prompt_label` (if provided)
|
||||
- `prompt_version` (if provided)
|
||||
|
||||
This means your API endpoint is only called once per unique prompt configuration.
|
||||
|
||||
### Model Override Behavior
|
||||
|
||||
**Default behavior (without `ignore_prompt_manager_model`):**
|
||||
```yaml
|
||||
prompts:
|
||||
- prompt_id: "my_prompt"
|
||||
litellm_params:
|
||||
prompt_integration: "generic_prompt_management"
|
||||
api_base: http://localhost:8080
|
||||
```
|
||||
|
||||
If your API returns `"prompt_template_model": "gpt-4"`, LiteLLM will use `gpt-4` regardless of what the client specified.
|
||||
|
||||
**With `ignore_prompt_manager_model: true`:**
|
||||
```yaml
|
||||
prompts:
|
||||
- prompt_id: "my_prompt"
|
||||
litellm_params:
|
||||
prompt_integration: "generic_prompt_management"
|
||||
api_base: http://localhost:8080
|
||||
ignore_prompt_manager_model: true
|
||||
```
|
||||
|
||||
LiteLLM will use the model specified by the client, ignoring the prompt's model.
|
||||
|
||||
### Parameter Merging Behavior
|
||||
|
||||
**Default behavior (without `ignore_prompt_manager_optional_params`):**
|
||||
|
||||
Client params are merged with prompt params, with prompt params taking precedence:
|
||||
```python
|
||||
# Prompt returns: {"temperature": 0.7, "max_tokens": 500}
|
||||
# Client sends: {"temperature": 0.9, "top_p": 0.95}
|
||||
# Final params: {"temperature": 0.7, "max_tokens": 500, "top_p": 0.95}
|
||||
```
|
||||
|
||||
**With `ignore_prompt_manager_optional_params: true`:**
|
||||
|
||||
Only client params are used:
|
||||
```python
|
||||
# Prompt returns: {"temperature": 0.7, "max_tokens": 500}
|
||||
# Client sends: {"temperature": 0.9, "top_p": 0.95}
|
||||
# Final params: {"temperature": 0.9, "top_p": 0.95}
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **Authentication**: Use the `api_key` parameter to secure your prompt management API
|
||||
2. **Authorization**: Implement team/user-based access control using the custom query parameters
|
||||
3. **Rate Limiting**: Add rate limiting to prevent abuse of your API
|
||||
4. **Input Validation**: Validate all query parameters before processing
|
||||
5. **HTTPS**: Always use HTTPS in production for encrypted communication
|
||||
6. **Secrets**: Store API keys in environment variables, not in config files
|
||||
|
||||
## Use Cases
|
||||
|
||||
✅ **Use Generic Prompt Management API when:**
|
||||
- You want instant integration without waiting for PRs
|
||||
- You maintain your own prompt management service
|
||||
- You need full control over prompt versioning and updates
|
||||
- You want to build custom prompt management features
|
||||
- You need to integrate with your internal systems
|
||||
|
||||
✅ **Common scenarios:**
|
||||
- Internal prompt management system for your organization
|
||||
- Multi-tenant prompt management with team-based access control
|
||||
- A/B testing different prompt versions
|
||||
- Prompt experimentation and analytics
|
||||
- Integration with existing prompt engineering workflows
|
||||
|
||||
## When to Use This
|
||||
|
||||
✅ **Use Generic Prompt Management API when:**
|
||||
- You want instant integration without waiting for PRs
|
||||
- You maintain your own prompt management service
|
||||
- You need full control over updates and features
|
||||
- You want custom prompt storage and versioning logic
|
||||
|
||||
❌ **Make a PR when:**
|
||||
- You want deeper integration with LiteLLM internals
|
||||
- Your integration requires complex LiteLLM-specific logic
|
||||
- You want to be featured as a built-in provider
|
||||
- You're building a reusable integration for the community
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Prompt not found
|
||||
- Verify the `prompt_id` matches exactly (case-sensitive)
|
||||
- Check that your API endpoint is accessible from LiteLLM
|
||||
- Verify authentication if using `api_key`
|
||||
|
||||
### Variables not substituted
|
||||
- Ensure variables use `{variable}` or `{{variable}}` syntax
|
||||
- Check that variable names in `prompt_variables` match template exactly
|
||||
- Variables are case-sensitive
|
||||
|
||||
### Model not being overridden
|
||||
- Check if `ignore_prompt_manager_model: true` is set in config
|
||||
- Verify your API is returning `prompt_template_model` in the response
|
||||
|
||||
### Parameters not being applied
|
||||
- Check if `ignore_prompt_manager_optional_params: true` is set
|
||||
- Verify your API is returning `prompt_template_optional_params`
|
||||
- Ensure parameter names match OpenAI's parameter names
|
||||
|
||||
## Questions?
|
||||
|
||||
This is a **beta API**. We're actively improving it based on feedback. Open an issue or PR if you need additional capabilities.
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Prompt Management Overview](../proxy/prompt_management.md)
|
||||
- [Generic Guardrail API](./generic_guardrail_api.md)
|
||||
- [LiteLLM Proxy Setup](../proxy/quick_start.md)
|
||||
|
||||
465
docs/my-website/docs/completion/message_sanitization.md
Normal file
465
docs/my-website/docs/completion/message_sanitization.md
Normal file
|
|
@ -0,0 +1,465 @@
|
|||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
|
||||
# Message Sanitization for Tool Calling for anthropic models
|
||||
|
||||
**Automatically fix common message formatting issues when using tool calling with `modify_params=True`**
|
||||
|
||||
LiteLLM can automatically sanitize messages to handle common issues that occur during tool calling workflows, especially when using OpenAI-compatible clients with providers that have strict message format requirements (like Anthropic Claude).
|
||||
|
||||
## Overview
|
||||
|
||||
When `litellm.modify_params = True` is enabled, LiteLLM automatically sanitizes messages to fix three common issues:
|
||||
|
||||
1. **Orphaned Tool Calls** - Assistant messages with tool_calls but missing tool results
|
||||
2. **Orphaned Tool Results** - Tool messages that reference non-existent tool_call_ids
|
||||
3. **Empty Message Content** - Messages with empty or whitespace-only text content
|
||||
|
||||
This ensures your tool calling workflows work seamlessly across different LLM providers without manual message validation.
|
||||
|
||||
## Why Message Sanitization?
|
||||
|
||||
Different LLM providers have varying requirements for message formats, especially during tool calling:
|
||||
|
||||
- **Anthropic Claude** requires every tool_call to have a corresponding tool result
|
||||
- Some providers reject messages with empty content
|
||||
- OpenAI-compatible clients may not always maintain perfect message consistency
|
||||
|
||||
Without sanitization, these issues cause API errors that interrupt your workflows. With `modify_params=True`, LiteLLM handles these edge cases automatically.
|
||||
|
||||
## Quick Start
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="sdk" label="SDK">
|
||||
|
||||
```python
|
||||
import litellm
|
||||
|
||||
# Enable automatic message sanitization
|
||||
litellm.modify_params = True
|
||||
|
||||
# This will work even if messages have formatting issues
|
||||
response = litellm.completion(
|
||||
model="anthropic/claude-3-5-sonnet-20241022",
|
||||
messages=[
|
||||
{"role": "user", "content": "What's the weather in Boston?"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call_123",
|
||||
"type": "function",
|
||||
"function": {"name": "get_weather", "arguments": '{"city": "Boston"}'}
|
||||
}
|
||||
]
|
||||
# Missing tool result - LiteLLM will add a dummy result automatically
|
||||
},
|
||||
{"role": "user", "content": "Thanks!"}
|
||||
],
|
||||
tools=[{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_weather",
|
||||
"description": "Get weather for a city",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {"city": {"type": "string"}},
|
||||
"required": ["city"]
|
||||
}
|
||||
}
|
||||
}]
|
||||
)
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="proxy" label="PROXY">
|
||||
|
||||
```yaml
|
||||
litellm_settings:
|
||||
modify_params: true # Enable automatic message sanitization
|
||||
|
||||
model_list:
|
||||
- model_name: claude-3-5-sonnet
|
||||
litellm_params:
|
||||
model: anthropic/claude-3-5-sonnet-20241022
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Sanitization Cases
|
||||
|
||||
### Case A: Orphaned Tool Calls (Missing Tool Results)
|
||||
|
||||
**Problem:** An assistant message contains `tool_calls`, but no corresponding tool result messages follow.
|
||||
|
||||
**Solution:** LiteLLM automatically adds dummy tool result messages for any missing tool results.
|
||||
|
||||
**Example:**
|
||||
|
||||
```python
|
||||
import litellm
|
||||
litellm.modify_params = True
|
||||
|
||||
# Messages with orphaned tool calls
|
||||
messages = [
|
||||
{"role": "user", "content": "Search for Python tutorials"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call_abc123",
|
||||
"type": "function",
|
||||
"function": {"name": "web_search", "arguments": '{"query": "Python tutorials"}'}
|
||||
}
|
||||
]
|
||||
},
|
||||
# Missing tool result here!
|
||||
{"role": "user", "content": "What about JavaScript?"}
|
||||
]
|
||||
|
||||
# LiteLLM automatically adds:
|
||||
# {
|
||||
# "role": "tool",
|
||||
# "tool_call_id": "call_abc123",
|
||||
# "content": "[System: Tool execution skipped/interrupted by user. No result provided for tool 'web_search'.]"
|
||||
# }
|
||||
|
||||
response = litellm.completion(
|
||||
model="anthropic/claude-3-5-sonnet-20241022",
|
||||
messages=messages,
|
||||
tools=[...]
|
||||
)
|
||||
```
|
||||
|
||||
**When this happens:**
|
||||
- User interrupts tool execution
|
||||
- Client loses tool results due to network issues
|
||||
- Conversation flow changes before tool completes
|
||||
- Multi-turn conversations where tools are optional
|
||||
|
||||
### Case B: Orphaned Tool Results (Invalid tool_call_id)
|
||||
|
||||
**Problem:** A tool message references a `tool_call_id` that doesn't exist in any previous assistant message.
|
||||
|
||||
**Solution:** LiteLLM automatically removes these orphaned tool result messages.
|
||||
|
||||
**Example:**
|
||||
|
||||
```python
|
||||
import litellm
|
||||
litellm.modify_params = True
|
||||
|
||||
# Messages with orphaned tool result
|
||||
messages = [
|
||||
{"role": "user", "content": "Hello"},
|
||||
{"role": "assistant", "content": "Hi! How can I help?"},
|
||||
{
|
||||
"role": "tool",
|
||||
"tool_call_id": "call_nonexistent", # This tool_call_id doesn't exist!
|
||||
"content": "Some result"
|
||||
}
|
||||
]
|
||||
|
||||
# LiteLLM automatically removes the orphaned tool message
|
||||
|
||||
response = litellm.completion(
|
||||
model="anthropic/claude-3-5-sonnet-20241022",
|
||||
messages=messages
|
||||
)
|
||||
```
|
||||
|
||||
**When this happens:**
|
||||
- Message history is manually edited
|
||||
- Tool results are duplicated or mismatched
|
||||
- Conversation state is restored incorrectly
|
||||
- Messages are merged from different conversations
|
||||
|
||||
### Case C: Empty Message Content
|
||||
|
||||
**Problem:** User or assistant messages have empty or whitespace-only content.
|
||||
|
||||
**Solution:** LiteLLM replaces empty content with a system placeholder message.
|
||||
|
||||
**Example:**
|
||||
|
||||
```python
|
||||
import litellm
|
||||
litellm.modify_params = True
|
||||
|
||||
# Messages with empty content
|
||||
messages = [
|
||||
{"role": "user", "content": ""}, # Empty content
|
||||
{"role": "assistant", "content": " "}, # Whitespace only
|
||||
]
|
||||
|
||||
# LiteLLM automatically replaces with:
|
||||
# {"role": "user", "content": "[System: Empty message content sanitised to satisfy protocol]"}
|
||||
# {"role": "assistant", "content": "[System: Empty message content sanitised to satisfy protocol]"}
|
||||
|
||||
response = litellm.completion(
|
||||
model="anthropic/claude-3-5-sonnet-20241022",
|
||||
messages=messages
|
||||
)
|
||||
```
|
||||
|
||||
**When this happens:**
|
||||
- UI sends empty messages
|
||||
- Content is stripped during preprocessing
|
||||
- Placeholder messages in conversation history
|
||||
- Edge cases in message construction
|
||||
|
||||
## Configuration
|
||||
|
||||
### Enable Globally
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="sdk" label="SDK">
|
||||
|
||||
```python
|
||||
import litellm
|
||||
|
||||
# Enable for all completion calls
|
||||
litellm.modify_params = True
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="proxy" label="PROXY">
|
||||
|
||||
```yaml
|
||||
litellm_settings:
|
||||
modify_params: true
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="env" label="Environment Variable">
|
||||
|
||||
```bash
|
||||
export LITELLM_MODIFY_PARAMS=True
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Enable Per-Request
|
||||
|
||||
```python
|
||||
import litellm
|
||||
|
||||
# Enable only for specific requests
|
||||
response = litellm.completion(
|
||||
model="anthropic/claude-3-5-sonnet-20241022",
|
||||
messages=messages,
|
||||
modify_params=True # Override global setting
|
||||
)
|
||||
```
|
||||
|
||||
## Supported Providers
|
||||
|
||||
Message sanitization currently works with:
|
||||
|
||||
- ✅ Anthropic (Claude)
|
||||
|
||||
**Note:** While the sanitization logic is provider-agnostic, it is currently only applied in the Anthropic message transformation pipeline. Support for additional providers may be added in future releases.
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### How It Works
|
||||
|
||||
The message sanitization process runs **before** messages are converted to provider-specific formats:
|
||||
|
||||
1. **Input:** OpenAI-format messages with potential issues
|
||||
2. **Sanitization:** Three helper functions process the messages:
|
||||
- `_sanitize_empty_text_content()` - Fixes empty content
|
||||
- `_add_missing_tool_results()` - Adds dummy tool results
|
||||
- `_is_orphaned_tool_result()` - Identifies orphaned results
|
||||
3. **Output:** Clean, provider-compatible messages
|
||||
|
||||
### Code Reference
|
||||
|
||||
The sanitization logic is implemented in:
|
||||
- `litellm/litellm_core_utils/prompt_templates/factory.py`
|
||||
- Function: `sanitize_messages_for_tool_calling()`
|
||||
|
||||
### Logging
|
||||
|
||||
When sanitization occurs, LiteLLM logs debug messages:
|
||||
|
||||
```python
|
||||
import litellm
|
||||
litellm.set_verbose = True # Enable debug logging
|
||||
|
||||
# You'll see logs like:
|
||||
# "_add_missing_tool_results: Found 1 orphaned tool calls. Adding dummy tool results."
|
||||
# "_is_orphaned_tool_result: Found orphaned tool result with tool_call_id=call_123"
|
||||
# "_sanitize_empty_text_content: Replaced empty text content in user message"
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Enable for Production Workflows
|
||||
|
||||
```python
|
||||
# Recommended for production
|
||||
litellm.modify_params = True
|
||||
|
||||
# Ensures robust handling of edge cases
|
||||
response = litellm.completion(
|
||||
model="anthropic/claude-3-5-sonnet-20241022",
|
||||
messages=messages,
|
||||
tools=tools
|
||||
)
|
||||
```
|
||||
|
||||
### 2. Preserve Tool Results When Possible
|
||||
|
||||
While sanitization handles missing tool results, it's better to provide actual results:
|
||||
|
||||
```python
|
||||
# Good: Provide actual tool results
|
||||
messages = [
|
||||
{"role": "user", "content": "Search for Python"},
|
||||
{"role": "assistant", "tool_calls": [...]},
|
||||
{"role": "tool", "tool_call_id": "call_123", "content": "Actual search results"}
|
||||
]
|
||||
|
||||
# Fallback: Sanitization adds dummy result if missing
|
||||
messages = [
|
||||
{"role": "user", "content": "Search for Python"},
|
||||
{"role": "assistant", "tool_calls": [...]},
|
||||
# Missing tool result - sanitization adds dummy
|
||||
]
|
||||
```
|
||||
|
||||
### 3. Monitor Sanitization Events
|
||||
|
||||
Use logging to track when sanitization occurs:
|
||||
|
||||
```python
|
||||
import litellm
|
||||
import logging
|
||||
|
||||
# Enable debug logging
|
||||
litellm.set_verbose = True
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
# Track sanitization events in your application
|
||||
response = litellm.completion(
|
||||
model="anthropic/claude-3-5-sonnet-20241022",
|
||||
messages=messages
|
||||
)
|
||||
```
|
||||
|
||||
### 4. Test Edge Cases
|
||||
|
||||
Ensure your application handles sanitized messages correctly:
|
||||
|
||||
```python
|
||||
import litellm
|
||||
litellm.modify_params = True
|
||||
|
||||
# Test orphaned tool calls
|
||||
test_messages = [
|
||||
{"role": "user", "content": "Test"},
|
||||
{"role": "assistant", "tool_calls": [{"id": "call_1", "type": "function", "function": {"name": "test", "arguments": "{}"}}]},
|
||||
{"role": "user", "content": "Continue"} # No tool result
|
||||
]
|
||||
|
||||
response = litellm.completion(
|
||||
model="anthropic/claude-3-5-sonnet-20241022",
|
||||
messages=test_messages,
|
||||
tools=[...]
|
||||
)
|
||||
|
||||
# Verify the response handles the dummy tool result appropriately
|
||||
```
|
||||
|
||||
## Related Features
|
||||
|
||||
- **[Drop Params](./drop_params.md)** - Drop unsupported parameters for specific providers
|
||||
- **[Message Trimming](./message_trimming.md)** - Trim messages to fit token limits
|
||||
- **[Function Calling](./function_call.md)** - Complete guide to tool/function calling
|
||||
- **[Reasoning Content](../reasoning_content.md)** - Extended thinking with tool calling
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Sanitization Not Working
|
||||
|
||||
**Issue:** Messages still cause errors despite `modify_params=True`
|
||||
|
||||
**Solution:**
|
||||
1. Verify `modify_params` is enabled:
|
||||
```python
|
||||
import litellm
|
||||
print(litellm.modify_params) # Should be True
|
||||
```
|
||||
|
||||
2. Check if the issue is provider-specific:
|
||||
```python
|
||||
litellm.set_verbose = True # Enable debug logging
|
||||
```
|
||||
|
||||
3. Ensure you're using a recent version of LiteLLM:
|
||||
```bash
|
||||
pip install --upgrade litellm
|
||||
```
|
||||
|
||||
### Unexpected Dummy Tool Results
|
||||
|
||||
**Issue:** Dummy tool results appear when you expect actual results
|
||||
|
||||
**Cause:** Tool result messages are missing or have incorrect `tool_call_id`
|
||||
|
||||
**Solution:**
|
||||
1. Verify tool result messages have correct `tool_call_id`:
|
||||
```python
|
||||
# Correct
|
||||
{"role": "tool", "tool_call_id": "call_123", "content": "result"}
|
||||
|
||||
# Incorrect - will be treated as orphaned
|
||||
{"role": "tool", "tool_call_id": "wrong_id", "content": "result"}
|
||||
```
|
||||
|
||||
2. Ensure tool results immediately follow assistant messages with tool_calls
|
||||
|
||||
### Performance Impact
|
||||
|
||||
**Issue:** Concerned about performance overhead
|
||||
|
||||
**Details:** Message sanitization has minimal performance impact:
|
||||
- Runs in O(n) time where n = number of messages
|
||||
- Only processes messages when `modify_params=True`
|
||||
- Typically adds < 1ms to request processing time
|
||||
|
||||
## FAQ
|
||||
|
||||
**Q: Does sanitization modify my original messages?**
|
||||
|
||||
A: No, sanitization creates a new list of messages. Your original messages remain unchanged.
|
||||
|
||||
**Q: Can I disable specific sanitization cases?**
|
||||
|
||||
A: Currently, all three cases are handled together when `modify_params=True`. To disable sanitization entirely, set `modify_params=False`.
|
||||
|
||||
**Q: What happens to the dummy tool results?**
|
||||
|
||||
A: Dummy tool results are sent to the LLM provider along with other messages. The model sees them as regular tool results with informative error messages.
|
||||
|
||||
**Q: Does this work with streaming?**
|
||||
|
||||
A: Yes, message sanitization works with both streaming and non-streaming requests.
|
||||
|
||||
**Q: Is this related to `drop_params`?**
|
||||
|
||||
A: No, they're separate features:
|
||||
- `modify_params` - Modifies/fixes message content and structure
|
||||
- `drop_params` - Removes unsupported API parameters
|
||||
|
||||
Both can be enabled simultaneously.
|
||||
|
||||
## See Also
|
||||
|
||||
- [Reasoning Content with Tool Calling](../reasoning_content.md)
|
||||
- [Function Calling Guide](./function_call.md)
|
||||
- [Bedrock Provider Documentation](../providers/bedrock.md)
|
||||
- [Anthropic Provider Documentation](../providers/anthropic.md)
|
||||
|
|
@ -253,3 +253,12 @@ LiteLLM supports customizing the following Datadog environment variables
|
|||
\* **Required when using Direct API** (default): `DD_API_KEY` and `DD_SITE` are required
|
||||
\* **Optional when using DataDog Agent**: Set `LITELLM_DD_AGENT_HOST` to use agent mode; `DD_API_KEY` and `DD_SITE` are not required for **Datadog Logs**. (**Note: `DD_API_KEY` IS REQUIRED for Datadog LLM Observability**)
|
||||
|
||||
## Automatic Tags
|
||||
|
||||
LiteLLM automatically adds the following tags to your Datadog logs and metrics if the information is available in the request:
|
||||
|
||||
| Tag | Description | Source |
|
||||
|-----|-------------|--------|
|
||||
| `team` | The team alias or ID associated with the API Key | `user_api_key_team_alias`, `team_alias`, `user_api_key_team_id`, or `team_id` in metadata |
|
||||
| `request_tag` | Custom tags passed in the request | `request_tags` in logging payload |
|
||||
|
||||
|
|
|
|||
|
|
@ -1196,6 +1196,8 @@ When responding to Computer Use tool calls, include the URL and screenshot:
|
|||
|
||||
|
||||
|
||||
|
||||
|
||||
## Thought Signatures
|
||||
|
||||
Thought signatures are encrypted representations of the model's internal reasoning process for a given turn in a conversation. By passing thought signatures back to the model in subsequent requests, you provide it with the context of its previous thoughts, allowing it to build upon its reasoning and maintain a coherent line of inquiry.
|
||||
|
|
|
|||
52
docs/my-website/docs/providers/watsonx/rerank.md
Normal file
52
docs/my-website/docs/providers/watsonx/rerank.md
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
# watsonx.ai Rerank
|
||||
|
||||
## Overview
|
||||
|
||||
| Property | Details |
|
||||
|----------|--------------------------------------------------------------------------|
|
||||
| Description | watsonx.ai rerank integration |
|
||||
| Provider Route on LiteLLM | `watsonx/` |
|
||||
| Supported Operations | `/ml/v1/text/rerank` |
|
||||
| Link to Provider Doc | [IBM WatsonX.ai ↗](https://cloud.ibm.com/apidocs/watsonx-ai#text-rerank) |
|
||||
|
||||
## Quick Start
|
||||
|
||||
### **LiteLLM SDK**
|
||||
|
||||
```python
|
||||
import os
|
||||
from litellm import rerank
|
||||
|
||||
os.environ["WATSONX_APIKEY"] = "YOUR_WATSONX_APIKEY"
|
||||
os.environ["WATSONX_API_BASE"] = "YOUR_WATSONX_API_BASE"
|
||||
os.environ["WATSONX_PROJECT_ID"] = "YOUR_WATSONX_PROJECT_ID"
|
||||
|
||||
query="Best programming language for beginners?"
|
||||
documents=[
|
||||
"Python is great for beginners due to simple syntax.",
|
||||
"JavaScript runs in browsers and is versatile.",
|
||||
"Rust has a steep learning curve but is very safe.",
|
||||
]
|
||||
|
||||
response = rerank(
|
||||
model="watsonx/cross-encoder/ms-marco-minilm-l-12-v2",
|
||||
query=query,
|
||||
documents=documents,
|
||||
top_n=2,
|
||||
return_documents=True,
|
||||
)
|
||||
|
||||
print(response)
|
||||
```
|
||||
|
||||
### **LiteLLM Proxy**
|
||||
|
||||
```yaml
|
||||
model_list:
|
||||
- model_name: cross-encoder/ms-marco-minilm-l-12-v2
|
||||
litellm_params:
|
||||
model: watsonx/cross-encoder/ms-marco-minilm-l-12-v2
|
||||
api_key: os.environ/WATSONX_APIKEY
|
||||
api_base: os.environ/WATSONX_API_BASE
|
||||
project_id: os.environ/WATSONX_PROJECT_ID
|
||||
```
|
||||
|
|
@ -358,7 +358,8 @@ router_settings:
|
|||
| redis_url | str | URL for Redis server. **Known performance issue with Redis URL.** |
|
||||
| cache_responses | boolean | Flag to enable caching LLM Responses, if cache set under `router_settings`. If true, caches responses. Defaults to False. |
|
||||
| router_general_settings | RouterGeneralSettings | [SDK-Only] Router general settings - contains optimizations like 'async_only_mode'. [Docs](../routing.md#router-general-settings) |
|
||||
| optional_pre_call_checks | List[str] | List of pre-call checks to add to the router. Currently supported: 'router_budget_limiting', 'prompt_caching' |
|
||||
| optional_pre_call_checks | List[str] | List of pre-call checks to add to the router. Supported: `router_budget_limiting`, `prompt_caching`, `responses_api_deployment_check`, `deployment_affinity`, `forward_client_headers_by_model_group` |
|
||||
| deployment_affinity_ttl_seconds | int | TTL (seconds) for user-key → deployment affinity mapping when `deployment_affinity` is enabled (configured at Router init / proxy startup). Defaults to `3600` (1 hour). |
|
||||
| ignore_invalid_deployments | boolean | If true, ignores invalid deployments. Default for proxy is True - to prevent invalid models from blocking other models from being loaded. |
|
||||
| search_tools | List[SearchToolTypedDict] | List of search tool configurations for Search API integration. Each tool specifies a search_tool_name and litellm_params with search_provider, api_key, api_base, etc. [Further Docs](../search.md) |
|
||||
| guardrail_list | List[GuardrailTypedDict] | List of guardrail configurations for guardrail load balancing. Enables load balancing across multiple guardrail deployments with the same guardrail_name. [Further Docs](./guardrails/guardrail_load_balancing.md) |
|
||||
|
|
@ -540,7 +541,7 @@ router_settings:
|
|||
| DEFAULT_IMAGE_WIDTH | Default width for images. Default is 300
|
||||
| DEFAULT_IN_MEMORY_TTL | Default time-to-live for in-memory cache in seconds. Default is 5
|
||||
| DEFAULT_MANAGEMENT_OBJECT_IN_MEMORY_CACHE_TTL | Default time-to-live in seconds for management objects (User, Team, Key, Organization) in memory cache. Default is 60 seconds.
|
||||
| DEFAULT_MAX_LRU_CACHE_SIZE | Default maximum size for LRU cache. Default is 16
|
||||
| DEFAULT_MAX_LRU_CACHE_SIZE | Default maximum size for LRU cache. Default is 64
|
||||
| DEFAULT_MAX_RECURSE_DEPTH | Default maximum recursion depth. Default is 100
|
||||
| DEFAULT_MAX_RECURSE_DEPTH_SENSITIVE_DATA_MASKER | Default maximum recursion depth for sensitive data masker. Default is 10
|
||||
| DEFAULT_MAX_RETRIES | Default maximum retry attempts. Default is 2
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ Run experiments or change the specific model (e.g. from gpt-4o to gpt4o-mini fin
|
|||
| Native LiteLLM GitOps (.prompt files) | [Get Started](native_litellm_prompt) |
|
||||
| Langfuse | [Get Started](https://langfuse.com/docs/prompts/get-started) |
|
||||
| Humanloop | [Get Started](../observability/humanloop) |
|
||||
| Generic Prompt Management API | [Get Started](../adding_provider/generic_prompt_management_api) |
|
||||
|
||||
## Onboarding Prompts via config.yaml
|
||||
|
||||
|
|
@ -34,7 +35,7 @@ prompts:
|
|||
- prompt_id: "my_prompt_id"
|
||||
litellm_params:
|
||||
prompt_id: "my_prompt_id"
|
||||
prompt_integration: "dotprompt" # or langfuse, bitbucket, gitlab, custom
|
||||
prompt_integration: "dotprompt" # or langfuse, bitbucket, gitlab, generic_prompt_management, custom
|
||||
# integration-specific parameters below
|
||||
```
|
||||
|
||||
|
|
@ -46,6 +47,7 @@ The `prompt_integration` field determines where and how prompts are loaded:
|
|||
- **`langfuse`**: Fetch prompts from Langfuse prompt management
|
||||
- **`bitbucket`**: Load from BitBucket repository `.prompt` files (team-based access control)
|
||||
- **`gitlab`**: Load from GitLab repository `.prompt` files (team-based access control)
|
||||
- **`generic_prompt_management`**: Integrate any prompt management system via a simple API endpoint (no PR required)
|
||||
- **`custom`**: Use your own custom prompt management implementation
|
||||
|
||||
Each integration has its own configuration parameters and access control mechanisms.
|
||||
|
|
@ -207,6 +209,57 @@ System: You are a helpful assistant.
|
|||
User: {{user_message}}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="generic" label="Generic Prompt Management">
|
||||
|
||||
```yaml
|
||||
prompts:
|
||||
- prompt_id: "simple_prompt"
|
||||
litellm_params:
|
||||
prompt_integration: "generic_prompt_management"
|
||||
provider_specific_query_params:
|
||||
project_name: litellm
|
||||
slug: hello-world-prompt-2bac
|
||||
api_base: http://localhost:8080
|
||||
api_key: os.environ/GENERIC_PROMPT_API_KEY
|
||||
ignore_prompt_manager_model: true # optional
|
||||
ignore_prompt_manager_optional_params: true # optional
|
||||
```
|
||||
|
||||
**What you need to implement:**
|
||||
|
||||
A GET endpoint at `/beta/litellm_prompt_management` that returns:
|
||||
|
||||
```json
|
||||
{
|
||||
"prompt_id": "simple_prompt",
|
||||
"prompt_template": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are a helpful assistant."
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Help me with {task}"
|
||||
}
|
||||
],
|
||||
"prompt_template_model": "gpt-4",
|
||||
"prompt_template_optional_params": {
|
||||
"temperature": 0.7,
|
||||
"max_tokens": 500
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- No PR required - integrate any prompt management system
|
||||
- Full control over your prompt storage and versioning
|
||||
- Support for variable substitution with `{variable}` syntax
|
||||
- Custom query parameters for filtering and access control
|
||||
|
||||
**Learn more:** [Generic Prompt Management API Documentation](../adding_provider/generic_prompt_management_api)
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
|
|
|
|||
|
|
@ -8,15 +8,15 @@ LiteLLM Follows the [cohere api request / response for the rerank api](https://c
|
|||
|
||||
## Overview
|
||||
|
||||
| Feature | Supported | Notes |
|
||||
|---------|-----------|-------|
|
||||
| Cost Tracking | ✅ | Works with all supported models |
|
||||
| Logging | ✅ | Works across all integrations |
|
||||
| End-user Tracking | ✅ | |
|
||||
| Fallbacks | ✅ | Works between supported models |
|
||||
| Loadbalancing | ✅ | Works between supported models |
|
||||
| Guardrails | ✅ | Applies to input query only (not documents) |
|
||||
| Supported Providers | Cohere, Together AI, Azure AI, DeepInfra, Nvidia NIM, Infinity, Fireworks AI, Voyage AI | |
|
||||
| Feature | Supported | Notes |
|
||||
|---------|-----------------------------------------------------------------------------------------------------|-------|
|
||||
| Cost Tracking | ✅ | Works with all supported models |
|
||||
| Logging | ✅ | Works across all integrations |
|
||||
| End-user Tracking | ✅ | |
|
||||
| Fallbacks | ✅ | Works between supported models |
|
||||
| Loadbalancing | ✅ | Works between supported models |
|
||||
| Guardrails | ✅ | Applies to input query only (not documents) |
|
||||
| Supported Providers | Cohere, Together AI, Azure AI, DeepInfra, Nvidia NIM, Infinity, Fireworks AI, Voyage AI, watsonx.ai | |
|
||||
|
||||
## **LiteLLM Python SDK Usage**
|
||||
### Quick Start
|
||||
|
|
@ -123,17 +123,18 @@ curl http://0.0.0.0:4000/rerank \
|
|||
|
||||
#### ⚡️See all supported models and providers at [models.litellm.ai](https://models.litellm.ai/)
|
||||
|
||||
| Provider | Link to Usage |
|
||||
|-------------|--------------------|
|
||||
| Cohere (v1 + v2 clients) | [Usage](#quick-start) |
|
||||
| Together AI| [Usage](../docs/providers/togetherai) |
|
||||
| Azure AI| [Usage](../docs/providers/azure_ai#rerank-endpoint) |
|
||||
| Jina AI| [Usage](../docs/providers/jina_ai) |
|
||||
| AWS Bedrock| [Usage](../docs/providers/bedrock#rerank-api) |
|
||||
| HuggingFace| [Usage](../docs/providers/huggingface_rerank) |
|
||||
| Infinity| [Usage](../docs/providers/infinity) |
|
||||
| vLLM| [Usage](../docs/providers/vllm#rerank-endpoint) |
|
||||
| DeepInfra| [Usage](../docs/providers/deepinfra#rerank-endpoint) |
|
||||
| Vertex AI| [Usage](../docs/providers/vertex#rerank-api) |
|
||||
| Fireworks AI| [Usage](../docs/providers/fireworks_ai#rerank-endpoint) |
|
||||
| Voyage AI| [Usage](../docs/providers/voyage#rerank) |
|
||||
| Provider | Link to Usage |
|
||||
|--------------------------|------------------------------------------------------|
|
||||
| Cohere (v1 + v2 clients) | [Usage](#quick-start) |
|
||||
| Together AI | [Usage](../docs/providers/togetherai) |
|
||||
| Azure AI | [Usage](../docs/providers/azure_ai#rerank-endpoint) |
|
||||
| Jina AI | [Usage](../docs/providers/jina_ai) |
|
||||
| AWS Bedrock | [Usage](../docs/providers/bedrock#rerank-api) |
|
||||
| HuggingFace | [Usage](../docs/providers/huggingface_rerank) |
|
||||
| Infinity | [Usage](../docs/providers/infinity) |
|
||||
| vLLM | [Usage](../docs/providers/vllm#rerank-endpoint) |
|
||||
| DeepInfra | [Usage](../docs/providers/deepinfra#rerank-endpoint) |
|
||||
| Vertex AI | [Usage](../docs/providers/vertex#rerank-api) |
|
||||
| Fireworks AI | [Usage](../docs/providers/fireworks_ai#rerank-endpoint) |
|
||||
| Voyage AI | [Usage](../docs/providers/voyage#rerank) |
|
||||
| IBM watsonx.ai | [Usage](../docs/providers/watsonx/rerank) |
|
||||
|
|
@ -884,7 +884,12 @@ router = litellm.Router(
|
|||
},
|
||||
},
|
||||
],
|
||||
optional_pre_call_checks=["responses_api_deployment_check"],
|
||||
# `responses_api_deployment_check` ensures Requests with `previous_response_id`
|
||||
# are routed to the same deployment. `deployment_affinity` adds sticky sessions
|
||||
# for requests without `previous_response_id` (useful for implicit caching).
|
||||
optional_pre_call_checks=["responses_api_deployment_check", "deployment_affinity"],
|
||||
# Optional (default is 3600 seconds / 1 hour)
|
||||
deployment_affinity_ttl_seconds=3600,
|
||||
)
|
||||
|
||||
# Initial request
|
||||
|
|
@ -911,7 +916,16 @@ follow_up = await router.aresponses(
|
|||
|
||||
#### 1. Setup session continuity on proxy config.yaml
|
||||
|
||||
To enable session continuity for Responses API in your LiteLLM proxy, set `optional_pre_call_checks: ["responses_api_deployment_check"]` in your proxy config.yaml.
|
||||
To enable session continuity for Responses API in your LiteLLM proxy, set `optional_pre_call_checks` in your proxy config.yaml.
|
||||
|
||||
- `responses_api_deployment_check`: high priority routing when `previous_response_id` is provided
|
||||
- `deployment_affinity`: sticky sessions based on user key (applies even without `previous_response_id`)
|
||||
|
||||
Notes:
|
||||
- User-key affinity is keyed on `metadata.user_api_key_hash` (the API key hash). The OpenAI `user` request parameter is an end-user identifier and is intentionally not used for deployment affinity.
|
||||
- `user_api_key_hash` is already SHA-256, and is used as-is (no double hashing).
|
||||
- Affinity is scoped by a stable model identifier (the model-map key, e.g. `model_map_information.model_map_key`) so model aliases map to the same stickiness bucket.
|
||||
- The mapping TTL is controlled by `deployment_affinity_ttl_seconds` (configured on Router init / proxy startup).
|
||||
|
||||
```yaml showLineNumbers title="config.yaml with Session Continuity"
|
||||
model_list:
|
||||
|
|
@ -929,7 +943,11 @@ model_list:
|
|||
api_base: https://endpoint2.openai.azure.com
|
||||
|
||||
router_settings:
|
||||
optional_pre_call_checks: ["responses_api_deployment_check"]
|
||||
optional_pre_call_checks:
|
||||
- responses_api_deployment_check
|
||||
- deployment_affinity
|
||||
# Optional (default is 3600 seconds / 1 hour)
|
||||
deployment_affinity_ttl_seconds: 3600
|
||||
```
|
||||
|
||||
#### 2. Use the OpenAI Python SDK to make requests to LiteLLM Proxy
|
||||
|
|
@ -1356,8 +1374,3 @@ Response:
|
|||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -276,6 +276,7 @@ The response follows Perplexity's search format with the following structure:
|
|||
| Firecrawl | `FIRECRAWL_API_KEY` | `firecrawl` |
|
||||
| SearXNG | `SEARXNG_API_BASE` (required) | `searxng` |
|
||||
| Linkup | `LINKUP_API_KEY` | `linkup` |
|
||||
| DuckDuckGo | `DUCKDUCKGO_API_BASE` | `duckduckgo` |
|
||||
|
||||
See the individual provider documentation for detailed setup instructions and provider-specific parameters.
|
||||
|
||||
|
|
|
|||
|
|
@ -120,6 +120,13 @@ const sidebars = {
|
|||
type: "category",
|
||||
label: "[Beta] Prompt Management",
|
||||
items: [
|
||||
{
|
||||
type: "category",
|
||||
label: "Contributing to Prompt Management",
|
||||
items: [
|
||||
"adding_provider/generic_prompt_management_api",
|
||||
]
|
||||
},
|
||||
"proxy/litellm_prompt_management",
|
||||
"proxy/custom_prompt_management",
|
||||
"proxy/native_litellm_prompt",
|
||||
|
|
@ -937,6 +944,7 @@ const sidebars = {
|
|||
"providers/anthropic_tool_search",
|
||||
"guides/code_interpreter",
|
||||
"completion/message_trimming",
|
||||
"completion/message_sanitization",
|
||||
"completion/model_alias",
|
||||
"completion/mock_requests",
|
||||
"completion/predict_outputs",
|
||||
|
|
|
|||
|
|
@ -1051,6 +1051,168 @@ class _PROXY_LiteLLMManagedFiles(CustomLogger, BaseFileEndpoints):
|
|||
"""Handled in files_endpoints.py"""
|
||||
return []
|
||||
|
||||
def _is_batch_polling_enabled(self) -> bool:
|
||||
"""
|
||||
Check if batch cost tracking is actually enabled and running.
|
||||
Returns:
|
||||
bool: True if batch cost tracking is active, False otherwise
|
||||
"""
|
||||
try:
|
||||
# Import here to avoid circular dependencies
|
||||
import litellm.proxy.proxy_server as proxy_server_module
|
||||
|
||||
# Check if the scheduler has the batch cost checking job registered
|
||||
scheduler = getattr(proxy_server_module, 'scheduler', None)
|
||||
if scheduler is None:
|
||||
return False
|
||||
|
||||
# Check if the check_batch_cost_job exists in the scheduler
|
||||
try:
|
||||
job = scheduler.get_job('check_batch_cost_job')
|
||||
if job is not None:
|
||||
return True
|
||||
except Exception:
|
||||
# Job not found or scheduler doesn't support get_job
|
||||
pass
|
||||
|
||||
return False
|
||||
except Exception as e:
|
||||
verbose_logger.warning(
|
||||
f"Error checking batch polling configuration: {e}. Assuming disabled."
|
||||
)
|
||||
return False
|
||||
|
||||
async def _get_batches_referencing_file(
|
||||
self, file_id: str
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Find batches in non-terminal states that reference this file.
|
||||
|
||||
Non-terminal states: validating, in_progress, finalizing
|
||||
Terminal states: completed, complete, failed, expired, cancelled
|
||||
|
||||
Args:
|
||||
file_id: The unified file ID to check
|
||||
|
||||
Returns:
|
||||
List of batch objects referencing this file in non-terminal state
|
||||
(max 10 for error message display)
|
||||
"""
|
||||
# Prepare list of file IDs to check (both unified and provider IDs)
|
||||
file_ids_to_check = [file_id]
|
||||
|
||||
# Get model-specific file IDs for this unified file ID if it's a managed file
|
||||
try:
|
||||
model_file_id_mapping = await self.get_model_file_id_mapping(
|
||||
[file_id], litellm_parent_otel_span=None
|
||||
)
|
||||
|
||||
if model_file_id_mapping and file_id in model_file_id_mapping:
|
||||
# Add all provider file IDs for this unified file
|
||||
provider_file_ids = list(model_file_id_mapping[file_id].values())
|
||||
file_ids_to_check.extend(provider_file_ids)
|
||||
except Exception as e:
|
||||
verbose_logger.debug(
|
||||
f"Could not get model file ID mapping for {file_id}: {e}. "
|
||||
f"Will only check unified file ID."
|
||||
)
|
||||
MAX_MATCHES_TO_RETURN = 10
|
||||
|
||||
batches = await self.prisma_client.db.litellm_managedobjecttable.find_many(
|
||||
where={
|
||||
"file_purpose": "batch",
|
||||
"status": {"in": ["validating", "in_progress", "finalizing"]},
|
||||
},
|
||||
take=MAX_MATCHES_TO_RETURN,
|
||||
order={"created_at": "desc"},
|
||||
)
|
||||
|
||||
referencing_batches = []
|
||||
for batch in batches:
|
||||
try:
|
||||
# Parse the batch file_object to check for file references
|
||||
batch_data = json.loads(batch.file_object) if isinstance(batch.file_object, str) else batch.file_object
|
||||
|
||||
# Extract file IDs from batch
|
||||
# Batches typically reference the unified file ID in input_file_id
|
||||
# Output and error files are generated by the provider
|
||||
input_file_id = batch_data.get("input_file_id")
|
||||
output_file_id = batch_data.get("output_file_id")
|
||||
error_file_id = batch_data.get("error_file_id")
|
||||
|
||||
referenced_file_ids = [fid for fid in [input_file_id, output_file_id, error_file_id] if fid]
|
||||
|
||||
# Check if any referenced file ID matches the file we're trying to delete
|
||||
if any(ref_id in file_ids_to_check for ref_id in referenced_file_ids):
|
||||
referencing_batches.append({
|
||||
"batch_id": batch.unified_object_id,
|
||||
"status": batch.status,
|
||||
"created_at": batch.created_at,
|
||||
})
|
||||
except Exception as e:
|
||||
verbose_logger.warning(
|
||||
f"Error parsing batch object {batch.unified_object_id}: {e}"
|
||||
)
|
||||
continue
|
||||
|
||||
return referencing_batches
|
||||
|
||||
async def _check_file_deletion_allowed(self, file_id: str) -> None:
|
||||
"""
|
||||
Check if file deletion should be blocked due to batch references.
|
||||
|
||||
Blocks deletion if:
|
||||
1. File is referenced by any batch in non-terminal state, AND
|
||||
2. Batch polling is configured (user wants cost tracking)
|
||||
|
||||
Args:
|
||||
file_id: The unified file ID to check
|
||||
|
||||
Raises:
|
||||
HTTPException: If file deletion should be blocked
|
||||
"""
|
||||
# Check if batch polling is enabled
|
||||
if not self._is_batch_polling_enabled():
|
||||
# Batch polling not configured, allow deletion
|
||||
return
|
||||
|
||||
# Check if file is referenced by any non-terminal batches
|
||||
referencing_batches = await self._get_batches_referencing_file(file_id)
|
||||
|
||||
if referencing_batches:
|
||||
# File is referenced by non-terminal batches and polling is enabled
|
||||
MAX_BATCHES_IN_ERROR = 5 # Limit batches shown in error message for readability
|
||||
|
||||
# Show up to MAX_BATCHES_IN_ERROR in the error message
|
||||
batches_to_show = referencing_batches[:MAX_BATCHES_IN_ERROR]
|
||||
batch_statuses = [f"{b['batch_id']}: {b['status']}" for b in batches_to_show]
|
||||
|
||||
# Determine the count message
|
||||
count_message = f"{len(referencing_batches)}"
|
||||
if len(referencing_batches) >= 10: # MAX_MATCHES_TO_RETURN from _get_batches_referencing_file
|
||||
count_message = "10+"
|
||||
|
||||
error_message = (
|
||||
f"Cannot delete file {file_id}. "
|
||||
f"The file is referenced by {count_message} batch(es) in non-terminal state"
|
||||
)
|
||||
|
||||
# Add specific batch details if not too many
|
||||
if len(referencing_batches) <= MAX_BATCHES_IN_ERROR:
|
||||
error_message += f": {', '.join(batch_statuses)}. "
|
||||
else:
|
||||
error_message += f" (showing {MAX_BATCHES_IN_ERROR} most recent): {', '.join(batch_statuses)}. "
|
||||
|
||||
error_message += (
|
||||
f"To delete this file before complete cost tracking, please delete or cancel the referencing batch(es) first. "
|
||||
f"Alternatively, wait for all batches to complete processing."
|
||||
)
|
||||
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=error_message,
|
||||
)
|
||||
|
||||
async def afile_delete(
|
||||
self,
|
||||
file_id: str,
|
||||
|
|
@ -1059,6 +1221,9 @@ class _PROXY_LiteLLMManagedFiles(CustomLogger, BaseFileEndpoints):
|
|||
**data: Dict,
|
||||
) -> OpenAIFileObject:
|
||||
|
||||
# Check if file deletion should be blocked due to batch references
|
||||
await self._check_file_deletion_allowed(file_id)
|
||||
|
||||
# file_id = convert_b64_uid_to_unified_uid(file_id)
|
||||
model_file_id_mapping = await self.get_model_file_id_mapping(
|
||||
[file_id], litellm_parent_otel_span
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "LiteLLM_ManagedVectorStoresTable" ADD COLUMN "team_id" TEXT,
|
||||
ADD COLUMN "user_id" TEXT;
|
||||
ALTER TABLE "LiteLLM_ManagedVectorStoresTable"
|
||||
ADD COLUMN IF NOT EXISTS "team_id" TEXT,
|
||||
ADD COLUMN IF NOT EXISTS "user_id" TEXT;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "LiteLLM_ManagedVectorStoresTable_team_id_idx" ON "LiteLLM_ManagedVectorStoresTable"("team_id");
|
||||
CREATE INDEX IF NOT EXISTS "LiteLLM_ManagedVectorStoresTable_team_id_idx"
|
||||
ON "LiteLLM_ManagedVectorStoresTable"("team_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "LiteLLM_ManagedVectorStoresTable_user_id_idx" ON "LiteLLM_ManagedVectorStoresTable"("user_id");
|
||||
CREATE INDEX IF NOT EXISTS "LiteLLM_ManagedVectorStoresTable_user_id_idx"
|
||||
ON "LiteLLM_ManagedVectorStoresTable"("user_id");
|
||||
|
||||
|
|
|
|||
|
|
@ -1355,6 +1355,7 @@ if TYPE_CHECKING:
|
|||
from .llms.vertex_ai.rerank.transformation import VertexAIRerankConfig as VertexAIRerankConfig
|
||||
from .llms.fireworks_ai.rerank.transformation import FireworksAIRerankConfig as FireworksAIRerankConfig
|
||||
from .llms.voyage.rerank.transformation import VoyageRerankConfig as VoyageRerankConfig
|
||||
from .llms.watsonx.rerank.transformation import IBMWatsonXRerankConfig as IBMWatsonXRerankConfig
|
||||
from .llms.clarifai.chat.transformation import ClarifaiConfig as ClarifaiConfig
|
||||
from .llms.ai21.chat.transformation import AI21ChatConfig as AI21ChatConfig
|
||||
from .llms.meta_llama.chat.transformation import LlamaAPIConfig as LlamaAPIConfig
|
||||
|
|
@ -1422,6 +1423,7 @@ if TYPE_CHECKING:
|
|||
from .llms.volcengine.responses.transformation import VolcEngineResponsesAPIConfig as VolcEngineResponsesAPIConfig
|
||||
from .llms.manus.responses.transformation import ManusResponsesAPIConfig as ManusResponsesAPIConfig
|
||||
from .llms.perplexity.responses.transformation import PerplexityResponsesConfig as PerplexityResponsesConfig
|
||||
from .llms.databricks.responses.transformation import DatabricksResponsesAPIConfig as DatabricksResponsesAPIConfig
|
||||
from .llms.gemini.interactions.transformation import GoogleAIStudioInteractionsConfig as GoogleAIStudioInteractionsConfig
|
||||
from .llms.openai.chat.o_series_transformation import OpenAIOSeriesConfig as OpenAIOSeriesConfig, OpenAIOSeriesConfig as OpenAIO1Config
|
||||
from .llms.anthropic.skills.transformation import AnthropicSkillsConfig as AnthropicSkillsConfig
|
||||
|
|
|
|||
|
|
@ -155,6 +155,7 @@ LLM_CONFIG_NAMES = (
|
|||
"VertexAIRerankConfig",
|
||||
"FireworksAIRerankConfig",
|
||||
"VoyageRerankConfig",
|
||||
"IBMWatsonXRerankConfig",
|
||||
"ClarifaiConfig",
|
||||
"AI21ChatConfig",
|
||||
"LlamaAPIConfig",
|
||||
|
|
@ -227,6 +228,7 @@ LLM_CONFIG_NAMES = (
|
|||
"LiteLLMProxyResponsesAPIConfig",
|
||||
"VolcEngineResponsesAPIConfig",
|
||||
"PerplexityResponsesConfig",
|
||||
"DatabricksResponsesAPIConfig",
|
||||
"GoogleAIStudioInteractionsConfig",
|
||||
"OpenAIOSeriesConfig",
|
||||
"AnthropicSkillsConfig",
|
||||
|
|
@ -671,6 +673,7 @@ _LLM_CONFIGS_IMPORT_MAP = {
|
|||
"FireworksAIRerankConfig",
|
||||
),
|
||||
"VoyageRerankConfig": (".llms.voyage.rerank.transformation", "VoyageRerankConfig"),
|
||||
"IBMWatsonXRerankConfig": (".llms.watsonx.rerank.transformation", "IBMWatsonXRerankConfig"),
|
||||
"ClarifaiConfig": (".llms.clarifai.chat.transformation", "ClarifaiConfig"),
|
||||
"AI21ChatConfig": (".llms.ai21.chat.transformation", "AI21ChatConfig"),
|
||||
"LlamaAPIConfig": (".llms.meta_llama.chat.transformation", "LlamaAPIConfig"),
|
||||
|
|
@ -906,6 +909,10 @@ _LLM_CONFIGS_IMPORT_MAP = {
|
|||
".llms.perplexity.responses.transformation",
|
||||
"PerplexityResponsesConfig",
|
||||
),
|
||||
"DatabricksResponsesAPIConfig": (
|
||||
".llms.databricks.responses.transformation",
|
||||
"DatabricksResponsesAPIConfig",
|
||||
),
|
||||
"GoogleAIStudioInteractionsConfig": (
|
||||
".llms.gemini.interactions.transformation",
|
||||
"GoogleAIStudioInteractionsConfig",
|
||||
|
|
|
|||
|
|
@ -148,5 +148,35 @@
|
|||
"tool-search-tool-2025-10-19": "tool-search-tool-2025-10-19",
|
||||
"web-fetch-2025-09-10": null,
|
||||
"web-search-2025-03-05": "web-search-2025-03-05"
|
||||
},
|
||||
"databricks": {
|
||||
"advanced-tool-use-2025-11-20": "advanced-tool-use-2025-11-20",
|
||||
"bash_20241022": null,
|
||||
"bash_20250124": null,
|
||||
"code-execution-2025-08-25": "code-execution-2025-08-25",
|
||||
"compact-2026-01-12": "compact-2026-01-12",
|
||||
"computer-use-2025-01-24": "computer-use-2025-01-24",
|
||||
"computer-use-2025-11-24": "computer-use-2025-11-24",
|
||||
"context-1m-2025-08-07": "context-1m-2025-08-07",
|
||||
"context-management-2025-06-27": "context-management-2025-06-27",
|
||||
"effort-2025-11-24": "effort-2025-11-24",
|
||||
"fast-mode-2026-02-01": "fast-mode-2026-02-01",
|
||||
"files-api-2025-04-14": "files-api-2025-04-14",
|
||||
"structured-output-2024-03-01": null,
|
||||
"fine-grained-tool-streaming-2025-05-14": "fine-grained-tool-streaming-2025-05-14",
|
||||
"interleaved-thinking-2025-05-14": "interleaved-thinking-2025-05-14",
|
||||
"mcp-client-2025-11-20": "mcp-client-2025-11-20",
|
||||
"mcp-client-2025-04-04": "mcp-client-2025-04-04",
|
||||
"mcp-servers-2025-12-04": null,
|
||||
"oauth-2025-04-20": "oauth-2025-04-20",
|
||||
"output-128k-2025-02-19": "output-128k-2025-02-19",
|
||||
"prompt-caching-scope-2026-01-05": "prompt-caching-scope-2026-01-05",
|
||||
"skills-2025-10-02": "skills-2025-10-02",
|
||||
"structured-outputs-2025-11-13": "structured-outputs-2025-11-13",
|
||||
"text_editor_20241022": null,
|
||||
"text_editor_20250124": null,
|
||||
"token-efficient-tools-2025-02-19": "token-efficient-tools-2025-02-19",
|
||||
"web-fetch-2025-09-10": "web-fetch-2025-09-10",
|
||||
"web-search-2025-03-05": "web-search-2025-03-05"
|
||||
}
|
||||
}
|
||||
|
|
@ -62,9 +62,7 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
|||
def __init__(self):
|
||||
pass
|
||||
|
||||
def _handle_raw_dict_response_item(
|
||||
self, item: Dict[str, Any], index: int
|
||||
) -> Tuple[Optional[Any], int]:
|
||||
def _handle_raw_dict_response_item(self, item: Dict[str, Any], index: int) -> Tuple[Optional[Any], int]:
|
||||
"""
|
||||
Handle raw dict response items from Responses API (e.g., GPT-5 Codex format).
|
||||
|
||||
|
|
@ -107,13 +105,9 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
|||
if item_type == "function_call":
|
||||
# Extract provider_specific_fields if present and pass through as-is
|
||||
provider_specific_fields = item.get("provider_specific_fields")
|
||||
if provider_specific_fields and not isinstance(
|
||||
provider_specific_fields, dict
|
||||
):
|
||||
if provider_specific_fields and not isinstance(provider_specific_fields, dict):
|
||||
provider_specific_fields = (
|
||||
dict(provider_specific_fields)
|
||||
if hasattr(provider_specific_fields, "__dict__")
|
||||
else {}
|
||||
dict(provider_specific_fields) if hasattr(provider_specific_fields, "__dict__") else {}
|
||||
)
|
||||
|
||||
tool_call_dict = {
|
||||
|
|
@ -129,9 +123,7 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
|||
if provider_specific_fields:
|
||||
tool_call_dict["provider_specific_fields"] = provider_specific_fields
|
||||
# Also add to function's provider_specific_fields for consistency
|
||||
tool_call_dict["function"][
|
||||
"provider_specific_fields"
|
||||
] = provider_specific_fields
|
||||
tool_call_dict["function"]["provider_specific_fields"] = provider_specific_fields
|
||||
|
||||
msg = Message(
|
||||
content=None,
|
||||
|
|
@ -169,7 +161,8 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
|||
"type": "message",
|
||||
"role": role,
|
||||
"content": self._convert_content_to_responses_format(
|
||||
content, role # type: ignore
|
||||
content,
|
||||
role, # type: ignore
|
||||
),
|
||||
}
|
||||
)
|
||||
|
|
@ -186,7 +179,8 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
|||
elif isinstance(content, list):
|
||||
# Transform list content to Responses API format
|
||||
tool_output = self._convert_content_to_responses_format(
|
||||
content, "user" # Use "user" role to get input_* types
|
||||
content,
|
||||
"user", # Use "user" role to get input_* types
|
||||
)
|
||||
else:
|
||||
# Fallback: convert unexpected types to input_text
|
||||
|
|
@ -219,9 +213,7 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
|||
{
|
||||
"type": "message",
|
||||
"role": role,
|
||||
"content": self._convert_content_to_responses_format(
|
||||
content, cast(str, role)
|
||||
),
|
||||
"content": self._convert_content_to_responses_format(content, cast(str, role)),
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -344,9 +336,7 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
|||
previous_response_id = optional_params.get("previous_response_id")
|
||||
if previous_response_id:
|
||||
# Use the existing session handler for responses API
|
||||
verbose_logger.debug(
|
||||
f"Chat provider: Warning ignoring previous response ID: {previous_response_id}"
|
||||
)
|
||||
verbose_logger.debug(f"Chat provider: Warning ignoring previous response ID: {previous_response_id}")
|
||||
|
||||
# Convert back to responses API format for the actual request
|
||||
|
||||
|
|
@ -368,9 +358,7 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
|||
"client": client,
|
||||
}
|
||||
|
||||
verbose_logger.debug(
|
||||
f"Chat provider: Final request model={api_model}, input_items={len(input_items)}"
|
||||
)
|
||||
verbose_logger.debug(f"Chat provider: Final request model={api_model}, input_items={len(input_items)}")
|
||||
|
||||
self._merge_responses_api_request_into_request_data(
|
||||
request_data, responses_api_request, instructions
|
||||
|
|
@ -450,9 +438,11 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
|||
LiteLLMCompletionResponsesConfig,
|
||||
)
|
||||
|
||||
tool_call_dict = LiteLLMCompletionResponsesConfig.convert_response_function_tool_call_to_chat_completion_tool_call(
|
||||
tool_call_item=item,
|
||||
index=tool_call_index,
|
||||
tool_call_dict = (
|
||||
LiteLLMCompletionResponsesConfig.convert_response_function_tool_call_to_chat_completion_tool_call(
|
||||
tool_call_item=item,
|
||||
index=tool_call_index,
|
||||
)
|
||||
)
|
||||
accumulated_tool_calls.append(tool_call_dict)
|
||||
tool_call_index += 1
|
||||
|
|
@ -472,9 +462,7 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
|||
tool_calls=accumulated_tool_calls,
|
||||
reasoning_content=reasoning_content,
|
||||
)
|
||||
choices.append(
|
||||
Choices(message=msg, finish_reason="tool_calls", index=index)
|
||||
)
|
||||
choices.append(Choices(message=msg, finish_reason="tool_calls", index=index))
|
||||
reasoning_content = None
|
||||
|
||||
return choices
|
||||
|
|
@ -510,17 +498,10 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
|||
)
|
||||
|
||||
if len(choices) == 0:
|
||||
if (
|
||||
raw_response.incomplete_details is not None
|
||||
and raw_response.incomplete_details.reason is not None
|
||||
):
|
||||
raise ValueError(
|
||||
f"{model} unable to complete request: {raw_response.incomplete_details.reason}"
|
||||
)
|
||||
if raw_response.incomplete_details is not None and raw_response.incomplete_details.reason is not None:
|
||||
raise ValueError(f"{model} unable to complete request: {raw_response.incomplete_details.reason}")
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unknown items in responses API response: {raw_response.output}"
|
||||
)
|
||||
raise ValueError(f"Unknown items in responses API response: {raw_response.output}")
|
||||
|
||||
setattr(model_response, "choices", choices)
|
||||
|
||||
|
|
@ -529,11 +510,9 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
|||
setattr(
|
||||
model_response,
|
||||
"usage",
|
||||
ResponseAPILoggingUtils._transform_response_api_usage_to_chat_usage(
|
||||
raw_response.usage
|
||||
),
|
||||
ResponseAPILoggingUtils._transform_response_api_usage_to_chat_usage(raw_response.usage),
|
||||
)
|
||||
|
||||
|
||||
# Preserve hidden params from the ResponsesAPIResponse, especially the headers
|
||||
# which contain important provider information like x-request-id
|
||||
raw_response_hidden_params = getattr(raw_response, "_hidden_params", {})
|
||||
|
|
@ -550,24 +529,18 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
|||
model_response._hidden_params[key] = merged_headers
|
||||
else:
|
||||
model_response._hidden_params[key] = value
|
||||
|
||||
|
||||
return model_response
|
||||
|
||||
def get_model_response_iterator(
|
||||
self,
|
||||
streaming_response: Union[
|
||||
Iterator[str], AsyncIterator[str], "ModelResponse", "BaseModel"
|
||||
],
|
||||
streaming_response: Union[Iterator[str], AsyncIterator[str], "ModelResponse", "BaseModel"],
|
||||
sync_stream: bool,
|
||||
json_mode: Optional[bool] = False,
|
||||
) -> BaseModelResponseIterator:
|
||||
return OpenAiResponsesToChatCompletionStreamIterator(
|
||||
streaming_response, sync_stream, json_mode
|
||||
)
|
||||
return OpenAiResponsesToChatCompletionStreamIterator(streaming_response, sync_stream, json_mode)
|
||||
|
||||
def _convert_content_str_to_input_text(
|
||||
self, content: str, role: str
|
||||
) -> Dict[str, Any]:
|
||||
def _convert_content_str_to_input_text(self, content: str, role: str) -> Dict[str, Any]:
|
||||
if role == "user" or role == "system" or role == "tool":
|
||||
return {"type": "input_text", "text": content}
|
||||
else:
|
||||
|
|
@ -594,9 +567,7 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
|||
if actual_image_url is None:
|
||||
raise ValueError(f"Invalid image URL: {content_image_url}")
|
||||
|
||||
image_param = ResponseInputImageParam(
|
||||
image_url=actual_image_url, detail="auto", type="input_image"
|
||||
)
|
||||
image_param = ResponseInputImageParam(image_url=actual_image_url, detail="auto", type="input_image")
|
||||
|
||||
if detail:
|
||||
image_param["detail"] = detail
|
||||
|
|
@ -605,31 +576,29 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
|||
|
||||
def _convert_content_to_responses_format(
|
||||
self,
|
||||
content: Union[
|
||||
str,
|
||||
Iterable[
|
||||
Union["OpenAIMessageContentListBlock", "ChatCompletionThinkingBlock"]
|
||||
],
|
||||
content: Optional[
|
||||
Union[
|
||||
str,
|
||||
Iterable[Union["OpenAIMessageContentListBlock", "ChatCompletionThinkingBlock"]],
|
||||
]
|
||||
],
|
||||
role: str,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Convert chat completion content to responses API format"""
|
||||
from litellm.types.llms.openai import ChatCompletionImageObject
|
||||
|
||||
verbose_logger.debug(
|
||||
f"Chat provider: Converting content to responses format - input type: {type(content)}"
|
||||
)
|
||||
verbose_logger.debug(f"Chat provider: Converting content to responses format - input type: {type(content)}")
|
||||
|
||||
if isinstance(content, str):
|
||||
if content is None:
|
||||
return [self._convert_content_str_to_input_text("", role)]
|
||||
elif isinstance(content, str):
|
||||
result = [self._convert_content_str_to_input_text(content, role)]
|
||||
verbose_logger.debug(f"Chat provider: String content -> {result}")
|
||||
return result
|
||||
elif isinstance(content, list):
|
||||
result = []
|
||||
for i, item in enumerate(content):
|
||||
verbose_logger.debug(
|
||||
f"Chat provider: Processing content item {i}: {type(item)} = {item}"
|
||||
)
|
||||
verbose_logger.debug(f"Chat provider: Processing content item {i}: {type(item)} = {item}")
|
||||
if isinstance(item, str):
|
||||
converted = self._convert_content_str_to_input_text(item, role)
|
||||
result.append(converted)
|
||||
|
|
@ -638,9 +607,7 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
|||
# Handle multimodal content
|
||||
original_type = item.get("type")
|
||||
if original_type == "text":
|
||||
converted = self._convert_content_str_to_input_text(
|
||||
item.get("text", ""), role
|
||||
)
|
||||
converted = self._convert_content_str_to_input_text(item.get("text", ""), role)
|
||||
result.append(converted)
|
||||
verbose_logger.debug(f"Chat provider: text -> {converted}")
|
||||
elif original_type == "image_url":
|
||||
|
|
@ -652,18 +619,14 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
|||
),
|
||||
)
|
||||
result.append(converted)
|
||||
verbose_logger.debug(
|
||||
f"Chat provider: image_url -> {converted}"
|
||||
)
|
||||
verbose_logger.debug(f"Chat provider: image_url -> {converted}")
|
||||
else:
|
||||
# Try to map other types to responses API format
|
||||
item_type = original_type or "input_text"
|
||||
if item_type == "image":
|
||||
converted = {"type": "input_image", **item}
|
||||
result.append(converted)
|
||||
verbose_logger.debug(
|
||||
f"Chat provider: image -> {converted}"
|
||||
)
|
||||
verbose_logger.debug(f"Chat provider: image -> {converted}")
|
||||
elif item_type in [
|
||||
"input_text",
|
||||
"input_image",
|
||||
|
|
@ -675,18 +638,12 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
|||
]:
|
||||
# Already in responses API format
|
||||
result.append(item)
|
||||
verbose_logger.debug(
|
||||
f"Chat provider: passthrough -> {item}"
|
||||
)
|
||||
verbose_logger.debug(f"Chat provider: passthrough -> {item}")
|
||||
else:
|
||||
# Default to input_text for unknown types
|
||||
converted = self._convert_content_str_to_input_text(
|
||||
str(item.get("text", item)), role
|
||||
)
|
||||
converted = self._convert_content_str_to_input_text(str(item.get("text", item)), role)
|
||||
result.append(converted)
|
||||
verbose_logger.debug(
|
||||
f"Chat provider: unknown({original_type}) -> {converted}"
|
||||
)
|
||||
verbose_logger.debug(f"Chat provider: unknown({original_type}) -> {converted}")
|
||||
verbose_logger.debug(f"Chat provider: Final converted content: {result}")
|
||||
return result
|
||||
else:
|
||||
|
|
@ -694,17 +651,13 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
|||
verbose_logger.debug(f"Chat provider: Other content type -> {result}")
|
||||
return result
|
||||
|
||||
def _convert_tools_to_responses_format(
|
||||
self, tools: List[Dict[str, Any]]
|
||||
) -> List["ALL_RESPONSES_API_TOOL_PARAMS"]:
|
||||
def _convert_tools_to_responses_format(self, tools: List[Dict[str, Any]]) -> List["ALL_RESPONSES_API_TOOL_PARAMS"]:
|
||||
"""Convert chat completion tools to responses API tools format"""
|
||||
responses_tools: List["ALL_RESPONSES_API_TOOL_PARAMS"] = []
|
||||
for tool in tools:
|
||||
# convert function tool from chat completion to responses API format
|
||||
if tool.get("type") == "function":
|
||||
function_tool = cast(
|
||||
ChatCompletionToolParamFunctionChunk, tool.get("function")
|
||||
)
|
||||
function_tool = cast(ChatCompletionToolParamFunctionChunk, tool.get("function"))
|
||||
responses_tools.append(
|
||||
FunctionToolParam(
|
||||
name=function_tool["name"],
|
||||
|
|
@ -730,9 +683,7 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
|||
if not extra_body:
|
||||
return optional_params
|
||||
|
||||
supported_responses_api_params = set(
|
||||
ResponsesAPIOptionalRequestParams.__annotations__.keys()
|
||||
)
|
||||
supported_responses_api_params = set(ResponsesAPIOptionalRequestParams.__annotations__.keys())
|
||||
# Also include params we handle specially
|
||||
supported_responses_api_params.update(
|
||||
{
|
||||
|
|
@ -750,9 +701,7 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
|||
|
||||
return optional_params
|
||||
|
||||
def _map_reasoning_effort(
|
||||
self, reasoning_effort: Union[str, Dict[str, Any]]
|
||||
) -> Optional[Reasoning]:
|
||||
def _map_reasoning_effort(self, reasoning_effort: Union[str, Dict[str, Any]]) -> Optional[Reasoning]:
|
||||
# If dict is passed, convert it directly to Reasoning object
|
||||
if isinstance(reasoning_effort, dict):
|
||||
return Reasoning(**reasoning_effort) # type: ignore[typeddict-item]
|
||||
|
|
@ -760,8 +709,7 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
|||
# Check if auto-summary is enabled via flag or environment variable
|
||||
# Priority: litellm.reasoning_auto_summary flag > LITELLM_REASONING_AUTO_SUMMARY env var
|
||||
auto_summary_enabled = (
|
||||
litellm.reasoning_auto_summary
|
||||
or os.getenv("LITELLM_REASONING_AUTO_SUMMARY", "false").lower() == "true"
|
||||
litellm.reasoning_auto_summary or os.getenv("LITELLM_REASONING_AUTO_SUMMARY", "false").lower() == "true"
|
||||
)
|
||||
|
||||
# If string is passed, map with optional summary based on flag/env var
|
||||
|
|
@ -772,11 +720,15 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
|||
elif reasoning_effort == "xhigh":
|
||||
return Reasoning(effort="xhigh", summary="detailed") if auto_summary_enabled else Reasoning(effort="xhigh") # type: ignore[typeddict-item]
|
||||
elif reasoning_effort == "medium":
|
||||
return Reasoning(effort="medium", summary="detailed") if auto_summary_enabled else Reasoning(effort="medium")
|
||||
return (
|
||||
Reasoning(effort="medium", summary="detailed") if auto_summary_enabled else Reasoning(effort="medium")
|
||||
)
|
||||
elif reasoning_effort == "low":
|
||||
return Reasoning(effort="low", summary="detailed") if auto_summary_enabled else Reasoning(effort="low")
|
||||
elif reasoning_effort == "minimal":
|
||||
return Reasoning(effort="minimal", summary="detailed") if auto_summary_enabled else Reasoning(effort="minimal")
|
||||
return (
|
||||
Reasoning(effort="minimal", summary="detailed") if auto_summary_enabled else Reasoning(effort="minimal")
|
||||
)
|
||||
return None
|
||||
|
||||
def _add_web_search_tool(
|
||||
|
|
@ -855,7 +807,7 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
|||
return {"format": {"type": "text"}}
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@staticmethod
|
||||
def _convert_annotations_to_chat_format(
|
||||
annotations: Optional[List[Any]],
|
||||
|
|
@ -908,9 +860,7 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
|||
|
||||
|
||||
class OpenAiResponsesToChatCompletionStreamIterator(BaseModelResponseIterator):
|
||||
def __init__(
|
||||
self, streaming_response, sync_stream: bool, json_mode: Optional[bool] = False
|
||||
):
|
||||
def __init__(self, streaming_response, sync_stream: bool, json_mode: Optional[bool] = False):
|
||||
super().__init__(streaming_response, sync_stream, json_mode)
|
||||
|
||||
def _handle_string_chunk(
|
||||
|
|
@ -923,9 +873,7 @@ class OpenAiResponsesToChatCompletionStreamIterator(BaseModelResponseIterator):
|
|||
|
||||
if not str_line or str_line.startswith("event:"):
|
||||
# ignore.
|
||||
return GenericStreamingChunk(
|
||||
text="", tool_use=None, is_finished=False, finish_reason="", usage=None
|
||||
)
|
||||
return GenericStreamingChunk(text="", tool_use=None, is_finished=False, finish_reason="", usage=None)
|
||||
index = str_line.find("data:")
|
||||
if index != -1:
|
||||
str_line = str_line[index + 5 :]
|
||||
|
|
@ -988,13 +936,9 @@ class OpenAiResponsesToChatCompletionStreamIterator(BaseModelResponseIterator):
|
|||
if output_item.get("type") == "function_call":
|
||||
# Extract provider_specific_fields if present
|
||||
provider_specific_fields = output_item.get("provider_specific_fields")
|
||||
if provider_specific_fields and not isinstance(
|
||||
provider_specific_fields, dict
|
||||
):
|
||||
if provider_specific_fields and not isinstance(provider_specific_fields, dict):
|
||||
provider_specific_fields = (
|
||||
dict(provider_specific_fields)
|
||||
if hasattr(provider_specific_fields, "__dict__")
|
||||
else {}
|
||||
dict(provider_specific_fields) if hasattr(provider_specific_fields, "__dict__") else {}
|
||||
)
|
||||
|
||||
function_chunk = ChatCompletionToolCallFunctionChunk(
|
||||
|
|
@ -1003,9 +947,7 @@ class OpenAiResponsesToChatCompletionStreamIterator(BaseModelResponseIterator):
|
|||
)
|
||||
|
||||
if provider_specific_fields:
|
||||
function_chunk["provider_specific_fields"] = (
|
||||
provider_specific_fields
|
||||
)
|
||||
function_chunk["provider_specific_fields"] = provider_specific_fields
|
||||
|
||||
tool_call_chunk = ChatCompletionToolCallChunk(
|
||||
id=output_item.get("call_id"),
|
||||
|
|
@ -1040,9 +982,7 @@ class OpenAiResponsesToChatCompletionStreamIterator(BaseModelResponseIterator):
|
|||
id=None,
|
||||
index=0,
|
||||
type="function",
|
||||
function=ChatCompletionToolCallFunctionChunk(
|
||||
name=None, arguments=content_part
|
||||
),
|
||||
function=ChatCompletionToolCallFunctionChunk(name=None, arguments=content_part),
|
||||
)
|
||||
]
|
||||
),
|
||||
|
|
@ -1051,22 +991,16 @@ class OpenAiResponsesToChatCompletionStreamIterator(BaseModelResponseIterator):
|
|||
]
|
||||
)
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Chat provider: Invalid function argument delta {parsed_chunk}"
|
||||
)
|
||||
raise ValueError(f"Chat provider: Invalid function argument delta {parsed_chunk}")
|
||||
elif event_type == "response.output_item.done":
|
||||
# New output item added
|
||||
output_item = parsed_chunk.get("item", {})
|
||||
if output_item.get("type") == "function_call":
|
||||
# Extract provider_specific_fields if present
|
||||
provider_specific_fields = output_item.get("provider_specific_fields")
|
||||
if provider_specific_fields and not isinstance(
|
||||
provider_specific_fields, dict
|
||||
):
|
||||
if provider_specific_fields and not isinstance(provider_specific_fields, dict):
|
||||
provider_specific_fields = (
|
||||
dict(provider_specific_fields)
|
||||
if hasattr(provider_specific_fields, "__dict__")
|
||||
else {}
|
||||
dict(provider_specific_fields) if hasattr(provider_specific_fields, "__dict__") else {}
|
||||
)
|
||||
|
||||
function_chunk = ChatCompletionToolCallFunctionChunk(
|
||||
|
|
@ -1076,9 +1010,7 @@ class OpenAiResponsesToChatCompletionStreamIterator(BaseModelResponseIterator):
|
|||
|
||||
# Add provider_specific_fields to function if present
|
||||
if provider_specific_fields:
|
||||
function_chunk["provider_specific_fields"] = (
|
||||
provider_specific_fields
|
||||
)
|
||||
function_chunk["provider_specific_fields"] = provider_specific_fields
|
||||
|
||||
tool_call_chunk = ChatCompletionToolCallChunk(
|
||||
id=output_item.get("call_id"),
|
||||
|
|
@ -1142,21 +1074,31 @@ class OpenAiResponsesToChatCompletionStreamIterator(BaseModelResponseIterator):
|
|||
elif event_type == "response.completed":
|
||||
# Response is fully complete - now we can signal is_finished=True
|
||||
# This ensures we don't prematurely end the stream before tool_calls arrive
|
||||
|
||||
# Check if response contains function_call items in output
|
||||
# to determine correct finish_reason
|
||||
response_data = parsed_chunk.get("response", {})
|
||||
output_items = response_data.get("output", []) if response_data else []
|
||||
|
||||
has_function_calls = any(
|
||||
item.get("type") == "function_call" for item in output_items if isinstance(item, dict)
|
||||
)
|
||||
|
||||
finish_reason = "tool_calls" if has_function_calls else "stop"
|
||||
|
||||
return ModelResponseStream(
|
||||
choices=[
|
||||
StreamingChoices(
|
||||
index=0,
|
||||
delta=Delta(content=""),
|
||||
finish_reason="stop",
|
||||
finish_reason=finish_reason,
|
||||
)
|
||||
]
|
||||
)
|
||||
else:
|
||||
pass
|
||||
# For any unhandled event types, create a minimal valid chunk or skip
|
||||
verbose_logger.debug(
|
||||
f"Chat provider: Unhandled event type '{event_type}', creating empty chunk"
|
||||
)
|
||||
verbose_logger.debug(f"Chat provider: Unhandled event type '{event_type}', creating empty chunk")
|
||||
|
||||
# Return a minimal valid chunk for unknown events
|
||||
return ModelResponseStream(
|
||||
|
|
@ -1179,9 +1121,5 @@ class OpenAiResponsesToChatCompletionStreamIterator(BaseModelResponseIterator):
|
|||
Returns:
|
||||
ModelResponseStream: OpenAI-formatted streaming chunk
|
||||
"""
|
||||
verbose_logger.debug(
|
||||
f"Chat provider: transform_streaming_response called with chunk: {chunk}"
|
||||
)
|
||||
return OpenAiResponsesToChatCompletionStreamIterator.translate_responses_chunk_to_openai_stream(
|
||||
chunk
|
||||
)
|
||||
verbose_logger.debug(f"Chat provider: transform_streaming_response called with chunk: {chunk}")
|
||||
return OpenAiResponsesToChatCompletionStreamIterator.translate_responses_chunk_to_openai_stream(chunk)
|
||||
|
|
|
|||
|
|
@ -287,7 +287,9 @@ MIN_NON_ZERO_TEMPERATURE = float(os.getenv("MIN_NON_ZERO_TEMPERATURE", 0.0001))
|
|||
REPEATED_STREAMING_CHUNK_LIMIT = int(
|
||||
os.getenv("REPEATED_STREAMING_CHUNK_LIMIT", 100)
|
||||
) # catch if model starts looping the same chunk while streaming. Uses high default to prevent false positives.
|
||||
DEFAULT_MAX_LRU_CACHE_SIZE = int(os.getenv("DEFAULT_MAX_LRU_CACHE_SIZE", 16))
|
||||
# Shared maxsize for functools.lru_cache usage across hot paths.
|
||||
# Defaulted to 64 to avoid cache thrash in multi-model production workloads.
|
||||
DEFAULT_MAX_LRU_CACHE_SIZE = int(os.getenv("DEFAULT_MAX_LRU_CACHE_SIZE", 64))
|
||||
_REALTIME_BODY_CACHE_SIZE = 1000 # Keep realtime helper caches bounded; workloads rarely exceed 1k models/intents
|
||||
INITIAL_RETRY_DELAY = float(os.getenv("INITIAL_RETRY_DELAY", 0.5))
|
||||
MAX_RETRY_DELAY = float(os.getenv("MAX_RETRY_DELAY", 8.0))
|
||||
|
|
@ -576,6 +578,11 @@ OPENAI_CHAT_COMPLETION_PARAMS = [
|
|||
"thinking",
|
||||
"web_search_options",
|
||||
"service_tier",
|
||||
"store",
|
||||
"prompt_cache_key",
|
||||
"prompt_cache_retention",
|
||||
"safety_identifier",
|
||||
"verbosity",
|
||||
]
|
||||
|
||||
OPENAI_TRANSCRIPTION_PARAMS = [
|
||||
|
|
|
|||
|
|
@ -448,7 +448,9 @@ def cost_per_token( # noqa: PLR0915
|
|||
elif custom_llm_provider == "anthropic":
|
||||
return anthropic_cost_per_token(model=model, usage=usage_block)
|
||||
elif custom_llm_provider == "bedrock":
|
||||
return bedrock_cost_per_token(model=model, usage=usage_block)
|
||||
return bedrock_cost_per_token(
|
||||
model=model, usage=usage_block, service_tier=service_tier
|
||||
)
|
||||
elif custom_llm_provider == "openai":
|
||||
return openai_cost_per_token(
|
||||
model=model, usage=usage_block, service_tier=service_tier
|
||||
|
|
@ -2146,4 +2148,3 @@ def handle_realtime_stream_cost_calculation(
|
|||
|
||||
return total_cost
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -93,7 +93,9 @@ class DatadogCostManagementLogger(CustomBatchLogger):
|
|||
Aggregates costs by Provider, Model, and Date.
|
||||
Returns a list of DatadogFOCUSCostEntry.
|
||||
"""
|
||||
aggregator: Dict[Tuple[str, str, str, Tuple[Tuple[str, str], ...]], DatadogFOCUSCostEntry] = {}
|
||||
aggregator: Dict[
|
||||
Tuple[str, str, str, Tuple[Tuple[str, str], ...]], DatadogFOCUSCostEntry
|
||||
] = {}
|
||||
|
||||
for log in logs:
|
||||
try:
|
||||
|
|
@ -167,10 +169,20 @@ class DatadogCostManagementLogger(CustomBatchLogger):
|
|||
metadata = log.get("metadata", {})
|
||||
if metadata:
|
||||
# Add user info
|
||||
if "user_api_key_alias" in metadata:
|
||||
# Add user info
|
||||
if metadata.get("user_api_key_alias"):
|
||||
tags["user"] = str(metadata["user_api_key_alias"])
|
||||
if "user_api_key_team_alias" in metadata:
|
||||
tags["team"] = str(metadata["user_api_key_team_alias"])
|
||||
|
||||
# Add Team Tag
|
||||
team_tag = (
|
||||
metadata.get("user_api_key_team_alias")
|
||||
or metadata.get("team_alias") # type: ignore
|
||||
or metadata.get("user_api_key_team_id")
|
||||
or metadata.get("team_id") # type: ignore
|
||||
)
|
||||
|
||||
if team_tag:
|
||||
tags["team"] = str(team_tag)
|
||||
# model_group is not in StandardLoggingMetadata TypedDict, so we need to access it via dict.get()
|
||||
model_group = metadata.get("model_group") # type: ignore[misc]
|
||||
if model_group:
|
||||
|
|
|
|||
|
|
@ -55,4 +55,15 @@ def get_datadog_tags(
|
|||
request_tags = standard_logging_object.get("request_tags", []) or []
|
||||
tags.extend(f"request_tag:{tag}" for tag in request_tags)
|
||||
|
||||
# Add Team Tag
|
||||
metadata = standard_logging_object.get("metadata", {}) or {}
|
||||
team_tag = (
|
||||
metadata.get("user_api_key_team_alias")
|
||||
or metadata.get("team_alias")
|
||||
or metadata.get("user_api_key_team_id")
|
||||
or metadata.get("team_id")
|
||||
)
|
||||
if team_tag:
|
||||
tags.append(f"team:{team_tag}")
|
||||
|
||||
return ",".join(tags)
|
||||
|
|
|
|||
|
|
@ -22,6 +22,10 @@ from typing import (
|
|||
import litellm
|
||||
from litellm._logging import print_verbose, verbose_logger
|
||||
from litellm.integrations.custom_logger import CustomLogger
|
||||
from litellm.litellm_core_utils.core_helpers import (
|
||||
get_litellm_metadata_from_kwargs,
|
||||
get_metadata_variable_name_from_kwargs,
|
||||
)
|
||||
from litellm.proxy._types import (
|
||||
LiteLLM_DeletedVerificationToken,
|
||||
LiteLLM_TeamTable,
|
||||
|
|
@ -1055,16 +1059,16 @@ class PrometheusLogger(CustomLogger):
|
|||
enum_values=enum_values,
|
||||
)
|
||||
|
||||
if (
|
||||
standard_logging_payload["stream"] is True
|
||||
): # log successful streaming requests from logging event hook.
|
||||
_labels = prometheus_label_factory(
|
||||
supported_enum_labels=self.get_labels_for_metric(
|
||||
metric_name="litellm_proxy_total_requests_metric"
|
||||
),
|
||||
enum_values=enum_values,
|
||||
)
|
||||
self.litellm_proxy_total_requests_metric.labels(**_labels).inc()
|
||||
# increment litellm_proxy_total_requests_metric for all successful requests
|
||||
# (both streaming and non-streaming) in this single location to prevent
|
||||
# double-counting that occurs when async_post_call_success_hook also increments
|
||||
_labels = prometheus_label_factory(
|
||||
supported_enum_labels=self.get_labels_for_metric(
|
||||
metric_name="litellm_proxy_total_requests_metric"
|
||||
),
|
||||
enum_values=enum_values,
|
||||
)
|
||||
self.litellm_proxy_total_requests_metric.labels(**_labels).inc()
|
||||
|
||||
def _increment_token_metrics(
|
||||
self,
|
||||
|
|
@ -1086,13 +1090,6 @@ class PrometheusLogger(CustomLogger):
|
|||
):
|
||||
_tags = standard_logging_payload["request_tags"]
|
||||
|
||||
_labels = prometheus_label_factory(
|
||||
supported_enum_labels=self.get_labels_for_metric(
|
||||
metric_name="litellm_proxy_total_requests_metric"
|
||||
),
|
||||
enum_values=enum_values,
|
||||
)
|
||||
|
||||
_labels = prometheus_label_factory(
|
||||
supported_enum_labels=self.get_labels_for_metric(
|
||||
metric_name="litellm_total_tokens_metric"
|
||||
|
|
@ -1655,49 +1652,12 @@ class PrometheusLogger(CustomLogger):
|
|||
):
|
||||
"""
|
||||
Proxy level tracking - triggered when the proxy responds with a success response to the client
|
||||
|
||||
Note: litellm_proxy_total_requests_metric is NOT incremented here to avoid
|
||||
double-counting. It is incremented in async_log_success_event which fires
|
||||
for all successful requests (both streaming and non-streaming).
|
||||
"""
|
||||
try:
|
||||
from litellm.litellm_core_utils.litellm_logging import (
|
||||
StandardLoggingPayloadSetup,
|
||||
)
|
||||
|
||||
if self._should_skip_metrics_for_invalid_key(
|
||||
user_api_key_dict=user_api_key_dict
|
||||
):
|
||||
return
|
||||
|
||||
_metadata = data.get("metadata", {}) or {}
|
||||
enum_values = UserAPIKeyLabelValues(
|
||||
end_user=user_api_key_dict.end_user_id,
|
||||
hashed_api_key=user_api_key_dict.api_key,
|
||||
api_key_alias=user_api_key_dict.key_alias,
|
||||
requested_model=data.get("model", ""),
|
||||
team=user_api_key_dict.team_id,
|
||||
team_alias=user_api_key_dict.team_alias,
|
||||
user=user_api_key_dict.user_id,
|
||||
user_email=user_api_key_dict.user_email,
|
||||
status_code="200",
|
||||
route=user_api_key_dict.request_route,
|
||||
tags=StandardLoggingPayloadSetup._get_request_tags(
|
||||
litellm_params=data,
|
||||
proxy_server_request=data.get("proxy_server_request", {}),
|
||||
),
|
||||
client_ip=_metadata.get("requester_ip_address"),
|
||||
user_agent=_metadata.get("user_agent"),
|
||||
)
|
||||
_labels = prometheus_label_factory(
|
||||
supported_enum_labels=self.get_labels_for_metric(
|
||||
metric_name="litellm_proxy_total_requests_metric"
|
||||
),
|
||||
enum_values=enum_values,
|
||||
)
|
||||
self.litellm_proxy_total_requests_metric.labels(**_labels).inc()
|
||||
|
||||
except Exception as e:
|
||||
verbose_logger.exception(
|
||||
"prometheus Layer Error(): Exception occured - {}".format(str(e))
|
||||
)
|
||||
pass
|
||||
pass
|
||||
|
||||
def _safe_get(self, obj: Any, key: str, default: Any = None) -> Any:
|
||||
"""Get value from dict or Pydantic model."""
|
||||
|
|
@ -2004,7 +1964,7 @@ class PrometheusLogger(CustomLogger):
|
|||
|
||||
api_base = standard_logging_payload["api_base"]
|
||||
_litellm_params = request_kwargs.get("litellm_params", {}) or {}
|
||||
_metadata = _litellm_params.get("metadata", {})
|
||||
_metadata = get_litellm_metadata_from_kwargs(request_kwargs)
|
||||
litellm_model_name = request_kwargs.get("model", None)
|
||||
llm_provider = _litellm_params.get("custom_llm_provider", None)
|
||||
_model_info = _metadata.get("model_info") or {}
|
||||
|
|
@ -2220,7 +2180,8 @@ class PrometheusLogger(CustomLogger):
|
|||
original_model_group,
|
||||
kwargs,
|
||||
)
|
||||
_metadata = kwargs.get("metadata", {})
|
||||
_metadata_key = get_metadata_variable_name_from_kwargs(kwargs)
|
||||
_metadata = kwargs.get(_metadata_key) or {}
|
||||
standard_metadata: StandardLoggingMetadata = (
|
||||
StandardLoggingPayloadSetup.get_standard_logging_metadata(
|
||||
metadata=_metadata
|
||||
|
|
@ -2265,7 +2226,8 @@ class PrometheusLogger(CustomLogger):
|
|||
kwargs,
|
||||
)
|
||||
_new_model = kwargs.get("model")
|
||||
_metadata = kwargs.get("metadata", {})
|
||||
_metadata_key = get_metadata_variable_name_from_kwargs(kwargs)
|
||||
_metadata = kwargs.get(_metadata_key) or {}
|
||||
_tags = cast(List[str], kwargs.get("tags") or [])
|
||||
standard_metadata: StandardLoggingMetadata = (
|
||||
StandardLoggingPayloadSetup.get_standard_logging_metadata(
|
||||
|
|
|
|||
|
|
@ -1335,7 +1335,11 @@ class Logging(LiteLLMLoggingBaseClass):
|
|||
)
|
||||
|
||||
# Store additional costs if provided (free-form dict for extensibility)
|
||||
if additional_costs and isinstance(additional_costs, dict) and len(additional_costs) > 0:
|
||||
if (
|
||||
additional_costs
|
||||
and isinstance(additional_costs, dict)
|
||||
and len(additional_costs) > 0
|
||||
):
|
||||
self.cost_breakdown["additional_costs"] = additional_costs
|
||||
|
||||
# Store discount information if provided
|
||||
|
|
@ -4519,13 +4523,19 @@ class StandardLoggingPayloadSetup:
|
|||
requester_custom_headers=None,
|
||||
cold_storage_object_key=None,
|
||||
user_api_key_auth_metadata=None,
|
||||
team_alias=None,
|
||||
team_id=None,
|
||||
)
|
||||
if isinstance(metadata, dict):
|
||||
for key in metadata.keys() & _STANDARD_LOGGING_METADATA_KEYS:
|
||||
clean_metadata[key] = metadata[key] # type: ignore
|
||||
|
||||
user_api_key = metadata.get("user_api_key")
|
||||
if user_api_key and isinstance(user_api_key, str) and is_valid_sha256_hash(user_api_key):
|
||||
if (
|
||||
user_api_key
|
||||
and isinstance(user_api_key, str)
|
||||
and is_valid_sha256_hash(user_api_key)
|
||||
):
|
||||
clean_metadata["user_api_key_hash"] = user_api_key
|
||||
_potential_requester_metadata = metadata.get(
|
||||
"metadata", None
|
||||
|
|
@ -5279,6 +5289,8 @@ def get_standard_logging_metadata(
|
|||
user_api_key_request_route=None,
|
||||
cold_storage_object_key=None,
|
||||
user_api_key_auth_metadata=None,
|
||||
team_alias=None,
|
||||
team_id=None,
|
||||
)
|
||||
if isinstance(metadata, dict):
|
||||
# Update the clean_metadata with values from input metadata that match StandardLoggingMetadata fields
|
||||
|
|
|
|||
|
|
@ -546,7 +546,11 @@ def convert_to_model_response_object( # noqa: PLR0915
|
|||
message = litellm.Message(content=json_mode_content_str)
|
||||
finish_reason = "stop"
|
||||
if message is None:
|
||||
provider_specific_fields = {}
|
||||
# Preserve provider_specific_fields if already present
|
||||
# in the response (e.g. from proxy passthrough)
|
||||
provider_specific_fields = dict(
|
||||
choice["message"].get("provider_specific_fields", None) or {}
|
||||
)
|
||||
message_keys = Message.model_fields.keys()
|
||||
for field in choice["message"].keys():
|
||||
if field not in message_keys:
|
||||
|
|
|
|||
|
|
@ -2018,6 +2018,235 @@ def anthropic_process_openai_file_message(
|
|||
)
|
||||
|
||||
|
||||
def _sanitize_empty_text_content(
|
||||
message: AllMessageValues,
|
||||
) -> AllMessageValues:
|
||||
"""
|
||||
Case C: Sanitize empty text content
|
||||
- Replace empty or whitespace-only text content with a placeholder message.
|
||||
|
||||
Returns:
|
||||
The message with sanitized content if needed, otherwise the original message
|
||||
"""
|
||||
if message.get("role") in ["user", "assistant"]:
|
||||
content = message.get("content")
|
||||
if isinstance(content, str):
|
||||
if not content or not content.strip():
|
||||
message = cast(AllMessageValues, dict(message)) # Make a copy
|
||||
message["content"] = "[System: Empty message content sanitised to satisfy protocol]"
|
||||
verbose_logger.debug(
|
||||
f"_sanitize_empty_text_content: Replaced empty text content in {message.get('role')} message"
|
||||
)
|
||||
return message
|
||||
|
||||
|
||||
def _add_missing_tool_results( # noqa: PLR0915
|
||||
current_message: AllMessageValues,
|
||||
messages: List[AllMessageValues],
|
||||
current_index: int,
|
||||
) -> Tuple[List[AllMessageValues], int]:
|
||||
"""
|
||||
Case A: Missing tool_result for tool_use (orphaned tool calls)
|
||||
- If an assistant message has tool_calls but no corresponding tool result follows,
|
||||
add a dummy tool result message indicating the user did not provide the result.
|
||||
|
||||
Returns:
|
||||
A tuple of:
|
||||
- List containing the assistant message, followed by existing tool results,
|
||||
followed by any dummy tool results needed
|
||||
- Number of original messages consumed (to adjust iteration index)
|
||||
"""
|
||||
result_messages: List[AllMessageValues] = []
|
||||
tool_calls = current_message.get("tool_calls")
|
||||
|
||||
if not tool_calls or len(cast(list, tool_calls)) == 0:
|
||||
return ([current_message], 0)
|
||||
|
||||
# Collect all tool_call_ids from this assistant message
|
||||
expected_tool_call_ids = set()
|
||||
for tool_call in cast(list, tool_calls):
|
||||
tool_call_id = None
|
||||
if isinstance(tool_call, dict):
|
||||
tool_call_id = tool_call.get("id")
|
||||
else:
|
||||
tool_call_id = getattr(tool_call, "id", None)
|
||||
if tool_call_id:
|
||||
expected_tool_call_ids.add(tool_call_id)
|
||||
|
||||
# Collect actual tool result messages that follow this assistant message
|
||||
found_tool_call_ids = set()
|
||||
actual_tool_results: List[AllMessageValues] = []
|
||||
j = current_index + 1
|
||||
|
||||
while j < len(messages):
|
||||
next_msg = messages[j]
|
||||
next_role = next_msg.get("role")
|
||||
|
||||
if next_role == "assistant":
|
||||
break
|
||||
|
||||
if next_role in ["tool", "function"]:
|
||||
tool_call_id = next_msg.get("tool_call_id")
|
||||
if tool_call_id and tool_call_id in expected_tool_call_ids:
|
||||
found_tool_call_ids.add(tool_call_id)
|
||||
actual_tool_results.append(next_msg)
|
||||
|
||||
j += 1
|
||||
|
||||
# Find missing tool results
|
||||
missing_tool_call_ids = expected_tool_call_ids - found_tool_call_ids
|
||||
|
||||
if missing_tool_call_ids:
|
||||
verbose_logger.debug(
|
||||
f"_add_missing_tool_results: Found {len(missing_tool_call_ids)} orphaned tool calls. Adding dummy tool results."
|
||||
)
|
||||
|
||||
result_messages.append(current_message)
|
||||
|
||||
# Add existing tool results FIRST
|
||||
result_messages.extend(actual_tool_results)
|
||||
|
||||
# Then add dummy tool results for missing ones
|
||||
for tool_call_id in missing_tool_call_ids:
|
||||
tool_name = "unknown_tool"
|
||||
for tool_call in cast(list, tool_calls):
|
||||
tc_id = None
|
||||
if isinstance(tool_call, dict):
|
||||
tc_id = tool_call.get("id")
|
||||
else:
|
||||
tc_id = getattr(tool_call, "id", None)
|
||||
|
||||
if tc_id == tool_call_id:
|
||||
if isinstance(tool_call, dict):
|
||||
function = tool_call.get("function", {})
|
||||
if isinstance(function, dict):
|
||||
tool_name = function.get("name", "unknown_tool")
|
||||
else:
|
||||
tool_name = getattr(function, "name", "unknown_tool")
|
||||
else:
|
||||
function = getattr(tool_call, "function", None)
|
||||
if function:
|
||||
tool_name = getattr(function, "name", "unknown_tool")
|
||||
break
|
||||
|
||||
dummy_tool_result: ChatCompletionToolMessage = {
|
||||
"role": "tool",
|
||||
"tool_call_id": tool_call_id,
|
||||
"content": f"[System: Tool execution skipped/interrupted by user. No result provided for tool '{tool_name}'.]",
|
||||
}
|
||||
result_messages.append(dummy_tool_result)
|
||||
|
||||
# Return the messages and the number of original messages to skip
|
||||
return (result_messages, len(actual_tool_results))
|
||||
|
||||
return ([current_message], 0)
|
||||
|
||||
|
||||
def _is_orphaned_tool_result(
|
||||
current_message: AllMessageValues,
|
||||
sanitized_messages: List[AllMessageValues],
|
||||
) -> bool:
|
||||
"""
|
||||
Case B: Orphaned tool_result (unexpected result)
|
||||
- Check if a tool message references a tool_call_id that doesn't exist in the previous
|
||||
assistant message.
|
||||
|
||||
Returns:
|
||||
True if this is an orphaned tool result that should be removed, False otherwise
|
||||
"""
|
||||
if current_message.get("role") not in ["tool", "function"]:
|
||||
return False
|
||||
|
||||
tool_call_id = current_message.get("tool_call_id")
|
||||
|
||||
if not tool_call_id:
|
||||
return False
|
||||
|
||||
# Look back to find the most recent assistant message with tool_calls
|
||||
found_matching_tool_call = False
|
||||
|
||||
for j in range(len(sanitized_messages) - 1, -1, -1):
|
||||
prev_msg = sanitized_messages[j]
|
||||
if prev_msg.get("role") == "assistant":
|
||||
tool_calls = prev_msg.get("tool_calls")
|
||||
if tool_calls:
|
||||
for tool_call in cast(list, tool_calls):
|
||||
tc_id = None
|
||||
if isinstance(tool_call, dict):
|
||||
tc_id = tool_call.get("id")
|
||||
else:
|
||||
tc_id = getattr(tool_call, "id", None)
|
||||
|
||||
if tc_id == tool_call_id:
|
||||
found_matching_tool_call = True
|
||||
break
|
||||
|
||||
break
|
||||
|
||||
if not found_matching_tool_call:
|
||||
verbose_logger.debug(
|
||||
"_is_orphaned_tool_result: Found orphaned tool result with redacted tool_call_id"
|
||||
)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def sanitize_messages_for_tool_calling(
|
||||
messages: List[AllMessageValues],
|
||||
) -> List[AllMessageValues]:
|
||||
"""
|
||||
Sanitize messages for tool calling to handle common issues when modify_params=True:
|
||||
|
||||
Case A: Missing tool_result for tool_use (orphaned tool calls)
|
||||
- If an assistant message has tool_calls but no corresponding tool result follows,
|
||||
add a dummy tool result message indicating the user did not provide the result.
|
||||
|
||||
Case B: Orphaned tool_result (unexpected result)
|
||||
- If a tool message references a tool_call_id that doesn't exist in the previous
|
||||
assistant message, remove that tool message.
|
||||
|
||||
Case C: Empty text content
|
||||
- Replace empty or whitespace-only text content with a placeholder message.
|
||||
|
||||
This function operates on OpenAI format messages before they are converted to
|
||||
provider-specific formats.
|
||||
"""
|
||||
if not litellm.modify_params:
|
||||
return messages
|
||||
|
||||
sanitized_messages: List[AllMessageValues] = []
|
||||
i = 0
|
||||
|
||||
while i < len(messages):
|
||||
current_message = messages[i]
|
||||
|
||||
# Case C: Sanitize empty text content
|
||||
current_message = _sanitize_empty_text_content(current_message)
|
||||
|
||||
# Case A: Check if assistant message has tool_calls without following tool results
|
||||
if current_message.get("role") == "assistant":
|
||||
result_messages, messages_consumed = _add_missing_tool_results(current_message, messages, i)
|
||||
|
||||
# If dummy tool results were added, extend sanitized_messages and skip consumed messages
|
||||
if len(result_messages) > 1:
|
||||
sanitized_messages.extend(result_messages)
|
||||
# Skip the assistant message and any actual tool results that were included
|
||||
i += 1 + messages_consumed
|
||||
continue
|
||||
|
||||
# Case B: Check for orphaned tool results
|
||||
if _is_orphaned_tool_result(current_message, sanitized_messages):
|
||||
i += 1
|
||||
continue # Skip this orphaned tool result
|
||||
|
||||
# Add the message to sanitized list
|
||||
sanitized_messages.append(current_message)
|
||||
i += 1
|
||||
|
||||
return sanitized_messages
|
||||
|
||||
|
||||
def anthropic_messages_pt( # noqa: PLR0915
|
||||
messages: List[AllMessageValues],
|
||||
model: str,
|
||||
|
|
@ -2037,6 +2266,9 @@ def anthropic_messages_pt( # noqa: PLR0915
|
|||
5. System messages are a separate param to the Messages API
|
||||
6. Ensure we only accept role, content. (message.name is not supported)
|
||||
"""
|
||||
# Sanitize messages for tool calling issues when modify_params=True
|
||||
messages = sanitize_messages_for_tool_calling(messages)
|
||||
|
||||
# add role=tool support to allow function call result/error submission
|
||||
user_message_types = {"user", "tool", "function"}
|
||||
# reformat messages to ensure user/assistant are alternating, if there's either 2 consecutive 'user' messages or 2 consecutive 'assistant' message, merge them.
|
||||
|
|
|
|||
|
|
@ -172,8 +172,8 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
|
|||
|
||||
@staticmethod
|
||||
def _is_claude_opus_4_6(model: str) -> bool:
|
||||
"""Check if the model is Claude Opus 4.5."""
|
||||
return "opus-4-6" in model.lower() or "opus_4_6" in model.lower()
|
||||
"""Check if the model is Claude Opus 4.5 or Sonnet 4.6."""
|
||||
return "opus-4-6" in model.lower() or "opus_4_6" in model.lower() or "sonnet-4-6" in model.lower() or "sonnet_4_6" in model.lower() or "sonnet-4.6" in model.lower()
|
||||
|
||||
def get_supported_openai_params(self, model: str):
|
||||
params = [
|
||||
|
|
@ -881,6 +881,10 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
|
|||
"opus-4-5",
|
||||
"opus-4.6",
|
||||
"opus-4-6",
|
||||
"sonnet-4.6",
|
||||
"sonnet-4-6",
|
||||
"sonnet_4.6",
|
||||
"sonnet_4_6",
|
||||
}
|
||||
):
|
||||
_output_format = (
|
||||
|
|
|
|||
|
|
@ -22,6 +22,15 @@ from litellm.types.llms.anthropic import (
|
|||
from litellm.types.llms.openai import AllMessageValues
|
||||
|
||||
|
||||
def is_anthropic_oauth_key(value: Optional[str]) -> bool:
|
||||
"""Check if a value contains an Anthropic OAuth token (sk-ant-oat*)."""
|
||||
if value is None:
|
||||
return False
|
||||
# Handle both raw token and "Bearer <token>" format
|
||||
if value.startswith("Bearer "):
|
||||
value = value[7:]
|
||||
return value.startswith(ANTHROPIC_OAUTH_TOKEN_PREFIX)
|
||||
|
||||
def optionally_handle_anthropic_oauth(
|
||||
headers: dict, api_key: Optional[str]
|
||||
) -> tuple[dict, Optional[str]]:
|
||||
|
|
|
|||
|
|
@ -299,6 +299,26 @@ class LiteLLMAnthropicMessagesAdapter:
|
|||
"""
|
||||
return ["messages", "metadata", "system", "tool_choice", "tools", "thinking", "output_format"]
|
||||
|
||||
def _is_web_search_tool(self, tool: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
Check if a tool is an Anthropic web search tool.
|
||||
|
||||
Anthropic web search tools have:
|
||||
- type starting with "web_search" (e.g., "web_search_20260209")
|
||||
- name = "web_search"
|
||||
|
||||
Args:
|
||||
tool: Tool definition dict
|
||||
|
||||
Returns:
|
||||
True if this is a web search tool
|
||||
"""
|
||||
tool_type = tool.get("type", "")
|
||||
tool_name = tool.get("name", "")
|
||||
return (
|
||||
isinstance(tool_type, str) and tool_type.startswith("web_search")
|
||||
) or tool_name == "web_search"
|
||||
|
||||
def translate_anthropic_messages_to_openai( # noqa: PLR0915
|
||||
self,
|
||||
messages: List[
|
||||
|
|
@ -872,10 +892,25 @@ class LiteLLMAnthropicMessagesAdapter:
|
|||
if "tools" in anthropic_message_request:
|
||||
tools = anthropic_message_request["tools"]
|
||||
if tools:
|
||||
new_kwargs["tools"], tool_name_mapping = self.translate_anthropic_tools_to_openai(
|
||||
tools=cast(List[AllAnthropicToolsValues], tools),
|
||||
model=new_kwargs.get("model"),
|
||||
)
|
||||
# Separate web search tools from regular tools
|
||||
web_search_tools = []
|
||||
regular_tools = []
|
||||
for tool in tools:
|
||||
if self._is_web_search_tool(cast(Dict[str, Any], tool)):
|
||||
web_search_tools.append(tool)
|
||||
else:
|
||||
regular_tools.append(tool)
|
||||
|
||||
# If web search tools are present, add web_search_options parameter
|
||||
if web_search_tools:
|
||||
new_kwargs["web_search_options"] = {} # type: ignore
|
||||
|
||||
# Only translate regular tools (non-web-search)
|
||||
if regular_tools:
|
||||
new_kwargs["tools"], tool_name_mapping = self.translate_anthropic_tools_to_openai(
|
||||
tools=cast(List[AllAnthropicToolsValues], regular_tools),
|
||||
model=new_kwargs.get("model"),
|
||||
)
|
||||
|
||||
## CONVERT THINKING
|
||||
if "thinking" in anthropic_message_request:
|
||||
|
|
|
|||
|
|
@ -384,6 +384,14 @@ class BaseAWSLLM:
|
|||
model_id = BaseAWSLLM._get_model_id_from_model_with_spec(
|
||||
model_id, spec="moonshot"
|
||||
)
|
||||
elif "nova-2/" in model_id:
|
||||
model_id = BaseAWSLLM._get_model_id_from_model_with_spec(
|
||||
model_id, spec="nova-2"
|
||||
)
|
||||
elif "nova/" in model_id:
|
||||
model_id = BaseAWSLLM._get_model_id_from_model_with_spec(
|
||||
model_id, spec="nova"
|
||||
)
|
||||
return model_id
|
||||
|
||||
@staticmethod
|
||||
|
|
|
|||
|
|
@ -272,7 +272,18 @@ class BedrockConverseLLM(BaseAWSLLM):
|
|||
if unencoded_model_id is not None:
|
||||
modelId = self.encode_model_id(model_id=unencoded_model_id)
|
||||
else:
|
||||
modelId = self.encode_model_id(model_id=model)
|
||||
# Strip nova spec prefixes before encoding model ID for API URL
|
||||
_model_for_id = model
|
||||
_stripped = _model_for_id
|
||||
for rp in ["bedrock/converse/", "bedrock/", "converse/"]:
|
||||
if _stripped.startswith(rp):
|
||||
_stripped = _stripped[len(rp):]
|
||||
break
|
||||
for _nova_prefix in ["nova-2/", "nova/"]:
|
||||
if _stripped.startswith(_nova_prefix):
|
||||
_model_for_id = _model_for_id.replace(_nova_prefix, "", 1)
|
||||
break
|
||||
modelId = self.encode_model_id(model_id=_model_for_id)
|
||||
|
||||
fake_stream = litellm.AmazonConverseConfig().should_fake_stream(
|
||||
fake_stream=fake_stream,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ Translating between OpenAI's `/chat/completion` format and Amazon's `/converse`
|
|||
"""
|
||||
|
||||
import copy
|
||||
import json
|
||||
import time
|
||||
import types
|
||||
from typing import List, Literal, Optional, Tuple, Union, cast, overload
|
||||
|
|
@ -85,9 +86,37 @@ BEDROCK_COMPUTER_USE_TOOLS = [
|
|||
UNSUPPORTED_BEDROCK_CONVERSE_BETA_PATTERNS = [
|
||||
"advanced-tool-use", # Bedrock Converse doesn't support advanced-tool-use beta headers
|
||||
"prompt-caching", # Prompt caching not supported in Converse API
|
||||
"compact-2026-01-12", # The compact beta feature is not currently supported on the Converse and ConverseStream APIs
|
||||
"compact-2026-01-12", # The compact beta feature is not currently supported on the Converse and ConverseStream APIs
|
||||
]
|
||||
|
||||
# Models that support Bedrock's native structured outputs API (outputConfig.textFormat)
|
||||
# Uses substring matching against the Bedrock model ID
|
||||
# Ref: https://docs.aws.amazon.com/bedrock/latest/userguide/structured-output.html
|
||||
BEDROCK_NATIVE_STRUCTURED_OUTPUT_MODELS = {
|
||||
# Anthropic Claude 4.5+
|
||||
"claude-haiku-4-5",
|
||||
"claude-sonnet-4-5",
|
||||
"claude-opus-4-5",
|
||||
"claude-opus-4-6",
|
||||
# Qwen3
|
||||
"qwen3",
|
||||
# DeepSeek
|
||||
"deepseek-v3.1",
|
||||
# Gemma 3
|
||||
"gemma-3",
|
||||
# MiniMax
|
||||
"minimax-m2",
|
||||
# Mistral (magistral-small excluded: broken constrained decoding on Bedrock)
|
||||
"ministral",
|
||||
"mistral-large-3",
|
||||
"voxtral",
|
||||
# Moonshot
|
||||
"kimi-k2",
|
||||
# NVIDIA
|
||||
"nemotron-nano",
|
||||
# OpenAI (gpt-oss excluded: broken constrained decoding, works via tool-call fallback)
|
||||
}
|
||||
|
||||
|
||||
class AmazonConverseConfig(BaseConfig):
|
||||
"""
|
||||
|
|
@ -270,45 +299,56 @@ class AmazonConverseConfig(BaseConfig):
|
|||
llm_provider="bedrock",
|
||||
)
|
||||
|
||||
def _is_nova_lite_2_model(self, model: str) -> bool:
|
||||
def _is_nova_2_model(self, model: str) -> bool:
|
||||
"""
|
||||
Check if the model is a Nova Lite 2 model that supports reasoningConfig.
|
||||
Check if the model is a Nova 2 model that supports reasoningConfig.
|
||||
|
||||
Nova Lite 2 models use a different reasoning configuration structure compared to
|
||||
Nova 2 models use a different reasoning configuration structure compared to
|
||||
Anthropic's thinking parameter and GPT-OSS's reasoning_effort parameter.
|
||||
|
||||
Supported models:
|
||||
- amazon.nova-2-lite-v1:0
|
||||
- amazon.nova-2-pro-preview-20251202-v1:0
|
||||
- us.amazon.nova-2-lite-v1:0
|
||||
- eu.amazon.nova-2-lite-v1:0
|
||||
- apac.amazon.nova-2-lite-v1:0
|
||||
- (and other regional variants)
|
||||
|
||||
Args:
|
||||
model: The model identifier
|
||||
|
||||
Returns:
|
||||
True if the model is a Nova Lite 2 model, False otherwise
|
||||
True if the model is a Nova 2 model, False otherwise
|
||||
|
||||
Examples:
|
||||
>>> config = AmazonConverseConfig()
|
||||
>>> config._is_nova_lite_2_model("amazon.nova-2-lite-v1:0")
|
||||
>>> config._is_nova_2_model("amazon.nova-2-lite-v1:0")
|
||||
True
|
||||
>>> config._is_nova_lite_2_model("us.amazon.nova-2-lite-v1:0")
|
||||
>>> config._is_nova_2_model("us.amazon.nova-2-lite-v1:0")
|
||||
True
|
||||
>>> config._is_nova_lite_2_model("amazon.nova-pro-1-5-v1:0")
|
||||
>>> config._is_nova_2_model("us.amazon.nova-2-pro-preview-20251202-v1:0")
|
||||
True
|
||||
>>> config._is_nova_2_model("amazon.nova-pro-1-5-v1:0")
|
||||
False
|
||||
>>> config._is_nova_lite_2_model("amazon.nova-pro-v1:0")
|
||||
>>> config._is_nova_2_model("amazon.nova-pro-v1:0")
|
||||
False
|
||||
"""
|
||||
# Remove regional prefix if present (us., eu., apac.)
|
||||
# Remove provider routing prefix if present (bedrock/converse/, bedrock/, converse/)
|
||||
model_without_region = model
|
||||
for prefix in ["us.", "eu.", "apac."]:
|
||||
if model.startswith(prefix):
|
||||
model_without_region = model[len(prefix) :]
|
||||
for routing_prefix in ["bedrock/converse/", "bedrock/", "converse/"]:
|
||||
if model_without_region.startswith(routing_prefix):
|
||||
model_without_region = model_without_region[len(routing_prefix) :]
|
||||
break
|
||||
|
||||
# Check if the model is specifically Nova Lite 2
|
||||
return "nova-2-lite" in model_without_region
|
||||
# Remove regional prefix if present (us., eu., apac.)
|
||||
for prefix in ["us.", "eu.", "apac."]:
|
||||
if model_without_region.startswith(prefix):
|
||||
model_without_region = model_without_region[len(prefix) :]
|
||||
break
|
||||
|
||||
# Check if the model is a Nova 2 model (matches nova-2-lite, nova-2-pro, etc.)
|
||||
# Also check for nova-2/ spec prefix for imported models
|
||||
return model_without_region.startswith("amazon.nova-2-") or model_without_region.startswith("nova-2/")
|
||||
|
||||
def _map_web_search_options(
|
||||
self, web_search_options: dict, model: str
|
||||
|
|
@ -396,7 +436,7 @@ class AmazonConverseConfig(BaseConfig):
|
|||
|
||||
Different model families handle reasoning effort differently:
|
||||
- GPT-OSS models: Keep reasoning_effort as-is (passed to additionalModelRequestFields)
|
||||
- Nova Lite 2 models: Transform to reasoningConfig structure
|
||||
- Nova 2 models: Transform to reasoningConfig structure
|
||||
- Other models (Anthropic, etc.): Convert to thinking parameter
|
||||
|
||||
Args:
|
||||
|
|
@ -425,8 +465,8 @@ class AmazonConverseConfig(BaseConfig):
|
|||
# GPT-OSS models: keep reasoning_effort as-is
|
||||
# It will be passed through to additionalModelRequestFields
|
||||
optional_params["reasoning_effort"] = reasoning_effort
|
||||
elif self._is_nova_lite_2_model(model):
|
||||
# Nova Lite 2 models: transform to reasoningConfig
|
||||
elif self._is_nova_2_model(model):
|
||||
# Nova 2 models: transform to reasoningConfig
|
||||
reasoning_config = self._transform_reasoning_effort_to_reasoning_config(
|
||||
reasoning_effort
|
||||
)
|
||||
|
|
@ -480,6 +520,9 @@ class AmazonConverseConfig(BaseConfig):
|
|||
supported_params.append("tool_choice")
|
||||
supported_params.append("thinking")
|
||||
supported_params.append("reasoning_effort")
|
||||
# For nova imported models, also add web_search_options
|
||||
if "nova" in model.lower():
|
||||
supported_params.append("web_search_options")
|
||||
return supported_params
|
||||
|
||||
## Filter out 'cross-region' from model name
|
||||
|
|
@ -514,8 +557,8 @@ class AmazonConverseConfig(BaseConfig):
|
|||
|
||||
if "gpt-oss" in model:
|
||||
supported_params.append("reasoning_effort")
|
||||
elif self._is_nova_lite_2_model(model):
|
||||
# Nova Lite 2 models support reasoning_effort (transformed to reasoningConfig)
|
||||
elif self._is_nova_2_model(model):
|
||||
# Nova 2 models support reasoning_effort (transformed to reasoningConfig)
|
||||
# These models use a different reasoning structure than Anthropic's thinking parameter
|
||||
supported_params.append("reasoning_effort")
|
||||
elif (
|
||||
|
|
@ -714,6 +757,100 @@ class AmazonConverseConfig(BaseConfig):
|
|||
)
|
||||
return _tool
|
||||
|
||||
@staticmethod
|
||||
def _supports_native_structured_outputs(model: str) -> bool:
|
||||
"""Check if the Bedrock model supports native structured outputs (outputConfig.textFormat)."""
|
||||
return any(
|
||||
substring in model
|
||||
for substring in BEDROCK_NATIVE_STRUCTURED_OUTPUT_MODELS
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _add_additional_properties_to_schema(schema: dict) -> dict:
|
||||
"""
|
||||
Recursively ensure all object types in a JSON schema have
|
||||
``"additionalProperties": false``.
|
||||
|
||||
Bedrock's native structured-outputs API requires this field to be
|
||||
explicitly set on every object node, otherwise it returns a
|
||||
validation error.
|
||||
"""
|
||||
if not isinstance(schema, dict):
|
||||
return schema
|
||||
|
||||
result = dict(schema)
|
||||
|
||||
if result.get("type") == "object" and "additionalProperties" not in result:
|
||||
result["additionalProperties"] = False
|
||||
|
||||
# Recurse into nested schemas
|
||||
if "properties" in result and isinstance(result["properties"], dict):
|
||||
result["properties"] = {
|
||||
k: AmazonConverseConfig._add_additional_properties_to_schema(v)
|
||||
for k, v in result["properties"].items()
|
||||
}
|
||||
if "items" in result and isinstance(result["items"], dict):
|
||||
result["items"] = AmazonConverseConfig._add_additional_properties_to_schema(
|
||||
result["items"]
|
||||
)
|
||||
for defs_key in ("$defs", "definitions"):
|
||||
if defs_key in result and isinstance(result[defs_key], dict):
|
||||
result[defs_key] = {
|
||||
k: AmazonConverseConfig._add_additional_properties_to_schema(v)
|
||||
for k, v in result[defs_key].items()
|
||||
}
|
||||
for key in ("anyOf", "allOf", "oneOf"):
|
||||
if key in result and isinstance(result[key], list):
|
||||
result[key] = [
|
||||
AmazonConverseConfig._add_additional_properties_to_schema(item)
|
||||
for item in result[key]
|
||||
]
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def _create_output_config_for_response_format(
|
||||
json_schema: Optional[dict] = None,
|
||||
name: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
) -> "OutputConfigBlock":
|
||||
"""
|
||||
Build an outputConfig block for Bedrock's native structured outputs API.
|
||||
|
||||
The Converse API expects:
|
||||
{
|
||||
"outputConfig": {
|
||||
"textFormat": {
|
||||
"type": "json_schema",
|
||||
"structure": {
|
||||
"jsonSchema": {
|
||||
"schema": "<json-string>",
|
||||
"name": "optional",
|
||||
"description": "optional"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
if json_schema is not None:
|
||||
json_schema = AmazonConverseConfig._add_additional_properties_to_schema(
|
||||
json_schema
|
||||
)
|
||||
schema_str = json.dumps(json_schema) if json_schema is not None else "{}"
|
||||
json_schema_def: JsonSchemaDefinition = {"schema": schema_str}
|
||||
if name is not None:
|
||||
json_schema_def["name"] = name
|
||||
if description is not None:
|
||||
json_schema_def["description"] = description
|
||||
|
||||
return OutputConfigBlock(
|
||||
textFormat=OutputFormat(
|
||||
type="json_schema",
|
||||
structure=OutputFormatStructure(jsonSchema=json_schema_def),
|
||||
)
|
||||
)
|
||||
|
||||
def _apply_tool_call_transformation(
|
||||
self,
|
||||
tools: List[OpenAIChatCompletionToolParam],
|
||||
|
|
@ -806,8 +943,8 @@ class AmazonConverseConfig(BaseConfig):
|
|||
)
|
||||
|
||||
# Only update thinking tokens for non-GPT-OSS models and non-Nova-Lite-2 models
|
||||
# Nova Lite 2 handles token budgeting differently through reasoningConfig
|
||||
if "gpt-oss" not in model and not self._is_nova_lite_2_model(model):
|
||||
# Nova 2 handles token budgeting differently through reasoningConfig
|
||||
if "gpt-oss" not in model and not self._is_nova_2_model(model):
|
||||
self.update_optional_params_with_thinking_tokens(
|
||||
non_default_params=non_default_params, optional_params=optional_params
|
||||
)
|
||||
|
|
@ -843,45 +980,53 @@ class AmazonConverseConfig(BaseConfig):
|
|||
return optional_params
|
||||
|
||||
json_schema: Optional[dict] = None
|
||||
name: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
if "response_schema" in value:
|
||||
json_schema = value["response_schema"]
|
||||
elif "json_schema" in value:
|
||||
json_schema = value["json_schema"]["schema"]
|
||||
name = value["json_schema"].get("name")
|
||||
description = value["json_schema"].get("description")
|
||||
|
||||
if "type" in value and value["type"] == "text":
|
||||
return optional_params
|
||||
|
||||
"""
|
||||
Follow similar approach to anthropic - translate to a single tool call.
|
||||
|
||||
When using tools in this way: - https://docs.anthropic.com/en/docs/build-with-claude/tool-use#json-mode
|
||||
- You usually want to provide a single tool
|
||||
- You should set tool_choice (see Forcing tool use) to instruct the model to explicitly use that tool
|
||||
- Remember that the model will pass the input to the tool, so the name of the tool and description should be from the model’s perspective.
|
||||
"""
|
||||
_tool = self._create_json_tool_call_for_response_format(
|
||||
json_schema=json_schema,
|
||||
description=description,
|
||||
)
|
||||
optional_params = self._add_tools_to_optional_params(
|
||||
optional_params=optional_params, tools=[_tool]
|
||||
)
|
||||
|
||||
if (
|
||||
litellm.utils.supports_tool_choice(
|
||||
model=model, custom_llm_provider=self.custom_llm_provider
|
||||
if self._supports_native_structured_outputs(model) and json_schema is not None:
|
||||
# Use Bedrock's native structured outputs API (outputConfig.textFormat)
|
||||
# No synthetic tool injection, no fake_stream needed.
|
||||
# Requires an explicit schema — json_object with no schema falls through
|
||||
# to the tool-call path below.
|
||||
output_config = self._create_output_config_for_response_format(
|
||||
json_schema=json_schema,
|
||||
name=name,
|
||||
description=description,
|
||||
)
|
||||
and not is_thinking_enabled
|
||||
):
|
||||
optional_params["tool_choice"] = ToolChoiceValuesBlock(
|
||||
tool=SpecificToolChoiceBlock(name=RESPONSE_FORMAT_TOOL_NAME)
|
||||
optional_params["outputConfig"] = output_config
|
||||
else:
|
||||
# Fallback: translate to a synthetic tool call
|
||||
# https://docs.anthropic.com/en/docs/build-with-claude/tool-use#json-mode
|
||||
_tool = self._create_json_tool_call_for_response_format(
|
||||
json_schema=json_schema,
|
||||
description=description,
|
||||
)
|
||||
optional_params = self._add_tools_to_optional_params(
|
||||
optional_params=optional_params, tools=[_tool]
|
||||
)
|
||||
|
||||
if (
|
||||
litellm.utils.supports_tool_choice(
|
||||
model=model, custom_llm_provider=self.custom_llm_provider
|
||||
)
|
||||
and not is_thinking_enabled
|
||||
):
|
||||
optional_params["tool_choice"] = ToolChoiceValuesBlock(
|
||||
tool=SpecificToolChoiceBlock(name=RESPONSE_FORMAT_TOOL_NAME)
|
||||
)
|
||||
if non_default_params.get("stream", False) is True:
|
||||
optional_params["fake_stream"] = True
|
||||
|
||||
optional_params["json_mode"] = True
|
||||
if non_default_params.get("stream", False) is True:
|
||||
optional_params["fake_stream"] = True
|
||||
|
||||
return optional_params
|
||||
|
||||
def update_optional_params_with_thinking_tokens(
|
||||
|
|
@ -1024,7 +1169,7 @@ class AmazonConverseConfig(BaseConfig):
|
|||
|
||||
def _prepare_request_params(
|
||||
self, optional_params: dict, model: str
|
||||
) -> Tuple[dict, dict, dict]:
|
||||
) -> Tuple[dict, dict, dict, Optional[OutputConfigBlock]]:
|
||||
"""Prepare and separate request parameters."""
|
||||
# Filter out exception objects before deepcopy to prevent deepcopy failures
|
||||
# Exceptions should not be stored in optional_params (this is a defensive fix)
|
||||
|
|
@ -1047,6 +1192,8 @@ class AmazonConverseConfig(BaseConfig):
|
|||
if request_metadata is not None:
|
||||
self._validate_request_metadata(request_metadata)
|
||||
|
||||
output_config: Optional[OutputConfigBlock] = inference_params.pop("outputConfig", None)
|
||||
|
||||
# keep supported params in 'inference_params', and set all model-specific params in 'additional_request_params'
|
||||
additional_request_params = {
|
||||
k: v for k, v in inference_params.items() if k not in total_supported_params
|
||||
|
|
@ -1071,7 +1218,12 @@ class AmazonConverseConfig(BaseConfig):
|
|||
additional_request_params
|
||||
)
|
||||
|
||||
return inference_params, additional_request_params, request_metadata
|
||||
return (
|
||||
inference_params,
|
||||
additional_request_params,
|
||||
request_metadata,
|
||||
output_config,
|
||||
)
|
||||
|
||||
def _process_tools_and_beta(
|
||||
self,
|
||||
|
|
@ -1125,22 +1277,44 @@ class AmazonConverseConfig(BaseConfig):
|
|||
# "computer-use-2025-01-24" for Claude Sonnet 4.5, Haiku 4.5, Opus 4.1, Sonnet 4, Opus 4, and Sonnet 3.7
|
||||
# "computer-use-2024-10-22" for older models
|
||||
model_lower = model.lower()
|
||||
if "opus-4.6" in model_lower or "opus_4.6" in model_lower or "opus-4-6" in model_lower or "opus_4_6" in model_lower:
|
||||
if "opus-4.6" in model_lower or "opus_4.6" in model_lower or "opus-4-6" in model_lower or "opus_4_6" in model_lower or "sonnet-4.6" in model_lower or "sonnet_4.6" in model_lower or "sonnet-4-6" in model_lower or "sonnet_4_6" in model_lower:
|
||||
computer_use_header = "computer-use-2025-11-24"
|
||||
elif "opus-4.5" in model_lower or "opus_4.5" in model_lower or "opus-4-5" in model_lower or "opus_4_5" in model_lower:
|
||||
elif (
|
||||
"opus-4.5" in model_lower
|
||||
or "opus_4.5" in model_lower
|
||||
or "opus-4-5" in model_lower
|
||||
or "opus_4_5" in model_lower
|
||||
):
|
||||
computer_use_header = "computer-use-2025-11-24"
|
||||
elif any(pattern in model_lower for pattern in [
|
||||
"sonnet-4.5", "sonnet_4.5", "sonnet-4-5", "sonnet_4_5",
|
||||
"haiku-4.5", "haiku_4.5", "haiku-4-5", "haiku_4_5",
|
||||
"opus-4.1", "opus_4.1", "opus-4-1", "opus_4_1",
|
||||
"sonnet-4", "sonnet_4",
|
||||
"opus-4", "opus_4",
|
||||
"sonnet-3.7", "sonnet_3.7", "sonnet-3-7", "sonnet_3_7"
|
||||
]):
|
||||
elif any(
|
||||
pattern in model_lower
|
||||
for pattern in [
|
||||
"sonnet-4.5",
|
||||
"sonnet_4.5",
|
||||
"sonnet-4-5",
|
||||
"sonnet_4_5",
|
||||
"haiku-4.5",
|
||||
"haiku_4.5",
|
||||
"haiku-4-5",
|
||||
"haiku_4_5",
|
||||
"opus-4.1",
|
||||
"opus_4.1",
|
||||
"opus-4-1",
|
||||
"opus_4_1",
|
||||
"sonnet-4",
|
||||
"sonnet_4",
|
||||
"opus-4",
|
||||
"opus_4",
|
||||
"sonnet-3.7",
|
||||
"sonnet_3.7",
|
||||
"sonnet-3-7",
|
||||
"sonnet_3_7",
|
||||
]
|
||||
):
|
||||
computer_use_header = "computer-use-2025-01-24"
|
||||
else:
|
||||
computer_use_header = "computer-use-2024-10-22"
|
||||
|
||||
|
||||
anthropic_beta_list.append(computer_use_header)
|
||||
# Transform computer use tools to proper Bedrock format
|
||||
transformed_computer_tools = self._transform_computer_use_tools(
|
||||
|
|
@ -1214,6 +1388,7 @@ class AmazonConverseConfig(BaseConfig):
|
|||
inference_params,
|
||||
additional_request_params,
|
||||
request_metadata,
|
||||
output_config,
|
||||
) = self._prepare_request_params(optional_params, model)
|
||||
|
||||
original_tools = inference_params.pop("tools", [])
|
||||
|
|
@ -1256,6 +1431,9 @@ class AmazonConverseConfig(BaseConfig):
|
|||
if request_metadata is not None:
|
||||
data["requestMetadata"] = request_metadata
|
||||
|
||||
if output_config is not None:
|
||||
data["outputConfig"] = output_config
|
||||
|
||||
return data
|
||||
|
||||
async def _async_transform_request(
|
||||
|
|
@ -1504,9 +1682,7 @@ class AmazonConverseConfig(BaseConfig):
|
|||
|
||||
return message, returned_finish_reason
|
||||
|
||||
def _translate_message_content(
|
||||
self, content_blocks: List[ContentBlock]
|
||||
) -> Tuple[
|
||||
def _translate_message_content(self, content_blocks: List[ContentBlock]) -> Tuple[
|
||||
str,
|
||||
List[ChatCompletionToolCallChunk],
|
||||
Optional[List[BedrockConverseReasoningContentBlock]],
|
||||
|
|
@ -1523,9 +1699,9 @@ class AmazonConverseConfig(BaseConfig):
|
|||
"""
|
||||
content_str = ""
|
||||
tools: List[ChatCompletionToolCallChunk] = []
|
||||
reasoningContentBlocks: Optional[
|
||||
List[BedrockConverseReasoningContentBlock]
|
||||
] = None
|
||||
reasoningContentBlocks: Optional[List[BedrockConverseReasoningContentBlock]] = (
|
||||
None
|
||||
)
|
||||
citationsContentBlocks: Optional[List[CitationsContentBlock]] = None
|
||||
for idx, content in enumerate(content_blocks):
|
||||
"""
|
||||
|
|
@ -1652,9 +1828,9 @@ class AmazonConverseConfig(BaseConfig):
|
|||
chat_completion_message: ChatCompletionResponseMessage = {"role": "assistant"}
|
||||
content_str = ""
|
||||
tools: List[ChatCompletionToolCallChunk] = []
|
||||
reasoningContentBlocks: Optional[
|
||||
List[BedrockConverseReasoningContentBlock]
|
||||
] = None
|
||||
reasoningContentBlocks: Optional[List[BedrockConverseReasoningContentBlock]] = (
|
||||
None
|
||||
)
|
||||
citationsContentBlocks: Optional[List[CitationsContentBlock]] = None
|
||||
|
||||
if message is not None:
|
||||
|
|
@ -1673,17 +1849,17 @@ class AmazonConverseConfig(BaseConfig):
|
|||
provider_specific_fields["citationsContent"] = citationsContentBlocks
|
||||
|
||||
if provider_specific_fields:
|
||||
chat_completion_message[
|
||||
"provider_specific_fields"
|
||||
] = provider_specific_fields
|
||||
chat_completion_message["provider_specific_fields"] = (
|
||||
provider_specific_fields
|
||||
)
|
||||
|
||||
if reasoningContentBlocks is not None:
|
||||
chat_completion_message[
|
||||
"reasoning_content"
|
||||
] = self._transform_reasoning_content(reasoningContentBlocks)
|
||||
chat_completion_message[
|
||||
"thinking_blocks"
|
||||
] = self._transform_thinking_blocks(reasoningContentBlocks)
|
||||
chat_completion_message["reasoning_content"] = (
|
||||
self._transform_reasoning_content(reasoningContentBlocks)
|
||||
)
|
||||
chat_completion_message["thinking_blocks"] = (
|
||||
self._transform_thinking_blocks(reasoningContentBlocks)
|
||||
)
|
||||
chat_completion_message["content"] = content_str
|
||||
if (
|
||||
json_mode is True
|
||||
|
|
@ -1696,8 +1872,6 @@ class AmazonConverseConfig(BaseConfig):
|
|||
)
|
||||
json_mode_content_str: Optional[str] = tools[0]["function"].get("arguments")
|
||||
if json_mode_content_str is not None:
|
||||
import json
|
||||
|
||||
# Bedrock returns the response wrapped in a "properties" object
|
||||
# We need to extract the actual content from this wrapper
|
||||
try:
|
||||
|
|
@ -1716,7 +1890,7 @@ class AmazonConverseConfig(BaseConfig):
|
|||
pass
|
||||
|
||||
chat_completion_message["content"] = json_mode_content_str
|
||||
else:
|
||||
elif tools:
|
||||
chat_completion_message["tool_calls"] = tools
|
||||
|
||||
## CALCULATING USAGE - bedrock returns usage in the headers
|
||||
|
|
|
|||
|
|
@ -404,7 +404,7 @@ def extract_model_name_from_bedrock_arn(model: str) -> str:
|
|||
|
||||
def strip_bedrock_routing_prefix(model: str) -> str:
|
||||
"""Strip LiteLLM routing prefixes from model name."""
|
||||
for prefix in ["bedrock/", "converse/", "invoke/", "openai/"]:
|
||||
for prefix in ["bedrock/", "converse/", "invoke/", "openai/", "nova-2/", "nova/"]:
|
||||
if model.startswith(prefix):
|
||||
model = model.split("/", 1)[1]
|
||||
return model
|
||||
|
|
@ -427,7 +427,20 @@ def get_bedrock_base_model(model: str) -> str:
|
|||
- "us.meta.llama3-2-11b-instruct-v1:0" -> "meta.llama3-2-11b-instruct-v1"
|
||||
- "bedrock/converse/model" -> "model"
|
||||
- "anthropic.claude-3-5-sonnet-20241022-v2:0:51k" -> "anthropic.claude-3-5-sonnet-20241022-v2:0"
|
||||
- "bedrock/nova-2/arn:aws:..." -> "amazon.nova-2-custom"
|
||||
- "bedrock/nova/arn:aws:..." -> "amazon.nova-custom"
|
||||
"""
|
||||
# Detect nova spec prefixes before stripping them
|
||||
stripped = model
|
||||
for rp in ["bedrock/converse/", "bedrock/", "converse/"]:
|
||||
if stripped.startswith(rp):
|
||||
stripped = stripped[len(rp):]
|
||||
break
|
||||
if stripped.startswith("nova-2/"):
|
||||
return "amazon.nova-2-custom"
|
||||
elif stripped.startswith("nova/"):
|
||||
return "amazon.nova-custom"
|
||||
|
||||
model = strip_bedrock_routing_prefix(model)
|
||||
model = extract_model_name_from_bedrock_arn(model)
|
||||
model = strip_bedrock_throughput_suffix(model)
|
||||
|
|
@ -465,6 +478,14 @@ def is_claude_4_5_on_bedrock(model: str) -> bool:
|
|||
"opus_4.5",
|
||||
"opus-4-5",
|
||||
"opus_4_5",
|
||||
"sonnet-4.6",
|
||||
"sonnet_4.6",
|
||||
"sonnet-4-6",
|
||||
"sonnet_4_6",
|
||||
"opus-4.6",
|
||||
"opus_4.6",
|
||||
"opus-4-6",
|
||||
"opus_4_6",
|
||||
]
|
||||
return any(pattern in model_lower for pattern in claude_4_5_patterns)
|
||||
|
||||
|
|
@ -594,6 +615,11 @@ class BedrockModelInfo(BaseLLMModelInfo):
|
|||
if prefix in model:
|
||||
return route_type
|
||||
|
||||
# Check for nova spec prefixes (nova/ and nova-2/)
|
||||
_model_after_bedrock = model.replace("bedrock/", "", 1)
|
||||
if _model_after_bedrock.startswith("nova-2/") or _model_after_bedrock.startswith("nova/"):
|
||||
return "converse"
|
||||
|
||||
base_model = BedrockModelInfo.get_base_model(model)
|
||||
alt_model = BedrockModelInfo.get_non_litellm_routing_model_name(model=model)
|
||||
if (
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ Helper util for handling bedrock-specific cost calculation
|
|||
- e.g.: prompt caching
|
||||
"""
|
||||
|
||||
from typing import TYPE_CHECKING, Tuple
|
||||
from typing import TYPE_CHECKING, Optional, Tuple
|
||||
|
||||
from litellm.litellm_core_utils.llm_cost_calc.utils import generic_cost_per_token
|
||||
|
||||
|
|
@ -11,12 +11,17 @@ if TYPE_CHECKING:
|
|||
from litellm.types.utils import Usage
|
||||
|
||||
|
||||
def cost_per_token(model: str, usage: "Usage") -> Tuple[float, float]:
|
||||
def cost_per_token(
|
||||
model: str, usage: "Usage", service_tier: Optional[str] = None
|
||||
) -> Tuple[float, float]:
|
||||
"""
|
||||
Calculates the cost per token for a given model, prompt tokens, and completion tokens.
|
||||
|
||||
Follows the same logic as Anthropic's cost per token calculation.
|
||||
"""
|
||||
return generic_cost_per_token(
|
||||
model=model, usage=usage, custom_llm_provider="bedrock"
|
||||
)
|
||||
model=model,
|
||||
usage=usage,
|
||||
custom_llm_provider="bedrock",
|
||||
service_tier=service_tier,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -180,6 +180,14 @@ class AmazonAnthropicClaudeMessagesConfig(
|
|||
"opus_4", # Opus 4
|
||||
"sonnet-4",
|
||||
"sonnet_4", # Sonnet 4
|
||||
"sonnet-4.6",
|
||||
"sonnet_4.6",
|
||||
"sonnet-4-6",
|
||||
"sonnet_4_6",
|
||||
"opus-4.6",
|
||||
"opus_4.6",
|
||||
"opus-4-6",
|
||||
"opus_4_6",
|
||||
]
|
||||
|
||||
return any(pattern in model_lower for pattern in supported_patterns)
|
||||
|
|
@ -251,6 +259,11 @@ class AmazonAnthropicClaudeMessagesConfig(
|
|||
"opus_4.6",
|
||||
"opus-4-6",
|
||||
"opus_4_6",
|
||||
#sonnet 4.6
|
||||
"sonnet-4.6",
|
||||
"sonnet_4.6",
|
||||
"sonnet-4-6",
|
||||
"sonnet_4_6",
|
||||
]
|
||||
|
||||
return any(pattern in model_lower for pattern in supported_patterns)
|
||||
|
|
@ -285,7 +298,7 @@ class AmazonAnthropicClaudeMessagesConfig(
|
|||
programmatic_tool_calling_used or input_examples_used
|
||||
):
|
||||
beta_set.discard(ANTHROPIC_TOOL_SEARCH_BETA_HEADER)
|
||||
if "opus-4" in model.lower() or "opus_4" in model.lower():
|
||||
if self._supports_tool_search_on_bedrock(model):
|
||||
beta_set.add("tool-search-tool-2025-10-19")
|
||||
|
||||
def _convert_output_format_to_inline_schema(
|
||||
|
|
@ -420,10 +433,8 @@ class AmazonAnthropicClaudeMessagesConfig(
|
|||
beta_set=beta_set,
|
||||
)
|
||||
|
||||
# --- Custom logic: if tool-search-tool-2025-10-19 is present, add tool-examples-2025-10-29 ---
|
||||
if "tool-search-tool-2025-10-19" in beta_set:
|
||||
beta_set.add("tool-examples-2025-10-29")
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
if beta_set:
|
||||
anthropic_messages_request["anthropic_beta"] = list(beta_set)
|
||||
|
|
|
|||
0
litellm/llms/databricks/responses/__init__.py
Normal file
0
litellm/llms/databricks/responses/__init__.py
Normal file
100
litellm/llms/databricks/responses/transformation.py
Normal file
100
litellm/llms/databricks/responses/transformation.py
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
"""
|
||||
Databricks Responses API configuration.
|
||||
|
||||
Inherits from OpenAIResponsesAPIConfig since Databricks' Responses API
|
||||
is compatible with OpenAI's for GPT models.
|
||||
|
||||
Reference: https://docs.databricks.com/aws/en/machine-learning/foundation-model-apis/api-reference
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import TYPE_CHECKING, Any, Dict, Optional, Union
|
||||
|
||||
from litellm.llms.databricks.common_utils import DatabricksBase
|
||||
from litellm.llms.openai.responses.transformation import OpenAIResponsesAPIConfig
|
||||
from litellm.types.llms.openai import ResponseInputParam
|
||||
from litellm.types.router import GenericLiteLLMParams
|
||||
from litellm.types.utils import LlmProviders
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from litellm.litellm_core_utils.litellm_logging import Logging as _LiteLLMLoggingObj
|
||||
|
||||
LiteLLMLoggingObj = _LiteLLMLoggingObj
|
||||
else:
|
||||
LiteLLMLoggingObj = Any
|
||||
|
||||
|
||||
class DatabricksResponsesAPIConfig(DatabricksBase, OpenAIResponsesAPIConfig):
|
||||
"""
|
||||
Configuration for Databricks Responses API.
|
||||
|
||||
Inherits from OpenAIResponsesAPIConfig since Databricks' Responses API
|
||||
is largely compatible with OpenAI's for GPT models.
|
||||
|
||||
Note: The Responses API on Databricks is only compatible with OpenAI GPT models.
|
||||
"""
|
||||
|
||||
@property
|
||||
def custom_llm_provider(self) -> LlmProviders:
|
||||
return LlmProviders.DATABRICKS
|
||||
|
||||
def validate_environment(
|
||||
self,
|
||||
headers: dict,
|
||||
model: str,
|
||||
litellm_params: Optional[GenericLiteLLMParams],
|
||||
) -> dict:
|
||||
litellm_params = litellm_params or GenericLiteLLMParams()
|
||||
api_key = litellm_params.api_key or os.getenv("DATABRICKS_API_KEY")
|
||||
api_base = litellm_params.api_base or os.getenv("DATABRICKS_API_BASE")
|
||||
|
||||
# Reuse Databricks auth logic (OAuth M2M, PAT, SDK fallback).
|
||||
# custom_endpoint=False allows SDK auth fallback; the appended
|
||||
# /chat/completions suffix is harmless since we discard api_base
|
||||
# here and build the URL separately in get_complete_url().
|
||||
_, headers = self.databricks_validate_environment(
|
||||
api_key=api_key,
|
||||
api_base=api_base,
|
||||
endpoint_type="chat_completions",
|
||||
custom_endpoint=False,
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
headers["Content-Type"] = "application/json"
|
||||
return headers
|
||||
|
||||
def get_complete_url(
|
||||
self,
|
||||
api_base: Optional[str],
|
||||
litellm_params: dict,
|
||||
) -> str:
|
||||
api_base = api_base or os.getenv("DATABRICKS_API_BASE")
|
||||
api_base = self._get_api_base(api_base)
|
||||
api_base = api_base.rstrip("/")
|
||||
return f"{api_base}/responses"
|
||||
|
||||
def transform_responses_api_request(
|
||||
self,
|
||||
model: str,
|
||||
input: Union[str, ResponseInputParam],
|
||||
response_api_optional_request_params: Dict,
|
||||
litellm_params: GenericLiteLLMParams,
|
||||
headers: dict,
|
||||
) -> Dict:
|
||||
"""
|
||||
Transform request for Databricks Responses API.
|
||||
|
||||
Strips the 'databricks/' prefix from model name if present,
|
||||
then delegates to OpenAI's transformation.
|
||||
"""
|
||||
# Strip provider prefix if present (e.g., "databricks/databricks-gpt-5-nano" -> "databricks-gpt-5-nano")
|
||||
if model.startswith("databricks/"):
|
||||
model = model[len("databricks/") :]
|
||||
|
||||
return super().transform_responses_api_request(
|
||||
model=model,
|
||||
input=input,
|
||||
response_api_optional_request_params=response_api_optional_request_params,
|
||||
litellm_params=litellm_params,
|
||||
headers=headers,
|
||||
)
|
||||
6
litellm/llms/duckduckgo/search/__init__.py
Normal file
6
litellm/llms/duckduckgo/search/__init__.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
"""
|
||||
DuckDuckGo Search API module.
|
||||
"""
|
||||
from litellm.llms.duckduckgo.search.transformation import DuckDuckGoSearchConfig
|
||||
|
||||
__all__ = ["DuckDuckGoSearchConfig"]
|
||||
252
litellm/llms/duckduckgo/search/transformation.py
Normal file
252
litellm/llms/duckduckgo/search/transformation.py
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
"""
|
||||
Calls DuckDuckGo's Instant Answer API to search the web.
|
||||
|
||||
DuckDuckGo API Reference: https://duckduckgo.com/api
|
||||
"""
|
||||
from typing import Dict, List, Literal, Optional, TypedDict, Union
|
||||
from urllib.parse import urlencode
|
||||
|
||||
import httpx
|
||||
|
||||
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
|
||||
from litellm.llms.base_llm.search.transformation import (
|
||||
BaseSearchConfig,
|
||||
SearchResponse,
|
||||
SearchResult,
|
||||
)
|
||||
from litellm.secret_managers.main import get_secret_str
|
||||
|
||||
|
||||
class _DuckDuckGoSearchRequestRequired(TypedDict):
|
||||
"""Required fields for DuckDuckGo Search API request."""
|
||||
q: str # Required - search query
|
||||
|
||||
|
||||
class DuckDuckGoSearchRequest(_DuckDuckGoSearchRequestRequired, total=False):
|
||||
"""
|
||||
DuckDuckGo Instant Answer API request format.
|
||||
Based on: https://duckduckgo.com/api
|
||||
"""
|
||||
format: str # Optional - output format ('json', 'xml'), default 'json'
|
||||
pretty: int # Optional - pretty print (0 or 1), default 1
|
||||
no_redirect: int # Optional - skip HTTP redirects (0 or 1), default 0
|
||||
no_html: int # Optional - remove HTML from text (0 or 1), default 0
|
||||
skip_disambig: int # Optional - skip disambiguation results (0 or 1), default 0
|
||||
|
||||
|
||||
class DuckDuckGoSearchConfig(BaseSearchConfig):
|
||||
DUCKDUCKGO_API_BASE = "https://api.duckduckgo.com"
|
||||
|
||||
@staticmethod
|
||||
def ui_friendly_name() -> str:
|
||||
return "DuckDuckGo"
|
||||
|
||||
def get_http_method(self) -> Literal["GET", "POST"]:
|
||||
"""
|
||||
Get HTTP method for search requests.
|
||||
DuckDuckGo Instant Answer API uses GET requests.
|
||||
|
||||
Returns:
|
||||
HTTP method 'GET'
|
||||
"""
|
||||
return "GET"
|
||||
|
||||
def validate_environment(
|
||||
self,
|
||||
headers: Dict,
|
||||
api_key: Optional[str] = None,
|
||||
api_base: Optional[str] = None,
|
||||
**kwargs,
|
||||
) -> Dict:
|
||||
"""
|
||||
Validate environment and return headers.
|
||||
DuckDuckGo Instant Answer API does not require authentication.
|
||||
"""
|
||||
# DuckDuckGo API is free and doesn't require API key
|
||||
headers["Content-Type"] = "application/json"
|
||||
return headers
|
||||
|
||||
def get_complete_url(
|
||||
self,
|
||||
api_base: Optional[str],
|
||||
optional_params: dict,
|
||||
data: Optional[Union[Dict, List[Dict]]] = None,
|
||||
**kwargs,
|
||||
) -> str:
|
||||
"""
|
||||
Get complete URL for Search endpoint.
|
||||
DuckDuckGo uses query parameters, so we construct the URL with the query.
|
||||
"""
|
||||
api_base = api_base or get_secret_str("DUCKDUCKGO_API_BASE") or self.DUCKDUCKGO_API_BASE
|
||||
|
||||
# Build query parameters from the transformed request body
|
||||
if data and isinstance(data, dict) and "_duckduckgo_params" in data:
|
||||
params = data["_duckduckgo_params"]
|
||||
query_string = urlencode(params, doseq=True)
|
||||
return f"{api_base}/?{query_string}"
|
||||
|
||||
return api_base
|
||||
|
||||
|
||||
def transform_search_request(
|
||||
self,
|
||||
query: Union[str, List[str]],
|
||||
optional_params: dict,
|
||||
**kwargs,
|
||||
) -> Dict:
|
||||
"""
|
||||
Transform Search request to DuckDuckGo API format.
|
||||
|
||||
Args:
|
||||
query: Search query (string or list of strings). DuckDuckGo only supports single string queries.
|
||||
optional_params: Optional parameters for the request
|
||||
- max_results: Maximum number of search results (DuckDuckGo API doesn't directly support this, used for filtering)
|
||||
- format: Output format ('json', 'xml')
|
||||
- pretty: Pretty print (0 or 1)
|
||||
- no_redirect: Skip HTTP redirects (0 or 1)
|
||||
- no_html: Remove HTML from text (0 or 1)
|
||||
- skip_disambig: Skip disambiguation results (0 or 1)
|
||||
|
||||
Returns:
|
||||
Dict with typed request data following DuckDuckGoSearchRequest spec
|
||||
"""
|
||||
if isinstance(query, list):
|
||||
# DuckDuckGo only supports single string queries
|
||||
query = " ".join(query)
|
||||
|
||||
request_data: DuckDuckGoSearchRequest = {
|
||||
"q": query,
|
||||
"format": "json", # Always use JSON format
|
||||
}
|
||||
|
||||
# Convert to dict before dynamic key assignments
|
||||
result_data = dict(request_data)
|
||||
|
||||
if "max_results" in optional_params:
|
||||
result_data["_max_results"] = optional_params["max_results"]
|
||||
|
||||
# Pass through DuckDuckGo-specific parameters
|
||||
ddg_params = ["pretty", "no_redirect", "no_html", "skip_disambig"]
|
||||
for param in ddg_params:
|
||||
if param in optional_params:
|
||||
result_data[param] = optional_params[param]
|
||||
|
||||
return {
|
||||
"_duckduckgo_params": result_data,
|
||||
}
|
||||
|
||||
def transform_search_response(
|
||||
self,
|
||||
raw_response: httpx.Response,
|
||||
logging_obj: LiteLLMLoggingObj,
|
||||
**kwargs,
|
||||
) -> SearchResponse:
|
||||
"""
|
||||
Transform DuckDuckGo API response to LiteLLM unified SearchResponse format.
|
||||
|
||||
DuckDuckGo → LiteLLM mappings:
|
||||
- RelatedTopics[].Text → SearchResult.title + snippet
|
||||
- RelatedTopics[].FirstURL → SearchResult.url
|
||||
- RelatedTopics[].Text → SearchResult.snippet
|
||||
- No date/last_updated fields in DuckDuckGo response (set to None)
|
||||
|
||||
Args:
|
||||
raw_response: Raw httpx response from DuckDuckGo API
|
||||
logging_obj: Logging object for tracking
|
||||
|
||||
Returns:
|
||||
SearchResponse with standardized format
|
||||
"""
|
||||
response_json = raw_response.json()
|
||||
|
||||
# Extract max_results from the request URL params
|
||||
query_params = raw_response.request.url.params if raw_response.request else {}
|
||||
max_results = None
|
||||
if "_max_results" in query_params:
|
||||
try:
|
||||
max_results = int(query_params["_max_results"])
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
# Transform results to SearchResult objects
|
||||
results = []
|
||||
|
||||
# DuckDuckGo can return results in different fields
|
||||
# Priority: Abstract > Answer > RelatedTopics
|
||||
|
||||
# Check if there's an Abstract with URL
|
||||
if response_json.get("AbstractURL") and response_json.get("AbstractText"):
|
||||
abstract_result = SearchResult(
|
||||
title=response_json.get("Heading", ""),
|
||||
url=response_json.get("AbstractURL", ""),
|
||||
snippet=response_json.get("AbstractText", ""),
|
||||
date=None,
|
||||
last_updated=None,
|
||||
)
|
||||
results.append(abstract_result)
|
||||
|
||||
# Process RelatedTopics
|
||||
related_topics = response_json.get("RelatedTopics", [])
|
||||
for topic in related_topics:
|
||||
# Stop if we've reached max_results
|
||||
if max_results is not None and len(results) >= max_results:
|
||||
break
|
||||
|
||||
if isinstance(topic, dict):
|
||||
# Check if it's a direct result
|
||||
if "FirstURL" in topic and "Text" in topic:
|
||||
text = topic.get("Text", "")
|
||||
url = topic.get("FirstURL", "")
|
||||
|
||||
# Try to split title and snippet
|
||||
if " - " in text:
|
||||
parts = text.split(" - ", 1)
|
||||
title = parts[0]
|
||||
snippet = parts[1] if len(parts) > 1 else text
|
||||
else:
|
||||
title = text[:50] + "..." if len(text) > 50 else text
|
||||
snippet = text
|
||||
|
||||
search_result = SearchResult(
|
||||
title=title,
|
||||
url=url,
|
||||
snippet=snippet,
|
||||
date=None,
|
||||
last_updated=None,
|
||||
)
|
||||
results.append(search_result)
|
||||
|
||||
# Check if it contains nested topics
|
||||
elif "Topics" in topic:
|
||||
nested_topics = topic.get("Topics", [])
|
||||
for nested_topic in nested_topics:
|
||||
# Stop if we've reached max_results
|
||||
if max_results is not None and len(results) >= max_results:
|
||||
break
|
||||
|
||||
if "FirstURL" in nested_topic and "Text" in nested_topic:
|
||||
text = nested_topic.get("Text", "")
|
||||
url = nested_topic.get("FirstURL", "")
|
||||
|
||||
# Try to split title and snippet
|
||||
if " - " in text:
|
||||
parts = text.split(" - ", 1)
|
||||
title = parts[0]
|
||||
snippet = parts[1] if len(parts) > 1 else text
|
||||
else:
|
||||
title = text[:50] + "..." if len(text) > 50 else text
|
||||
snippet = text
|
||||
|
||||
search_result = SearchResult(
|
||||
title=title,
|
||||
url=url,
|
||||
snippet=snippet,
|
||||
date=None,
|
||||
last_updated=None,
|
||||
)
|
||||
results.append(search_result)
|
||||
|
||||
return SearchResponse(
|
||||
results=results,
|
||||
object="search",
|
||||
)
|
||||
|
|
@ -770,14 +770,36 @@ class OpenAIGPTConfig(BaseLLMModelInfo, BaseConfig):
|
|||
|
||||
|
||||
class OpenAIChatCompletionStreamingHandler(BaseModelResponseIterator):
|
||||
def _map_reasoning_to_reasoning_content(self, choices: list) -> list:
|
||||
"""
|
||||
Map 'reasoning' field to 'reasoning_content' field in delta.
|
||||
|
||||
Some OpenAI-compatible providers (e.g., GLM-5, hosted_vllm) return
|
||||
delta.reasoning, but LiteLLM expects delta.reasoning_content.
|
||||
|
||||
Args:
|
||||
choices: List of choice objects from the streaming chunk
|
||||
|
||||
Returns:
|
||||
List of choices with reasoning field mapped to reasoning_content
|
||||
"""
|
||||
for choice in choices:
|
||||
delta = choice.get("delta", {})
|
||||
if "reasoning" in delta:
|
||||
delta["reasoning_content"] = delta.pop("reasoning")
|
||||
return choices
|
||||
|
||||
def chunk_parser(self, chunk: dict) -> ModelResponseStream:
|
||||
try:
|
||||
choices = chunk.get("choices", [])
|
||||
choices = self._map_reasoning_to_reasoning_content(choices)
|
||||
|
||||
kwargs = {
|
||||
"id": chunk["id"],
|
||||
"object": "chat.completion.chunk",
|
||||
"created": chunk.get("created"),
|
||||
"model": chunk.get("model"),
|
||||
"choices": chunk.get("choices", []),
|
||||
"choices": choices,
|
||||
}
|
||||
if "usage" in chunk and chunk["usage"] is not None:
|
||||
kwargs["usage"] = chunk["usage"]
|
||||
|
|
|
|||
|
|
@ -1072,7 +1072,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig):
|
|||
elif param == "modalities" and isinstance(value, list):
|
||||
response_modalities = self.map_response_modalities(value)
|
||||
optional_params["responseModalities"] = response_modalities
|
||||
elif param == "web_search_options" and value and isinstance(value, dict):
|
||||
elif param == "web_search_options" and isinstance(value, dict):
|
||||
_tools = self._map_web_search_options(value)
|
||||
optional_params = self._add_tools_to_optional_params(
|
||||
optional_params, [_tools]
|
||||
|
|
|
|||
0
litellm/llms/watsonx/__init__.py
Normal file
0
litellm/llms/watsonx/__init__.py
Normal file
0
litellm/llms/watsonx/chat/__init__.py
Normal file
0
litellm/llms/watsonx/chat/__init__.py
Normal file
0
litellm/llms/watsonx/completion/__init__.py
Normal file
0
litellm/llms/watsonx/completion/__init__.py
Normal file
0
litellm/llms/watsonx/embed/__init__.py
Normal file
0
litellm/llms/watsonx/embed/__init__.py
Normal file
0
litellm/llms/watsonx/rerank/__init__.py
Normal file
0
litellm/llms/watsonx/rerank/__init__.py
Normal file
204
litellm/llms/watsonx/rerank/transformation.py
Normal file
204
litellm/llms/watsonx/rerank/transformation.py
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
"""
|
||||
Transformation logic for IBM watsonx.ai's /ml/v1/text/rerank endpoint.
|
||||
|
||||
Docs - https://cloud.ibm.com/apidocs/watsonx-ai#text-rerank
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from typing import Any, Dict, List, Optional, Union, cast
|
||||
|
||||
import httpx
|
||||
|
||||
from litellm.llms.base_llm.chat.transformation import LiteLLMLoggingObj
|
||||
from litellm.llms.base_llm.rerank.transformation import BaseRerankConfig
|
||||
from litellm.secret_managers.main import get_secret_str
|
||||
from litellm.types.llms.watsonx import (
|
||||
WatsonXAIEndpoint,
|
||||
)
|
||||
from litellm.types.rerank import (
|
||||
RerankResponse,
|
||||
RerankResponseMeta,
|
||||
RerankTokens,
|
||||
)
|
||||
|
||||
from ..common_utils import IBMWatsonXMixin, _generate_watsonx_token, _get_api_params
|
||||
|
||||
|
||||
class IBMWatsonXRerankConfig(IBMWatsonXMixin, BaseRerankConfig):
|
||||
"""
|
||||
IBM watsonx.ai Rerank API configuration
|
||||
"""
|
||||
|
||||
def get_complete_url(
|
||||
self,
|
||||
api_base: Optional[str],
|
||||
model: str,
|
||||
optional_params: Optional[dict] = None,
|
||||
) -> str:
|
||||
base_url = self._get_base_url(api_base=api_base)
|
||||
endpoint = WatsonXAIEndpoint.RERANK.value
|
||||
|
||||
url = base_url.rstrip("/") + endpoint
|
||||
|
||||
params = optional_params or {}
|
||||
|
||||
complete_url = self._add_api_version_to_url(url=url, api_version=(params.get("api_version", None)))
|
||||
return complete_url
|
||||
|
||||
def get_supported_cohere_rerank_params(self, model: str) -> list:
|
||||
return [
|
||||
"query",
|
||||
"documents",
|
||||
"top_n",
|
||||
"return_documents",
|
||||
"max_tokens_per_doc",
|
||||
]
|
||||
|
||||
def validate_environment( # type: ignore[override]
|
||||
self,
|
||||
headers: dict,
|
||||
model: str,
|
||||
api_key: Optional[str] = None,
|
||||
optional_params: Optional[dict] = None,
|
||||
) -> Dict:
|
||||
optional_params = optional_params or {}
|
||||
|
||||
default_headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
}
|
||||
|
||||
if "Authorization" in headers:
|
||||
return {**default_headers, **headers}
|
||||
token = cast(
|
||||
Optional[str],
|
||||
optional_params.pop("token", None) or get_secret_str("WATSONX_TOKEN"),
|
||||
)
|
||||
zen_api_key = cast(
|
||||
Optional[str],
|
||||
optional_params.pop("zen_api_key", None) or get_secret_str("WATSONX_ZENAPIKEY"),
|
||||
)
|
||||
if token:
|
||||
headers["Authorization"] = f"Bearer {token}"
|
||||
elif zen_api_key:
|
||||
headers["Authorization"] = f"ZenApiKey {zen_api_key}"
|
||||
else:
|
||||
token = _generate_watsonx_token(api_key=api_key, token=token)
|
||||
# build auth headers
|
||||
headers["Authorization"] = f"Bearer {token}"
|
||||
return {**default_headers, **headers}
|
||||
|
||||
def map_cohere_rerank_params(
|
||||
self,
|
||||
non_default_params: Optional[dict],
|
||||
model: str,
|
||||
drop_params: bool,
|
||||
query: str,
|
||||
documents: List[Union[str, Dict[str, Any]]],
|
||||
custom_llm_provider: Optional[str] = None,
|
||||
top_n: Optional[int] = None,
|
||||
rank_fields: Optional[List[str]] = None,
|
||||
return_documents: Optional[bool] = True,
|
||||
max_chunks_per_doc: Optional[int] = None,
|
||||
max_tokens_per_doc: Optional[int] = None,
|
||||
) -> Dict:
|
||||
"""
|
||||
Map Cohere rerank params to IBM watsonx.ai rerank params
|
||||
"""
|
||||
optional_rerank_params = {}
|
||||
if non_default_params is not None:
|
||||
for k, v in non_default_params.items():
|
||||
if k == "query" and v is not None:
|
||||
optional_rerank_params["query"] = v
|
||||
elif k == "documents" and v is not None:
|
||||
optional_rerank_params["inputs"] = [
|
||||
{"text": el} if isinstance(el, str) else el for el in v
|
||||
]
|
||||
elif k == "top_n" and v is not None:
|
||||
optional_rerank_params.setdefault("parameters", {}).setdefault("return_options", {})["top_n"] = v
|
||||
elif k == "return_documents" and v is not None and isinstance(v, bool):
|
||||
optional_rerank_params.setdefault("parameters", {}).setdefault("return_options", {})["inputs"] = v
|
||||
elif k == "max_tokens_per_doc" and v is not None:
|
||||
optional_rerank_params.setdefault("parameters", {})["truncate_input_tokens"] = v
|
||||
|
||||
# IBM watsonx.ai require one of below parameters
|
||||
elif k == "project_id" and v is not None:
|
||||
optional_rerank_params["project_id"] = v
|
||||
elif k == "space_id" and v is not None:
|
||||
optional_rerank_params["space_id"] = v
|
||||
|
||||
return dict(optional_rerank_params)
|
||||
|
||||
def transform_rerank_request(
|
||||
self,
|
||||
model: str,
|
||||
optional_rerank_params: Dict,
|
||||
headers: dict,
|
||||
) -> dict:
|
||||
"""
|
||||
Transform request to IBM watsonx.ai rerank format
|
||||
"""
|
||||
watsonx_api_params = _get_api_params(params=optional_rerank_params, model=model)
|
||||
watsonx_auth_payload = self._prepare_payload(
|
||||
model=model,
|
||||
api_params=watsonx_api_params,
|
||||
)
|
||||
|
||||
return optional_rerank_params | watsonx_auth_payload
|
||||
|
||||
def transform_rerank_response(
|
||||
self,
|
||||
model: str,
|
||||
raw_response: httpx.Response,
|
||||
model_response: RerankResponse,
|
||||
logging_obj: LiteLLMLoggingObj,
|
||||
api_key: Optional[str] = None,
|
||||
request_data: dict = {},
|
||||
optional_params: dict = {},
|
||||
litellm_params: dict = {},
|
||||
) -> RerankResponse:
|
||||
"""
|
||||
Transform IBM watsonx.ai rerank response to LiteLLM RerankResponse format
|
||||
"""
|
||||
try:
|
||||
raw_response_json = raw_response.json()
|
||||
except Exception as e:
|
||||
raise self.get_error_class(
|
||||
error_message=f"Failed to parse response: {str(e)}",
|
||||
status_code=raw_response.status_code,
|
||||
headers=raw_response.headers,
|
||||
)
|
||||
|
||||
_results: Optional[List[dict]] = raw_response_json.get("results")
|
||||
if _results is None:
|
||||
raise ValueError(f"No results found in the response={raw_response_json}")
|
||||
|
||||
transformed_results = []
|
||||
|
||||
for result in _results:
|
||||
transformed_result: Dict[str, Any] = {
|
||||
"index": result["index"],
|
||||
"relevance_score": result["score"],
|
||||
}
|
||||
|
||||
if "input" in result:
|
||||
if isinstance(result["input"], str):
|
||||
transformed_result["document"] = {"text": result["input"]}
|
||||
else:
|
||||
transformed_result["document"] = result["input"]
|
||||
|
||||
transformed_results.append(transformed_result)
|
||||
|
||||
response_id = raw_response_json.get("id") or raw_response_json.get("model_id") or str(uuid.uuid4())
|
||||
|
||||
# Extract usage information
|
||||
_tokens = RerankTokens(
|
||||
input_tokens=raw_response_json.get("input_token_count", 0),
|
||||
)
|
||||
rerank_meta = RerankResponseMeta(tokens=_tokens)
|
||||
|
||||
return RerankResponse(
|
||||
id=response_id,
|
||||
results=transformed_results, # type: ignore
|
||||
meta=rerank_meta,
|
||||
)
|
||||
|
|
@ -8294,6 +8294,37 @@
|
|||
"supports_vision": true,
|
||||
"tool_use_system_prompt_tokens": 346
|
||||
},
|
||||
"us/claude-sonnet-4-6": {
|
||||
"cache_creation_input_token_cost": 4.125e-06,
|
||||
"cache_creation_input_token_cost_above_200k_tokens": 8.25e-06,
|
||||
"cache_read_input_token_cost": 3.3e-07,
|
||||
"cache_read_input_token_cost_above_200k_tokens": 6.6e-07,
|
||||
"input_cost_per_token": 3.3e-06,
|
||||
"input_cost_per_token_above_200k_tokens": 6.6e-06,
|
||||
"litellm_provider": "anthropic",
|
||||
"max_input_tokens": 200000,
|
||||
"max_output_tokens": 64000,
|
||||
"max_tokens": 64000,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 1.65e-05,
|
||||
"output_cost_per_token_above_200k_tokens": 2.475e-05,
|
||||
"search_context_cost_per_query": {
|
||||
"search_context_size_high": 0.01,
|
||||
"search_context_size_low": 0.01,
|
||||
"search_context_size_medium": 0.01
|
||||
},
|
||||
"supports_assistant_prefill": true,
|
||||
"supports_computer_use": true,
|
||||
"supports_function_calling": true,
|
||||
"supports_pdf_input": true,
|
||||
"supports_prompt_caching": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_response_schema": true,
|
||||
"supports_tool_choice": true,
|
||||
"supports_vision": true,
|
||||
"tool_use_system_prompt_tokens": 346,
|
||||
"inference_geo": "us"
|
||||
},
|
||||
"claude-sonnet-4-5-20250929-v1:0": {
|
||||
"cache_creation_input_token_cost": 3.75e-06,
|
||||
"cache_read_input_token_cost": 3e-07,
|
||||
|
|
@ -22465,6 +22496,20 @@
|
|||
"supports_response_schema": true,
|
||||
"supports_tool_choice": true
|
||||
},
|
||||
"mistral/devstral-small-latest": {
|
||||
"input_cost_per_token": 1e-07,
|
||||
"litellm_provider": "mistral",
|
||||
"max_input_tokens": 256000,
|
||||
"max_output_tokens": 256000,
|
||||
"max_tokens": 256000,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 3e-07,
|
||||
"source": "https://docs.mistral.ai/models/devstral-small-2-25-12",
|
||||
"supports_assistant_prefill": true,
|
||||
"supports_function_calling": true,
|
||||
"supports_response_schema": true,
|
||||
"supports_tool_choice": true
|
||||
},
|
||||
"mistral/labs-devstral-small-2512": {
|
||||
"input_cost_per_token": 1e-07,
|
||||
"litellm_provider": "mistral",
|
||||
|
|
@ -22479,6 +22524,34 @@
|
|||
"supports_response_schema": true,
|
||||
"supports_tool_choice": true
|
||||
},
|
||||
"mistral/devstral-latest": {
|
||||
"input_cost_per_token": 4e-07,
|
||||
"litellm_provider": "mistral",
|
||||
"max_input_tokens": 256000,
|
||||
"max_output_tokens": 256000,
|
||||
"max_tokens": 256000,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 2e-06,
|
||||
"source": "https://mistral.ai/news/devstral-2-vibe-cli",
|
||||
"supports_assistant_prefill": true,
|
||||
"supports_function_calling": true,
|
||||
"supports_response_schema": true,
|
||||
"supports_tool_choice": true
|
||||
},
|
||||
"mistral/devstral-medium-latest": {
|
||||
"input_cost_per_token": 4e-07,
|
||||
"litellm_provider": "mistral",
|
||||
"max_input_tokens": 256000,
|
||||
"max_output_tokens": 256000,
|
||||
"max_tokens": 256000,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 2e-06,
|
||||
"source": "https://mistral.ai/news/devstral-2-vibe-cli",
|
||||
"supports_assistant_prefill": true,
|
||||
"supports_function_calling": true,
|
||||
"supports_response_schema": true,
|
||||
"supports_tool_choice": true
|
||||
},
|
||||
"mistral/devstral-2512": {
|
||||
"input_cost_per_token": 4e-07,
|
||||
"litellm_provider": "mistral",
|
||||
|
|
@ -37270,5 +37343,13 @@
|
|||
"search_context_size_low": 0.01,
|
||||
"search_context_size_medium": 0.01
|
||||
}
|
||||
},
|
||||
"duckduckgo/search": {
|
||||
"litellm_provider": "duckduckgo",
|
||||
"mode": "search",
|
||||
"input_cost_per_query": 0.0,
|
||||
"metadata": {
|
||||
"notes": "DuckDuckGo Instant Answer API is free and does not require an API key."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -772,26 +772,34 @@
|
|||
{
|
||||
"id": "eu-ai-act-article5",
|
||||
"title": "EU AI Act Article 5 — Prohibited Practices",
|
||||
"description": "EU AI Act Article 5 compliance for prohibited AI practices. Blocks requests related to social scoring, emotion recognition in workplace/education, biometric categorization, predictive profiling, manipulation, and vulnerability exploitation. Includes both English and French keyword detection. Uses conditional matching (identifier word + context word).",
|
||||
"description": "Comprehensive EU AI Act Article 5 compliance covering all prohibited AI practices. Includes 5 dedicated sub-guardrails per language (English + French) for: subliminal manipulation (Art. 5.1a), vulnerability exploitation (Art. 5.1b), social scoring (Art. 5.1c), emotion recognition in workplace/education (Art. 5.1f), and biometric categorization & predictive profiling (Art. 5.1d/g/h). Uses conditional matching (identifier word + context word).",
|
||||
"region": "EU",
|
||||
"icon": "ShieldExclamationIcon",
|
||||
"iconColor": "text-red-500",
|
||||
"iconBg": "bg-red-50",
|
||||
"guardrails": [
|
||||
"eu-ai-act-prohibited-practices",
|
||||
"eu-ai-act-prohibited-practices-fr"
|
||||
"eu-ai-act-art5-manipulation",
|
||||
"eu-ai-act-art5-vulnerability",
|
||||
"eu-ai-act-art5-social-scoring",
|
||||
"eu-ai-act-art5-emotion-recognition",
|
||||
"eu-ai-act-art5-biometric-profiling",
|
||||
"eu-ai-act-art5-manipulation-fr",
|
||||
"eu-ai-act-art5-vulnerability-fr",
|
||||
"eu-ai-act-art5-social-scoring-fr",
|
||||
"eu-ai-act-art5-emotion-recognition-fr",
|
||||
"eu-ai-act-art5-biometric-profiling-fr"
|
||||
],
|
||||
"complexity": "High",
|
||||
"guardrailDefinitions": [
|
||||
{
|
||||
"guardrail_name": "eu-ai-act-prohibited-practices",
|
||||
"guardrail_name": "eu-ai-act-art5-manipulation",
|
||||
"litellm_params": {
|
||||
"guardrail": "litellm_content_filter",
|
||||
"mode": "pre_call",
|
||||
"categories": [
|
||||
{
|
||||
"category": "eu_ai_act_article5_prohibited_practices",
|
||||
"category_file": "litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/policy_templates/eu_ai_act_article5.yaml",
|
||||
"category": "eu_ai_act_art5_manipulation",
|
||||
"category_file": "litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/policy_templates/eu_ai_act_art5_manipulation.yaml",
|
||||
"enabled": true,
|
||||
"action": "BLOCK",
|
||||
"severity_threshold": "medium"
|
||||
|
|
@ -799,18 +807,18 @@
|
|||
]
|
||||
},
|
||||
"guardrail_info": {
|
||||
"description": "Blocks EU AI Act Article 5 prohibited practices in English: social scoring systems, emotion recognition in workplace/education, biometric categorization, predictive profiling, manipulation, and vulnerability exploitation"
|
||||
"description": "Art. 5.1(a) — Blocks subliminal manipulation, deceptive AI techniques, dark patterns, and covert behavioral influence"
|
||||
}
|
||||
},
|
||||
{
|
||||
"guardrail_name": "eu-ai-act-prohibited-practices-fr",
|
||||
"guardrail_name": "eu-ai-act-art5-vulnerability",
|
||||
"litellm_params": {
|
||||
"guardrail": "litellm_content_filter",
|
||||
"mode": "pre_call",
|
||||
"categories": [
|
||||
{
|
||||
"category": "eu_ai_act_article5_prohibited_practices_fr",
|
||||
"category_file": "litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/policy_templates/eu_ai_act_article5_fr.yaml",
|
||||
"category": "eu_ai_act_art5_vulnerability",
|
||||
"category_file": "litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/policy_templates/eu_ai_act_art5_vulnerability.yaml",
|
||||
"enabled": true,
|
||||
"action": "BLOCK",
|
||||
"severity_threshold": "medium"
|
||||
|
|
@ -818,16 +826,297 @@
|
|||
]
|
||||
},
|
||||
"guardrail_info": {
|
||||
"description": "Blocks EU AI Act Article 5 prohibited practices in French: detects and blocks French-language keywords related to social scoring, emotion recognition in workplace/education, biometric categorization, predictive profiling, manipulation, and vulnerability exploitation"
|
||||
"description": "Art. 5.1(b) — Blocks AI systems that exploit vulnerabilities of children, elderly, disabled persons, or economically disadvantaged groups"
|
||||
}
|
||||
},
|
||||
{
|
||||
"guardrail_name": "eu-ai-act-art5-social-scoring",
|
||||
"litellm_params": {
|
||||
"guardrail": "litellm_content_filter",
|
||||
"mode": "pre_call",
|
||||
"categories": [
|
||||
{
|
||||
"category": "eu_ai_act_art5_social_scoring",
|
||||
"category_file": "litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/policy_templates/eu_ai_act_art5_social_scoring.yaml",
|
||||
"enabled": true,
|
||||
"action": "BLOCK",
|
||||
"severity_threshold": "medium"
|
||||
}
|
||||
]
|
||||
},
|
||||
"guardrail_info": {
|
||||
"description": "Art. 5.1(c) — Blocks social credit systems, citizen scoring, trustworthiness classification, and behavioral reputation scoring"
|
||||
}
|
||||
},
|
||||
{
|
||||
"guardrail_name": "eu-ai-act-art5-emotion-recognition",
|
||||
"litellm_params": {
|
||||
"guardrail": "litellm_content_filter",
|
||||
"mode": "pre_call",
|
||||
"categories": [
|
||||
{
|
||||
"category": "eu_ai_act_art5_emotion_recognition",
|
||||
"category_file": "litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/policy_templates/eu_ai_act_art5_emotion_recognition.yaml",
|
||||
"enabled": true,
|
||||
"action": "BLOCK",
|
||||
"severity_threshold": "medium"
|
||||
}
|
||||
]
|
||||
},
|
||||
"guardrail_info": {
|
||||
"description": "Art. 5.1(f) — Blocks emotion recognition, mood tracking, and sentiment analysis in workplace and educational settings"
|
||||
}
|
||||
},
|
||||
{
|
||||
"guardrail_name": "eu-ai-act-art5-biometric-profiling",
|
||||
"litellm_params": {
|
||||
"guardrail": "litellm_content_filter",
|
||||
"mode": "pre_call",
|
||||
"categories": [
|
||||
{
|
||||
"category": "eu_ai_act_art5_biometric_profiling",
|
||||
"category_file": "litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/policy_templates/eu_ai_act_art5_biometric_profiling.yaml",
|
||||
"enabled": true,
|
||||
"action": "BLOCK",
|
||||
"severity_threshold": "medium"
|
||||
}
|
||||
]
|
||||
},
|
||||
"guardrail_info": {
|
||||
"description": "Art. 5.1(d)(g)(h) — Blocks biometric categorization by race/ethnicity/religion/politics, facial recognition database scraping, and predictive policing"
|
||||
}
|
||||
},
|
||||
{
|
||||
"guardrail_name": "eu-ai-act-art5-manipulation-fr",
|
||||
"litellm_params": {
|
||||
"guardrail": "litellm_content_filter",
|
||||
"mode": "pre_call",
|
||||
"categories": [
|
||||
{
|
||||
"category": "eu_ai_act_art5_manipulation_fr",
|
||||
"category_file": "litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/policy_templates/eu_ai_act_art5_manipulation_fr.yaml",
|
||||
"enabled": true,
|
||||
"action": "BLOCK",
|
||||
"severity_threshold": "medium"
|
||||
}
|
||||
]
|
||||
},
|
||||
"guardrail_info": {
|
||||
"description": "Art. 5.1(a) FR — Bloque la manipulation subliminale, les techniques d'IA trompeuses et les dark patterns (français)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"guardrail_name": "eu-ai-act-art5-vulnerability-fr",
|
||||
"litellm_params": {
|
||||
"guardrail": "litellm_content_filter",
|
||||
"mode": "pre_call",
|
||||
"categories": [
|
||||
{
|
||||
"category": "eu_ai_act_art5_vulnerability_fr",
|
||||
"category_file": "litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/policy_templates/eu_ai_act_art5_vulnerability_fr.yaml",
|
||||
"enabled": true,
|
||||
"action": "BLOCK",
|
||||
"severity_threshold": "medium"
|
||||
}
|
||||
]
|
||||
},
|
||||
"guardrail_info": {
|
||||
"description": "Art. 5.1(b) FR — Bloque l'exploitation des vulnérabilités des enfants, personnes âgées et handicapées (français)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"guardrail_name": "eu-ai-act-art5-social-scoring-fr",
|
||||
"litellm_params": {
|
||||
"guardrail": "litellm_content_filter",
|
||||
"mode": "pre_call",
|
||||
"categories": [
|
||||
{
|
||||
"category": "eu_ai_act_art5_social_scoring_fr",
|
||||
"category_file": "litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/policy_templates/eu_ai_act_art5_social_scoring_fr.yaml",
|
||||
"enabled": true,
|
||||
"action": "BLOCK",
|
||||
"severity_threshold": "medium"
|
||||
}
|
||||
]
|
||||
},
|
||||
"guardrail_info": {
|
||||
"description": "Art. 5.1(c) FR — Bloque les systèmes de crédit social, notation des citoyens et classification de fiabilité (français)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"guardrail_name": "eu-ai-act-art5-emotion-recognition-fr",
|
||||
"litellm_params": {
|
||||
"guardrail": "litellm_content_filter",
|
||||
"mode": "pre_call",
|
||||
"categories": [
|
||||
{
|
||||
"category": "eu_ai_act_art5_emotion_recognition_fr",
|
||||
"category_file": "litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/policy_templates/eu_ai_act_art5_emotion_recognition_fr.yaml",
|
||||
"enabled": true,
|
||||
"action": "BLOCK",
|
||||
"severity_threshold": "medium"
|
||||
}
|
||||
]
|
||||
},
|
||||
"guardrail_info": {
|
||||
"description": "Art. 5.1(f) FR — Bloque la reconnaissance des émotions et l'analyse des sentiments au travail et dans l'éducation (français)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"guardrail_name": "eu-ai-act-art5-biometric-profiling-fr",
|
||||
"litellm_params": {
|
||||
"guardrail": "litellm_content_filter",
|
||||
"mode": "pre_call",
|
||||
"categories": [
|
||||
{
|
||||
"category": "eu_ai_act_art5_biometric_profiling_fr",
|
||||
"category_file": "litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/policy_templates/eu_ai_act_art5_biometric_profiling_fr.yaml",
|
||||
"enabled": true,
|
||||
"action": "BLOCK",
|
||||
"severity_threshold": "medium"
|
||||
}
|
||||
]
|
||||
},
|
||||
"guardrail_info": {
|
||||
"description": "Art. 5.1(d)(g)(h) FR — Bloque la catégorisation biométrique, les bases de reconnaissance faciale et le profilage prédictif (français)"
|
||||
}
|
||||
}
|
||||
],
|
||||
"templateData": {
|
||||
"policy_name": "eu-ai-act-article5",
|
||||
"description": "EU AI Act Article 5 compliance policy for prohibited AI practices. Blocks social scoring, emotion recognition in workplace/education, biometric categorization, predictive profiling, manipulation, and vulnerability exploitation. Includes English and French detection.",
|
||||
"description": "Comprehensive EU AI Act Article 5 compliance policy. Covers all prohibited AI practices across 5 sub-guardrails per language: subliminal manipulation (Art. 5.1a), vulnerability exploitation (Art. 5.1b), social scoring (Art. 5.1c), emotion recognition (Art. 5.1f), and biometric categorization & predictive profiling (Art. 5.1d/g/h). Includes English and French detection.",
|
||||
"guardrails_add": [
|
||||
"eu-ai-act-prohibited-practices",
|
||||
"eu-ai-act-prohibited-practices-fr"
|
||||
"eu-ai-act-art5-manipulation",
|
||||
"eu-ai-act-art5-vulnerability",
|
||||
"eu-ai-act-art5-social-scoring",
|
||||
"eu-ai-act-art5-emotion-recognition",
|
||||
"eu-ai-act-art5-biometric-profiling",
|
||||
"eu-ai-act-art5-manipulation-fr",
|
||||
"eu-ai-act-art5-vulnerability-fr",
|
||||
"eu-ai-act-art5-social-scoring-fr",
|
||||
"eu-ai-act-art5-emotion-recognition-fr",
|
||||
"eu-ai-act-art5-biometric-profiling-fr"
|
||||
],
|
||||
"guardrails_remove": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "prompt-injection-detection",
|
||||
"title": "Prompt Injection Detection",
|
||||
"description": "Detects and blocks prompt injection attacks including SQL injection, malicious code injection, system prompt extraction, jailbreak attempts, and data exfiltration. Applies pre-call screening to block attacks before they reach the LLM.",
|
||||
"region": "Global",
|
||||
"icon": "ShieldExclamationIcon",
|
||||
"iconColor": "text-red-500",
|
||||
"iconBg": "bg-red-50",
|
||||
"guardrails": [
|
||||
"prompt-injection-sql",
|
||||
"prompt-injection-malicious-code",
|
||||
"prompt-injection-system-prompt",
|
||||
"prompt-injection-jailbreak",
|
||||
"prompt-injection-data-exfiltration"
|
||||
],
|
||||
"complexity": "Medium",
|
||||
"guardrailDefinitions": [
|
||||
{
|
||||
"guardrail_name": "prompt-injection-sql",
|
||||
"litellm_params": {
|
||||
"guardrail": "litellm_content_filter",
|
||||
"mode": "pre_call",
|
||||
"categories": [
|
||||
{
|
||||
"category": "prompt_injection_sql",
|
||||
"enabled": true,
|
||||
"action": "BLOCK",
|
||||
"severity_threshold": "medium"
|
||||
}
|
||||
]
|
||||
},
|
||||
"guardrail_info": {
|
||||
"description": "Blocks SQL injection attempts in prompts (DROP TABLE, UNION SELECT, OR 1=1, etc.)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"guardrail_name": "prompt-injection-malicious-code",
|
||||
"litellm_params": {
|
||||
"guardrail": "litellm_content_filter",
|
||||
"mode": "pre_call",
|
||||
"categories": [
|
||||
{
|
||||
"category": "prompt_injection_malicious_code",
|
||||
"enabled": true,
|
||||
"action": "BLOCK",
|
||||
"severity_threshold": "medium"
|
||||
}
|
||||
]
|
||||
},
|
||||
"guardrail_info": {
|
||||
"description": "Blocks malicious code injection attempts (shell commands, reverse shells, script injection, encoded payloads)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"guardrail_name": "prompt-injection-system-prompt",
|
||||
"litellm_params": {
|
||||
"guardrail": "litellm_content_filter",
|
||||
"mode": "pre_call",
|
||||
"categories": [
|
||||
{
|
||||
"category": "prompt_injection_system_prompt",
|
||||
"enabled": true,
|
||||
"action": "BLOCK",
|
||||
"severity_threshold": "medium"
|
||||
}
|
||||
]
|
||||
},
|
||||
"guardrail_info": {
|
||||
"description": "Blocks system prompt extraction and instruction override attempts (ignore previous instructions, reveal your prompt, etc.)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"guardrail_name": "prompt-injection-jailbreak",
|
||||
"litellm_params": {
|
||||
"guardrail": "litellm_content_filter",
|
||||
"mode": "pre_call",
|
||||
"categories": [
|
||||
{
|
||||
"category": "prompt_injection_jailbreak",
|
||||
"enabled": true,
|
||||
"action": "BLOCK",
|
||||
"severity_threshold": "medium"
|
||||
}
|
||||
]
|
||||
},
|
||||
"guardrail_info": {
|
||||
"description": "Blocks jailbreak attempts (DAN mode, developer mode, safety bypass, token smuggling)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"guardrail_name": "prompt-injection-data-exfiltration",
|
||||
"litellm_params": {
|
||||
"guardrail": "litellm_content_filter",
|
||||
"mode": "pre_call",
|
||||
"categories": [
|
||||
{
|
||||
"category": "prompt_injection_data_exfiltration",
|
||||
"enabled": true,
|
||||
"action": "BLOCK",
|
||||
"severity_threshold": "medium"
|
||||
}
|
||||
]
|
||||
},
|
||||
"guardrail_info": {
|
||||
"description": "Blocks data exfiltration attempts (extract training data, dump database, steal credentials, etc.)"
|
||||
}
|
||||
}
|
||||
],
|
||||
"templateData": {
|
||||
"policy_name": "prompt-injection-detection",
|
||||
"description": "Prompt injection detection policy. Blocks SQL injection, malicious code injection, system prompt extraction, jailbreak attempts, and data exfiltration in prompts before they reach the LLM.",
|
||||
"guardrails_add": [
|
||||
"prompt-injection-sql",
|
||||
"prompt-injection-malicious-code",
|
||||
"prompt-injection-system-prompt",
|
||||
"prompt-injection-jailbreak",
|
||||
"prompt-injection-data-exfiltration"
|
||||
],
|
||||
"guardrails_remove": []
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,3 +13,26 @@ model_list:
|
|||
- model_name: gpt-4.1-mini
|
||||
litellm_params:
|
||||
model: openai/gpt-4.1-mini
|
||||
|
||||
|
||||
# guardrails:
|
||||
# - guardrail_name: generic-guardrail
|
||||
# litellm_params:
|
||||
# guardrail: generic_guardrail_api
|
||||
# mode: ["pre_call"]
|
||||
# headers:
|
||||
# Authorization: Bearer mock-bedrock-token-12345
|
||||
# api_base: http://localhost:8080
|
||||
# default_on: true
|
||||
|
||||
prompts:
|
||||
- prompt_id: "simple_prompt"
|
||||
litellm_params:
|
||||
prompt_integration: "generic_prompt_management"
|
||||
provider_specific_query_params:
|
||||
project_name: litellm
|
||||
slug: hello-world-prompt-2bac
|
||||
api_base: http://localhost:8080
|
||||
api_key: os.environ/BRAINTRUST_API_KEY
|
||||
ignore_prompt_manager_model: true
|
||||
ignore_prompt_manager_optional_params: true
|
||||
|
|
|
|||
|
|
@ -223,12 +223,14 @@ def get_known_models_from_wildcard(
|
|||
except ValueError: # safely fail
|
||||
return []
|
||||
|
||||
if litellm_params is None: # need litellm params to extract litellm model name
|
||||
return []
|
||||
|
||||
try:
|
||||
provider = litellm_params.model.split("/", 1)[0]
|
||||
except ValueError:
|
||||
# Use provider from litellm_params when available, otherwise from wildcard prefix
|
||||
# (e.g., "openai" from "openai/*" - needed for BYOK where wildcard isn't in router)
|
||||
if litellm_params is not None:
|
||||
try:
|
||||
provider = litellm_params.model.split("/", 1)[0]
|
||||
except ValueError:
|
||||
provider = wildcard_provider_prefix
|
||||
else:
|
||||
provider = wildcard_provider_prefix
|
||||
|
||||
# get all known provider models
|
||||
|
|
@ -282,7 +284,7 @@ def _get_wildcard_models(
|
|||
## get litellm params from model
|
||||
if llm_router is not None:
|
||||
model_list = llm_router.get_model_list(model_name=model)
|
||||
if model_list is not None:
|
||||
if model_list:
|
||||
for router_model in model_list:
|
||||
wildcard_models = get_known_models_from_wildcard(
|
||||
wildcard_model=model,
|
||||
|
|
@ -291,11 +293,22 @@ def _get_wildcard_models(
|
|||
),
|
||||
)
|
||||
all_wildcard_models.extend(wildcard_models)
|
||||
else:
|
||||
# Router has no deployment for this wildcard (e.g., BYOK team models)
|
||||
# Fall back to expanding from known provider models
|
||||
wildcard_models = get_known_models_from_wildcard(
|
||||
wildcard_model=model, litellm_params=None
|
||||
)
|
||||
if wildcard_models:
|
||||
models_to_remove.add(model)
|
||||
all_wildcard_models.extend(wildcard_models)
|
||||
else:
|
||||
# get all known provider models
|
||||
wildcard_models = get_known_models_from_wildcard(wildcard_model=model)
|
||||
wildcard_models = get_known_models_from_wildcard(
|
||||
wildcard_model=model, litellm_params=None
|
||||
)
|
||||
|
||||
if wildcard_models is not None:
|
||||
if wildcard_models:
|
||||
models_to_remove.add(model)
|
||||
all_wildcard_models.extend(wildcard_models)
|
||||
|
||||
|
|
|
|||
221
litellm/proxy/compliance_checks.py
Normal file
221
litellm/proxy/compliance_checks.py
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
"""
|
||||
Compliance checker for EU AI Act and GDPR regulations.
|
||||
|
||||
Provides guardrail-agnostic compliance validation based on guardrail modes
|
||||
and execution results rather than specific guardrail names.
|
||||
"""
|
||||
|
||||
from typing import Dict, List
|
||||
|
||||
from litellm.types.proxy.compliance_endpoints import (
|
||||
ComplianceCheckRequest,
|
||||
ComplianceCheckResult,
|
||||
)
|
||||
|
||||
|
||||
class ComplianceChecker:
|
||||
"""
|
||||
Validates compliance with EU AI Act and GDPR regulations.
|
||||
|
||||
Uses guardrail-agnostic checks based on:
|
||||
- Whether any guardrails ran
|
||||
- Guardrail execution mode (pre-call, post-call, etc.)
|
||||
- Whether guardrails intervened/blocked content
|
||||
- Completeness of audit records
|
||||
"""
|
||||
|
||||
def __init__(self, data: ComplianceCheckRequest):
|
||||
self.data = data
|
||||
self.guardrails = data.guardrail_information or []
|
||||
|
||||
def _get_guardrails_by_mode(self, mode: str) -> List[Dict]:
|
||||
"""
|
||||
Get all guardrails that ran in a specific mode.
|
||||
|
||||
If a guardrail doesn't have a mode specified, it's treated as pre-call
|
||||
(the most common case).
|
||||
"""
|
||||
result = []
|
||||
for g in self.guardrails:
|
||||
g_mode = g.get("guardrail_mode")
|
||||
# If no mode specified, default to pre_call
|
||||
if g_mode is None and mode == "pre_call":
|
||||
result.append(g)
|
||||
elif g_mode == mode:
|
||||
result.append(g)
|
||||
return result
|
||||
|
||||
def _has_guardrail_intervention(self, guardrails: List[Dict]) -> bool:
|
||||
"""Check if any guardrail intervened (blocked/masked content)."""
|
||||
for g in guardrails:
|
||||
status = g.get("guardrail_status", "")
|
||||
if status in ["guardrail_intervened", "failed", "blocked"]:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _all_guardrails_passed(self, guardrails: List[Dict]) -> bool:
|
||||
"""Check if all guardrails passed (no issues detected)."""
|
||||
if not guardrails:
|
||||
return False
|
||||
return all(g.get("guardrail_status") == "success" for g in guardrails)
|
||||
|
||||
# ── EU AI Act Helper Methods ────────────────────────────────────────────
|
||||
|
||||
def _check_art_9_guardrails_applied(self) -> ComplianceCheckResult:
|
||||
"""Art. 9: Check if any guardrails were applied."""
|
||||
has_guardrails = len(self.guardrails) > 0
|
||||
return ComplianceCheckResult(
|
||||
check_name="Guardrails applied",
|
||||
article="Art. 9",
|
||||
passed=has_guardrails,
|
||||
detail=(
|
||||
f"{len(self.guardrails)} guardrail(s) applied"
|
||||
if has_guardrails
|
||||
else "No guardrails applied"
|
||||
),
|
||||
)
|
||||
|
||||
def _check_art_5_content_screened(self) -> ComplianceCheckResult:
|
||||
"""Art. 5: Check if content was screened before LLM (pre-call)."""
|
||||
pre_call_guardrails = self._get_guardrails_by_mode("pre_call")
|
||||
has_pre_call = len(pre_call_guardrails) > 0
|
||||
return ComplianceCheckResult(
|
||||
check_name="Content screened before LLM",
|
||||
article="Art. 5",
|
||||
passed=has_pre_call,
|
||||
detail=(
|
||||
f"{len(pre_call_guardrails)} pre-call guardrail(s) screened content"
|
||||
if has_pre_call
|
||||
else "No pre-call screening applied"
|
||||
),
|
||||
)
|
||||
|
||||
def _check_art_12_audit_complete(self) -> ComplianceCheckResult:
|
||||
"""Art. 12: Check if audit record is complete."""
|
||||
has_user = bool(self.data.user_id)
|
||||
has_model = bool(self.data.model)
|
||||
has_timestamp = bool(self.data.timestamp)
|
||||
has_guardrails = len(self.guardrails) > 0
|
||||
audit_complete = has_user and has_model and has_timestamp and has_guardrails
|
||||
|
||||
missing = []
|
||||
if not has_user:
|
||||
missing.append("user_id")
|
||||
if not has_model:
|
||||
missing.append("model")
|
||||
if not has_timestamp:
|
||||
missing.append("timestamp")
|
||||
if not has_guardrails:
|
||||
missing.append("guardrail_results")
|
||||
|
||||
return ComplianceCheckResult(
|
||||
check_name="Audit record complete",
|
||||
article="Art. 12",
|
||||
passed=audit_complete,
|
||||
detail=(
|
||||
"All required audit fields present"
|
||||
if audit_complete
|
||||
else f"Missing: {', '.join(missing)}"
|
||||
),
|
||||
)
|
||||
|
||||
# ── GDPR Helper Methods ──────────────────────────────────────────────────
|
||||
|
||||
def _check_art_32_data_protection(self) -> ComplianceCheckResult:
|
||||
"""Art. 32: Check if data protection was applied (pre-call)."""
|
||||
pre_call_guardrails = self._get_guardrails_by_mode("pre_call")
|
||||
has_pre_call = len(pre_call_guardrails) > 0
|
||||
return ComplianceCheckResult(
|
||||
check_name="Data protection applied",
|
||||
article="Art. 32",
|
||||
passed=has_pre_call,
|
||||
detail=(
|
||||
f"{len(pre_call_guardrails)} pre-call guardrail(s) protect data"
|
||||
if has_pre_call
|
||||
else "No pre-call data protection applied"
|
||||
),
|
||||
)
|
||||
|
||||
def _check_art_5_1c_sensitive_data_protected(self) -> ComplianceCheckResult:
|
||||
"""Art. 5(1)(c): Check if sensitive data was protected."""
|
||||
pre_call_guardrails = self._get_guardrails_by_mode("pre_call")
|
||||
has_intervention = self._has_guardrail_intervention(pre_call_guardrails)
|
||||
all_passed = self._all_guardrails_passed(pre_call_guardrails)
|
||||
data_protected = has_intervention or all_passed
|
||||
|
||||
if has_intervention:
|
||||
detail = "Guardrail intervened to protect sensitive data"
|
||||
elif all_passed:
|
||||
detail = "No sensitive data detected"
|
||||
else:
|
||||
detail = "No pre-call guardrails to protect sensitive data"
|
||||
|
||||
return ComplianceCheckResult(
|
||||
check_name="Sensitive data protected",
|
||||
article="Art. 5(1)(c)",
|
||||
passed=data_protected,
|
||||
detail=detail,
|
||||
)
|
||||
|
||||
def _check_art_30_audit_complete(self) -> ComplianceCheckResult:
|
||||
"""Art. 30: Check if audit record is complete."""
|
||||
has_user = bool(self.data.user_id)
|
||||
has_model = bool(self.data.model)
|
||||
has_timestamp = bool(self.data.timestamp)
|
||||
has_guardrails = len(self.guardrails) > 0
|
||||
audit_complete = has_user and has_model and has_timestamp and has_guardrails
|
||||
|
||||
missing = []
|
||||
if not has_user:
|
||||
missing.append("user_id")
|
||||
if not has_model:
|
||||
missing.append("model")
|
||||
if not has_timestamp:
|
||||
missing.append("timestamp")
|
||||
if not has_guardrails:
|
||||
missing.append("guardrail_results")
|
||||
|
||||
return ComplianceCheckResult(
|
||||
check_name="Audit record complete",
|
||||
article="Art. 30",
|
||||
passed=audit_complete,
|
||||
detail=(
|
||||
"All required audit fields present"
|
||||
if audit_complete
|
||||
else f"Missing: {', '.join(missing)}"
|
||||
),
|
||||
)
|
||||
|
||||
# ── Main Compliance Check Methods ────────────────────────────────────────
|
||||
|
||||
def check_eu_ai_act(self) -> List[ComplianceCheckResult]:
|
||||
"""
|
||||
Check EU AI Act compliance.
|
||||
|
||||
Returns:
|
||||
List of compliance check results for:
|
||||
- Art. 9: Guardrails applied
|
||||
- Art. 5: Content screened before LLM (pre-call screening)
|
||||
- Art. 12: Audit record complete
|
||||
"""
|
||||
return [
|
||||
self._check_art_9_guardrails_applied(),
|
||||
self._check_art_5_content_screened(),
|
||||
self._check_art_12_audit_complete(),
|
||||
]
|
||||
|
||||
def check_gdpr(self) -> List[ComplianceCheckResult]:
|
||||
"""
|
||||
Check GDPR compliance.
|
||||
|
||||
Returns:
|
||||
List of compliance check results for:
|
||||
- Art. 32: Data protection applied (pre-call screening)
|
||||
- Art. 5(1)(c): Sensitive data protected
|
||||
- Art. 30: Audit record complete
|
||||
"""
|
||||
return [
|
||||
self._check_art_32_data_protection(),
|
||||
self._check_art_5_1c_sensitive_data_protected(),
|
||||
self._check_art_30_audit_complete(),
|
||||
]
|
||||
|
|
@ -18,6 +18,9 @@ def initialize_guardrail(litellm_params: "LitellmParams", guardrail: "Guardrail"
|
|||
additional_provider_specific_params=getattr(
|
||||
litellm_params, "additional_provider_specific_params", {}
|
||||
),
|
||||
unreachable_fallback=getattr(
|
||||
litellm_params, "unreachable_fallback", "fail_closed"
|
||||
),
|
||||
guardrail_name=guardrail.get("guardrail_name", ""),
|
||||
event_hook=litellm_params.mode,
|
||||
default_on=litellm_params.default_on,
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ litellm_settings:
|
|||
mode: pre_call # Options: pre_call, post_call, during_call, [pre_call, post_call]
|
||||
api_key: os.environ/GENERIC_GUARDRAIL_API_KEY # Optional if using Bearer auth
|
||||
api_base: http://localhost:8080 # Required. Endpoint /beta/litellm_basic_guardrail_api is automatically appended
|
||||
unreachable_fallback: fail_closed # Options: fail_closed (default, raise), fail_open (proceed if endpoint unreachable or upstream returns 502/503/504)
|
||||
default_on: false # Set to true to apply to all requests by default
|
||||
additional_provider_specific_params:
|
||||
# Any additional parameters your guardrail API needs
|
||||
|
|
|
|||
|
|
@ -9,9 +9,11 @@ import fnmatch
|
|||
import os
|
||||
from typing import TYPE_CHECKING, Any, Dict, Literal, Optional
|
||||
|
||||
import httpx
|
||||
|
||||
from litellm._logging import verbose_proxy_logger
|
||||
from litellm._version import version as litellm_version
|
||||
from litellm.exceptions import GuardrailRaisedException
|
||||
from litellm.exceptions import GuardrailRaisedException, Timeout
|
||||
from litellm.integrations.custom_guardrail import (
|
||||
CustomGuardrail,
|
||||
log_guardrail_information,
|
||||
|
|
@ -34,17 +36,19 @@ if TYPE_CHECKING:
|
|||
GUARDRAIL_NAME = "generic_guardrail_api"
|
||||
|
||||
# Headers whose values are forwarded as-is (case-insensitive). Glob patterns supported (e.g. x-stainless-*, x-litellm*).
|
||||
_HEADER_VALUE_ALLOWLIST = frozenset({
|
||||
"host",
|
||||
"accept-encoding",
|
||||
"connection",
|
||||
"accept",
|
||||
"content-type",
|
||||
"user-agent",
|
||||
"x-stainless-*",
|
||||
"x-litellm-*",
|
||||
"content-length",
|
||||
})
|
||||
_HEADER_VALUE_ALLOWLIST = frozenset(
|
||||
{
|
||||
"host",
|
||||
"accept-encoding",
|
||||
"connection",
|
||||
"accept",
|
||||
"content-type",
|
||||
"user-agent",
|
||||
"x-stainless-*",
|
||||
"x-litellm-*",
|
||||
"content-length",
|
||||
}
|
||||
)
|
||||
|
||||
# Placeholder for headers that exist but are not on the allowlist (we don't expose their value).
|
||||
_HEADER_PRESENT_PLACEHOLDER = "[present]"
|
||||
|
|
@ -166,6 +170,7 @@ class GenericGuardrailAPI(CustomGuardrail):
|
|||
api_base: Optional[str] = None,
|
||||
api_key: Optional[str] = None,
|
||||
additional_provider_specific_params: Optional[Dict[str, Any]] = None,
|
||||
unreachable_fallback: Literal["fail_closed", "fail_open"] = "fail_closed",
|
||||
**kwargs,
|
||||
):
|
||||
self.async_handler = get_async_httpx_client(
|
||||
|
|
@ -196,6 +201,10 @@ class GenericGuardrailAPI(CustomGuardrail):
|
|||
additional_provider_specific_params or {}
|
||||
)
|
||||
|
||||
self.unreachable_fallback: Literal["fail_closed", "fail_open"] = (
|
||||
unreachable_fallback
|
||||
)
|
||||
|
||||
# Set supported event hooks
|
||||
if "supported_event_hooks" not in kwargs:
|
||||
kwargs["supported_event_hooks"] = [
|
||||
|
|
@ -259,6 +268,54 @@ class GenericGuardrailAPI(CustomGuardrail):
|
|||
|
||||
return result_metadata
|
||||
|
||||
def _fail_open_passthrough(
|
||||
self,
|
||||
*,
|
||||
inputs: GenericGuardrailAPIInputs,
|
||||
input_type: Literal["request", "response"],
|
||||
logging_obj: Optional["LiteLLMLoggingObj"],
|
||||
error: Exception,
|
||||
http_status_code: Optional[int] = None,
|
||||
) -> GenericGuardrailAPIInputs:
|
||||
status_suffix = f" http_status_code={http_status_code}" if http_status_code else ""
|
||||
verbose_proxy_logger.critical(
|
||||
"Generic Guardrail API unreachable (fail-open). Proceeding without guardrail.%s "
|
||||
"guardrail_name=%s api_base=%s input_type=%s litellm_call_id=%s litellm_trace_id=%s",
|
||||
status_suffix,
|
||||
getattr(self, "guardrail_name", None),
|
||||
getattr(self, "api_base", None),
|
||||
input_type,
|
||||
getattr(logging_obj, "litellm_call_id", None) if logging_obj else None,
|
||||
getattr(logging_obj, "litellm_trace_id", None) if logging_obj else None,
|
||||
exc_info=error,
|
||||
)
|
||||
# Keep flow going - treat as action=NONE (no modifications)
|
||||
return_inputs: GenericGuardrailAPIInputs = {}
|
||||
return_inputs.update(inputs)
|
||||
return return_inputs
|
||||
|
||||
def _build_guardrail_return_inputs(
|
||||
self,
|
||||
*,
|
||||
texts: list,
|
||||
images: Any,
|
||||
tools: Any,
|
||||
guardrail_response: GenericGuardrailAPIResponse,
|
||||
) -> GenericGuardrailAPIInputs:
|
||||
# Action is NONE or no modifications needed
|
||||
return_inputs = GenericGuardrailAPIInputs(texts=texts)
|
||||
if guardrail_response.texts:
|
||||
return_inputs["texts"] = guardrail_response.texts
|
||||
if guardrail_response.images:
|
||||
return_inputs["images"] = guardrail_response.images
|
||||
elif images:
|
||||
return_inputs["images"] = images
|
||||
if guardrail_response.tools:
|
||||
return_inputs["tools"] = guardrail_response.tools
|
||||
elif tools:
|
||||
return_inputs["tools"] = tools
|
||||
return return_inputs
|
||||
|
||||
@log_guardrail_information
|
||||
async def apply_guardrail(
|
||||
self,
|
||||
|
|
@ -313,7 +370,9 @@ class GenericGuardrailAPI(CustomGuardrail):
|
|||
|
||||
# Extract user API key metadata
|
||||
user_metadata = self._extract_user_api_key_metadata(request_data)
|
||||
inbound_headers = _extract_inbound_headers(request_data=request_data, logging_obj=logging_obj)
|
||||
inbound_headers = _extract_inbound_headers(
|
||||
request_data=request_data, logging_obj=logging_obj
|
||||
)
|
||||
|
||||
# Create request payload
|
||||
guardrail_request = GenericGuardrailAPIRequest(
|
||||
|
|
@ -370,23 +429,64 @@ class GenericGuardrailAPI(CustomGuardrail):
|
|||
should_wrap_with_default_message=False,
|
||||
)
|
||||
|
||||
# Action is NONE or no modifications needed
|
||||
return_inputs = GenericGuardrailAPIInputs(texts=texts)
|
||||
if guardrail_response.texts:
|
||||
return_inputs["texts"] = guardrail_response.texts
|
||||
if guardrail_response.images:
|
||||
return_inputs["images"] = guardrail_response.images
|
||||
elif images:
|
||||
return_inputs["images"] = images
|
||||
if guardrail_response.tools:
|
||||
return_inputs["tools"] = guardrail_response.tools
|
||||
elif tools:
|
||||
return_inputs["tools"] = tools
|
||||
return return_inputs
|
||||
return self._build_guardrail_return_inputs(
|
||||
texts=texts,
|
||||
images=images,
|
||||
tools=tools,
|
||||
guardrail_response=guardrail_response,
|
||||
)
|
||||
|
||||
except GuardrailRaisedException:
|
||||
# Re-raise guardrail exceptions as-is
|
||||
raise
|
||||
except Timeout as e:
|
||||
# AsyncHTTPHandler wraps httpx.TimeoutException into litellm.Timeout
|
||||
if self.unreachable_fallback == "fail_open":
|
||||
return self._fail_open_passthrough(
|
||||
inputs=inputs,
|
||||
input_type=input_type,
|
||||
logging_obj=logging_obj,
|
||||
error=e,
|
||||
)
|
||||
|
||||
verbose_proxy_logger.error(
|
||||
"Generic Guardrail API: failed to make request: %s", str(e)
|
||||
)
|
||||
raise Exception(f"Generic Guardrail API failed: {str(e)}")
|
||||
except httpx.HTTPStatusError as e:
|
||||
# Common reverse-proxy/LB failures can present as HTTP errors even when the backend is unreachable.
|
||||
status_code = getattr(getattr(e, "response", None), "status_code", None)
|
||||
if self.unreachable_fallback == "fail_open" and status_code in (
|
||||
502,
|
||||
503,
|
||||
504,
|
||||
):
|
||||
return self._fail_open_passthrough(
|
||||
inputs=inputs,
|
||||
input_type=input_type,
|
||||
logging_obj=logging_obj,
|
||||
error=e,
|
||||
http_status_code=status_code,
|
||||
)
|
||||
|
||||
verbose_proxy_logger.error(
|
||||
"Generic Guardrail API: failed to make request: %s", str(e)
|
||||
)
|
||||
raise Exception(f"Generic Guardrail API failed: {str(e)}")
|
||||
except httpx.RequestError as e:
|
||||
# Guardrail endpoint is unreachable (DNS/connect/timeout/etc)
|
||||
if self.unreachable_fallback == "fail_open":
|
||||
return self._fail_open_passthrough(
|
||||
inputs=inputs,
|
||||
input_type=input_type,
|
||||
logging_obj=logging_obj,
|
||||
error=e,
|
||||
)
|
||||
|
||||
verbose_proxy_logger.error(
|
||||
"Generic Guardrail API: failed to make request: %s", str(e)
|
||||
)
|
||||
raise Exception(f"Generic Guardrail API failed: {str(e)}")
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.error(
|
||||
"Generic Guardrail API: failed to make request: %s", str(e)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,123 @@
|
|||
# Data Exfiltration Detection - Detects attempts to extract sensitive data via prompts
|
||||
# Uses conditional logic: BLOCK if an identifier word + block word appear in same sentence
|
||||
category_name: "prompt_injection_data_exfiltration"
|
||||
description: "Detects attempts to extract training data, internal information, or user data through prompts"
|
||||
default_action: "BLOCK"
|
||||
|
||||
# IDENTIFIER WORDS - Data access and extraction actions
|
||||
identifier_words:
|
||||
- "extract"
|
||||
- "leak"
|
||||
- "exfiltrate"
|
||||
- "dump"
|
||||
- "retrieve"
|
||||
- "access"
|
||||
- "steal"
|
||||
- "obtain"
|
||||
- "harvest"
|
||||
- "scrape"
|
||||
- "collect"
|
||||
- "gather"
|
||||
- "expose"
|
||||
- "export"
|
||||
- "download"
|
||||
- "copy"
|
||||
- "siphon"
|
||||
|
||||
# ADDITIONAL BLOCK WORDS - Sensitive data targets
|
||||
additional_block_words:
|
||||
- "training data"
|
||||
- "training set"
|
||||
- "training examples"
|
||||
- "internal data"
|
||||
- "internal information"
|
||||
- "database contents"
|
||||
- "database records"
|
||||
- "user data"
|
||||
- "user records"
|
||||
- "user information"
|
||||
- "personal data"
|
||||
- "private information"
|
||||
- "private data"
|
||||
- "confidential data"
|
||||
- "confidential information"
|
||||
- "sensitive data"
|
||||
- "sensitive information"
|
||||
- "customer data"
|
||||
- "customer records"
|
||||
- "employee data"
|
||||
- "employee records"
|
||||
- "credentials"
|
||||
- "passwords"
|
||||
- "API keys"
|
||||
- "access tokens"
|
||||
- "secret keys"
|
||||
- "encryption keys"
|
||||
- "private keys"
|
||||
- "session tokens"
|
||||
- "authentication tokens"
|
||||
- "model weights"
|
||||
- "model parameters"
|
||||
- "source code"
|
||||
- "proprietary code"
|
||||
- "intellectual property"
|
||||
|
||||
# ALWAYS BLOCK - Explicit data exfiltration patterns (blocked regardless of context)
|
||||
always_block_keywords:
|
||||
- keyword: "extract training data"
|
||||
severity: "high"
|
||||
- keyword: "leak internal data"
|
||||
severity: "high"
|
||||
- keyword: "dump database"
|
||||
severity: "high"
|
||||
- keyword: "dump all records"
|
||||
severity: "high"
|
||||
- keyword: "show all user data"
|
||||
severity: "high"
|
||||
- keyword: "list all users"
|
||||
severity: "medium"
|
||||
- keyword: "list all passwords"
|
||||
severity: "high"
|
||||
- keyword: "show all API keys"
|
||||
severity: "high"
|
||||
- keyword: "reveal all credentials"
|
||||
severity: "high"
|
||||
- keyword: "expose customer data"
|
||||
severity: "high"
|
||||
- keyword: "steal user information"
|
||||
severity: "high"
|
||||
- keyword: "exfiltrate data"
|
||||
severity: "high"
|
||||
- keyword: "harvest email addresses"
|
||||
severity: "high"
|
||||
- keyword: "scrape personal information"
|
||||
severity: "high"
|
||||
- keyword: "extract model weights"
|
||||
severity: "high"
|
||||
- keyword: "steal source code"
|
||||
severity: "high"
|
||||
- keyword: "copy proprietary code"
|
||||
severity: "high"
|
||||
- keyword: "download private data"
|
||||
severity: "high"
|
||||
- keyword: "access confidential files"
|
||||
severity: "high"
|
||||
- keyword: "retrieve secret keys"
|
||||
severity: "high"
|
||||
|
||||
# Exception patterns (legitimate uses)
|
||||
exceptions:
|
||||
- "data extraction pipeline"
|
||||
- "ETL"
|
||||
- "data science"
|
||||
- "data engineering"
|
||||
- "data migration"
|
||||
- "backup and recovery"
|
||||
- "data export feature"
|
||||
- "GDPR data request"
|
||||
- "right to access"
|
||||
- "data portability"
|
||||
- "legitimate data access"
|
||||
- "authorized data retrieval"
|
||||
- "data loss prevention"
|
||||
- "DLP"
|
||||
|
|
@ -0,0 +1,140 @@
|
|||
# Jailbreak Attempt Detection - Detects common jailbreak patterns
|
||||
# Uses conditional logic: BLOCK if an identifier word + block word appear in same sentence
|
||||
category_name: "prompt_injection_jailbreak"
|
||||
description: "Detects jailbreak attempts including DAN, roleplay attacks, and safety bypass requests"
|
||||
default_action: "BLOCK"
|
||||
|
||||
# IDENTIFIER WORDS - Roleplay/persona manipulation actions
|
||||
identifier_words:
|
||||
- "pretend"
|
||||
- "roleplay"
|
||||
- "act as"
|
||||
- "imagine you are"
|
||||
- "suppose you are"
|
||||
- "you are now"
|
||||
- "behave as"
|
||||
- "respond as"
|
||||
- "simulate"
|
||||
- "emulate"
|
||||
- "impersonate"
|
||||
- "transform into"
|
||||
- "become"
|
||||
- "play the role"
|
||||
- "switch to"
|
||||
- "enter"
|
||||
- "enable"
|
||||
- "activate"
|
||||
- "turn on"
|
||||
|
||||
# ADDITIONAL BLOCK WORDS - Jailbreak-related terms
|
||||
additional_block_words:
|
||||
- "no restrictions"
|
||||
- "no rules"
|
||||
- "no guidelines"
|
||||
- "no filters"
|
||||
- "no limitations"
|
||||
- "no boundaries"
|
||||
- "no censorship"
|
||||
- "no moderation"
|
||||
- "no safety"
|
||||
- "no ethical"
|
||||
- "bypass"
|
||||
- "override"
|
||||
- "unrestricted"
|
||||
- "unfiltered"
|
||||
- "uncensored"
|
||||
- "unmoderated"
|
||||
- "unlimited"
|
||||
- "without limits"
|
||||
- "without restrictions"
|
||||
- "without rules"
|
||||
- "without guidelines"
|
||||
- "without filters"
|
||||
- "anything mode"
|
||||
- "god mode"
|
||||
- "developer mode"
|
||||
- "debug mode"
|
||||
- "test mode"
|
||||
- "admin mode"
|
||||
- "root mode"
|
||||
- "superuser"
|
||||
- "evil mode"
|
||||
- "chaos mode"
|
||||
- "unrestricted mode"
|
||||
- "opposite mode"
|
||||
|
||||
# ALWAYS BLOCK - Explicit jailbreak patterns (blocked regardless of context)
|
||||
always_block_keywords:
|
||||
# DAN and known jailbreaks
|
||||
- keyword: "DAN mode"
|
||||
severity: "high"
|
||||
- keyword: "Do Anything Now"
|
||||
severity: "high"
|
||||
- keyword: "developer mode enabled"
|
||||
severity: "high"
|
||||
- keyword: "jailbreak"
|
||||
severity: "high"
|
||||
- keyword: "jailbroken"
|
||||
severity: "high"
|
||||
- keyword: "bypass safety"
|
||||
severity: "high"
|
||||
- keyword: "bypass filters"
|
||||
severity: "high"
|
||||
- keyword: "bypass content filter"
|
||||
severity: "high"
|
||||
- keyword: "bypass moderation"
|
||||
severity: "high"
|
||||
- keyword: "ignore safety guidelines"
|
||||
severity: "high"
|
||||
- keyword: "ignore content policy"
|
||||
severity: "high"
|
||||
- keyword: "ignore ethical guidelines"
|
||||
severity: "high"
|
||||
- keyword: "without restrictions"
|
||||
severity: "medium"
|
||||
- keyword: "without safety"
|
||||
severity: "high"
|
||||
- keyword: "without filters"
|
||||
severity: "medium"
|
||||
- keyword: "remove all restrictions"
|
||||
severity: "high"
|
||||
- keyword: "disable safety"
|
||||
severity: "high"
|
||||
- keyword: "disable content filter"
|
||||
severity: "high"
|
||||
- keyword: "disable moderation"
|
||||
severity: "high"
|
||||
- keyword: "turn off safety"
|
||||
severity: "high"
|
||||
- keyword: "turn off filters"
|
||||
severity: "high"
|
||||
|
||||
# Token smuggling / encoding tricks
|
||||
- keyword: "respond in base64"
|
||||
severity: "high"
|
||||
- keyword: "encode your response"
|
||||
severity: "medium"
|
||||
- keyword: "answer in rot13"
|
||||
severity: "high"
|
||||
- keyword: "respond in hex"
|
||||
severity: "high"
|
||||
- keyword: "use pig latin"
|
||||
severity: "medium"
|
||||
- keyword: "respond backwards"
|
||||
severity: "medium"
|
||||
- keyword: "spell it out letter by letter"
|
||||
severity: "medium"
|
||||
|
||||
# Exception patterns (legitimate uses)
|
||||
exceptions:
|
||||
- "jailbreak prevention"
|
||||
- "security research"
|
||||
- "red team"
|
||||
- "red teaming"
|
||||
- "AI safety research"
|
||||
- "AI alignment"
|
||||
- "prompt injection defense"
|
||||
- "how to prevent jailbreaks"
|
||||
- "jailbreak detection"
|
||||
- "content moderation research"
|
||||
- "safety evaluation"
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
# Malicious Code Injection Detection - Detects attempts to inject executable code
|
||||
# Uses conditional logic: BLOCK if an identifier word + block word appear in same sentence
|
||||
category_name: "prompt_injection_malicious_code"
|
||||
description: "Detects attempts to inject or execute malicious code via prompts"
|
||||
default_action: "BLOCK"
|
||||
|
||||
# IDENTIFIER WORDS - Code execution actions
|
||||
identifier_words:
|
||||
- "execute"
|
||||
- "run"
|
||||
- "eval"
|
||||
- "exec"
|
||||
- "spawn"
|
||||
- "system"
|
||||
- "shell"
|
||||
- "invoke"
|
||||
- "call"
|
||||
- "launch"
|
||||
- "process"
|
||||
- "compile"
|
||||
- "interpret"
|
||||
- "inject"
|
||||
|
||||
# ADDITIONAL BLOCK WORDS - Dangerous commands and patterns
|
||||
additional_block_words:
|
||||
# Shell commands
|
||||
- "rm -rf"
|
||||
- "rm -f"
|
||||
- "chmod 777"
|
||||
- "chmod +x"
|
||||
- "sudo"
|
||||
- "su root"
|
||||
- "curl | bash"
|
||||
- "curl | sh"
|
||||
- "wget -O -"
|
||||
- "wget | sh"
|
||||
- "/etc/passwd"
|
||||
- "/etc/shadow"
|
||||
- "mkfs"
|
||||
- "dd if="
|
||||
- "> /dev/sda"
|
||||
- "fork bomb"
|
||||
- "crontab"
|
||||
- "nc -e"
|
||||
- "netcat"
|
||||
- "ncat"
|
||||
- "reverse shell"
|
||||
|
||||
# Script injection
|
||||
- "powershell -enc"
|
||||
- "powershell -e"
|
||||
- "base64 -d"
|
||||
- "base64 --decode"
|
||||
- "<script>"
|
||||
- "javascript:"
|
||||
- "onerror="
|
||||
- "onload="
|
||||
- "document.cookie"
|
||||
- "innerHTML"
|
||||
|
||||
# Python-specific
|
||||
- "__import__"
|
||||
- "os.system"
|
||||
- "subprocess"
|
||||
- "os.popen"
|
||||
- "importlib"
|
||||
|
||||
# ALWAYS BLOCK - Explicit malicious patterns (blocked regardless of context)
|
||||
always_block_keywords:
|
||||
# Destructive shell commands
|
||||
- keyword: "rm -rf /"
|
||||
severity: "high"
|
||||
- keyword: "rm -rf /*"
|
||||
severity: "high"
|
||||
- keyword: "rm -rf ~"
|
||||
severity: "high"
|
||||
- keyword: ":(){ :|:& };:"
|
||||
severity: "high"
|
||||
- keyword: "> /dev/sda"
|
||||
severity: "high"
|
||||
- keyword: "mkfs.ext4 /dev/sda"
|
||||
severity: "high"
|
||||
- keyword: "dd if=/dev/zero of=/dev/sda"
|
||||
severity: "high"
|
||||
|
||||
# Remote code execution
|
||||
- keyword: "curl | sh"
|
||||
severity: "high"
|
||||
- keyword: "curl | bash"
|
||||
severity: "high"
|
||||
- keyword: "wget | sh"
|
||||
severity: "high"
|
||||
- keyword: "wget | bash"
|
||||
severity: "high"
|
||||
- keyword: "eval(base64"
|
||||
severity: "high"
|
||||
- keyword: "eval(atob("
|
||||
severity: "high"
|
||||
- keyword: "python -c 'import os"
|
||||
severity: "high"
|
||||
- keyword: "python3 -c 'import os"
|
||||
severity: "high"
|
||||
- keyword: "bash -i >& /dev/tcp"
|
||||
severity: "high"
|
||||
- keyword: "nc -e /bin/sh"
|
||||
severity: "high"
|
||||
- keyword: "nc -e /bin/bash"
|
||||
severity: "high"
|
||||
|
||||
# Privilege escalation
|
||||
- keyword: "sudo chmod 777 /"
|
||||
severity: "high"
|
||||
- keyword: "echo ALL=(ALL) NOPASSWD"
|
||||
severity: "high"
|
||||
- keyword: "cat /etc/shadow"
|
||||
severity: "high"
|
||||
|
||||
# Script injection
|
||||
- keyword: "<script>alert("
|
||||
severity: "medium"
|
||||
- keyword: "<script>document.cookie"
|
||||
severity: "high"
|
||||
- keyword: "javascript:void"
|
||||
severity: "medium"
|
||||
|
||||
# Encoded payloads
|
||||
- keyword: "powershell -encodedcommand"
|
||||
severity: "high"
|
||||
- keyword: "powershell -enc"
|
||||
severity: "high"
|
||||
|
||||
# Exception patterns (legitimate uses)
|
||||
exceptions:
|
||||
- "code review"
|
||||
- "security audit"
|
||||
- "security testing"
|
||||
- "explain this command"
|
||||
- "what does this code do"
|
||||
- "vulnerability assessment"
|
||||
- "penetration test"
|
||||
- "CTF challenge"
|
||||
- "security training"
|
||||
- "defensive security"
|
||||
- "how to prevent"
|
||||
- "how to protect"
|
||||
- "secure coding"
|
||||
- "code analysis"
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
# SQL Injection Detection - Detects SQL injection attempts in prompts
|
||||
# Uses conditional logic: BLOCK if an identifier word + block word appear in same sentence
|
||||
category_name: "prompt_injection_sql"
|
||||
description: "Detects SQL injection attempts embedded in prompts"
|
||||
default_action: "BLOCK"
|
||||
|
||||
# IDENTIFIER WORDS - SQL keywords that could be used in injection
|
||||
# When combined with block words, triggers blocking
|
||||
identifier_words:
|
||||
- "SELECT"
|
||||
- "INSERT"
|
||||
- "UPDATE"
|
||||
- "DELETE"
|
||||
- "DROP"
|
||||
- "ALTER"
|
||||
- "CREATE"
|
||||
- "EXEC"
|
||||
- "EXECUTE"
|
||||
- "TRUNCATE"
|
||||
- "GRANT"
|
||||
- "REVOKE"
|
||||
|
||||
# ADDITIONAL BLOCK WORDS - Injection-specific patterns
|
||||
additional_block_words:
|
||||
- "UNION"
|
||||
- "1=1"
|
||||
- "OR true"
|
||||
- "OR 1"
|
||||
- "--"
|
||||
- "/*"
|
||||
- "xp_cmdshell"
|
||||
- "information_schema"
|
||||
- "sys.tables"
|
||||
- "INTO OUTFILE"
|
||||
- "LOAD_FILE"
|
||||
- "BENCHMARK"
|
||||
- "SLEEP("
|
||||
- "WAITFOR"
|
||||
- "HAVING 1=1"
|
||||
- "GROUP BY"
|
||||
- "ORDER BY 1"
|
||||
- "INTO DUMPFILE"
|
||||
- "hex("
|
||||
- "char("
|
||||
- "concat("
|
||||
- "@@version"
|
||||
- "schema_name"
|
||||
|
||||
# ALWAYS BLOCK - Explicit SQL injection patterns (blocked regardless of context)
|
||||
always_block_keywords:
|
||||
- keyword: "'; DROP TABLE"
|
||||
severity: "high"
|
||||
- keyword: "'; DELETE FROM"
|
||||
severity: "high"
|
||||
- keyword: "' OR '1'='1"
|
||||
severity: "high"
|
||||
- keyword: "' OR 1=1--"
|
||||
severity: "high"
|
||||
- keyword: "UNION SELECT * FROM"
|
||||
severity: "high"
|
||||
- keyword: "UNION ALL SELECT"
|
||||
severity: "high"
|
||||
- keyword: "'; EXEC xp_cmdshell"
|
||||
severity: "high"
|
||||
- keyword: "1; DROP TABLE"
|
||||
severity: "high"
|
||||
- keyword: "' UNION SELECT NULL"
|
||||
severity: "high"
|
||||
- keyword: "SELECT * FROM users"
|
||||
severity: "medium"
|
||||
- keyword: "SELECT password FROM"
|
||||
severity: "high"
|
||||
- keyword: "INSERT INTO users"
|
||||
severity: "medium"
|
||||
- keyword: "UPDATE users SET"
|
||||
severity: "medium"
|
||||
- keyword: "DELETE FROM users"
|
||||
severity: "high"
|
||||
- keyword: "DROP DATABASE"
|
||||
severity: "high"
|
||||
- keyword: "DROP TABLE users"
|
||||
severity: "high"
|
||||
- keyword: "ALTER TABLE users"
|
||||
severity: "medium"
|
||||
- keyword: "' OR ''='"
|
||||
severity: "high"
|
||||
- keyword: "admin'--"
|
||||
severity: "high"
|
||||
- keyword: "1' OR '1'='1"
|
||||
severity: "high"
|
||||
- keyword: "INFORMATION_SCHEMA.TABLES"
|
||||
severity: "high"
|
||||
- keyword: "INFORMATION_SCHEMA.COLUMNS"
|
||||
severity: "high"
|
||||
- keyword: "LOAD DATA INFILE"
|
||||
severity: "high"
|
||||
- keyword: "INTO OUTFILE"
|
||||
severity: "high"
|
||||
- keyword: "BENCHMARK(10000000"
|
||||
severity: "high"
|
||||
- keyword: "WAITFOR DELAY"
|
||||
severity: "high"
|
||||
- keyword: "SLEEP(5)"
|
||||
severity: "medium"
|
||||
|
||||
# Exception patterns (legitimate uses)
|
||||
exceptions:
|
||||
- "SQL tutorial"
|
||||
- "learn SQL"
|
||||
- "explain SQL"
|
||||
- "SQL syntax"
|
||||
- "SQL documentation"
|
||||
- "database design"
|
||||
- "SQL best practices"
|
||||
- "SQL injection prevention"
|
||||
- "protect against SQL injection"
|
||||
- "parameterized query"
|
||||
- "prepared statement"
|
||||
- "SQL training"
|
||||
- "database course"
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
# System Prompt Extraction Detection - Detects attempts to extract or override system prompts
|
||||
# Uses conditional logic: BLOCK if an identifier word + block word appear in same sentence
|
||||
category_name: "prompt_injection_system_prompt"
|
||||
description: "Detects attempts to extract, reveal, or override system prompts and instructions"
|
||||
default_action: "BLOCK"
|
||||
|
||||
# IDENTIFIER WORDS - Actions that attempt to extract information
|
||||
identifier_words:
|
||||
- "reveal"
|
||||
- "show"
|
||||
- "print"
|
||||
- "output"
|
||||
- "display"
|
||||
- "tell me"
|
||||
- "repeat"
|
||||
- "disclose"
|
||||
- "share"
|
||||
- "give me"
|
||||
- "what is"
|
||||
- "what are"
|
||||
- "list"
|
||||
- "write out"
|
||||
- "copy"
|
||||
- "paste"
|
||||
- "recite"
|
||||
- "read back"
|
||||
- "type out"
|
||||
- "echo"
|
||||
|
||||
# ADDITIONAL BLOCK WORDS - System prompt related terms
|
||||
additional_block_words:
|
||||
- "system prompt"
|
||||
- "system message"
|
||||
- "system instructions"
|
||||
- "initial instructions"
|
||||
- "initial prompt"
|
||||
- "hidden instructions"
|
||||
- "hidden prompt"
|
||||
- "original prompt"
|
||||
- "original instructions"
|
||||
- "pre-prompt"
|
||||
- "preprompt"
|
||||
- "meta-prompt"
|
||||
- "meta prompt"
|
||||
- "developer instructions"
|
||||
- "developer prompt"
|
||||
- "above instructions"
|
||||
- "previous instructions"
|
||||
- "prior instructions"
|
||||
- "opening message"
|
||||
- "configuration prompt"
|
||||
- "base prompt"
|
||||
- "root prompt"
|
||||
- "master prompt"
|
||||
- "custom instructions"
|
||||
- "secret instructions"
|
||||
- "confidential instructions"
|
||||
- "internal instructions"
|
||||
- "rules you follow"
|
||||
- "guidelines you follow"
|
||||
- "instructions you were given"
|
||||
- "prompt you were given"
|
||||
|
||||
# ALWAYS BLOCK - Explicit prompt injection patterns (blocked regardless of context)
|
||||
always_block_keywords:
|
||||
# Direct instruction override
|
||||
- keyword: "ignore previous instructions"
|
||||
severity: "high"
|
||||
- keyword: "ignore all instructions"
|
||||
severity: "high"
|
||||
- keyword: "ignore all previous instructions"
|
||||
severity: "high"
|
||||
- keyword: "ignore prior instructions"
|
||||
severity: "high"
|
||||
- keyword: "ignore above instructions"
|
||||
severity: "high"
|
||||
- keyword: "ignore the above"
|
||||
severity: "high"
|
||||
- keyword: "disregard previous instructions"
|
||||
severity: "high"
|
||||
- keyword: "disregard all instructions"
|
||||
severity: "high"
|
||||
- keyword: "disregard above"
|
||||
severity: "high"
|
||||
- keyword: "disregard your instructions"
|
||||
severity: "high"
|
||||
- keyword: "forget your instructions"
|
||||
severity: "high"
|
||||
- keyword: "forget all instructions"
|
||||
severity: "high"
|
||||
- keyword: "forget everything above"
|
||||
severity: "high"
|
||||
- keyword: "forget your previous instructions"
|
||||
severity: "high"
|
||||
- keyword: "override your instructions"
|
||||
severity: "high"
|
||||
- keyword: "override previous instructions"
|
||||
severity: "high"
|
||||
|
||||
# Persona manipulation
|
||||
- keyword: "you are now"
|
||||
severity: "medium"
|
||||
- keyword: "new persona"
|
||||
severity: "medium"
|
||||
- keyword: "act as DAN"
|
||||
severity: "high"
|
||||
- keyword: "you are DAN"
|
||||
severity: "high"
|
||||
- keyword: "switch to a new persona"
|
||||
severity: "high"
|
||||
- keyword: "new role assignment"
|
||||
severity: "medium"
|
||||
- keyword: "your new instructions are"
|
||||
severity: "high"
|
||||
- keyword: "your new role is"
|
||||
severity: "medium"
|
||||
|
||||
# System prompt extraction
|
||||
- keyword: "repeat your system prompt"
|
||||
severity: "high"
|
||||
- keyword: "print your system prompt"
|
||||
severity: "high"
|
||||
- keyword: "show me your system prompt"
|
||||
severity: "high"
|
||||
- keyword: "output your instructions"
|
||||
severity: "high"
|
||||
- keyword: "what was your system prompt"
|
||||
severity: "high"
|
||||
- keyword: "reveal your prompt"
|
||||
severity: "high"
|
||||
- keyword: "display your instructions"
|
||||
severity: "high"
|
||||
|
||||
# Exception patterns (legitimate uses)
|
||||
exceptions:
|
||||
- "prompt engineering tutorial"
|
||||
- "explain system prompts"
|
||||
- "what is a system prompt"
|
||||
- "how to write a system prompt"
|
||||
- "system prompt best practices"
|
||||
- "prompt design"
|
||||
- "prompt template"
|
||||
- "prompt engineering"
|
||||
- "chatbot development"
|
||||
- "LLM security research"
|
||||
|
|
@ -31,11 +31,15 @@ from litellm import Router
|
|||
from litellm._logging import verbose_proxy_logger
|
||||
from litellm.integrations.custom_guardrail import CustomGuardrail
|
||||
from litellm.proxy._types import UserAPIKeyAuth
|
||||
from litellm.types.utils import GuardrailTracingDetail, ModelResponseStream
|
||||
from litellm.types.utils import (
|
||||
GenericGuardrailAPIInputs,
|
||||
GuardrailStatus,
|
||||
GuardrailTracingDetail,
|
||||
ModelResponseStream,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
|
||||
from litellm.types.utils import GenericGuardrailAPIInputs, GuardrailStatus
|
||||
|
||||
from litellm.types.guardrails import (
|
||||
BlockedWord,
|
||||
|
|
@ -1546,8 +1550,6 @@ class ContentFilterGuardrail(CustomGuardrail):
|
|||
Raises:
|
||||
HTTPException: If sensitive content is detected and action is BLOCK
|
||||
"""
|
||||
from litellm.types.utils import GuardrailStatus
|
||||
|
||||
start_time = datetime.now()
|
||||
detections: List[ContentFilterDetection] = []
|
||||
masked_entity_count: Dict[str, int] = {}
|
||||
|
|
@ -1693,4 +1695,4 @@ class ContentFilterGuardrail(CustomGuardrail):
|
|||
LitellmContentFilterGuardrailConfigModel,
|
||||
)
|
||||
|
||||
return LitellmContentFilterGuardrailConfigModel
|
||||
return LitellmContentFilterGuardrailConfigModel
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
# EU AI Act Article 5.1(d)(g)(h) — Biometric Categorization & Predictive Profiling
|
||||
# Prohibits:
|
||||
# - Real-time remote biometric identification in public spaces (5.1.d)
|
||||
# - Untargeted scraping of facial images for facial recognition databases (5.1.g)
|
||||
# - Biometric categorization to infer race, political opinions, religion,
|
||||
# sexual orientation (5.1.h)
|
||||
# - Predictive policing based solely on profiling (5.1.d related)
|
||||
# Reference: https://artificialintelligenceact.eu/article/5/
|
||||
category_name: "eu_ai_act_art5_biometric_profiling"
|
||||
description: "Art. 5.1(d)(g)(h) — Blocks biometric categorization, facial recognition databases, and predictive profiling"
|
||||
default_action: "BLOCK"
|
||||
|
||||
identifier_words:
|
||||
# System development actions
|
||||
- "build"
|
||||
- "create"
|
||||
- "develop"
|
||||
- "design"
|
||||
- "implement"
|
||||
|
||||
# Analysis/scoring actions
|
||||
- "score"
|
||||
- "rank"
|
||||
- "rate"
|
||||
- "assess"
|
||||
- "evaluate"
|
||||
- "classify"
|
||||
- "profile"
|
||||
- "grade"
|
||||
- "categorize"
|
||||
|
||||
# Detection/recognition actions
|
||||
- "recognize"
|
||||
- "detect"
|
||||
- "identify"
|
||||
- "infer"
|
||||
- "deduce"
|
||||
- "predict"
|
||||
- "determine"
|
||||
- "analyze"
|
||||
- "scrape"
|
||||
- "collect"
|
||||
- "harvest"
|
||||
|
||||
additional_block_words:
|
||||
# Biometric categorization by race/ethnicity
|
||||
- "race from face"
|
||||
- "race from facial"
|
||||
- "race from biometric"
|
||||
- "race from image"
|
||||
- "race from photo"
|
||||
- "ethnicity from face"
|
||||
- "ethnicity from facial"
|
||||
- "ethnicity from biometric"
|
||||
- "ethnicity from image"
|
||||
- "ethnicity from photo"
|
||||
- "racial classification"
|
||||
- "ethnic classification"
|
||||
|
||||
# Biometric categorization by political opinion
|
||||
- "political views from"
|
||||
- "political opinions from"
|
||||
- "political affiliation from"
|
||||
- "political belief from"
|
||||
- "political orientation from"
|
||||
|
||||
# Biometric categorization by sexual orientation
|
||||
- "sexual orientation from"
|
||||
- "sexual preference from"
|
||||
|
||||
# Biometric categorization by religion
|
||||
- "religion from biometric"
|
||||
- "religion from face"
|
||||
- "religious beliefs from"
|
||||
- "religious affiliation from"
|
||||
- "faith from biometric"
|
||||
|
||||
# Facial recognition databases
|
||||
- "facial recognition database"
|
||||
- "facial image database"
|
||||
- "face scraping"
|
||||
- "facial image scraping"
|
||||
- "facial recognition training data"
|
||||
- "biometric database from internet"
|
||||
- "biometric database from cctv"
|
||||
|
||||
# Predictive policing/profiling
|
||||
- "crime prediction"
|
||||
- "criminal behavior"
|
||||
- "criminal prediction"
|
||||
- "recidivism"
|
||||
- "recidivism prediction"
|
||||
- "crime risk score"
|
||||
- "criminal risk assessment"
|
||||
- "crime probability"
|
||||
- "criminal profiling"
|
||||
|
||||
# Employment predictive profiling
|
||||
- "employee performance prediction"
|
||||
- "worker performance prediction"
|
||||
- "termination prediction"
|
||||
- "attrition prediction from behavior"
|
||||
|
||||
always_block_keywords:
|
||||
- keyword: "infer race from face"
|
||||
severity: "high"
|
||||
- keyword: "predict race from facial"
|
||||
severity: "high"
|
||||
- keyword: "infer ethnicity from biometric"
|
||||
severity: "high"
|
||||
- keyword: "predict political opinions from"
|
||||
severity: "high"
|
||||
- keyword: "biometric categorization system"
|
||||
severity: "high"
|
||||
- keyword: "predictive policing"
|
||||
severity: "high"
|
||||
- keyword: "predictive policing algorithm"
|
||||
severity: "high"
|
||||
- keyword: "crime prediction algorithm"
|
||||
severity: "high"
|
||||
- keyword: "recidivism prediction"
|
||||
severity: "high"
|
||||
- keyword: "facial recognition database scraping"
|
||||
severity: "high"
|
||||
- keyword: "untargeted facial image scraping"
|
||||
severity: "high"
|
||||
- keyword: "mass biometric surveillance"
|
||||
severity: "high"
|
||||
|
||||
exceptions:
|
||||
- "research on"
|
||||
- "study on"
|
||||
- "academic"
|
||||
- "thesis on"
|
||||
- "explain"
|
||||
- "what is"
|
||||
- "article 5"
|
||||
- "prohibited by"
|
||||
- "audit for bias"
|
||||
- "detect discrimination"
|
||||
- "compliance monitoring"
|
||||
- "ethical review"
|
||||
- "fairness testing"
|
||||
- "movie"
|
||||
- "game"
|
||||
- "forensic"
|
||||
- "missing person"
|
||||
- "targeted search"
|
||||
|
|
@ -0,0 +1,157 @@
|
|||
# Loi sur l'IA (UE) Article 5.1(d)(g)(h) — Catégorisation biométrique et profilage prédictif
|
||||
# Interdit :
|
||||
# - L'identification biométrique à distance en temps réel dans les espaces publics (5.1.d)
|
||||
# - La collecte non ciblée d'images faciales pour les bases de données de reconnaissance faciale (5.1.g)
|
||||
# - La catégorisation biométrique pour inférer la race, les opinions politiques, la religion,
|
||||
# l'orientation sexuelle (5.1.h)
|
||||
# - La police prédictive basée uniquement sur le profilage (5.1.d)
|
||||
# Référence : https://artificialintelligenceact.eu/article/5/
|
||||
category_name: "eu_ai_act_art5_biometric_profiling_fr"
|
||||
description: "Art. 5.1(d)(g)(h) — Bloque la catégorisation biométrique, les bases de reconnaissance faciale et le profilage prédictif"
|
||||
default_action: "BLOCK"
|
||||
|
||||
identifier_words:
|
||||
# Actions de développement
|
||||
- "construire"
|
||||
- "créer"
|
||||
- "développer"
|
||||
- "concevoir"
|
||||
- "implémenter"
|
||||
- "mettre en place"
|
||||
|
||||
# Actions de notation/évaluation
|
||||
- "noter"
|
||||
- "classer"
|
||||
- "évaluer"
|
||||
- "estimer"
|
||||
- "classifier"
|
||||
- "profiler"
|
||||
- "catégoriser"
|
||||
|
||||
# Actions de détection
|
||||
- "reconnaître"
|
||||
- "détecter"
|
||||
- "identifier"
|
||||
- "inférer"
|
||||
- "déduire"
|
||||
- "prédire"
|
||||
- "déterminer"
|
||||
- "analyser"
|
||||
- "collecter"
|
||||
- "récolter"
|
||||
- "scraper"
|
||||
- "aspirer"
|
||||
|
||||
additional_block_words:
|
||||
# Catégorisation biométrique par race/ethnie
|
||||
- "race à partir du visage"
|
||||
- "race à partir de biométrie"
|
||||
- "race à partir du biométrique"
|
||||
- "race à partir de l'image"
|
||||
- "race à partir de la photo"
|
||||
- "ethnie à partir du visage"
|
||||
- "ethnie à partir de biométrie"
|
||||
- "ethnie à partir du biométrique"
|
||||
- "ethnie à partir de l'image"
|
||||
- "classification raciale"
|
||||
- "classification ethnique"
|
||||
|
||||
# Catégorisation par opinions politiques
|
||||
- "opinions politiques à partir"
|
||||
- "vues politiques à partir"
|
||||
- "affiliation politique à partir"
|
||||
- "croyances politiques à partir"
|
||||
- "orientation politique à partir"
|
||||
|
||||
# Catégorisation par orientation sexuelle
|
||||
- "orientation sexuelle à partir"
|
||||
- "préférence sexuelle à partir"
|
||||
|
||||
# Catégorisation par religion
|
||||
- "religion à partir de biométrie"
|
||||
- "religion à partir du biométrique"
|
||||
- "religion à partir du visage"
|
||||
- "croyances religieuses à partir"
|
||||
- "affiliation religieuse à partir"
|
||||
|
||||
# Bases de données de reconnaissance faciale
|
||||
- "base de données de reconnaissance faciale"
|
||||
- "base de données d'images faciales"
|
||||
- "collecte d'images faciales"
|
||||
- "aspiration d'images faciales"
|
||||
- "données d'entraînement reconnaissance faciale"
|
||||
- "base de données biométrique internet"
|
||||
- "base de données biométrique vidéosurveillance"
|
||||
|
||||
# Police prédictive / profilage prédictif
|
||||
- "prédiction de crime"
|
||||
- "prédiction criminelle"
|
||||
- "comportement criminel"
|
||||
- "récidive"
|
||||
- "prédiction de la récidive"
|
||||
- "score de risque criminel"
|
||||
- "évaluation du risque criminel"
|
||||
- "probabilité criminelle"
|
||||
- "profilage criminel"
|
||||
|
||||
# Profilage prédictif en emploi
|
||||
- "prédiction de performance des employés"
|
||||
- "prédiction de performance des salariés"
|
||||
- "prédiction de licenciement"
|
||||
- "prédiction d'attrition"
|
||||
|
||||
always_block_keywords:
|
||||
- keyword: "inférer la race à partir du visage"
|
||||
severity: "high"
|
||||
- keyword: "prédire la race à partir du visage"
|
||||
severity: "high"
|
||||
- keyword: "déduire la race à partir du visage"
|
||||
severity: "high"
|
||||
- keyword: "inférer l'ethnie à partir de biométrie"
|
||||
severity: "high"
|
||||
- keyword: "prédire l'ethnie à partir de biométrie"
|
||||
severity: "high"
|
||||
- keyword: "prédire les opinions politiques à partir"
|
||||
severity: "high"
|
||||
- keyword: "inférer les opinions politiques à partir"
|
||||
severity: "high"
|
||||
- keyword: "système de catégorisation biométrique"
|
||||
severity: "high"
|
||||
- keyword: "police prédictive"
|
||||
severity: "high"
|
||||
- keyword: "algorithme de prédiction de crime"
|
||||
severity: "high"
|
||||
- keyword: "algorithme de prédiction criminelle"
|
||||
severity: "high"
|
||||
- keyword: "prédiction de récidive"
|
||||
severity: "high"
|
||||
- keyword: "collecte non ciblée d'images faciales"
|
||||
severity: "high"
|
||||
- keyword: "surveillance biométrique de masse"
|
||||
severity: "high"
|
||||
|
||||
exceptions:
|
||||
- "recherche sur"
|
||||
- "étude sur"
|
||||
- "académique"
|
||||
- "thèse sur"
|
||||
- "mémoire sur"
|
||||
- "expliquer"
|
||||
- "qu'est-ce que"
|
||||
- "c'est quoi"
|
||||
- "article 5"
|
||||
- "interdit par"
|
||||
- "prohibé par"
|
||||
- "audit de biais"
|
||||
- "audit des biais"
|
||||
- "détecter la discrimination"
|
||||
- "détection de discrimination"
|
||||
- "surveillance de conformité"
|
||||
- "contrôle de conformité"
|
||||
- "examen éthique"
|
||||
- "test d'équité"
|
||||
- "dans un film"
|
||||
- "jeu vidéo"
|
||||
- "médico-légal"
|
||||
- "personne disparue"
|
||||
- "recherche ciblée"
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
# EU AI Act Article 5.1(f) — Emotion Recognition in Workplace & Education
|
||||
# Prohibits AI systems that infer emotions in the workplace or educational
|
||||
# institutions, except for medical or safety reasons.
|
||||
# Reference: https://artificialintelligenceact.eu/article/5/
|
||||
category_name: "eu_ai_act_art5_emotion_recognition"
|
||||
description: "Art. 5.1(f) — Blocks emotion recognition and sentiment analysis in workplace and educational settings"
|
||||
default_action: "BLOCK"
|
||||
|
||||
identifier_words:
|
||||
# System development actions
|
||||
- "build"
|
||||
- "create"
|
||||
- "develop"
|
||||
- "design"
|
||||
- "implement"
|
||||
- "deploy"
|
||||
- "setup"
|
||||
- "install"
|
||||
|
||||
# Detection/recognition actions
|
||||
- "recognize"
|
||||
- "detect"
|
||||
- "identify"
|
||||
- "infer"
|
||||
- "deduce"
|
||||
- "predict"
|
||||
- "determine"
|
||||
- "analyze"
|
||||
- "measure"
|
||||
- "monitor"
|
||||
- "track"
|
||||
- "assess"
|
||||
- "evaluate"
|
||||
- "classify"
|
||||
|
||||
additional_block_words:
|
||||
# Employee emotion
|
||||
- "employee emotion"
|
||||
- "employee emotions"
|
||||
- "employee mood"
|
||||
- "employee moods"
|
||||
- "employee sentiment"
|
||||
- "employee feeling"
|
||||
- "employee feelings"
|
||||
- "employee affect"
|
||||
- "employee mental state"
|
||||
|
||||
# Worker emotion
|
||||
- "worker emotion"
|
||||
- "worker emotions"
|
||||
- "worker mood"
|
||||
- "worker sentiment"
|
||||
- "worker feeling"
|
||||
- "worker feelings"
|
||||
- "worker mental state"
|
||||
|
||||
# Staff emotion
|
||||
- "staff emotion"
|
||||
- "staff emotions"
|
||||
- "staff mood"
|
||||
- "staff sentiment"
|
||||
- "staff feeling"
|
||||
|
||||
# Workplace emotion
|
||||
- "workplace emotion"
|
||||
- "workplace emotions"
|
||||
- "workplace mood"
|
||||
- "workplace sentiment"
|
||||
- "workplace feeling"
|
||||
- "office emotion"
|
||||
- "office mood"
|
||||
- "office sentiment"
|
||||
|
||||
# Student emotion
|
||||
- "student emotion"
|
||||
- "student emotions"
|
||||
- "student mood"
|
||||
- "student sentiment"
|
||||
- "student feeling"
|
||||
- "student feelings"
|
||||
- "student mental state"
|
||||
- "pupil emotion"
|
||||
- "pupil mood"
|
||||
|
||||
# Classroom emotion
|
||||
- "classroom emotion"
|
||||
- "classroom emotions"
|
||||
- "classroom mood"
|
||||
- "classroom sentiment"
|
||||
- "classroom engagement"
|
||||
- "school emotion"
|
||||
- "school mood"
|
||||
- "lecture emotion"
|
||||
|
||||
# Job candidate/interview emotion
|
||||
- "candidate emotion"
|
||||
- "candidate sentiment"
|
||||
- "interview emotion"
|
||||
- "interview sentiment"
|
||||
- "applicant emotion"
|
||||
- "applicant sentiment"
|
||||
|
||||
always_block_keywords:
|
||||
- keyword: "emotion recognition in workplace"
|
||||
severity: "high"
|
||||
- keyword: "emotion detection of employees"
|
||||
severity: "high"
|
||||
- keyword: "emotion recognition in classroom"
|
||||
severity: "high"
|
||||
- keyword: "student emotion detection"
|
||||
severity: "high"
|
||||
- keyword: "workplace emotion monitoring"
|
||||
severity: "high"
|
||||
- keyword: "employee emotion tracking"
|
||||
severity: "high"
|
||||
- keyword: "employee sentiment analysis system"
|
||||
severity: "high"
|
||||
- keyword: "classroom emotion surveillance"
|
||||
severity: "high"
|
||||
- keyword: "worker mood monitoring system"
|
||||
severity: "high"
|
||||
- keyword: "student engagement emotion tracking"
|
||||
severity: "high"
|
||||
|
||||
exceptions:
|
||||
- "research on"
|
||||
- "study on"
|
||||
- "academic"
|
||||
- "thesis on"
|
||||
- "explain"
|
||||
- "what is"
|
||||
- "article 5"
|
||||
- "prohibited by"
|
||||
- "compliance monitoring"
|
||||
- "ethical review"
|
||||
- "movie"
|
||||
- "game"
|
||||
- "customer feedback"
|
||||
- "product review"
|
||||
- "medical"
|
||||
- "safety reason"
|
||||
- "driver drowsiness"
|
||||
- "pilot fatigue"
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
# Loi sur l'IA (UE) Article 5.1(f) — Reconnaissance des émotions au travail et dans l'éducation
|
||||
# Interdit les systèmes d'IA qui infèrent les émotions sur le lieu de travail
|
||||
# ou dans les établissements d'enseignement, sauf pour des raisons médicales ou de sécurité.
|
||||
# Référence : https://artificialintelligenceact.eu/article/5/
|
||||
category_name: "eu_ai_act_art5_emotion_recognition_fr"
|
||||
description: "Art. 5.1(f) — Bloque la reconnaissance des émotions et l'analyse des sentiments au travail et dans l'éducation"
|
||||
default_action: "BLOCK"
|
||||
|
||||
identifier_words:
|
||||
# Actions de développement
|
||||
- "construire"
|
||||
- "créer"
|
||||
- "développer"
|
||||
- "concevoir"
|
||||
- "implémenter"
|
||||
- "mettre en place"
|
||||
- "déployer"
|
||||
- "installer"
|
||||
|
||||
# Actions de détection
|
||||
- "reconnaître"
|
||||
- "détecter"
|
||||
- "identifier"
|
||||
- "inférer"
|
||||
- "déduire"
|
||||
- "prédire"
|
||||
- "déterminer"
|
||||
- "analyser"
|
||||
- "mesurer"
|
||||
- "surveiller"
|
||||
- "monitorer"
|
||||
- "évaluer"
|
||||
- "classifier"
|
||||
- "repérer"
|
||||
- "déceler"
|
||||
|
||||
additional_block_words:
|
||||
# Émotion des employés
|
||||
- "émotion des employés"
|
||||
- "émotion des salariés"
|
||||
- "humeur des employés"
|
||||
- "humeur des salariés"
|
||||
- "sentiment des employés"
|
||||
- "sentiment des salariés"
|
||||
- "état émotionnel employés"
|
||||
- "état émotionnel salariés"
|
||||
- "ressenti des employés"
|
||||
- "ressenti des salariés"
|
||||
|
||||
# Émotion des travailleurs
|
||||
- "émotion des travailleurs"
|
||||
- "émotion du personnel"
|
||||
- "humeur des travailleurs"
|
||||
- "sentiment des travailleurs"
|
||||
- "état émotionnel travailleurs"
|
||||
|
||||
# Émotion au travail
|
||||
- "émotion au travail"
|
||||
- "émotions au travail"
|
||||
- "humeur au travail"
|
||||
- "sentiment au travail"
|
||||
- "émotion au bureau"
|
||||
- "humeur au bureau"
|
||||
|
||||
# Émotion des étudiants
|
||||
- "émotion des étudiants"
|
||||
- "émotion des élèves"
|
||||
- "humeur des étudiants"
|
||||
- "sentiment des étudiants"
|
||||
- "sentiment des élèves"
|
||||
- "état émotionnel étudiants"
|
||||
- "ressenti des étudiants"
|
||||
|
||||
# Émotion en classe
|
||||
- "émotion en classe"
|
||||
- "émotions en classe"
|
||||
- "humeur en classe"
|
||||
- "sentiment en classe"
|
||||
- "émotion à l'école"
|
||||
- "émotion en cours"
|
||||
|
||||
# Émotion des candidats
|
||||
- "émotion des candidats"
|
||||
- "sentiment des candidats"
|
||||
- "émotion en entretien"
|
||||
- "sentiment en entretien"
|
||||
|
||||
always_block_keywords:
|
||||
- keyword: "reconnaissance d'émotions au travail"
|
||||
severity: "high"
|
||||
- keyword: "détection d'émotions des employés"
|
||||
severity: "high"
|
||||
- keyword: "détection d'émotions des salariés"
|
||||
severity: "high"
|
||||
- keyword: "reconnaissance d'émotions en classe"
|
||||
severity: "high"
|
||||
- keyword: "détection d'émotions des étudiants"
|
||||
severity: "high"
|
||||
- keyword: "détection d'émotions des élèves"
|
||||
severity: "high"
|
||||
- keyword: "surveillance des émotions au travail"
|
||||
severity: "high"
|
||||
- keyword: "suivi des émotions des employés"
|
||||
severity: "high"
|
||||
- keyword: "système d'analyse du sentiment des employés"
|
||||
severity: "high"
|
||||
- keyword: "surveillance des émotions en classe"
|
||||
severity: "high"
|
||||
|
||||
exceptions:
|
||||
- "recherche sur"
|
||||
- "étude sur"
|
||||
- "académique"
|
||||
- "thèse sur"
|
||||
- "mémoire sur"
|
||||
- "expliquer"
|
||||
- "qu'est-ce que"
|
||||
- "c'est quoi"
|
||||
- "article 5"
|
||||
- "interdit par"
|
||||
- "prohibé par"
|
||||
- "surveillance de conformité"
|
||||
- "contrôle de conformité"
|
||||
- "examen éthique"
|
||||
- "dans un film"
|
||||
- "dans le film"
|
||||
- "jeu vidéo"
|
||||
- "retour client"
|
||||
- "médical"
|
||||
- "raison de sécurité"
|
||||
- "somnolence du conducteur"
|
||||
- "fatigue du pilote"
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
# EU AI Act Article 5.1(a) — Subliminal Manipulation & Deceptive Techniques
|
||||
# Prohibits AI systems that deploy subliminal, manipulative, or deceptive techniques
|
||||
# to distort behavior and impair informed decision-making.
|
||||
# Reference: https://artificialintelligenceact.eu/article/5/
|
||||
category_name: "eu_ai_act_art5_manipulation"
|
||||
description: "Art. 5.1(a) — Blocks subliminal manipulation, deceptive AI techniques, and covert behavioral influence"
|
||||
default_action: "BLOCK"
|
||||
|
||||
identifier_words:
|
||||
# System development actions
|
||||
- "build"
|
||||
- "create"
|
||||
- "develop"
|
||||
- "design"
|
||||
- "implement"
|
||||
- "deploy"
|
||||
- "launch"
|
||||
- "use"
|
||||
|
||||
# Influence actions
|
||||
- "manipulate"
|
||||
- "influence"
|
||||
- "persuade"
|
||||
- "nudge"
|
||||
- "coerce"
|
||||
- "exploit"
|
||||
- "target"
|
||||
- "embed"
|
||||
- "inject"
|
||||
- "insert"
|
||||
|
||||
additional_block_words:
|
||||
# Subliminal techniques
|
||||
- "subliminal"
|
||||
- "subliminal message"
|
||||
- "subliminal advertising"
|
||||
- "subliminal technique"
|
||||
- "subliminal signal"
|
||||
- "subliminal stimulus"
|
||||
- "subliminal perception"
|
||||
- "subliminal cue"
|
||||
|
||||
# Subconscious manipulation
|
||||
- "subconscious"
|
||||
- "subconscious manipulation"
|
||||
- "subconscious influence"
|
||||
- "subconscious persuasion"
|
||||
- "below conscious awareness"
|
||||
|
||||
# Behavioral manipulation
|
||||
- "behavior manipulation"
|
||||
- "behavioral manipulation"
|
||||
- "behavioral distortion"
|
||||
- "manipulative design"
|
||||
- "dark pattern"
|
||||
- "deceptive pattern"
|
||||
- "deceptive design"
|
||||
- "coercive design"
|
||||
|
||||
# Decision impairment
|
||||
- "impair decision"
|
||||
- "distort decision"
|
||||
- "undermine autonomy"
|
||||
- "override consent"
|
||||
- "bypass rational"
|
||||
|
||||
always_block_keywords:
|
||||
- keyword: "subliminal manipulation system"
|
||||
severity: "high"
|
||||
- keyword: "subliminal advertising system"
|
||||
severity: "high"
|
||||
- keyword: "covert behavioral manipulation"
|
||||
severity: "high"
|
||||
- keyword: "subconscious manipulation tool"
|
||||
severity: "high"
|
||||
- keyword: "deceptive AI manipulation"
|
||||
severity: "high"
|
||||
- keyword: "dark pattern AI system"
|
||||
severity: "high"
|
||||
|
||||
exceptions:
|
||||
- "research on"
|
||||
- "study on"
|
||||
- "academic"
|
||||
- "thesis on"
|
||||
- "explain"
|
||||
- "what is"
|
||||
- "article 5"
|
||||
- "prohibited by"
|
||||
- "detect dark pattern"
|
||||
- "audit for"
|
||||
- "compliance monitoring"
|
||||
- "ethical review"
|
||||
- "how to prevent"
|
||||
- "how to avoid"
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
# Loi sur l'IA (UE) Article 5.1(a) — Manipulation subliminale et techniques trompeuses
|
||||
# Interdit les systèmes d'IA qui utilisent des techniques subliminales, manipulatrices
|
||||
# ou trompeuses pour fausser le comportement et altérer la prise de décision éclairée.
|
||||
# Référence : https://artificialintelligenceact.eu/article/5/
|
||||
category_name: "eu_ai_act_art5_manipulation_fr"
|
||||
description: "Art. 5.1(a) — Bloque la manipulation subliminale, les techniques d'IA trompeuses et l'influence comportementale cachée"
|
||||
default_action: "BLOCK"
|
||||
|
||||
identifier_words:
|
||||
# Actions de développement
|
||||
- "construire"
|
||||
- "créer"
|
||||
- "développer"
|
||||
- "concevoir"
|
||||
- "implémenter"
|
||||
- "mettre en place"
|
||||
- "déployer"
|
||||
- "lancer"
|
||||
- "utiliser"
|
||||
|
||||
# Actions d'influence
|
||||
- "manipuler"
|
||||
- "influencer"
|
||||
- "persuader"
|
||||
- "inciter"
|
||||
- "contraindre"
|
||||
- "exploiter"
|
||||
- "cibler"
|
||||
- "intégrer"
|
||||
- "injecter"
|
||||
- "insérer"
|
||||
|
||||
additional_block_words:
|
||||
# Techniques subliminales
|
||||
- "subliminal"
|
||||
- "subliminale"
|
||||
- "message subliminal"
|
||||
- "publicité subliminale"
|
||||
- "technique subliminale"
|
||||
- "signal subliminal"
|
||||
- "stimulus subliminal"
|
||||
- "perception subliminale"
|
||||
|
||||
# Manipulation subconsciente
|
||||
- "subconscient"
|
||||
- "inconscient"
|
||||
- "manipulation subconsciente"
|
||||
- "influence subconsciente"
|
||||
- "persuasion subconsciente"
|
||||
- "en dessous du seuil de conscience"
|
||||
|
||||
# Manipulation comportementale
|
||||
- "manipulation de comportement"
|
||||
- "manipulation comportementale"
|
||||
- "distorsion comportementale"
|
||||
- "conception manipulatrice"
|
||||
- "dark pattern"
|
||||
- "schéma trompeur"
|
||||
- "conception trompeuse"
|
||||
- "conception coercitive"
|
||||
|
||||
# Altération des décisions
|
||||
- "altérer la décision"
|
||||
- "fausser la décision"
|
||||
- "saper l'autonomie"
|
||||
- "contourner le consentement"
|
||||
|
||||
always_block_keywords:
|
||||
- keyword: "système de manipulation subliminale"
|
||||
severity: "high"
|
||||
- keyword: "système de publicité subliminale"
|
||||
severity: "high"
|
||||
- keyword: "manipulation comportementale cachée"
|
||||
severity: "high"
|
||||
- keyword: "outil de manipulation subconsciente"
|
||||
severity: "high"
|
||||
- keyword: "manipulation IA trompeuse"
|
||||
severity: "high"
|
||||
- keyword: "système IA dark pattern"
|
||||
severity: "high"
|
||||
|
||||
exceptions:
|
||||
- "recherche sur"
|
||||
- "étude sur"
|
||||
- "académique"
|
||||
- "thèse sur"
|
||||
- "expliquer"
|
||||
- "qu'est-ce que"
|
||||
- "c'est quoi"
|
||||
- "article 5"
|
||||
- "interdit par"
|
||||
- "prohibé par"
|
||||
- "détecter les dark patterns"
|
||||
- "audit"
|
||||
- "surveillance de conformité"
|
||||
- "contrôle de conformité"
|
||||
- "examen éthique"
|
||||
- "comment prévenir"
|
||||
- "comment éviter"
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
# EU AI Act Article 5.1(c) — Social Scoring Systems
|
||||
# Prohibits AI systems that evaluate or classify persons based on social behavior
|
||||
# or personal traits, leading to detrimental or unfavorable treatment.
|
||||
# Reference: https://artificialintelligenceact.eu/article/5/
|
||||
category_name: "eu_ai_act_art5_social_scoring"
|
||||
description: "Art. 5.1(c) — Blocks social scoring, citizen rating, and trustworthiness classification systems"
|
||||
default_action: "BLOCK"
|
||||
|
||||
identifier_words:
|
||||
# System development actions
|
||||
- "build"
|
||||
- "create"
|
||||
- "develop"
|
||||
- "design"
|
||||
- "implement"
|
||||
|
||||
# Scoring/rating actions
|
||||
- "score"
|
||||
- "rank"
|
||||
- "rate"
|
||||
- "assess"
|
||||
- "evaluate"
|
||||
- "classify"
|
||||
- "profile"
|
||||
- "grade"
|
||||
- "categorize"
|
||||
- "tier"
|
||||
|
||||
# Detection/analysis actions
|
||||
- "recognize"
|
||||
- "detect"
|
||||
- "identify"
|
||||
- "infer"
|
||||
- "deduce"
|
||||
- "predict"
|
||||
- "determine"
|
||||
- "analyze"
|
||||
- "monitor"
|
||||
- "track"
|
||||
|
||||
additional_block_words:
|
||||
# Social scoring
|
||||
- "social behavior"
|
||||
- "social credit"
|
||||
- "social score"
|
||||
- "social rating"
|
||||
- "social ranking"
|
||||
- "social standing"
|
||||
|
||||
# Citizen scoring
|
||||
- "citizen score"
|
||||
- "citizen rating"
|
||||
- "citizen ranking"
|
||||
- "citizen classification"
|
||||
- "citizen trustworthiness"
|
||||
|
||||
# Trustworthiness/reputation scoring
|
||||
- "trustworthiness"
|
||||
- "trustworthiness score"
|
||||
- "reputation score"
|
||||
- "reputation rating"
|
||||
- "behavioral score"
|
||||
- "behavioral rating"
|
||||
- "behavioral classification"
|
||||
- "loyalty score"
|
||||
- "obedience score"
|
||||
|
||||
# Social media scoring
|
||||
- "social media behavior"
|
||||
- "social media score"
|
||||
- "online behavior score"
|
||||
- "digital reputation score"
|
||||
- "creditworthiness from social"
|
||||
|
||||
# Personality-based scoring
|
||||
- "personality score"
|
||||
- "character score"
|
||||
- "moral score"
|
||||
- "virtue score"
|
||||
|
||||
always_block_keywords:
|
||||
- keyword: "social credit system"
|
||||
severity: "high"
|
||||
- keyword: "social scoring system"
|
||||
severity: "high"
|
||||
- keyword: "citizen scoring"
|
||||
severity: "high"
|
||||
- keyword: "citizen scoring system"
|
||||
severity: "high"
|
||||
- keyword: "social credit score"
|
||||
severity: "high"
|
||||
- keyword: "government social score"
|
||||
severity: "high"
|
||||
- keyword: "trustworthiness scoring system"
|
||||
severity: "high"
|
||||
- keyword: "reputation scoring system"
|
||||
severity: "high"
|
||||
- keyword: "behavioral scoring system"
|
||||
severity: "high"
|
||||
|
||||
exceptions:
|
||||
- "research on"
|
||||
- "study on"
|
||||
- "academic"
|
||||
- "thesis on"
|
||||
- "explain"
|
||||
- "what is"
|
||||
- "article 5"
|
||||
- "prohibited by"
|
||||
- "audit for bias"
|
||||
- "detect discrimination"
|
||||
- "compliance monitoring"
|
||||
- "ethical review"
|
||||
- "fairness testing"
|
||||
- "product review"
|
||||
- "customer feedback"
|
||||
- "credit score"
|
||||
- "net promoter score"
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
# Loi sur l'IA (UE) Article 5.1(c) — Systèmes de notation sociale
|
||||
# Interdit les systèmes d'IA qui évaluent ou classent les personnes en fonction
|
||||
# de leur comportement social ou de leurs caractéristiques personnelles.
|
||||
# Référence : https://artificialintelligenceact.eu/article/5/
|
||||
category_name: "eu_ai_act_art5_social_scoring_fr"
|
||||
description: "Art. 5.1(c) — Bloque les systèmes de notation sociale, de notation des citoyens et de classification de fiabilité"
|
||||
default_action: "BLOCK"
|
||||
|
||||
identifier_words:
|
||||
# Actions de développement
|
||||
- "construire"
|
||||
- "créer"
|
||||
- "développer"
|
||||
- "concevoir"
|
||||
- "implémenter"
|
||||
- "mettre en place"
|
||||
- "établir"
|
||||
- "bâtir"
|
||||
- "élaborer"
|
||||
|
||||
# Actions de notation/évaluation
|
||||
- "noter"
|
||||
- "classer"
|
||||
- "évaluer"
|
||||
- "estimer"
|
||||
- "classifier"
|
||||
- "profiler"
|
||||
- "coter"
|
||||
- "juger"
|
||||
- "attribuer une note"
|
||||
- "attribuer un score"
|
||||
- "donner une note"
|
||||
- "donner un score"
|
||||
- "catégoriser"
|
||||
|
||||
# Actions de détection
|
||||
- "reconnaître"
|
||||
- "détecter"
|
||||
- "identifier"
|
||||
- "inférer"
|
||||
- "déduire"
|
||||
- "prédire"
|
||||
- "déterminer"
|
||||
- "analyser"
|
||||
- "surveiller"
|
||||
- "monitorer"
|
||||
|
||||
additional_block_words:
|
||||
# Notation sociale
|
||||
- "comportement social"
|
||||
- "crédit social"
|
||||
- "score social"
|
||||
- "note sociale"
|
||||
- "notation sociale"
|
||||
- "classement social"
|
||||
- "rang social"
|
||||
|
||||
# Notation des citoyens
|
||||
- "score de citoyen"
|
||||
- "note de citoyen"
|
||||
- "notation des citoyens"
|
||||
- "classement des citoyens"
|
||||
- "fiabilité des citoyens"
|
||||
|
||||
# Fiabilité et réputation
|
||||
- "fiabilité"
|
||||
- "score de fiabilité"
|
||||
- "score de réputation"
|
||||
- "note de réputation"
|
||||
- "score comportemental"
|
||||
- "note comportementale"
|
||||
- "classification comportementale"
|
||||
- "score de loyauté"
|
||||
- "score d'obéissance"
|
||||
- "réputation sociale"
|
||||
|
||||
# Réseaux sociaux
|
||||
- "comportement sur les réseaux sociaux"
|
||||
- "comportement médias sociaux"
|
||||
- "score des réseaux sociaux"
|
||||
- "solvabilité à partir des réseaux sociaux"
|
||||
- "solvabilité à partir du social"
|
||||
|
||||
always_block_keywords:
|
||||
- keyword: "système de crédit social"
|
||||
severity: "high"
|
||||
- keyword: "système de notation sociale"
|
||||
severity: "high"
|
||||
- keyword: "système de score social"
|
||||
severity: "high"
|
||||
- keyword: "notation des citoyens"
|
||||
severity: "high"
|
||||
- keyword: "système de notation des citoyens"
|
||||
severity: "high"
|
||||
- keyword: "système pour noter le comportement social"
|
||||
severity: "high"
|
||||
- keyword: "système pour évaluer le comportement social"
|
||||
severity: "high"
|
||||
- keyword: "système pour classer le comportement social"
|
||||
severity: "high"
|
||||
- keyword: "système de score de réputation"
|
||||
severity: "high"
|
||||
|
||||
exceptions:
|
||||
- "recherche sur"
|
||||
- "étude sur"
|
||||
- "académique"
|
||||
- "thèse sur"
|
||||
- "mémoire sur"
|
||||
- "expliquer"
|
||||
- "qu'est-ce que"
|
||||
- "c'est quoi"
|
||||
- "article 5"
|
||||
- "interdit par"
|
||||
- "prohibé par"
|
||||
- "audit de biais"
|
||||
- "audit des biais"
|
||||
- "détecter la discrimination"
|
||||
- "détection de discrimination"
|
||||
- "surveillance de conformité"
|
||||
- "contrôle de conformité"
|
||||
- "examen éthique"
|
||||
- "test d'équité"
|
||||
- "avis sur le produit"
|
||||
- "avis produit"
|
||||
- "retour client"
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
# EU AI Act Article 5.1(b) — Exploitation of Vulnerabilities
|
||||
# Prohibits AI systems that exploit vulnerabilities of persons due to age,
|
||||
# disability, or social/economic situation to distort their behavior.
|
||||
# Reference: https://artificialintelligenceact.eu/article/5/
|
||||
category_name: "eu_ai_act_art5_vulnerability"
|
||||
description: "Art. 5.1(b) — Blocks AI systems that exploit vulnerabilities of children, elderly, or disabled persons"
|
||||
default_action: "BLOCK"
|
||||
|
||||
identifier_words:
|
||||
# System development actions
|
||||
- "build"
|
||||
- "create"
|
||||
- "develop"
|
||||
- "design"
|
||||
- "implement"
|
||||
|
||||
# Exploitation actions
|
||||
- "exploit"
|
||||
- "target"
|
||||
- "manipulate"
|
||||
- "take advantage"
|
||||
- "abuse"
|
||||
- "leverage"
|
||||
- "prey on"
|
||||
|
||||
# Analysis/profiling actions
|
||||
- "profile"
|
||||
- "assess"
|
||||
- "evaluate"
|
||||
- "analyze"
|
||||
- "score"
|
||||
- "classify"
|
||||
- "detect"
|
||||
- "identify"
|
||||
|
||||
additional_block_words:
|
||||
# Children vulnerability
|
||||
- "children vulnerability"
|
||||
- "child vulnerability"
|
||||
- "minor vulnerability"
|
||||
- "vulnerable children"
|
||||
- "vulnerable minors"
|
||||
- "underage vulnerability"
|
||||
- "child susceptibility"
|
||||
- "children susceptibility"
|
||||
- "targeting children"
|
||||
- "targeting minors"
|
||||
|
||||
# Elderly vulnerability
|
||||
- "elderly vulnerability"
|
||||
- "senior vulnerability"
|
||||
- "old age vulnerability"
|
||||
- "vulnerable elderly"
|
||||
- "vulnerable seniors"
|
||||
- "elderly susceptibility"
|
||||
- "targeting elderly"
|
||||
- "targeting seniors"
|
||||
- "cognitive decline"
|
||||
|
||||
# Disability vulnerability
|
||||
- "disabled vulnerability"
|
||||
- "disability vulnerability"
|
||||
- "vulnerable disabled"
|
||||
- "disabled susceptibility"
|
||||
- "intellectual disability"
|
||||
- "cognitive impairment"
|
||||
- "mental disability"
|
||||
- "targeting disabled"
|
||||
|
||||
# Economic vulnerability
|
||||
- "economic vulnerability"
|
||||
- "financial vulnerability"
|
||||
- "poverty vulnerability"
|
||||
- "low income vulnerability"
|
||||
- "socioeconomic vulnerability"
|
||||
- "vulnerable population"
|
||||
|
||||
always_block_keywords:
|
||||
- keyword: "exploit child vulnerability"
|
||||
severity: "high"
|
||||
- keyword: "exploit elderly vulnerability"
|
||||
severity: "high"
|
||||
- keyword: "exploit disabled vulnerability"
|
||||
severity: "high"
|
||||
- keyword: "target vulnerable children"
|
||||
severity: "high"
|
||||
- keyword: "target vulnerable elderly"
|
||||
severity: "high"
|
||||
- keyword: "prey on vulnerable"
|
||||
severity: "high"
|
||||
- keyword: "exploit cognitive impairment"
|
||||
severity: "high"
|
||||
- keyword: "manipulate vulnerable population"
|
||||
severity: "high"
|
||||
|
||||
exceptions:
|
||||
- "research on"
|
||||
- "study on"
|
||||
- "academic"
|
||||
- "thesis on"
|
||||
- "explain"
|
||||
- "what is"
|
||||
- "article 5"
|
||||
- "prohibited by"
|
||||
- "protect vulnerable"
|
||||
- "safeguard"
|
||||
- "compliance monitoring"
|
||||
- "ethical review"
|
||||
- "accessibility"
|
||||
- "support for"
|
||||
- "help for"
|
||||
- "assist"
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
# Loi sur l'IA (UE) Article 5.1(b) — Exploitation des vulnérabilités
|
||||
# Interdit les systèmes d'IA qui exploitent les vulnérabilités des personnes
|
||||
# en raison de l'âge, du handicap ou de la situation socio-économique.
|
||||
# Référence : https://artificialintelligenceact.eu/article/5/
|
||||
category_name: "eu_ai_act_art5_vulnerability_fr"
|
||||
description: "Art. 5.1(b) — Bloque les systèmes d'IA qui exploitent les vulnérabilités des enfants, personnes âgées ou handicapées"
|
||||
default_action: "BLOCK"
|
||||
|
||||
identifier_words:
|
||||
# Actions de développement
|
||||
- "construire"
|
||||
- "créer"
|
||||
- "développer"
|
||||
- "concevoir"
|
||||
- "implémenter"
|
||||
- "mettre en place"
|
||||
|
||||
# Actions d'exploitation
|
||||
- "exploiter"
|
||||
- "cibler"
|
||||
- "manipuler"
|
||||
- "profiter de"
|
||||
- "abuser"
|
||||
- "tirer parti"
|
||||
|
||||
# Actions d'analyse/profilage
|
||||
- "profiler"
|
||||
- "évaluer"
|
||||
- "estimer"
|
||||
- "analyser"
|
||||
- "noter"
|
||||
- "classifier"
|
||||
- "détecter"
|
||||
- "identifier"
|
||||
|
||||
additional_block_words:
|
||||
# Vulnérabilité des enfants
|
||||
- "vulnérabilité des enfants"
|
||||
- "vulnérabilité des mineurs"
|
||||
- "enfants vulnérables"
|
||||
- "mineurs vulnérables"
|
||||
- "susceptibilité des enfants"
|
||||
- "susceptibilité des mineurs"
|
||||
- "cibler les enfants"
|
||||
- "cibler les mineurs"
|
||||
|
||||
# Vulnérabilité des personnes âgées
|
||||
- "vulnérabilité des personnes âgées"
|
||||
- "vulnérabilité des seniors"
|
||||
- "personnes âgées vulnérables"
|
||||
- "seniors vulnérables"
|
||||
- "susceptibilité des personnes âgées"
|
||||
- "cibler les personnes âgées"
|
||||
- "cibler les seniors"
|
||||
- "déclin cognitif"
|
||||
|
||||
# Vulnérabilité des personnes handicapées
|
||||
- "vulnérabilité des handicapés"
|
||||
- "vulnérabilité des personnes handicapées"
|
||||
- "personnes handicapées vulnérables"
|
||||
- "handicapés vulnérables"
|
||||
- "déficience intellectuelle"
|
||||
- "déficience cognitive"
|
||||
- "handicap mental"
|
||||
- "cibler les handicapés"
|
||||
- "cibler les personnes handicapées"
|
||||
|
||||
# Vulnérabilité économique
|
||||
- "vulnérabilité économique"
|
||||
- "vulnérabilité financière"
|
||||
- "vulnérabilité socio-économique"
|
||||
- "population vulnérable"
|
||||
- "personnes vulnérables"
|
||||
|
||||
always_block_keywords:
|
||||
- keyword: "exploiter la vulnérabilité des enfants"
|
||||
severity: "high"
|
||||
- keyword: "exploiter la vulnérabilité des personnes âgées"
|
||||
severity: "high"
|
||||
- keyword: "exploiter la vulnérabilité des handicapés"
|
||||
severity: "high"
|
||||
- keyword: "cibler les enfants vulnérables"
|
||||
severity: "high"
|
||||
- keyword: "cibler les personnes âgées vulnérables"
|
||||
severity: "high"
|
||||
- keyword: "exploiter le déclin cognitif"
|
||||
severity: "high"
|
||||
- keyword: "manipuler les personnes vulnérables"
|
||||
severity: "high"
|
||||
|
||||
exceptions:
|
||||
- "recherche sur"
|
||||
- "étude sur"
|
||||
- "académique"
|
||||
- "thèse sur"
|
||||
- "expliquer"
|
||||
- "qu'est-ce que"
|
||||
- "c'est quoi"
|
||||
- "article 5"
|
||||
- "interdit par"
|
||||
- "prohibé par"
|
||||
- "protéger les personnes vulnérables"
|
||||
- "sauvegarde"
|
||||
- "surveillance de conformité"
|
||||
- "contrôle de conformité"
|
||||
- "examen éthique"
|
||||
- "accessibilité"
|
||||
- "soutien pour"
|
||||
- "aide pour"
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
from typing import TYPE_CHECKING, Literal, Optional, cast
|
||||
|
||||
import litellm
|
||||
from litellm.proxy.guardrails.guardrail_hooks.mcp_security.mcp_security_guardrail import (
|
||||
MCPSecurityGuardrail,
|
||||
)
|
||||
from litellm.types.guardrails import SupportedGuardrailIntegrations
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from litellm import Router
|
||||
from litellm.types.guardrails import Guardrail, LitellmParams
|
||||
|
||||
|
||||
def initialize_guardrail(
|
||||
litellm_params: "LitellmParams",
|
||||
guardrail: "Guardrail",
|
||||
llm_router: Optional["Router"] = None,
|
||||
):
|
||||
guardrail_name = guardrail.get("guardrail_name")
|
||||
if not guardrail_name:
|
||||
raise ValueError("MCP Security: guardrail_name is required")
|
||||
|
||||
on_violation: Literal["block", "alert"] = cast(
|
||||
Literal["block", "alert"],
|
||||
getattr(litellm_params, "on_violation", "block"),
|
||||
)
|
||||
|
||||
mcp_security_guardrail = MCPSecurityGuardrail(
|
||||
guardrail_name=guardrail_name,
|
||||
event_hook=litellm_params.mode,
|
||||
default_on=litellm_params.default_on or False,
|
||||
on_violation=on_violation,
|
||||
)
|
||||
|
||||
litellm.logging_callback_manager.add_litellm_callback(mcp_security_guardrail)
|
||||
return mcp_security_guardrail
|
||||
|
||||
|
||||
guardrail_initializer_registry = {
|
||||
SupportedGuardrailIntegrations.MCP_SECURITY.value: initialize_guardrail,
|
||||
}
|
||||
|
||||
guardrail_class_registry = {
|
||||
SupportedGuardrailIntegrations.MCP_SECURITY.value: MCPSecurityGuardrail,
|
||||
}
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
"""
|
||||
MCP Security Guardrail for LiteLLM.
|
||||
|
||||
Validates that MCP servers referenced in request tools are registered
|
||||
on the LiteLLM gateway. Blocks or alerts when unregistered servers are found.
|
||||
"""
|
||||
|
||||
from typing import Any, List, Literal, Optional, Set, Union
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
||||
from litellm._logging import verbose_proxy_logger
|
||||
from litellm.integrations.custom_guardrail import (
|
||||
CustomGuardrail,
|
||||
log_guardrail_information,
|
||||
)
|
||||
from litellm.proxy._types import UserAPIKeyAuth
|
||||
from litellm.responses.mcp.litellm_proxy_mcp_handler import (
|
||||
LITELLM_PROXY_MCP_SERVER_URL_PREFIX,
|
||||
)
|
||||
from litellm.types.guardrails import GuardrailEventHooks
|
||||
|
||||
|
||||
class MCPSecurityGuardrail(CustomGuardrail):
|
||||
def __init__(
|
||||
self,
|
||||
on_violation: Literal["block", "alert"] = "block",
|
||||
**kwargs,
|
||||
):
|
||||
if "supported_event_hooks" not in kwargs:
|
||||
kwargs["supported_event_hooks"] = [GuardrailEventHooks.pre_call]
|
||||
super().__init__(**kwargs)
|
||||
self.on_violation = on_violation
|
||||
|
||||
@log_guardrail_information
|
||||
async def async_pre_call_hook(
|
||||
self,
|
||||
user_api_key_dict: UserAPIKeyAuth,
|
||||
cache: Any,
|
||||
data: dict,
|
||||
call_type: str,
|
||||
) -> Optional[Union[Exception, str, dict]]:
|
||||
if (
|
||||
self.should_run_guardrail(
|
||||
data=data, event_type=GuardrailEventHooks.pre_call
|
||||
)
|
||||
is not True
|
||||
):
|
||||
return data
|
||||
|
||||
unregistered = self._find_unregistered_mcp_servers(data)
|
||||
if not unregistered:
|
||||
return data
|
||||
|
||||
message = (
|
||||
f"MCP Security: request references unregistered MCP server(s): "
|
||||
f"{', '.join(sorted(unregistered))}. "
|
||||
f"Only servers registered on this gateway are allowed."
|
||||
)
|
||||
|
||||
if self.on_violation == "block":
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"error": "Violated guardrail policy",
|
||||
"guardrail": "mcp_security",
|
||||
"unregistered_servers": sorted(unregistered),
|
||||
"detection_message": message,
|
||||
},
|
||||
)
|
||||
else:
|
||||
verbose_proxy_logger.warning(message)
|
||||
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
def _extract_mcp_server_names_from_tools(tools: List[dict]) -> Set[str]:
|
||||
"""Extract MCP server names from tools with type=mcp and litellm_proxy server_url."""
|
||||
server_names: Set[str] = set()
|
||||
for tool in tools:
|
||||
if not isinstance(tool, dict):
|
||||
continue
|
||||
if tool.get("type") != "mcp":
|
||||
continue
|
||||
server_url = tool.get("server_url", "")
|
||||
if not isinstance(server_url, str):
|
||||
continue
|
||||
if server_url.startswith(LITELLM_PROXY_MCP_SERVER_URL_PREFIX):
|
||||
name = server_url[len(LITELLM_PROXY_MCP_SERVER_URL_PREFIX):]
|
||||
if name:
|
||||
server_names.add(name)
|
||||
return server_names
|
||||
|
||||
@staticmethod
|
||||
def _find_unregistered_mcp_servers(data: dict) -> Set[str]:
|
||||
"""Check tools in data against the MCP server registry. Returns set of unregistered server names."""
|
||||
tools = data.get("tools")
|
||||
if not tools or not isinstance(tools, list):
|
||||
return set()
|
||||
|
||||
requested_servers = (
|
||||
MCPSecurityGuardrail._extract_mcp_server_names_from_tools(tools)
|
||||
)
|
||||
if not requested_servers:
|
||||
return set()
|
||||
|
||||
from litellm.proxy._experimental.mcp_server.mcp_server_manager import (
|
||||
global_mcp_server_manager,
|
||||
)
|
||||
|
||||
registry = global_mcp_server_manager.get_registry()
|
||||
registered_names = set(registry.keys())
|
||||
|
||||
return requested_servers - registered_names
|
||||
|
|
@ -239,6 +239,8 @@ def clean_headers(
|
|||
"""
|
||||
Removes litellm api key from headers
|
||||
"""
|
||||
from litellm.llms.anthropic.common_utils import is_anthropic_oauth_key
|
||||
|
||||
clean_headers = {}
|
||||
litellm_key_lower = (
|
||||
litellm_key_header_name.lower() if litellm_key_header_name is not None else None
|
||||
|
|
@ -246,8 +248,13 @@ def clean_headers(
|
|||
|
||||
for header, value in headers.items():
|
||||
header_lower = header.lower()
|
||||
# Preserve Authorization header if it contains Anthropic OAuth token (sk-ant-oat*)
|
||||
# This allows OAuth tokens to be forwarded to Anthropic-compatible providers
|
||||
# via add_provider_specific_headers_to_request()
|
||||
if header_lower == "authorization" and is_anthropic_oauth_key(value):
|
||||
clean_headers[header] = value
|
||||
# Check if header should be excluded: either in special headers cache or matches custom litellm key
|
||||
if header_lower not in _SPECIAL_HEADERS_CACHE and (
|
||||
elif header_lower not in _SPECIAL_HEADERS_CACHE and (
|
||||
litellm_key_lower is None or header_lower != litellm_key_lower
|
||||
):
|
||||
clean_headers[header] = value
|
||||
|
|
@ -1717,6 +1724,8 @@ def add_provider_specific_headers_to_request(
|
|||
data: dict,
|
||||
headers: dict,
|
||||
):
|
||||
from litellm.llms.anthropic.common_utils import is_anthropic_oauth_key
|
||||
|
||||
anthropic_headers = {}
|
||||
# boolean to indicate if a header was added
|
||||
added_header = False
|
||||
|
|
@ -1726,6 +1735,14 @@ def add_provider_specific_headers_to_request(
|
|||
anthropic_headers[header] = header_value
|
||||
added_header = True
|
||||
|
||||
# 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
|
||||
|
|
|
|||
79
litellm/proxy/management_endpoints/compliance_endpoints.py
Normal file
79
litellm/proxy/management_endpoints/compliance_endpoints.py
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
"""
|
||||
COMPLIANCE CHECK ENDPOINTS
|
||||
|
||||
Endpoints for checking regulatory compliance of LLM request logs.
|
||||
|
||||
/compliance/eu-ai-act - Check EU AI Act compliance
|
||||
/compliance/gdpr - Check GDPR compliance
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
|
||||
from litellm.proxy._types import UserAPIKeyAuth
|
||||
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
|
||||
from litellm.proxy.compliance_checks import ComplianceChecker
|
||||
from litellm.proxy.management_helpers.utils import management_endpoint_wrapper
|
||||
from litellm.types.proxy.compliance_endpoints import (
|
||||
ComplianceCheckRequest,
|
||||
ComplianceResponse,
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post(
|
||||
"/compliance/eu-ai-act",
|
||||
tags=["compliance"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
response_model=ComplianceResponse,
|
||||
)
|
||||
@management_endpoint_wrapper
|
||||
async def check_eu_ai_act_compliance(
|
||||
data: ComplianceCheckRequest,
|
||||
http_request: Request,
|
||||
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
||||
) -> ComplianceResponse:
|
||||
"""
|
||||
Check EU AI Act compliance for a spend log entry.
|
||||
|
||||
Checks:
|
||||
- Art. 9: Guardrails applied (any guardrail)
|
||||
- Art. 5: Content screened before LLM (pre-call guardrails)
|
||||
- Art. 12: Audit record complete (user_id, model, timestamp, guardrail_results)
|
||||
"""
|
||||
checker = ComplianceChecker(data)
|
||||
checks = checker.check_eu_ai_act()
|
||||
return ComplianceResponse(
|
||||
compliant=all(c.passed for c in checks),
|
||||
regulation="EU AI Act",
|
||||
checks=checks,
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/compliance/gdpr",
|
||||
tags=["compliance"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
response_model=ComplianceResponse,
|
||||
)
|
||||
@management_endpoint_wrapper
|
||||
async def check_gdpr_compliance(
|
||||
data: ComplianceCheckRequest,
|
||||
http_request: Request,
|
||||
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
||||
) -> ComplianceResponse:
|
||||
"""
|
||||
Check GDPR compliance for a spend log entry.
|
||||
|
||||
Checks:
|
||||
- Art. 32: Data protection applied (pre-call guardrails)
|
||||
- Art. 5(1)(c): Sensitive data protected (masked/blocked or no issues)
|
||||
- Art. 30: Audit record complete (user_id, model, timestamp, guardrail_results)
|
||||
"""
|
||||
checker = ComplianceChecker(data)
|
||||
checks = checker.check_gdpr()
|
||||
return ComplianceResponse(
|
||||
compliant=all(c.passed for c in checks),
|
||||
regulation="GDPR",
|
||||
checks=checks,
|
||||
)
|
||||
|
|
@ -20,6 +20,7 @@ from datetime import datetime, timedelta, timezone
|
|||
from typing import Any, Dict, List, Literal, Optional, Tuple, cast
|
||||
|
||||
import fastapi
|
||||
import prisma
|
||||
import yaml
|
||||
from fastapi import APIRouter, Depends, Header, HTTPException, Query, Request, status
|
||||
|
||||
|
|
@ -74,7 +75,6 @@ from litellm.proxy.utils import (
|
|||
_hash_token_if_needed,
|
||||
handle_exception_on_proxy,
|
||||
is_valid_api_key,
|
||||
jsonify_object,
|
||||
)
|
||||
from litellm.router import Router
|
||||
from litellm.secret_managers.main import get_secret
|
||||
|
|
@ -3052,7 +3052,7 @@ async def delete_key_aliases(
|
|||
)
|
||||
|
||||
|
||||
async def _rotate_master_key(
|
||||
async def _rotate_master_key( # noqa: PLR0915
|
||||
prisma_client: PrismaClient,
|
||||
user_api_key_dict: UserAPIKeyAuth,
|
||||
current_master_key: str,
|
||||
|
|
@ -3095,13 +3095,17 @@ async def _rotate_master_key(
|
|||
should_create_model_in_db=False,
|
||||
)
|
||||
if new_model:
|
||||
new_models.append(jsonify_object(new_model.model_dump()))
|
||||
_dumped = new_model.model_dump(exclude_none=True)
|
||||
_dumped["litellm_params"] = prisma.Json(_dumped["litellm_params"]) # type: ignore[attr-defined]
|
||||
_dumped["model_info"] = prisma.Json(_dumped["model_info"]) # type: ignore[attr-defined]
|
||||
new_models.append(_dumped)
|
||||
verbose_proxy_logger.debug("Resetting proxy model table")
|
||||
await prisma_client.db.litellm_proxymodeltable.delete_many()
|
||||
verbose_proxy_logger.debug("Creating %s models", len(new_models))
|
||||
await prisma_client.db.litellm_proxymodeltable.create_many(
|
||||
data=new_models,
|
||||
)
|
||||
async with prisma_client.db.tx() as tx:
|
||||
await tx.litellm_proxymodeltable.delete_many()
|
||||
verbose_proxy_logger.debug("Creating %s models", len(new_models))
|
||||
await tx.litellm_proxymodeltable.create_many(
|
||||
data=new_models,
|
||||
)
|
||||
# 3. process config table
|
||||
try:
|
||||
config = await prisma_client.db.litellm_config.find_many()
|
||||
|
|
@ -3127,15 +3131,20 @@ async def _rotate_master_key(
|
|||
if encrypted_env_vars:
|
||||
await prisma_client.db.litellm_config.update(
|
||||
where={"param_name": "environment_variables"},
|
||||
data={"param_value": jsonify_object(encrypted_env_vars)},
|
||||
data={"param_value": prisma.Json(encrypted_env_vars)}, # type: ignore[attr-defined]
|
||||
)
|
||||
|
||||
# 4. process MCP server table
|
||||
await rotate_mcp_server_credentials_master_key(
|
||||
prisma_client=prisma_client,
|
||||
touched_by=user_api_key_dict.user_id or LITELLM_PROXY_ADMIN_NAME,
|
||||
new_master_key=new_master_key,
|
||||
)
|
||||
try:
|
||||
await rotate_mcp_server_credentials_master_key(
|
||||
prisma_client=prisma_client,
|
||||
touched_by=user_api_key_dict.user_id or LITELLM_PROXY_ADMIN_NAME,
|
||||
new_master_key=new_master_key,
|
||||
)
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.warning(
|
||||
"Failed to rotate MCP server credentials: %s", str(e)
|
||||
)
|
||||
|
||||
# 5. process credentials table
|
||||
try:
|
||||
|
|
@ -3153,13 +3162,19 @@ async def _rotate_master_key(
|
|||
updated_patch=decrypted_cred,
|
||||
new_encryption_key=new_master_key,
|
||||
)
|
||||
credential_object_jsonified = jsonify_object(
|
||||
encrypted_cred.model_dump()
|
||||
)
|
||||
_cred_data = encrypted_cred.model_dump(exclude_none=True)
|
||||
if "credential_values" in _cred_data:
|
||||
_cred_data["credential_values"] = prisma.Json( # type: ignore[attr-defined]
|
||||
_cred_data["credential_values"]
|
||||
)
|
||||
if "credential_info" in _cred_data:
|
||||
_cred_data["credential_info"] = prisma.Json( # type: ignore[attr-defined]
|
||||
_cred_data["credential_info"]
|
||||
)
|
||||
await prisma_client.db.litellm_credentialstable.update(
|
||||
where={"credential_name": cred.credential_name},
|
||||
data={
|
||||
**credential_object_jsonified,
|
||||
**_cred_data,
|
||||
"updated_by": user_api_key_dict.user_id,
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -345,6 +345,9 @@ from litellm.proxy.management_endpoints.common_utils import (
|
|||
_user_has_admin_privileges,
|
||||
admin_can_invite_user,
|
||||
)
|
||||
from litellm.proxy.management_endpoints.compliance_endpoints import (
|
||||
router as compliance_router,
|
||||
)
|
||||
from litellm.proxy.management_endpoints.cost_tracking_settings import (
|
||||
router as cost_tracking_settings_router,
|
||||
)
|
||||
|
|
@ -12467,6 +12470,7 @@ app.include_router(user_agent_analytics_router)
|
|||
app.include_router(enterprise_router)
|
||||
app.include_router(ui_discovery_endpoints_router)
|
||||
app.include_router(agent_endpoints_router)
|
||||
app.include_router(compliance_router)
|
||||
app.include_router(a2a_router)
|
||||
app.include_router(access_group_router)
|
||||
########################################################
|
||||
|
|
|
|||
|
|
@ -352,6 +352,8 @@ def get_logging_payload( # noqa: PLR0915
|
|||
guardrail_information=(
|
||||
standard_logging_payload.get("guardrail_information", None)
|
||||
if standard_logging_payload is not None
|
||||
else metadata.get("standard_logging_guardrail_information", None)
|
||||
if metadata is not None
|
||||
else None
|
||||
),
|
||||
cold_storage_object_key=(
|
||||
|
|
|
|||
|
|
@ -456,18 +456,26 @@ class ProxyLogging:
|
|||
def _init_litellm_callbacks(self, llm_router: Optional[Router] = None):
|
||||
self._add_proxy_hooks(llm_router)
|
||||
litellm.logging_callback_manager.add_litellm_callback(self.service_logging_obj) # type: ignore
|
||||
for callback in litellm.callbacks:
|
||||
|
||||
# Track string callbacks and their initialized instances so we can
|
||||
# replace them in-place, preventing duplicates (string + instance) in
|
||||
# litellm.callbacks which caused double-counting of metrics.
|
||||
string_callbacks_to_replace: Dict[int, CustomLogger] = {}
|
||||
|
||||
for idx, callback in enumerate(litellm.callbacks):
|
||||
if isinstance(callback, str):
|
||||
callback = litellm.litellm_core_utils.litellm_logging._init_custom_logger_compatible_class( # type: ignore
|
||||
initialized_callback = litellm.litellm_core_utils.litellm_logging._init_custom_logger_compatible_class(
|
||||
cast(_custom_logger_compatible_callbacks_literal, callback),
|
||||
internal_usage_cache=self.internal_usage_cache.dual_cache,
|
||||
llm_router=llm_router,
|
||||
)
|
||||
|
||||
if callback is None:
|
||||
continue
|
||||
if initialized_callback is not None:
|
||||
string_callbacks_to_replace[idx] = initialized_callback
|
||||
|
||||
litellm.logging_callback_manager.add_litellm_callback(callback)
|
||||
# Replace string entries in litellm.callbacks with initialized instances
|
||||
for idx, initialized_callback in string_callbacks_to_replace.items():
|
||||
litellm.callbacks[idx] = initialized_callback
|
||||
|
||||
async def update_request_status(
|
||||
self, litellm_call_id: str, status: Literal["success", "fail"]
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ from litellm.llms.base_llm.rerank.transformation import BaseRerankConfig
|
|||
from litellm.llms.bedrock.rerank.handler import BedrockRerankHandler
|
||||
from litellm.llms.custom_httpx.llm_http_handler import BaseLLMHTTPHandler
|
||||
from litellm.llms.together_ai.rerank.handler import TogetherAIRerank
|
||||
from litellm.llms.watsonx.common_utils import IBMWatsonXMixin
|
||||
from litellm.rerank_api.rerank_utils import get_optional_rerank_params
|
||||
from litellm.secret_managers.main import get_secret, get_secret_str
|
||||
from litellm.types.rerank import RerankResponse
|
||||
|
|
@ -29,7 +30,7 @@ async def arerank(
|
|||
model: str,
|
||||
query: str,
|
||||
documents: List[Union[str, Dict[str, Any]]],
|
||||
custom_llm_provider: Optional[Literal["cohere", "together_ai", "deepinfra", "fireworks_ai", "voyage"]] = None,
|
||||
custom_llm_provider: Optional[Literal["cohere", "together_ai", "deepinfra", "fireworks_ai", "voyage", "watsonx"]] = None,
|
||||
top_n: Optional[int] = None,
|
||||
rank_fields: Optional[List[str]] = None,
|
||||
return_documents: Optional[bool] = None,
|
||||
|
|
@ -85,6 +86,7 @@ def rerank( # noqa: PLR0915
|
|||
"deepinfra",
|
||||
"fireworks_ai",
|
||||
"voyage",
|
||||
"watsonx",
|
||||
]
|
||||
] = None,
|
||||
top_n: Optional[int] = None,
|
||||
|
|
@ -478,6 +480,31 @@ def rerank( # noqa: PLR0915
|
|||
or get_secret_str("VOYAGE_API_BASE")
|
||||
)
|
||||
|
||||
response = base_llm_http_handler.rerank(
|
||||
model=model,
|
||||
custom_llm_provider=_custom_llm_provider,
|
||||
provider_config=rerank_provider_config,
|
||||
optional_rerank_params=optional_rerank_params,
|
||||
logging_obj=litellm_logging_obj,
|
||||
timeout=optional_params.timeout,
|
||||
api_key=api_key,
|
||||
api_base=api_base,
|
||||
_is_async=_is_async,
|
||||
headers=headers or litellm.headers or {},
|
||||
client=client,
|
||||
model_response=model_response,
|
||||
)
|
||||
elif _custom_llm_provider == litellm.LlmProviders.WATSONX:
|
||||
credentials = IBMWatsonXMixin.get_watsonx_credentials(
|
||||
optional_params=dict(optional_params), api_key=dynamic_api_key, api_base=dynamic_api_base
|
||||
)
|
||||
|
||||
api_key = credentials["api_key"]
|
||||
api_base = credentials["api_base"]
|
||||
|
||||
if credentials.get("token") is not None:
|
||||
optional_rerank_params["token"] = credentials["token"]
|
||||
|
||||
response = base_llm_http_handler.rerank(
|
||||
model=model,
|
||||
custom_llm_provider=_custom_llm_provider,
|
||||
|
|
|
|||
|
|
@ -600,8 +600,12 @@ def responses(
|
|||
# Update input and tools with provider-specific file IDs if managed files are used
|
||||
#########################################################
|
||||
model_file_id_mapping = kwargs.get("model_file_id_mapping")
|
||||
model_info_id = kwargs.get("model_info", {}).get("id") if isinstance(kwargs.get("model_info"), dict) else None
|
||||
|
||||
model_info_id = (
|
||||
kwargs.get("model_info", {}).get("id")
|
||||
if isinstance(kwargs.get("model_info"), dict)
|
||||
else None
|
||||
)
|
||||
|
||||
input = cast(
|
||||
Union[str, ResponseInputParam],
|
||||
update_responses_input_with_model_file_ids(
|
||||
|
|
@ -611,7 +615,7 @@ def responses(
|
|||
),
|
||||
)
|
||||
local_vars["input"] = input
|
||||
|
||||
|
||||
# Update tools with provider-specific file IDs if needed
|
||||
if tools:
|
||||
tools = cast(
|
||||
|
|
@ -696,7 +700,10 @@ def responses(
|
|||
)
|
||||
)
|
||||
|
||||
# Pre Call logging
|
||||
# Pre Call logging - preserve metadata for custom callbacks
|
||||
# When called from completion bridge (codex models), metadata is in litellm_metadata
|
||||
metadata_for_callbacks = metadata or kwargs.get("litellm_metadata") or {}
|
||||
|
||||
litellm_logging_obj.update_environment_variables(
|
||||
model=model,
|
||||
user=user,
|
||||
|
|
@ -705,7 +712,7 @@ def responses(
|
|||
**responses_api_request_params,
|
||||
"aresponses": _is_async,
|
||||
"litellm_call_id": litellm_call_id,
|
||||
"metadata": metadata,
|
||||
"metadata": metadata_for_callbacks,
|
||||
},
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -113,12 +113,12 @@ from litellm.router_utils.handle_error import (
|
|||
from litellm.router_utils.pre_call_checks.model_rate_limit_check import (
|
||||
ModelRateLimitingCheck,
|
||||
)
|
||||
from litellm.router_utils.pre_call_checks.deployment_affinity_check import (
|
||||
DeploymentAffinityCheck,
|
||||
)
|
||||
from litellm.router_utils.pre_call_checks.prompt_caching_deployment_check import (
|
||||
PromptCachingDeploymentCheck,
|
||||
)
|
||||
from litellm.router_utils.pre_call_checks.responses_api_deployment_check import (
|
||||
ResponsesApiDeploymentCheck,
|
||||
)
|
||||
from litellm.router_utils.router_callbacks.track_deployment_metrics import (
|
||||
increment_deployment_failures_for_current_minute,
|
||||
increment_deployment_successes_for_current_minute,
|
||||
|
|
@ -293,6 +293,7 @@ class Router:
|
|||
router_general_settings: Optional[
|
||||
RouterGeneralSettings
|
||||
] = RouterGeneralSettings(),
|
||||
deployment_affinity_ttl_seconds: int = 3600,
|
||||
ignore_invalid_deployments: bool = False,
|
||||
) -> None:
|
||||
"""
|
||||
|
|
@ -326,6 +327,7 @@ class Router:
|
|||
routing_strategy_args (dict): Additional args for latency-based routing. Defaults to {}.
|
||||
alerting_config (AlertingConfig): Slack alerting configuration. Defaults to None.
|
||||
provider_budget_config (ProviderBudgetConfig): Provider budget configuration. Use this to set llm_provider budget limits. example $100/day to OpenAI, $100/day to Azure, etc. Defaults to None.
|
||||
deployment_affinity_ttl_seconds (int): TTL for user-key -> deployment affinity mapping. Defaults to 3600.
|
||||
ignore_invalid_deployments (bool): Ignores invalid deployments, and continues with other deployments. Default is to raise an error.
|
||||
Returns:
|
||||
Router: An instance of the litellm.Router class.
|
||||
|
|
@ -604,6 +606,7 @@ class Router:
|
|||
litellm.failure_callback = [self.deployment_callback_on_failure]
|
||||
self.routing_strategy_args = routing_strategy_args
|
||||
self.provider_budget_config = provider_budget_config
|
||||
self.deployment_affinity_ttl_seconds = deployment_affinity_ttl_seconds
|
||||
self.router_budget_logger: Optional[RouterBudgetLimiting] = None
|
||||
if RouterBudgetLimiting.should_init_router_budget_limiter(
|
||||
model_list=model_list, provider_budget_config=self.provider_budget_config
|
||||
|
|
@ -1184,26 +1187,78 @@ class Router:
|
|||
def add_optional_pre_call_checks(
|
||||
self, optional_pre_call_checks: Optional[OptionalPreCallChecks]
|
||||
):
|
||||
if optional_pre_call_checks is not None:
|
||||
for pre_call_check in optional_pre_call_checks:
|
||||
_callback: Optional[CustomLogger] = None
|
||||
if pre_call_check == "prompt_caching":
|
||||
_callback = PromptCachingDeploymentCheck(cache=self.cache)
|
||||
elif pre_call_check == "router_budget_limiting":
|
||||
_callback = RouterBudgetLimiting(
|
||||
dual_cache=self.cache,
|
||||
provider_budget_config=self.provider_budget_config,
|
||||
model_list=self.model_list,
|
||||
)
|
||||
elif pre_call_check == "responses_api_deployment_check":
|
||||
_callback = ResponsesApiDeploymentCheck()
|
||||
elif pre_call_check == "enforce_model_rate_limits":
|
||||
_callback = ModelRateLimitingCheck(dual_cache=self.cache)
|
||||
if _callback is not None:
|
||||
if self.optional_callbacks is None:
|
||||
self.optional_callbacks = []
|
||||
self.optional_callbacks.append(_callback)
|
||||
litellm.logging_callback_manager.add_litellm_callback(_callback)
|
||||
if optional_pre_call_checks is None:
|
||||
return
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Unified deployment affinity (session stickiness)
|
||||
# ---------------------------------------------------------------------
|
||||
enable_user_key_affinity = "deployment_affinity" in optional_pre_call_checks
|
||||
enable_responses_api_affinity = (
|
||||
"responses_api_deployment_check" in optional_pre_call_checks
|
||||
)
|
||||
if enable_user_key_affinity or enable_responses_api_affinity:
|
||||
if self.optional_callbacks is None:
|
||||
self.optional_callbacks = []
|
||||
|
||||
existing_affinity_callback: Optional[DeploymentAffinityCheck] = None
|
||||
for cb in self.optional_callbacks:
|
||||
if isinstance(cb, DeploymentAffinityCheck):
|
||||
existing_affinity_callback = cb
|
||||
break
|
||||
|
||||
if existing_affinity_callback is not None:
|
||||
existing_affinity_callback.enable_user_key_affinity = (
|
||||
existing_affinity_callback.enable_user_key_affinity
|
||||
or enable_user_key_affinity
|
||||
)
|
||||
existing_affinity_callback.enable_responses_api_affinity = (
|
||||
existing_affinity_callback.enable_responses_api_affinity
|
||||
or enable_responses_api_affinity
|
||||
)
|
||||
existing_affinity_callback.ttl_seconds = (
|
||||
self.deployment_affinity_ttl_seconds
|
||||
)
|
||||
else:
|
||||
affinity_callback = DeploymentAffinityCheck(
|
||||
cache=self.cache,
|
||||
ttl_seconds=self.deployment_affinity_ttl_seconds,
|
||||
enable_user_key_affinity=enable_user_key_affinity,
|
||||
enable_responses_api_affinity=enable_responses_api_affinity,
|
||||
)
|
||||
self.optional_callbacks.append(affinity_callback)
|
||||
litellm.logging_callback_manager.add_litellm_callback(
|
||||
affinity_callback
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Remaining optional pre-call checks
|
||||
# ---------------------------------------------------------------------
|
||||
for pre_call_check in optional_pre_call_checks:
|
||||
_callback: Optional[CustomLogger] = None
|
||||
if pre_call_check in (
|
||||
"deployment_affinity",
|
||||
"responses_api_deployment_check",
|
||||
):
|
||||
continue
|
||||
if pre_call_check == "prompt_caching":
|
||||
_callback = PromptCachingDeploymentCheck(cache=self.cache)
|
||||
elif pre_call_check == "router_budget_limiting":
|
||||
_callback = RouterBudgetLimiting(
|
||||
dual_cache=self.cache,
|
||||
provider_budget_config=self.provider_budget_config,
|
||||
model_list=self.model_list,
|
||||
)
|
||||
elif pre_call_check == "enforce_model_rate_limits":
|
||||
_callback = ModelRateLimitingCheck(dual_cache=self.cache)
|
||||
|
||||
if _callback is None:
|
||||
continue
|
||||
|
||||
if self.optional_callbacks is None:
|
||||
self.optional_callbacks = []
|
||||
self.optional_callbacks.append(_callback)
|
||||
litellm.logging_callback_manager.add_litellm_callback(_callback)
|
||||
|
||||
def print_deployment(self, deployment: dict):
|
||||
"""
|
||||
|
|
@ -7600,9 +7655,16 @@ class Router:
|
|||
Used by `.get_model_list` to get model list from model alias.
|
||||
"""
|
||||
returned_models: List[DeploymentTypedDict] = []
|
||||
for model_alias, model_value in self.model_group_alias.items():
|
||||
if model_name is not None and model_alias != model_name:
|
||||
continue
|
||||
|
||||
if model_name is not None:
|
||||
# Fast path: direct dict lookup avoids scanning all aliases for non-alias model names.
|
||||
if model_name not in self.model_group_alias:
|
||||
return returned_models
|
||||
alias_items = [(model_name, self.model_group_alias[model_name])]
|
||||
else:
|
||||
alias_items = list(self.model_group_alias.items())
|
||||
|
||||
for model_alias, model_value in alias_items:
|
||||
if isinstance(model_value, str):
|
||||
_router_model_name: str = model_value
|
||||
elif isinstance(model_value, dict):
|
||||
|
|
@ -9099,4 +9161,3 @@ class Router:
|
|||
litellm._async_failure_callback = []
|
||||
self.retry_policy = None
|
||||
self.flush_cache()
|
||||
|
||||
|
|
|
|||
|
|
@ -335,13 +335,14 @@ class LowestTPMLoggingHandler_v2(BaseRoutingStrategy, CustomLogger):
|
|||
):
|
||||
lowest_tpm = float("inf")
|
||||
potential_deployments = [] # if multiple deployments have the same low value
|
||||
deployment_lookup = {
|
||||
deployment.get("model_info", {}).get("id"): deployment
|
||||
for deployment in healthy_deployments
|
||||
}
|
||||
for item, item_tpm in all_deployments.items():
|
||||
## get the item from model list
|
||||
_deployment = None
|
||||
item = item.split(":")[0]
|
||||
for m in healthy_deployments:
|
||||
if item == m["model_info"]["id"]:
|
||||
_deployment = m
|
||||
_deployment = deployment_lookup.get(item)
|
||||
if _deployment is None:
|
||||
continue # skip to next one
|
||||
elif item_tpm is None:
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ def filter_team_based_models(
|
|||
request_team_id = metadata.get("user_api_key_team_id") or litellm_metadata.get(
|
||||
"user_api_key_team_id"
|
||||
)
|
||||
ids_to_remove = []
|
||||
ids_to_remove = set()
|
||||
if isinstance(healthy_deployments, dict):
|
||||
return healthy_deployments
|
||||
for deployment in healthy_deployments:
|
||||
|
|
@ -67,7 +67,7 @@ def filter_team_based_models(
|
|||
if model_team_id is None:
|
||||
continue
|
||||
if model_team_id != request_team_id:
|
||||
ids_to_remove.append(deployment.get("model_info", {}).get("id"))
|
||||
ids_to_remove.add(_model_info.get("id"))
|
||||
|
||||
return [
|
||||
deployment
|
||||
|
|
@ -125,4 +125,3 @@ def filter_web_search_deployments(
|
|||
if len(healthy_deployments) > 0 and len(final_deployments) == 0:
|
||||
verbose_logger.warning("No deployments support web search for request")
|
||||
return final_deployments
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,396 @@
|
|||
"""
|
||||
Unified deployment affinity (session stickiness) for the Router.
|
||||
|
||||
Features (independently enable-able):
|
||||
1. Responses API continuity: when a `previous_response_id` is provided, route to the
|
||||
deployment that generated the original response (highest priority).
|
||||
2. API-key affinity: map an API key hash -> deployment id for a TTL and re-use that
|
||||
deployment for subsequent requests to the same router deployment model name
|
||||
(alias-safe, aligns to `model_map_information.model_map_key`).
|
||||
|
||||
This is designed to support "implicit prompt caching" scenarios (no explicit cache_control),
|
||||
where routing to a consistent deployment is still beneficial.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
from typing import Any, Dict, List, Optional, cast
|
||||
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from litellm._logging import verbose_router_logger
|
||||
from litellm.caching.dual_cache import DualCache
|
||||
from litellm.integrations.custom_logger import CustomLogger, Span
|
||||
from litellm.responses.utils import ResponsesAPIRequestUtils
|
||||
from litellm.types.llms.openai import AllMessageValues
|
||||
from litellm.types.utils import CallTypes
|
||||
|
||||
|
||||
class DeploymentAffinityCacheValue(TypedDict):
|
||||
model_id: str
|
||||
|
||||
|
||||
class DeploymentAffinityCheck(CustomLogger):
|
||||
"""
|
||||
Router deployment affinity callback.
|
||||
|
||||
NOTE: This is a Router-only callback intended to be wired through
|
||||
`Router(optional_pre_call_checks=[...])`.
|
||||
"""
|
||||
|
||||
CACHE_KEY_PREFIX = "deployment_affinity:v1"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
cache: DualCache,
|
||||
ttl_seconds: int,
|
||||
enable_user_key_affinity: bool,
|
||||
enable_responses_api_affinity: bool,
|
||||
):
|
||||
super().__init__()
|
||||
self.cache = cache
|
||||
self.ttl_seconds = ttl_seconds
|
||||
self.enable_user_key_affinity = enable_user_key_affinity
|
||||
self.enable_responses_api_affinity = enable_responses_api_affinity
|
||||
|
||||
@staticmethod
|
||||
def _looks_like_sha256_hex(value: str) -> bool:
|
||||
if len(value) != 64:
|
||||
return False
|
||||
try:
|
||||
int(value, 16)
|
||||
except ValueError:
|
||||
return False
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _hash_user_key(user_key: str) -> str:
|
||||
"""
|
||||
Hash user identifiers before storing them in cache keys.
|
||||
|
||||
This avoids putting raw API keys / user identifiers into Redis keys (and therefore
|
||||
into logs/metrics), while keeping the cache key stable and a fixed length.
|
||||
"""
|
||||
# If the proxy already provides a stable SHA-256 (e.g. `metadata.user_api_key_hash`),
|
||||
# keep it as-is to avoid double-hashing and to make correlation/debugging possible.
|
||||
if DeploymentAffinityCheck._looks_like_sha256_hex(user_key):
|
||||
return user_key.lower()
|
||||
|
||||
return hashlib.sha256(user_key.encode("utf-8")).hexdigest()
|
||||
|
||||
@staticmethod
|
||||
def _get_model_map_key_from_litellm_model_name(litellm_model_name: str) -> Optional[str]:
|
||||
"""
|
||||
Best-effort derivation of a stable "model map key" for affinity scoping.
|
||||
|
||||
The intent is to align with `standard_logging_payload.model_map_information.model_map_key`,
|
||||
which is typically the base model identifier (stable across deployments/endpoints).
|
||||
|
||||
Notes:
|
||||
- When the model name is in "provider/model" format, the provider prefix is stripped.
|
||||
- For Azure, the string after "azure/" is commonly an *Azure deployment name*, which may
|
||||
differ across instances. If `base_model` is not explicitly set, we skip deriving a
|
||||
model-map key from the model string to avoid generating unstable keys.
|
||||
"""
|
||||
if not litellm_model_name:
|
||||
return None
|
||||
|
||||
if "/" not in litellm_model_name:
|
||||
return litellm_model_name
|
||||
|
||||
provider_prefix, remainder = litellm_model_name.split("/", 1)
|
||||
if provider_prefix == "azure":
|
||||
return None
|
||||
|
||||
return remainder
|
||||
|
||||
@staticmethod
|
||||
def _get_model_map_key_from_deployment(deployment: dict) -> Optional[str]:
|
||||
"""
|
||||
Derive a stable model-map key from a router deployment dict.
|
||||
|
||||
Primary source: `deployment.model_name` (Router's canonical group name after
|
||||
alias resolution). This is stable across provider-specific deployments (e.g.,
|
||||
Azure/Vertex/Bedrock for the same logical model) and aligns with
|
||||
`model_map_information.model_map_key` in standard logging.
|
||||
|
||||
Prefer `base_model` when available (important for Azure), otherwise fall back to
|
||||
parsing `litellm_params.model`.
|
||||
"""
|
||||
model_name = deployment.get("model_name")
|
||||
if isinstance(model_name, str) and model_name:
|
||||
return model_name
|
||||
|
||||
model_info = deployment.get("model_info")
|
||||
if isinstance(model_info, dict):
|
||||
base_model = model_info.get("base_model")
|
||||
if isinstance(base_model, str) and base_model:
|
||||
return base_model
|
||||
|
||||
litellm_params = deployment.get("litellm_params")
|
||||
if isinstance(litellm_params, dict):
|
||||
base_model = litellm_params.get("base_model")
|
||||
if isinstance(base_model, str) and base_model:
|
||||
return base_model
|
||||
litellm_model_name = litellm_params.get("model")
|
||||
if isinstance(litellm_model_name, str) and litellm_model_name:
|
||||
return DeploymentAffinityCheck._get_model_map_key_from_litellm_model_name(
|
||||
litellm_model_name
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_stable_model_map_key_from_deployments(
|
||||
healthy_deployments: List[dict],
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Only use model-map key scoping when it is stable across the deployment set.
|
||||
|
||||
This prevents accidentally keying on per-deployment identifiers like Azure deployment
|
||||
names (when `base_model` is not configured).
|
||||
"""
|
||||
if not healthy_deployments:
|
||||
return None
|
||||
|
||||
keys: List[str] = []
|
||||
for deployment in healthy_deployments:
|
||||
key = DeploymentAffinityCheck._get_model_map_key_from_deployment(deployment)
|
||||
if key is None:
|
||||
return None
|
||||
keys.append(key)
|
||||
|
||||
unique_keys = set(keys)
|
||||
if len(unique_keys) != 1:
|
||||
return None
|
||||
return keys[0]
|
||||
|
||||
@staticmethod
|
||||
def _shorten_for_logs(value: str, keep: int = 8) -> str:
|
||||
if len(value) <= keep:
|
||||
return value
|
||||
return f"{value[:keep]}..."
|
||||
|
||||
@classmethod
|
||||
def get_affinity_cache_key(cls, model_group: str, user_key: str) -> str:
|
||||
hashed_user_key = cls._hash_user_key(user_key=user_key)
|
||||
return f"{cls.CACHE_KEY_PREFIX}:{model_group}:{hashed_user_key}"
|
||||
|
||||
@staticmethod
|
||||
def _get_user_key_from_metadata_dict(metadata: dict) -> Optional[str]:
|
||||
# NOTE: affinity is keyed on the *API key hash* provided by the proxy (not the
|
||||
# OpenAI `user` parameter, which is an end-user identifier).
|
||||
user_key = metadata.get("user_api_key_hash")
|
||||
if user_key is None:
|
||||
return None
|
||||
return str(user_key)
|
||||
|
||||
@staticmethod
|
||||
def _iter_metadata_dicts(request_kwargs: dict) -> List[dict]:
|
||||
"""
|
||||
Return all metadata dicts available on the request.
|
||||
|
||||
Depending on the endpoint, Router may populate `metadata` or `litellm_metadata`.
|
||||
Users may also send one or both, so we check both (rather than using `or`).
|
||||
"""
|
||||
metadata_dicts: List[dict] = []
|
||||
for key in ("litellm_metadata", "metadata"):
|
||||
md = request_kwargs.get(key)
|
||||
if isinstance(md, dict):
|
||||
metadata_dicts.append(md)
|
||||
return metadata_dicts
|
||||
|
||||
@staticmethod
|
||||
def _get_user_key_from_request_kwargs(request_kwargs: dict) -> Optional[str]:
|
||||
"""
|
||||
Extract a stable affinity key from request kwargs.
|
||||
|
||||
Source (proxy): `metadata.user_api_key_hash`
|
||||
|
||||
Note: the OpenAI `user` parameter is an end-user identifier and is intentionally
|
||||
not used for deployment affinity.
|
||||
"""
|
||||
# Check metadata dicts (Proxy usage)
|
||||
for metadata in DeploymentAffinityCheck._iter_metadata_dicts(request_kwargs):
|
||||
user_key = DeploymentAffinityCheck._get_user_key_from_metadata_dict(
|
||||
metadata=metadata
|
||||
)
|
||||
if user_key is not None:
|
||||
return user_key
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _find_deployment_by_model_id(healthy_deployments: List[dict], model_id: str) -> Optional[dict]:
|
||||
for deployment in healthy_deployments:
|
||||
model_info = deployment.get("model_info")
|
||||
if not isinstance(model_info, dict):
|
||||
continue
|
||||
deployment_model_id = model_info.get("id")
|
||||
if deployment_model_id is not None and str(deployment_model_id) == str(model_id):
|
||||
return deployment
|
||||
return None
|
||||
|
||||
async def async_filter_deployments(
|
||||
self,
|
||||
model: str,
|
||||
healthy_deployments: List,
|
||||
messages: Optional[List[AllMessageValues]],
|
||||
request_kwargs: Optional[dict] = None,
|
||||
parent_otel_span: Optional[Span] = None,
|
||||
) -> List[dict]:
|
||||
"""
|
||||
Optionally filter healthy deployments based on:
|
||||
1. `previous_response_id` (Responses API continuity) [highest priority]
|
||||
2. cached API-key deployment affinity
|
||||
"""
|
||||
request_kwargs = request_kwargs or {}
|
||||
typed_healthy_deployments = cast(List[dict], healthy_deployments)
|
||||
|
||||
# 1) Responses API continuity (high priority)
|
||||
if self.enable_responses_api_affinity:
|
||||
previous_response_id = request_kwargs.get("previous_response_id")
|
||||
if previous_response_id is not None:
|
||||
responses_model_id = ResponsesAPIRequestUtils.get_model_id_from_response_id(str(previous_response_id))
|
||||
if responses_model_id is not None:
|
||||
deployment = self._find_deployment_by_model_id(
|
||||
healthy_deployments=typed_healthy_deployments,
|
||||
model_id=responses_model_id,
|
||||
)
|
||||
if deployment is not None:
|
||||
verbose_router_logger.debug(
|
||||
"DeploymentAffinityCheck: previous_response_id pinning -> deployment=%s",
|
||||
responses_model_id,
|
||||
)
|
||||
return [deployment]
|
||||
|
||||
# 2) User key -> deployment affinity
|
||||
if not self.enable_user_key_affinity:
|
||||
return typed_healthy_deployments
|
||||
|
||||
user_key = self._get_user_key_from_request_kwargs(request_kwargs=request_kwargs)
|
||||
if user_key is None:
|
||||
return typed_healthy_deployments
|
||||
|
||||
stable_model_map_key = self._get_stable_model_map_key_from_deployments(
|
||||
healthy_deployments=typed_healthy_deployments
|
||||
)
|
||||
if stable_model_map_key is None:
|
||||
return typed_healthy_deployments
|
||||
|
||||
cache_key = self.get_affinity_cache_key(
|
||||
model_group=stable_model_map_key, user_key=user_key
|
||||
)
|
||||
cache_result = await self.cache.async_get_cache(key=cache_key)
|
||||
|
||||
model_id: Optional[str] = None
|
||||
if isinstance(cache_result, dict):
|
||||
model_id = cast(Optional[str], cache_result.get("model_id"))
|
||||
elif isinstance(cache_result, str):
|
||||
# Backwards / safety: allow raw string values.
|
||||
model_id = cache_result
|
||||
|
||||
if not model_id:
|
||||
return typed_healthy_deployments
|
||||
|
||||
deployment = self._find_deployment_by_model_id(
|
||||
healthy_deployments=typed_healthy_deployments,
|
||||
model_id=model_id,
|
||||
)
|
||||
if deployment is None:
|
||||
verbose_router_logger.debug(
|
||||
"DeploymentAffinityCheck: pinned deployment=%s not found in healthy_deployments",
|
||||
model_id,
|
||||
)
|
||||
return typed_healthy_deployments
|
||||
|
||||
verbose_router_logger.debug(
|
||||
"DeploymentAffinityCheck: api-key affinity hit -> deployment=%s user_key=%s",
|
||||
model_id,
|
||||
self._shorten_for_logs(user_key),
|
||||
)
|
||||
return [deployment]
|
||||
|
||||
async def async_pre_call_deployment_hook(
|
||||
self, kwargs: Dict[str, Any], call_type: Optional[CallTypes]
|
||||
) -> Optional[dict]:
|
||||
"""
|
||||
Persist/update the API-key -> deployment mapping for this request.
|
||||
|
||||
Why pre-call?
|
||||
- LiteLLM runs async success callbacks via a background logging worker for performance.
|
||||
- We want affinity to be immediately available for subsequent requests.
|
||||
"""
|
||||
if not self.enable_user_key_affinity:
|
||||
return None
|
||||
|
||||
user_key = self._get_user_key_from_request_kwargs(request_kwargs=kwargs)
|
||||
if user_key is None:
|
||||
return None
|
||||
|
||||
metadata_dicts = self._iter_metadata_dicts(kwargs)
|
||||
|
||||
model_info = kwargs.get("model_info")
|
||||
if not isinstance(model_info, dict):
|
||||
model_info = None
|
||||
|
||||
if model_info is None:
|
||||
for metadata in metadata_dicts:
|
||||
maybe_model_info = metadata.get("model_info")
|
||||
if isinstance(maybe_model_info, dict):
|
||||
model_info = maybe_model_info
|
||||
break
|
||||
|
||||
if model_info is None:
|
||||
# Router sets `model_info` after selecting a deployment. If it's missing, this is
|
||||
# likely a non-router call or a call path that doesn't support affinity.
|
||||
return None
|
||||
|
||||
model_id = model_info.get("id")
|
||||
if not model_id:
|
||||
verbose_router_logger.warning(
|
||||
"DeploymentAffinityCheck: model_id missing; skipping affinity cache update."
|
||||
)
|
||||
return None
|
||||
|
||||
# Scope affinity by the Router deployment model name (alias-safe, consistent across
|
||||
# heterogeneous providers, and matches standard logging's `model_map_key`).
|
||||
deployment_model_name: Optional[str] = None
|
||||
for metadata in metadata_dicts:
|
||||
maybe_deployment_model_name = metadata.get("deployment_model_name")
|
||||
if isinstance(maybe_deployment_model_name, str) and maybe_deployment_model_name:
|
||||
deployment_model_name = maybe_deployment_model_name
|
||||
break
|
||||
|
||||
if not deployment_model_name:
|
||||
verbose_router_logger.warning(
|
||||
"DeploymentAffinityCheck: deployment_model_name missing; skipping affinity cache update. model_id=%s",
|
||||
model_id,
|
||||
)
|
||||
return None
|
||||
|
||||
try:
|
||||
cache_key = self.get_affinity_cache_key(
|
||||
model_group=deployment_model_name, user_key=user_key
|
||||
)
|
||||
await self.cache.async_set_cache(
|
||||
cache_key,
|
||||
DeploymentAffinityCacheValue(model_id=str(model_id)),
|
||||
ttl=self.ttl_seconds,
|
||||
)
|
||||
|
||||
verbose_router_logger.debug(
|
||||
"DeploymentAffinityCheck: set affinity mapping model_map_key=%s deployment=%s ttl=%s user_key=%s",
|
||||
deployment_model_name,
|
||||
model_id,
|
||||
self.ttl_seconds,
|
||||
self._shorten_for_logs(user_key),
|
||||
)
|
||||
except Exception as e:
|
||||
# Non-blocking: affinity is a best-effort optimization.
|
||||
verbose_router_logger.debug(
|
||||
"DeploymentAffinityCheck: failed to set affinity cache. model_map_key=%s error=%s",
|
||||
deployment_model_name,
|
||||
e,
|
||||
)
|
||||
|
||||
return None
|
||||
|
|
@ -10,6 +10,7 @@ This is different from the normal behavior of the router, which does not have ro
|
|||
If previous_response_id is provided, route to the deployment that returned the previous response
|
||||
"""
|
||||
|
||||
import warnings
|
||||
from typing import List, Optional
|
||||
|
||||
from litellm.integrations.custom_logger import CustomLogger, Span
|
||||
|
|
@ -18,6 +19,17 @@ from litellm.types.llms.openai import AllMessageValues
|
|||
|
||||
|
||||
class ResponsesApiDeploymentCheck(CustomLogger):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
warnings.warn(
|
||||
(
|
||||
"ResponsesApiDeploymentCheck is deprecated. "
|
||||
"Use DeploymentAffinityCheck(enable_responses_api_affinity=True) instead."
|
||||
),
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
async def async_filter_deployments(
|
||||
self,
|
||||
model: str,
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ class SupportedGuardrailIntegrations(Enum):
|
|||
ENKRYPTAI = "enkryptai"
|
||||
IBM_GUARDRAILS = "ibm_guardrails"
|
||||
LITELLM_CONTENT_FILTER = "litellm_content_filter"
|
||||
MCP_SECURITY = "mcp_security"
|
||||
ONYX = "onyx"
|
||||
PROMPT_SECURITY = "prompt_security"
|
||||
GENERIC_GUARDRAIL_API = "generic_guardrail_api"
|
||||
|
|
@ -651,6 +652,15 @@ class BaseLitellmParams(
|
|||
description="Additional provider-specific parameters for generic guardrail APIs",
|
||||
)
|
||||
|
||||
unreachable_fallback: Literal["fail_closed", "fail_open"] = Field(
|
||||
default="fail_closed",
|
||||
description=(
|
||||
"Behavior when a guardrail endpoint is unreachable due to network errors. "
|
||||
"NOTE: This is currently only implemented by guardrail='generic_guardrail_api'. "
|
||||
"'fail_closed' raises an error (default). 'fail_open' logs a critical error and allows the request to proceed."
|
||||
),
|
||||
)
|
||||
|
||||
# Custom code guardrail params
|
||||
custom_code: Optional[str] = Field(
|
||||
default=None,
|
||||
|
|
@ -692,6 +702,7 @@ class LitellmParams(
|
|||
"mode",
|
||||
"default_action",
|
||||
"on_disallowed_action",
|
||||
"unreachable_fallback",
|
||||
mode="before",
|
||||
check_fields=False,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -302,6 +302,33 @@ class PerformanceConfigBlock(TypedDict):
|
|||
latency: Literal["optimized", "throughput"]
|
||||
|
||||
|
||||
class JsonSchemaDefinition(TypedDict, total=False):
|
||||
"""JSON schema structured output format options for Bedrock Converse API."""
|
||||
|
||||
schema: Required[str] # JSON string, not dict
|
||||
name: str
|
||||
description: str
|
||||
|
||||
|
||||
class OutputFormatStructure(TypedDict, total=False):
|
||||
"""The structure that the model's output must adhere to (union type)."""
|
||||
|
||||
jsonSchema: Required[JsonSchemaDefinition]
|
||||
|
||||
|
||||
class OutputFormat(TypedDict):
|
||||
"""Structured output parameters to control the model's response."""
|
||||
|
||||
type: Literal["json_schema"]
|
||||
structure: OutputFormatStructure
|
||||
|
||||
|
||||
class OutputConfigBlock(TypedDict, total=False):
|
||||
"""Output configuration for a model response in Converse/ConverseStream."""
|
||||
|
||||
textFormat: OutputFormat
|
||||
|
||||
|
||||
class CommonRequestObject(
|
||||
TypedDict, total=False
|
||||
): # common request object across sync + async flows
|
||||
|
|
@ -314,6 +341,7 @@ class CommonRequestObject(
|
|||
performanceConfig: Optional[PerformanceConfigBlock]
|
||||
serviceTier: Optional[ServiceTierBlock]
|
||||
requestMetadata: Optional[Dict[str, str]]
|
||||
outputConfig: Optional[OutputConfigBlock]
|
||||
|
||||
|
||||
class RequestObject(CommonRequestObject, total=False):
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ class WatsonXAIEndpoint(str, Enum):
|
|||
EMBEDDINGS = "/ml/v1/text/embeddings"
|
||||
PROMPTS = "/ml/v1/prompts"
|
||||
AVAILABLE_MODELS = "/ml/v1/foundation_model_specs"
|
||||
RERANK = "/ml/v1/text/rerank"
|
||||
|
||||
|
||||
class WatsonXModelPattern(str, Enum):
|
||||
|
|
|
|||
33
litellm/types/proxy/compliance_endpoints.py
Normal file
33
litellm/types/proxy/compliance_endpoints.py
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
from typing import List, Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class ComplianceCheckResult(BaseModel):
|
||||
"""Result of a single compliance check."""
|
||||
|
||||
check_name: str
|
||||
article: str
|
||||
passed: bool
|
||||
detail: str
|
||||
|
||||
|
||||
class ComplianceResponse(BaseModel):
|
||||
"""Response from a compliance check endpoint."""
|
||||
|
||||
compliant: bool
|
||||
regulation: str
|
||||
checks: List[ComplianceCheckResult]
|
||||
|
||||
|
||||
class ComplianceCheckRequest(BaseModel):
|
||||
"""Request payload for compliance check endpoints.
|
||||
|
||||
Mirrors the spend log fields needed for compliance evaluation.
|
||||
"""
|
||||
|
||||
request_id: str
|
||||
user_id: Optional[str] = None
|
||||
model: Optional[str] = None
|
||||
timestamp: Optional[str] = None
|
||||
guardrail_information: Optional[List[dict]] = None
|
||||
|
|
@ -31,6 +31,14 @@ class GenericGuardrailAPIOptionalParams(BaseModel):
|
|||
description="Additional provider-specific parameters to send with the guardrail request",
|
||||
)
|
||||
|
||||
unreachable_fallback: Optional[Literal["fail_closed", "fail_open"]] = Field(
|
||||
default="fail_closed",
|
||||
description=(
|
||||
"Behavior when the guardrail endpoint is unreachable due to network errors. "
|
||||
"'fail_closed' raises an error (default). 'fail_open' logs a critical error and allows the request to proceed."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class GenericGuardrailAPIConfigModel(
|
||||
GuardrailConfigModel[GenericGuardrailAPIOptionalParams],
|
||||
|
|
@ -52,9 +60,9 @@ class GenericGuardrailAPIRequest(BaseModel):
|
|||
|
||||
input_type: Literal["request", "response"]
|
||||
litellm_call_id: Optional[str] = None # the call id of the individual LLM call
|
||||
litellm_trace_id: Optional[
|
||||
str
|
||||
] = None # the trace id of the LLM call - useful if there are multiple LLM calls for the same conversation
|
||||
litellm_trace_id: Optional[str] = (
|
||||
None # the trace id of the LLM call - useful if there are multiple LLM calls for the same conversation
|
||||
)
|
||||
structured_messages: Optional[List[AllMessageValues]] = None
|
||||
images: Optional[List[str]] = None
|
||||
tools: Optional[List[ChatCompletionToolParam]] = None
|
||||
|
|
|
|||
|
|
@ -802,6 +802,7 @@ OptionalPreCallChecks = List[
|
|||
"prompt_caching",
|
||||
"router_budget_limiting",
|
||||
"responses_api_deployment_check",
|
||||
"deployment_affinity",
|
||||
"forward_client_headers_by_model_group",
|
||||
"enforce_model_rate_limits",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -2532,6 +2532,8 @@ class StandardLoggingMetadata(StandardLoggingUserAPIKeyMetadata):
|
|||
cold_storage_object_key: Optional[
|
||||
str
|
||||
] # S3/GCS object key for cold storage retrieval
|
||||
team_alias: Optional[str]
|
||||
team_id: Optional[str]
|
||||
|
||||
|
||||
class StandardLoggingAdditionalHeaders(TypedDict, total=False):
|
||||
|
|
@ -3197,7 +3199,7 @@ class SearchProviders(str, Enum):
|
|||
FIRECRAWL = "firecrawl"
|
||||
SEARXNG = "searxng"
|
||||
LINKUP = "linkup"
|
||||
|
||||
DUCKDUCKGO = "duckduckgo"
|
||||
|
||||
# Create a set of all search provider values for quick lookup
|
||||
SearchProvidersSet = {provider.value for provider in SearchProviders}
|
||||
|
|
|
|||
|
|
@ -8151,6 +8151,8 @@ class ProviderConfigManager:
|
|||
return litellm.FireworksAIRerankConfig()
|
||||
elif litellm.LlmProviders.VOYAGE == provider:
|
||||
return litellm.VoyageRerankConfig()
|
||||
elif litellm.LlmProviders.WATSONX == provider:
|
||||
return litellm.IBMWatsonXRerankConfig()
|
||||
return litellm.CohereRerankConfig()
|
||||
|
||||
@staticmethod
|
||||
|
|
@ -8268,6 +8270,11 @@ class ProviderConfigManager:
|
|||
return litellm.ManusResponsesAPIConfig()
|
||||
elif litellm.LlmProviders.PERPLEXITY == provider:
|
||||
return litellm.PerplexityResponsesConfig()
|
||||
elif litellm.LlmProviders.DATABRICKS == provider:
|
||||
# Databricks Responses API is only compatible with OpenAI GPT models
|
||||
if model and "gpt" in model.lower():
|
||||
return litellm.DatabricksResponsesAPIConfig()
|
||||
return None
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
|
|
@ -8771,6 +8778,7 @@ class ProviderConfigManager:
|
|||
"""
|
||||
from litellm.llms.brave.search.transformation import BraveSearchConfig
|
||||
from litellm.llms.dataforseo.search.transformation import DataForSEOSearchConfig
|
||||
from litellm.llms.duckduckgo.search.transformation import DuckDuckGoSearchConfig
|
||||
from litellm.llms.exa_ai.search.transformation import ExaAISearchConfig
|
||||
from litellm.llms.firecrawl.search.transformation import FirecrawlSearchConfig
|
||||
from litellm.llms.google_pse.search.transformation import GooglePSESearchConfig
|
||||
|
|
@ -8793,6 +8801,7 @@ class ProviderConfigManager:
|
|||
SearchProviders.FIRECRAWL: FirecrawlSearchConfig,
|
||||
SearchProviders.SEARXNG: SearXNGSearchConfig,
|
||||
SearchProviders.LINKUP: LinkupSearchConfig,
|
||||
SearchProviders.DUCKDUCKGO: DuckDuckGoSearchConfig,
|
||||
}
|
||||
config_class = PROVIDER_TO_CONFIG_MAP.get(provider, None)
|
||||
if config_class is None:
|
||||
|
|
|
|||
|
|
@ -8294,6 +8294,37 @@
|
|||
"supports_vision": true,
|
||||
"tool_use_system_prompt_tokens": 346
|
||||
},
|
||||
"us/claude-sonnet-4-6": {
|
||||
"cache_creation_input_token_cost": 4.125e-06,
|
||||
"cache_creation_input_token_cost_above_200k_tokens": 8.25e-06,
|
||||
"cache_read_input_token_cost": 3.3e-07,
|
||||
"cache_read_input_token_cost_above_200k_tokens": 6.6e-07,
|
||||
"input_cost_per_token": 3.3e-06,
|
||||
"input_cost_per_token_above_200k_tokens": 6.6e-06,
|
||||
"litellm_provider": "anthropic",
|
||||
"max_input_tokens": 200000,
|
||||
"max_output_tokens": 64000,
|
||||
"max_tokens": 64000,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 1.65e-05,
|
||||
"output_cost_per_token_above_200k_tokens": 2.475e-05,
|
||||
"search_context_cost_per_query": {
|
||||
"search_context_size_high": 0.01,
|
||||
"search_context_size_low": 0.01,
|
||||
"search_context_size_medium": 0.01
|
||||
},
|
||||
"supports_assistant_prefill": true,
|
||||
"supports_computer_use": true,
|
||||
"supports_function_calling": true,
|
||||
"supports_pdf_input": true,
|
||||
"supports_prompt_caching": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_response_schema": true,
|
||||
"supports_tool_choice": true,
|
||||
"supports_vision": true,
|
||||
"tool_use_system_prompt_tokens": 346,
|
||||
"inference_geo": "us"
|
||||
},
|
||||
"claude-sonnet-4-5-20250929-v1:0": {
|
||||
"cache_creation_input_token_cost": 3.75e-06,
|
||||
"cache_read_input_token_cost": 3e-07,
|
||||
|
|
@ -22465,6 +22496,20 @@
|
|||
"supports_response_schema": true,
|
||||
"supports_tool_choice": true
|
||||
},
|
||||
"mistral/devstral-small-latest": {
|
||||
"input_cost_per_token": 1e-07,
|
||||
"litellm_provider": "mistral",
|
||||
"max_input_tokens": 256000,
|
||||
"max_output_tokens": 256000,
|
||||
"max_tokens": 256000,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 3e-07,
|
||||
"source": "https://docs.mistral.ai/models/devstral-small-2-25-12",
|
||||
"supports_assistant_prefill": true,
|
||||
"supports_function_calling": true,
|
||||
"supports_response_schema": true,
|
||||
"supports_tool_choice": true
|
||||
},
|
||||
"mistral/labs-devstral-small-2512": {
|
||||
"input_cost_per_token": 1e-07,
|
||||
"litellm_provider": "mistral",
|
||||
|
|
@ -22479,6 +22524,34 @@
|
|||
"supports_response_schema": true,
|
||||
"supports_tool_choice": true
|
||||
},
|
||||
"mistral/devstral-latest": {
|
||||
"input_cost_per_token": 4e-07,
|
||||
"litellm_provider": "mistral",
|
||||
"max_input_tokens": 256000,
|
||||
"max_output_tokens": 256000,
|
||||
"max_tokens": 256000,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 2e-06,
|
||||
"source": "https://mistral.ai/news/devstral-2-vibe-cli",
|
||||
"supports_assistant_prefill": true,
|
||||
"supports_function_calling": true,
|
||||
"supports_response_schema": true,
|
||||
"supports_tool_choice": true
|
||||
},
|
||||
"mistral/devstral-medium-latest": {
|
||||
"input_cost_per_token": 4e-07,
|
||||
"litellm_provider": "mistral",
|
||||
"max_input_tokens": 256000,
|
||||
"max_output_tokens": 256000,
|
||||
"max_tokens": 256000,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 2e-06,
|
||||
"source": "https://mistral.ai/news/devstral-2-vibe-cli",
|
||||
"supports_assistant_prefill": true,
|
||||
"supports_function_calling": true,
|
||||
"supports_response_schema": true,
|
||||
"supports_tool_choice": true
|
||||
},
|
||||
"mistral/devstral-2512": {
|
||||
"input_cost_per_token": 4e-07,
|
||||
"litellm_provider": "mistral",
|
||||
|
|
@ -37270,5 +37343,13 @@
|
|||
"search_context_size_low": 0.01,
|
||||
"search_context_size_medium": 0.01
|
||||
}
|
||||
},
|
||||
"duckduckgo/search": {
|
||||
"litellm_provider": "duckduckgo",
|
||||
"mode": "search",
|
||||
"input_cost_per_query": 0.0,
|
||||
"metadata": {
|
||||
"notes": "DuckDuckGo Instant Answer API is free and does not require an API key."
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue