Merge branch 'BerriAI:main' into fix-today-selector-date-mutation-bug

This commit is contained in:
Cole McIntosh
2025-06-26 12:29:44 -06:00
committed by GitHub
73 changed files with 4061 additions and 748 deletions
+55 -30
View File
@@ -33,17 +33,29 @@ jobs:
env:
CIRCLE_TOKEN: ${{ secrets.CIRCLE_TOKEN }}
run: |
# Get the latest CircleCI pipeline for this commit
COMMIT_SHA="${{ github.sha }}"
# Get the actual commit SHA after checkout
COMMIT_SHA=$(git rev-parse HEAD)
echo "Fetching CircleCI results for commit: $COMMIT_SHA"
# Get pipeline info
# Search for pipelines across all branches for this commit
PIPELINE_INFO=$(curl -s -H "Circle-Token: $CIRCLE_TOKEN" \
"https://circleci.com/api/v2/project/github/BerriAI/litellm/pipeline?branch=main" | \
"https://circleci.com/api/v2/project/github/BerriAI/litellm/pipeline" | \
jq -r ".items[] | select(.vcs.revision == \"$COMMIT_SHA\") | .id" | head -1)
# If not found, try searching recent pipelines more broadly
if [ -z "$PIPELINE_INFO" ]; then
echo "Trying broader search for recent pipelines..."
PIPELINE_INFO=$(curl -s -H "Circle-Token: $CIRCLE_TOKEN" \
"https://circleci.com/api/v2/project/github/BerriAI/litellm/pipeline" | \
jq -r ".items[0:20][] | select(.vcs.revision == \"$COMMIT_SHA\") | .id" | head -1)
fi
if [ -z "$PIPELINE_INFO" ]; then
echo "No CircleCI pipeline found for commit $COMMIT_SHA"
echo "Checking recent pipelines..."
curl -s -H "Circle-Token: $CIRCLE_TOKEN" \
"https://circleci.com/api/v2/project/github/BerriAI/litellm/pipeline" | \
jq -r ".items[0:5][] | \"Pipeline: \(.id) | Commit: \(.vcs.revision) | Branch: \(.vcs.branch) | Status: \(.state)\""
echo "Creating placeholder test results..."
echo '<?xml version="1.0" encoding="utf-8"?>' > test-results/junit.xml
echo '<testsuite name="llm_translation" tests="0" failures="0" errors="0" skipped="0" time="0">' >> test-results/junit.xml
@@ -54,13 +66,20 @@ jobs:
echo "Found pipeline: $PIPELINE_INFO"
# Get workflow info
# Get workflow info - look for any workflow that might contain tests
WORKFLOW_ID=$(curl -s -H "Circle-Token: $CIRCLE_TOKEN" \
"https://circleci.com/api/v2/pipeline/$PIPELINE_INFO/workflow" | \
jq -r '.items[] | select(.name | contains("test")) | .id' | head -1)
jq -r '.items[] | select(.name | test("test|Test|TEST")) | .id' | head -1)
if [ -z "$WORKFLOW_ID" ]; then
echo "No test workflow found in pipeline"
echo "No test workflow found, trying any workflow..."
WORKFLOW_ID=$(curl -s -H "Circle-Token: $CIRCLE_TOKEN" \
"https://circleci.com/api/v2/pipeline/$PIPELINE_INFO/workflow" | \
jq -r '.items[0].id')
fi
if [ -z "$WORKFLOW_ID" ]; then
echo "No workflow found in pipeline"
exit 1
fi
@@ -69,32 +88,50 @@ jobs:
# Get job info for llm_translation tests
JOB_INFO=$(curl -s -H "Circle-Token: $CIRCLE_TOKEN" \
"https://circleci.com/api/v2/workflow/$WORKFLOW_ID/job" | \
jq -r '.items[] | select(.name | contains("llm_translation")) | select(.status == "success" or .status == "failed") | .job_number' | head -1)
jq -r '.items[] | select(.name | test("llm_translation|llm-translation")) | select(.status == "success" or .status == "failed") | .job_number' | head -1)
if [ -z "$JOB_INFO" ]; then
echo "No completed llm_translation job found"
exit 1
echo "No completed llm_translation job found, checking all jobs:"
curl -s -H "Circle-Token: $CIRCLE_TOKEN" \
"https://circleci.com/api/v2/workflow/$WORKFLOW_ID/job" | \
jq -r '.items[] | "Job: \(.name) | Status: \(.status) | Number: \(.job_number)"'
echo "Creating placeholder test results..."
echo '<?xml version="1.0" encoding="utf-8"?>' > test-results/junit.xml
echo '<testsuite name="llm_translation" tests="0" failures="0" errors="0" skipped="0" time="0">' >> test-results/junit.xml
echo '<system-out>No llm_translation job found in CircleCI</system-out>' >> test-results/junit.xml
echo '</testsuite>' >> test-results/junit.xml
exit 0
fi
echo "Found job: $JOB_INFO"
# Download artifacts
ARTIFACT_COUNT=0
curl -s -H "Circle-Token: $CIRCLE_TOKEN" \
"https://circleci.com/api/v2/project/github/BerriAI/litellm/$JOB_INFO/artifacts" | \
jq -r '.items[] | select(.path | contains("junit") or contains("coverage") or contains("report")) | .url' | \
jq -r '.items[] | select(.path | test("junit|coverage|report|xml|html")) | .url' | \
while read -r artifact_url; do
filename=$(basename "$artifact_url" | sed 's/[?&].*//')
echo "Downloading artifact: $filename"
curl -s -H "Circle-Token: $CIRCLE_TOKEN" -o "test-results/$filename" "$artifact_url"
if [ -n "$artifact_url" ]; then
filename=$(basename "$artifact_url" | sed 's/[?&].*//')
echo "Downloading artifact: $filename from $artifact_url"
if curl -s -H "Circle-Token: $CIRCLE_TOKEN" -o "test-results/$filename" "$artifact_url"; then
echo "Successfully downloaded $filename"
ARTIFACT_COUNT=$((ARTIFACT_COUNT + 1))
else
echo "Failed to download $filename"
fi
fi
done
# If no artifacts found, create placeholder
if [ ! -f "test-results/junit.xml" ]; then
if [ ! -f "test-results/junit.xml" ] && [ "$ARTIFACT_COUNT" -eq 0 ]; then
echo "No test artifacts found, creating placeholder..."
echo '<?xml version="1.0" encoding="utf-8"?>' > test-results/junit.xml
echo '<testsuite name="llm_translation" tests="0" failures="0" errors="0" skipped="0" time="0">' >> test-results/junit.xml
echo '<system-out>Test artifacts not available from CircleCI</system-out>' >> test-results/junit.xml
echo '</testsuite>' >> test-results/junit.xml
else
echo "Successfully retrieved $ARTIFACT_COUNT artifacts"
fi
continue-on-error: true
@@ -136,22 +173,10 @@ jobs:
echo "## Test Files Covered" >> test-results/summary.md
ls tests/llm_translation/*.py | sed 's/^/- /' >> test-results/summary.md
- name: Upload test results
- name: Upload test artifacts
uses: actions/upload-artifact@v4
if: always()
with:
name: llm-translation-test-results-${{ github.event.inputs.release_candidate_tag || github.ref_name }}
path: |
test-results/
coverage.xml
htmlcov/
.coverage
name: llm-translation-test-artifacts-${{ github.event.inputs.release_candidate_tag || github.ref_name }}
path: test-results/
retention-days: 30
- name: Upload JUnit test results
uses: actions/upload-artifact@v4
if: always()
with:
name: junit-xml-${{ github.event.inputs.release_candidate_tag || github.ref_name }}
path: test-results/junit.xml
retention-days: 30
+1
View File
@@ -10,6 +10,7 @@
# Misc
.DS_Store
.env
.env.local
.env.development.local
.env.test.local
@@ -1,14 +1,14 @@
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
# SSL Security Settings
# SSL, HTTP Proxy Security Settings
If you're in an environment using an older TTS bundle, with an older encryption, follow this guide.
LiteLLM uses HTTPX for network requests, unless otherwise specified.
1. Disable SSL verification
## 1. Disable SSL verification
<Tabs>
@@ -35,7 +35,7 @@ export SSL_VERIFY="False"
</TabItem>
</Tabs>
2. Lower security settings
## 2. Lower security settings
<Tabs>
<TabItem value="sdk" label="SDK">
@@ -63,4 +63,29 @@ export SSL_CERTIFICATE="/path/to/certificate.pem"
</TabItem>
</Tabs>
## 3. Use HTTP_PROXY environment variable
Both httpx and aiohttp libraries use `urllib.request.getproxies` from environment variables. Before client initialization, you may set proxy (and optional SSL_CERT_FILE) by setting the environment variables:
<Tabs>
<TabItem value="sdk" label="SDK">
```python
import litellm
litellm.aiohttp_trust_env = True
```
```bash
export HTTPS_PROXY='http://username:password@proxy_uri:port'
```
</TabItem>
<TabItem value="proxy" label="PROXY">
```bash
export HTTPS_PROXY='http://username:password@proxy_uri:port'
export AIOHTTP_TRUST_ENV='True'
```
</TabItem>
</Tabs>
@@ -307,6 +307,7 @@ router_settings:
| AGENTOPS_SERVICE_NAME | Service Name for AgentOps logging integration
| AISPEND_ACCOUNT_ID | Account ID for AI Spend
| AISPEND_API_KEY | API Key for AI Spend
| AIOHTTP_TRUST_ENV | Flag to enable aiohttp trust environment. When this is set to True, aiohttp will respect HTTP(S)_PROXY env vars. **Default is False**
| ALLOWED_EMAIL_DOMAINS | List of email domains allowed for access
| ARIZE_API_KEY | API key for Arize platform integration
| ARIZE_SPACE_KEY | Space key for Arize platform
@@ -32,7 +32,7 @@ These headers are useful for clients to understand the current rate limit status
## Latency Headers
| Header | Type | Description |
|--------|------|-------------|
| `x-litellm-response-duration-ms` | float | Total duration of the API response in milliseconds |
| `x-litellm-response-duration-ms` | float | Total duration from the moment that a request gets to LiteLLM Proxy to the moment it gets returned to the client. |
| `x-litellm-overhead-duration-ms` | float | LiteLLM processing overhead in milliseconds |
## Retry, Fallback Headers
@@ -0,0 +1,251 @@
import Image from '@theme/IdealImage';
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
# Elasticsearch Logging with LiteLLM
Send your LLM requests, responses, costs, and performance data to Elasticsearch for analytics and monitoring using OpenTelemetry.
<Image img={require('../../img/elasticsearch_demo.png')} />
## Quick Start
### 1. Start Elasticsearch
```bash
# Using Docker (simplest)
docker run -d \
--name elasticsearch \
-p 9200:9200 \
-e "discovery.type=single-node" \
-e "xpack.security.enabled=false" \
docker.elastic.co/elasticsearch/elasticsearch:8.18.2
```
### 2. Set up OpenTelemetry Collector
Create an OTEL collector configuration file `otel_config.yaml`:
```yaml
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
batch:
timeout: 1s
send_batch_size: 1024
exporters:
debug:
verbosity: detailed
otlphttp/elastic:
endpoint: "http://localhost:9200"
headers:
"Content-Type": "application/json"
service:
pipelines:
metrics:
receivers: [otlp]
exporters: [debug, otlphttp/elastic]
traces:
receivers: [otlp]
exporters: [debug, otlphttp/elastic]
logs:
receivers: [otlp]
exporters: [debug, otlphttp/elastic]
```
Start the OpenTelemetry collector:
```bash
docker run -p 4317:4317 -p 4318:4318 \
-v $(pwd)/otel_config.yaml:/etc/otel-collector-config.yaml \
otel/opentelemetry-collector:latest \
--config=/etc/otel-collector-config.yaml
```
### 3. Install OpenTelemetry Dependencies
```bash
pip install opentelemetry-api opentelemetry-sdk opentelemetry-exporter-otlp
```
### 4. Configure LiteLLM
<Tabs>
<TabItem value="proxy" label="LiteLLM Proxy">
Create a `config.yaml` file:
```yaml
model_list:
- model_name: gpt-4.1
litellm_params:
model: openai/gpt-4.1
api_key: os.environ/OPENAI_API_KEY
litellm_settings:
callbacks: ["otel"]
general_settings:
otel: true
```
Set environment variables and start the proxy:
```bash
export OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4317"
litellm --config config.yaml
```
</TabItem>
<TabItem value="python-sdk" label="Python SDK">
Configure OpenTelemetry in your Python code:
```python
import litellm
import os
# Configure OpenTelemetry
os.environ["OTEL_EXPORTER_OTLP_ENDPOINT"] = "http://localhost:4317"
# Enable OTEL logging
litellm.callbacks = ["otel"]
# Make your LLM calls
response = litellm.completion(
model="gpt-4.1",
messages=[{"role": "user", "content": "Hello, world!"}]
)
```
</TabItem>
</Tabs>
### 5. Test the Integration
Make a test request to verify logging is working:
<Tabs>
<TabItem value="curl-proxy" label="Test Proxy">
```bash
curl -X POST "http://localhost:4000/v1/chat/completions" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk-1234" \
-d '{
"model": "gpt-4.1",
"messages": [{"role": "user", "content": "Hello from LiteLLM!"}]
}'
```
</TabItem>
<TabItem value="python-test" label="Test Python SDK">
```python
import litellm
response = litellm.completion(
model="gpt-4.1",
messages=[{"role": "user", "content": "Hello from LiteLLM!"}],
user="test-user"
)
print("Response:", response.choices[0].message.content)
```
</TabItem>
</Tabs>
### 6. Verify It's Working
```bash
# Check if traces are being created in Elasticsearch
curl "localhost:9200/_search?pretty&size=1"
```
You should see OpenTelemetry trace data with structured fields for your LLM requests.
### 7. Visualize in Kibana
Start Kibana to visualize your LLM telemetry data:
```bash
docker run -d --name kibana --link elasticsearch:elasticsearch -p 5601:5601 docker.elastic.co/kibana/kibana:8.18.2
```
Open Kibana at http://localhost:5601 and create an index pattern for your LiteLLM traces:
<Image img={require('../../img/elasticsearch_demo.png')} />
## Production Setup
**With Elasticsearch Cloud:**
Update your `otel_config.yaml`:
```yaml
exporters:
otlphttp/elastic:
endpoint: "https://your-deployment.es.region.cloud.es.io"
headers:
"Authorization": "Bearer your-api-key"
"Content-Type": "application/json"
```
**Docker Compose (Full Stack):**
```yaml
# docker-compose.yml
version: '3.8'
services:
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:8.18.2
environment:
- discovery.type=single-node
- xpack.security.enabled=false
ports:
- "9200:9200"
otel-collector:
image: otel/opentelemetry-collector:latest
command: ["--config=/etc/otel-collector-config.yaml"]
volumes:
- ./otel_config.yaml:/etc/otel-collector-config.yaml
ports:
- "4317:4317"
- "4318:4318"
depends_on:
- elasticsearch
litellm:
image: ghcr.io/berriai/litellm:main-latest
ports:
- "4000:4000"
environment:
- OPENAI_API_KEY=${OPENAI_API_KEY}
- OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
command: ["--config", "/app/config.yaml"]
volumes:
- ./config.yaml:/app/config.yaml
depends_on:
- otel-collector
```
**config.yaml:**
```yaml
model_list:
- model_name: gpt-4.1
litellm_params:
model: openai/gpt-4.1
api_key: os.environ/OPENAI_API_KEY
litellm_settings:
callbacks: ["otel"]
general_settings:
master_key: sk-1234
otel: true
```
@@ -0,0 +1,68 @@
# Use LiteLLM with Gemini CLI
This tutorial shows you how to integrate the Gemini CLI with LiteLLM Proxy, allowing you to route requests through LiteLLM's unified interface.
## Prerequisites
Before you begin, ensure you have:
- Node.js and npm installed on your system
- A running LiteLLM Proxy instance
- A valid LiteLLM Proxy API key
- Git installed for cloning the repository
## Quick Start Guide
### Step 1: Install Gemini CLI
Clone the Gemini CLI repository and navigate to the project directory:
```bash
git clone https://github.com/ishaan-jaff/gemini-cli.git
cd gemini-cli
```
Install the required dependencies:
```bash
npm install
```
### Step 2: Configure Gemini CLI for LiteLLM Proxy
Configure the Gemini CLI to point to your LiteLLM Proxy instance by setting the required environment variables:
```bash
export BASE_URL=http://localhost:4000
export GEMINI_API_KEY=sk-1234567890
```
**Note:** Replace the values with your actual LiteLLM Proxy configuration:
- `BASE_URL`: The URL where your LiteLLM Proxy is running
- `GEMINI_API_KEY`: Your LiteLLM Proxy API key
### Step 3: Build and Start Gemini CLI
Build the project and start the CLI:
```bash
npm run build && npm start
```
### Step 4: Test the Integration
Once the CLI is running, you can send test requests. These requests will be automatically routed through LiteLLM Proxy to the configured Gemini model.
The CLI will now use LiteLLM Proxy as the backend, giving you access to LiteLLM's features like:
- Request/response logging
- Rate limiting
- Cost tracking
- Model routing and fallbacks
## Troubleshooting
If you encounter issues:
1. **Connection errors**: Verify that your LiteLLM Proxy is running and accessible at the configured `BASE_URL`
2. **Authentication errors**: Ensure your `GEMINI_API_KEY` is valid and has the necessary permissions
3. **Build failures**: Make sure all dependencies are installed with `npm install`
+50 -9
View File
@@ -1,9 +1,48 @@
// @ts-check
// Note: type annotations allow type checking and IDEs autocompletion
require('dotenv').config();
// @ts-ignore
const lightCodeTheme = require('prism-react-renderer/themes/github');
// @ts-ignore
const darkCodeTheme = require('prism-react-renderer/themes/dracula');
const inkeepConfig = {
baseSettings: {
apiKey: "0cb9c9916ec71bfe0e53c9d7f83ff046daee3fa9ef318f6a",
organizationDisplayName: 'liteLLM',
primaryBrandColor: '#4965f5',
theme: {
styles: [
{
key: "custom-theme",
type: "style",
value: `
.ikp-chat-button__button {
margin-right: 80px !important;
}
`,
},
],
syntaxHighlighter: {
lightTheme: lightCodeTheme,
darkTheme: darkCodeTheme,
},
},
},
searchSettings: {
searchBarPlaceholder: 'Search docs...',
},
aiChatSettings: {
quickQuestions: [
'How do I use the proxy?',
'How do I cache responses?',
'How do I stream responses?',
],
aiAssistantAvatar: '/img/favicon.ico',
},
};
/** @type {import('@docusaurus/types').Config} */
const config = {
title: 'liteLLM',
@@ -27,6 +66,17 @@ const config = {
locales: ['en'],
},
plugins: [
[
'@inkeep/cxkit-docusaurus',
{
SearchBar: {
...inkeepConfig,
},
ChatButton: {
...inkeepConfig,
},
},
],
[
'@docusaurus/plugin-ideal-image',
{
@@ -101,15 +151,6 @@ const config = {
({
// Replace with your project's social card
image: 'img/docusaurus-social-card.png',
algolia: {
// The application ID provided by Algolia
appId: 'NU85Y4NU0B',
// Public API key: it is safe to commit it
apiKey: '4e0cf8c3020d0c876ad9174cea5c01fb',
indexName: 'litellm',
},
navbar: {
title: '🚅 LiteLLM',
items: [
Binary file not shown.

After

Width:  |  Height:  |  Size: 538 KiB

+3 -1
View File
@@ -18,6 +18,7 @@
"@docusaurus/plugin-google-gtag": "3.8.1",
"@docusaurus/plugin-ideal-image": "3.8.1",
"@docusaurus/preset-classic": "3.8.1",
"@inkeep/cxkit-docusaurus": "^0.5.89",
"@mdx-js/react": "^3.0.0",
"clsx": "^1.2.1",
"prism-react-renderer": "^1.3.5",
@@ -27,7 +28,8 @@
"uuid": "^9.0.1"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "3.8.1"
"@docusaurus/module-type-aliases": "3.8.1",
"dotenv": "^16.4.5"
},
"browserslist": {
"production": [
+2
View File
@@ -69,6 +69,7 @@ const sidebars = {
items: [
"tutorials/openweb_ui",
"tutorials/openai_codex",
"tutorials/litellm_gemini_cli",
"tutorials/claude_responses_api",
]
},
@@ -525,6 +526,7 @@ const sidebars = {
"tutorials/prompt_caching",
"tutorials/tag_management",
'tutorials/litellm_proxy_aporia',
"tutorials/elasticsearch_logging",
"tutorials/gemini_realtime_with_audio",
"tutorials/claude_responses_api",
{
+3 -3
View File
@@ -1,8 +1,8 @@
from typing import Dict, Literal, Type, Union
from litellm.integrations.custom_logger import CustomLogger
from litellm_enterprise.proxy.hooks.managed_files import _PROXY_LiteLLMManagedFiles
from .managed_files import _PROXY_LiteLLMManagedFiles
from litellm.integrations.custom_logger import CustomLogger
ENTERPRISE_PROXY_HOOKS: Dict[str, Type[CustomLogger]] = {
"managed_files": _PROXY_LiteLLMManagedFiles,
@@ -16,7 +16,7 @@ def get_enterprise_proxy_hook(
"max_parallel_requests",
],
str,
]
],
):
"""
Factory method to get a enterprise hook instance by name
@@ -0,0 +1,134 @@
"""
Polls LiteLLM_ManagedObjectTable to check if the batch job is complete, and if the cost has been tracked.
"""
from typing import TYPE_CHECKING, Optional, cast
from litellm._logging import verbose_proxy_logger
if TYPE_CHECKING:
from litellm.proxy.utils import PrismaClient, ProxyLogging
from litellm.router import Router
class CheckBatchCost:
def __init__(
self,
proxy_logging_obj: "ProxyLogging",
prisma_client: "PrismaClient",
llm_router: "Router",
):
from litellm.proxy.utils import PrismaClient, ProxyLogging
from litellm.router import Router
self.proxy_logging_obj: ProxyLogging = proxy_logging_obj
self.prisma_client: PrismaClient = prisma_client
self.llm_router: Router = llm_router
async def check_batch_cost(self):
"""
Check if the batch JOB has been tracked.
- get all status="validating" and file_purpose="batch" jobs
- check if batch is now complete
- if not, return False
- if so, return True
"""
from litellm_enterprise.proxy.hooks.managed_files import (
_PROXY_LiteLLMManagedFiles,
)
from litellm.batches.batch_utils import (
_get_file_content_as_dictionary,
calculate_batch_cost_and_usage,
)
from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider
from litellm.proxy.openai_files_endpoints.common_utils import (
_is_base64_encoded_unified_file_id,
get_batch_id_from_unified_batch_id,
get_model_id_from_unified_batch_id,
)
jobs = await self.prisma_client.db.litellm_managedobjecttable.find_many(
where={
"status": "validating",
"file_purpose": "batch",
}
)
for job in jobs:
# get the model from the job
unified_object_id = job.unified_object_id
decoded_unified_object_id = _is_base64_encoded_unified_file_id(
unified_object_id
)
if not decoded_unified_object_id:
verbose_proxy_logger.info(
f"Skipping job {unified_object_id} because it is not a valid unified object id"
)
continue
else:
unified_object_id = decoded_unified_object_id
model_id = get_model_id_from_unified_batch_id(unified_object_id)
batch_id = get_batch_id_from_unified_batch_id(unified_object_id)
if model_id is None:
verbose_proxy_logger.info(
f"Skipping job {unified_object_id} because it is not a valid model id"
)
continue
response = await self.llm_router.aretrieve_batch(
model=model_id,
batch_id=batch_id,
)
## RETRIEVE THE BATCH JOB OUTPUT FILE
managed_files_obj = cast(
Optional[_PROXY_LiteLLMManagedFiles],
self.proxy_logging_obj.get_proxy_hook("managed_files"),
)
if (
response.status == "completed"
and response.output_file_id is not None
and managed_files_obj is not None
):
# track cost
model_file_id_mapping = {
response.output_file_id: {model_id: response.output_file_id}
}
_file_content = await managed_files_obj.afile_content(
file_id=response.output_file_id,
litellm_parent_otel_span=None,
llm_router=self.llm_router,
model_file_id_mapping=model_file_id_mapping,
)
file_content_as_dict = _get_file_content_as_dictionary(
_file_content.content
)
deployment_info = self.llm_router.get_deployment(model_id=model_id)
if deployment_info is None:
verbose_proxy_logger.info(
f"Skipping job {unified_object_id} because it is not a valid deployment info"
)
continue
custom_llm_provider = deployment_info.litellm_params.custom_llm_provider
litellm_model_name = deployment_info.litellm_params.model
_, llm_provider, _, _ = get_llm_provider(
model=litellm_model_name,
custom_llm_provider=custom_llm_provider,
)
batch_cost, batch_usage, batch_models = (
await calculate_batch_cost_and_usage(
file_content_dictionary=file_content_as_dict,
custom_llm_provider=llm_provider, # type: ignore
)
)
if response.status != "validating":
# mark for updating
pass
@@ -23,6 +23,8 @@ from litellm.proxy._types import (
from litellm.proxy.openai_files_endpoints.common_utils import (
_is_base64_encoded_unified_file_id,
convert_b64_uid_to_unified_uid,
get_batch_id_from_unified_batch_id,
get_model_id_from_unified_batch_id,
)
from litellm.types.llms.openai import (
AllMessageValues,
@@ -40,6 +42,10 @@ from litellm.types.utils import (
SpecialEnums,
)
if TYPE_CHECKING:
from litellm.types.llms.openai import HttpxBinaryResponseContent
if TYPE_CHECKING:
from opentelemetry.trace import Span as _Span
@@ -66,7 +72,7 @@ class _PROXY_LiteLLMManagedFiles(CustomLogger, BaseFileEndpoints):
async def store_unified_file_id(
self,
file_id: str,
file_object: OpenAIFileObject,
file_object: Optional[OpenAIFileObject],
litellm_parent_otel_span: Optional[Span],
model_mappings: Dict[str, str],
user_api_key_dict: UserAPIKeyAuth,
@@ -74,29 +80,39 @@ class _PROXY_LiteLLMManagedFiles(CustomLogger, BaseFileEndpoints):
verbose_logger.info(
f"Storing LiteLLM Managed File object with id={file_id} in cache"
)
litellm_managed_file_object = LiteLLM_ManagedFileTable(
unified_file_id=file_id,
file_object=file_object,
model_mappings=model_mappings,
flat_model_file_ids=list(model_mappings.values()),
created_by=user_api_key_dict.user_id,
updated_by=user_api_key_dict.user_id,
)
await self.internal_usage_cache.async_set_cache(
key=file_id,
value=litellm_managed_file_object.model_dump(),
litellm_parent_otel_span=litellm_parent_otel_span,
)
if file_object is not None:
litellm_managed_file_object = LiteLLM_ManagedFileTable(
unified_file_id=file_id,
file_object=file_object,
model_mappings=model_mappings,
flat_model_file_ids=list(model_mappings.values()),
created_by=user_api_key_dict.user_id,
updated_by=user_api_key_dict.user_id,
)
await self.internal_usage_cache.async_set_cache(
key=file_id,
value=litellm_managed_file_object.model_dump(),
litellm_parent_otel_span=litellm_parent_otel_span,
)
await self.prisma_client.db.litellm_managedfiletable.create(
data={
"unified_file_id": file_id,
"file_object": file_object.model_dump_json(),
"model_mappings": json.dumps(model_mappings),
"flat_model_file_ids": list(model_mappings.values()),
"created_by": user_api_key_dict.user_id,
"updated_by": user_api_key_dict.user_id,
}
## STORE MODEL MAPPINGS IN DB
db_data = {
"unified_file_id": file_id,
"model_mappings": json.dumps(model_mappings),
"flat_model_file_ids": list(model_mappings.values()),
"created_by": user_api_key_dict.user_id,
"updated_by": user_api_key_dict.user_id,
}
if file_object is not None:
db_data["file_object"] = file_object.model_dump_json()
result = await self.prisma_client.db.litellm_managedfiletable.create(
data=db_data
)
verbose_logger.debug(
f"LiteLLM Managed File object with id={file_id} stored in db: {result}"
)
async def store_unified_object_id(
@@ -131,6 +147,7 @@ class _PROXY_LiteLLMManagedFiles(CustomLogger, BaseFileEndpoints):
"file_purpose": file_purpose,
"created_by": user_api_key_dict.user_id,
"updated_by": user_api_key_dict.user_id,
"status": file_object.status,
}
)
@@ -182,10 +199,12 @@ class _PROXY_LiteLLMManagedFiles(CustomLogger, BaseFileEndpoints):
self, unified_file_id: str, user_api_key_dict: UserAPIKeyAuth
) -> bool:
## check if the user has access to the unified file id
user_id = user_api_key_dict.user_id
managed_file = await self.prisma_client.db.litellm_managedfiletable.find_first(
where={"unified_file_id": unified_file_id}
)
if managed_file:
return managed_file.created_by == user_id
return False
@@ -347,7 +366,7 @@ class _PROXY_LiteLLMManagedFiles(CustomLogger, BaseFileEndpoints):
)
## for managed batch id - get the model id
potential_model_id = self.get_model_id_from_unified_batch_id(
potential_model_id = get_model_id_from_unified_batch_id(
potential_llm_object_id
)
if potential_model_id is None:
@@ -355,7 +374,7 @@ class _PROXY_LiteLLMManagedFiles(CustomLogger, BaseFileEndpoints):
f"LiteLLM Managed {accessor_key} with id={retrieve_object_id} is invalid - does not contain encoded model_id."
)
data["model"] = potential_model_id
data[accessor_key] = self.get_batch_id_from_unified_batch_id(
data[accessor_key] = get_batch_id_from_unified_batch_id(
potential_llm_object_id
)
elif call_type == CallTypes.acreate_fine_tuning_job.value:
@@ -367,6 +386,36 @@ class _PROXY_LiteLLMManagedFiles(CustomLogger, BaseFileEndpoints):
return data
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]:
if request_kwargs is None:
return healthy_deployments
input_file_id = cast(Optional[str], request_kwargs.get("input_file_id"))
model_file_id_mapping = cast(
Optional[Dict[str, Dict[str, str]]],
request_kwargs.get("model_file_id_mapping"),
)
allowed_model_ids = []
if input_file_id and model_file_id_mapping:
model_id_dict = model_file_id_mapping.get(input_file_id, {})
allowed_model_ids = list(model_id_dict.keys())
if len(allowed_model_ids) == 0:
return healthy_deployments
return [
deployment
for deployment in healthy_deployments
if deployment.get("model_info", {}).get("id") in allowed_model_ids
]
async def async_pre_call_deployment_hook(
self, kwargs: Dict[str, Any], call_type: Optional[CallTypes]
) -> Optional[dict]:
@@ -500,15 +549,13 @@ class _PROXY_LiteLLMManagedFiles(CustomLogger, BaseFileEndpoints):
## STORE MODEL MAPPINGS IN DB
model_mappings: Dict[str, str] = {}
for file_object in responses:
model_id = file_object._hidden_params.get("model_id")
if model_id is None:
verbose_logger.warning(
f"Skipping file_object: {file_object} because model_id in hidden_params={file_object._hidden_params} is None"
)
continue
file_id = file_object.id
model_mappings[model_id] = file_id
model_file_id_mapping = file_object._hidden_params.get(
"model_file_id_mapping"
)
if model_file_id_mapping and isinstance(model_file_id_mapping, dict):
model_mappings.update(model_file_id_mapping)
await self.store_unified_file_id(
file_id=response.id,
@@ -583,13 +630,13 @@ class _PROXY_LiteLLMManagedFiles(CustomLogger, BaseFileEndpoints):
return base64.urlsafe_b64encode(unified_batch_id.encode()).decode().rstrip("=")
def get_unified_output_file_id(
self, output_file_id: str, model_id: str, model_name: str
self, output_file_id: str, model_id: str, model_name: Optional[str]
) -> str:
unified_output_file_id = (
SpecialEnums.LITELLM_MANAGED_FILE_COMPLETE_STR.value.format(
"application/json",
str(uuid.uuid4()),
model_name,
model_name or "",
output_file_id,
model_id,
)
@@ -606,25 +653,6 @@ class _PROXY_LiteLLMManagedFiles(CustomLogger, BaseFileEndpoints):
def get_output_file_id_from_unified_file_id(self, file_id: str) -> str:
return file_id.split("llm_output_file_id,")[1].split(";")[0]
def get_model_id_from_unified_batch_id(self, file_id: str) -> Optional[str]:
"""
Get the model_id from the file_id
Expected format: litellm_proxy;model_id:{};llm_batch_id:{};llm_output_file_id:{}
"""
## use regex to get the model_id from the file_id
try:
return file_id.split("model_id:")[1].split(";")[0]
except Exception:
return None
def get_batch_id_from_unified_batch_id(self, file_id: str) -> str:
## use regex to get the batch_id from the file_id
if "llm_batch_id" in file_id:
return file_id.split("llm_batch_id:")[1].split(",")[0]
else:
return file_id.split("generic_response_id:")[1].split(",")[0]
async def async_post_call_success_hook(
self, data: Dict, user_api_key_dict: UserAPIKeyAuth, response: LLMResponseTypes
) -> Any:
@@ -639,19 +667,28 @@ class _PROXY_LiteLLMManagedFiles(CustomLogger, BaseFileEndpoints):
model_id = cast(Optional[str], response._hidden_params.get("model_id"))
model_name = cast(Optional[str], response._hidden_params.get("model_name"))
original_response_id = response.id
if (unified_batch_id or unified_file_id) and model_id:
response.id = self.get_unified_batch_id(
batch_id=response.id, model_id=model_id
)
if (
response.output_file_id and model_name and model_id
response.output_file_id and model_id
): # return a file id with the model_id and output_file_id
original_output_file_id = response.output_file_id
response.output_file_id = self.get_unified_output_file_id(
output_file_id=response.output_file_id,
model_id=model_id,
model_name=model_name,
)
await self.store_unified_file_id( # need to store otherwise any retrieve call will fail
file_id=response.output_file_id,
file_object=None,
litellm_parent_otel_span=user_api_key_dict.parent_otel_span,
model_mappings={model_id: original_output_file_id},
user_api_key_dict=user_api_key_dict,
)
asyncio.create_task(
self.store_unified_object_id(
unified_object_id=response.id,
@@ -763,12 +800,14 @@ class _PROXY_LiteLLMManagedFiles(CustomLogger, BaseFileEndpoints):
litellm_parent_otel_span: Optional[Span],
llm_router: Router,
**data: Dict,
) -> str:
) -> "HttpxBinaryResponseContent":
"""
Get the content of a file from first model that has it
"""
model_file_id_mapping = await self.get_model_file_id_mapping(
[file_id], litellm_parent_otel_span
model_file_id_mapping = data.pop("model_file_id_mapping", None)
model_file_id_mapping = (
model_file_id_mapping
or await self.get_model_file_id_mapping([file_id], litellm_parent_otel_span)
)
specific_model_file_id_mapping = model_file_id_mapping.get(file_id)
@@ -0,0 +1,9 @@
-- DropForeignKey
ALTER TABLE "LiteLLM_TeamMembership" DROP CONSTRAINT "LiteLLM_TeamMembership_budget_id_fkey";
-- AlterTable
ALTER TABLE "LiteLLM_ManagedFileTable" ALTER COLUMN "file_object" DROP NOT NULL;
-- AddForeignKey
ALTER TABLE "LiteLLM_TeamMembership" ADD CONSTRAINT "LiteLLM_TeamMembership_budget_id_fkey" FOREIGN KEY ("budget_id") REFERENCES "LiteLLM_BudgetTable"("budget_id") ON DELETE SET NULL ON UPDATE CASCADE;
@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "LiteLLM_ManagedObjectTable" ADD COLUMN "status" TEXT;
@@ -156,7 +156,6 @@ model LiteLLM_ObjectPermissionTable {
object_permission_id String @id @default(uuid())
mcp_servers String[] @default([])
vector_stores String[] @default([])
teams LiteLLM_TeamTable[]
verification_tokens LiteLLM_VerificationToken[]
organizations LiteLLM_OrganizationTable[]
@@ -453,8 +452,8 @@ enum JobStatus {
model LiteLLM_ManagedFileTable {
id String @id @default(uuid())
unified_file_id String @unique // The base64 encoded unified file ID
file_object Json // Stores the OpenAIFileObject
model_mappings Json
file_object Json? // Stores the OpenAIFileObject
model_mappings Json
flat_model_file_ids String[] @default([]) // Flat list of model file id's - for faster querying of model id -> unified file id
created_at DateTime @default(now())
created_by String?
@@ -469,7 +468,8 @@ model LiteLLM_ManagedObjectTable { // for batches or finetuning jobs which use t
unified_object_id String @unique // The base64 encoded unified file ID
model_object_id String @unique // the id returned by the backend API provider
file_object Json // Stores the OpenAIFileObject
file_purpose String // either 'batch' or 'fine-tune'
file_purpose String // either 'batch' or 'fine-tune'
status String? // check if batch cost has been tracked
created_at DateTime @default(now())
created_by String?
updated_at DateTime @updatedAt
+2 -2
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "litellm-proxy-extras"
version = "0.2.5"
version = "0.2.6"
description = "Additional files for the LiteLLM Proxy. Reduces the size of the main litellm package."
authors = ["BerriAI"]
readme = "README.md"
@@ -22,7 +22,7 @@ requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
[tool.commitizen]
version = "0.2.5"
version = "0.2.6"
version_files = [
"pyproject.toml:version",
"../requirements.txt:litellm-proxy-extras==",
+1 -1
View File
@@ -323,6 +323,7 @@ priority_reservation: Optional[Dict[str, float]] = None
use_aiohttp_transport: bool = (
True # Older variable, aiohttp is now the default. use disable_aiohttp_transport instead.
)
aiohttp_trust_env: bool = False # set to true to use HTTP_ Proxy settings
disable_aiohttp_transport: bool = False # Set this to true to use httpx instead
disable_aiohttp_trust_env: bool = False # When False, aiohttp will respect HTTP(S)_PROXY env vars
force_ipv4: bool = (
@@ -1151,7 +1152,6 @@ from .fine_tuning.main import *
from .files.main import *
from .scheduler import *
from .cost_calculator import response_cost_calculator, cost_per_token
### ADAPTERS ###
from .types.adapter import AdapterItem
import litellm.anthropic_interface as anthropic
+89 -1
View File
@@ -19,6 +19,7 @@ import redis.asyncio as async_redis # type: ignore
from litellm import get_secret, get_secret_str
from litellm.constants import REDIS_CONNECTION_POOL_TIMEOUT, REDIS_SOCKET_TIMEOUT
from litellm.litellm_core_utils.sensitive_data_masker import SensitiveDataMasker
from ._logging import verbose_logger
@@ -309,7 +310,7 @@ def get_redis_async_client(
# Check for Redis Sentinel
if "sentinel_nodes" in redis_kwargs and "service_name" in redis_kwargs:
return _init_async_redis_sentinel(redis_kwargs)
_pretty_print_redis_config(redis_kwargs=redis_kwargs)
return async_redis.Redis(
**redis_kwargs,
)
@@ -331,3 +332,90 @@ def get_redis_connection_pool(**env_overrides):
return async_redis.BlockingConnectionPool(
timeout=REDIS_CONNECTION_POOL_TIMEOUT, **redis_kwargs
)
def _pretty_print_redis_config(redis_kwargs: dict) -> None:
"""Pretty print the Redis configuration using rich with sensitive data masking"""
try:
import logging
from rich.console import Console
from rich.panel import Panel
from rich.table import Table
from rich.text import Text
if not verbose_logger.isEnabledFor(logging.DEBUG):
return
console = Console()
# Initialize the sensitive data masker
masker = SensitiveDataMasker()
# Mask sensitive data in redis_kwargs
masked_redis_kwargs = masker.mask_dict(redis_kwargs)
# Create main panel title
title = Text("Redis Configuration", style="bold blue")
# Create configuration table
config_table = Table(
title="🔧 Redis Connection Parameters",
show_header=True,
header_style="bold magenta",
title_justify="left",
)
config_table.add_column("Parameter", style="cyan", no_wrap=True)
config_table.add_column("Value", style="yellow")
# Add rows for each configuration parameter
for key, value in masked_redis_kwargs.items():
if value is not None:
# Special handling for complex objects
if isinstance(value, list):
if key == "startup_nodes" and value:
# Special handling for cluster nodes
value_str = f"[{len(value)} cluster nodes]"
elif key == "sentinel_nodes" and value:
# Special handling for sentinel nodes
value_str = f"[{len(value)} sentinel nodes]"
else:
value_str = str(value)
else:
value_str = str(value)
config_table.add_row(key, value_str)
# Determine connection type
connection_type = "Standard Redis"
if masked_redis_kwargs.get("startup_nodes"):
connection_type = "Redis Cluster"
elif masked_redis_kwargs.get("sentinel_nodes"):
connection_type = "Redis Sentinel"
elif masked_redis_kwargs.get("url"):
connection_type = "Redis (URL-based)"
# Create connection type info
info_table = Table(
title="📊 Connection Info",
show_header=True,
header_style="bold green",
title_justify="left",
)
info_table.add_column("Property", style="cyan", no_wrap=True)
info_table.add_column("Value", style="yellow")
info_table.add_row("Connection Type", connection_type)
# Print everything in a nice panel
console.print("\n")
console.print(Panel(title, border_style="blue"))
console.print(info_table)
console.print(config_table)
console.print("\n")
except ImportError:
# Fallback to simple logging if rich is not available
masker = SensitiveDataMasker()
masked_redis_kwargs = masker.mask_dict(redis_kwargs)
verbose_logger.info(f"Redis configuration: {masked_redis_kwargs}")
except Exception as e:
verbose_logger.error(f"Error pretty printing Redis configuration: {e}")
+24 -2
View File
@@ -7,6 +7,28 @@ from litellm.types.llms.openai import Batch
from litellm.types.utils import CallTypes, Usage
async def calculate_batch_cost_and_usage(
file_content_dictionary: List[dict],
custom_llm_provider: Literal["openai", "azure", "vertex_ai"],
) -> Tuple[float, Usage, List[str]]:
"""
Calculate the cost and usage of a batch
"""
# Calculate costs and usage
batch_cost = _batch_cost_calculator(
custom_llm_provider=custom_llm_provider,
file_content_dictionary=file_content_dictionary,
)
batch_usage = _get_batch_job_total_usage_from_file_content(
file_content_dictionary=file_content_dictionary,
custom_llm_provider=custom_llm_provider,
)
batch_models = _get_batch_models_from_file_content(file_content_dictionary)
return batch_cost, batch_usage, batch_models
async def _handle_completed_batch(
batch: Batch,
custom_llm_provider: Literal["openai", "azure", "vertex_ai"],
@@ -18,7 +40,7 @@ async def _handle_completed_batch(
)
# Calculate costs and usage
batch_cost = await _batch_cost_calculator(
batch_cost = _batch_cost_calculator(
custom_llm_provider=custom_llm_provider,
file_content_dictionary=file_content_dictionary,
)
@@ -48,7 +70,7 @@ def _get_batch_models_from_file_content(
return batch_models
async def _batch_cost_calculator(
def _batch_cost_calculator(
file_content_dictionary: List[dict],
custom_llm_provider: Literal["openai", "azure", "vertex_ai"] = "openai",
) -> float:
+123
View File
@@ -0,0 +1,123 @@
# LiteLLM Google GenAI Interface
Interface to interact with Google GenAI Functions in the native Google interface format.
## Overview
This module provides a native interface to Google's Generative AI API, allowing you to use Google's content generation capabilities with both streaming and non-streaming modes, in both synchronous and asynchronous contexts.
## Available Functions
### Non-Streaming Functions
- `generate_content()` - Synchronous content generation
- `agenerate_content()` - Asynchronous content generation
### Streaming Functions
- `generate_content_stream()` - Synchronous streaming content generation
- `agenerate_content_stream()` - Asynchronous streaming content generation
## Usage Examples
### Basic Non-Streaming Usage
```python
from litellm.google_genai import generate_content, agenerate_content
from google.genai.types import ContentDict, PartDict
# Synchronous usage
contents = ContentDict(
parts=[
PartDict(text="Hello, can you tell me a short joke?")
],
)
response = generate_content(
contents=contents,
model="gemini-pro", # or your preferred model
# Add other model-specific parameters as needed
)
print(response)
```
### Async Non-Streaming Usage
```python
import asyncio
from litellm.google_genai import agenerate_content
from google.genai.types import ContentDict, PartDict
async def main():
contents = ContentDict(
parts=[
PartDict(text="Hello, can you tell me a short joke?")
],
)
response = await agenerate_content(
contents=contents,
model="gemini-pro",
# Add other model-specific parameters as needed
)
print(response)
# Run the async function
asyncio.run(main())
```
### Streaming Usage
```python
from litellm.google_genai import generate_content_stream
from google.genai.types import ContentDict, PartDict
# Synchronous streaming
contents = ContentDict(
parts=[
PartDict(text="Tell me a story about space exploration")
],
)
for chunk in generate_content_stream(
contents=contents,
model="gemini-pro",
):
print(f"Chunk: {chunk}")
```
### Async Streaming Usage
```python
import asyncio
from litellm.google_genai import agenerate_content_stream
from google.genai.types import ContentDict, PartDict
async def main():
contents = ContentDict(
parts=[
PartDict(text="Tell me a story about space exploration")
],
)
async for chunk in agenerate_content_stream(
contents=contents,
model="gemini-pro",
):
print(f"Async chunk: {chunk}")
asyncio.run(main())
```
## Testing
This module includes comprehensive tests covering:
- Sync and async non-streaming requests
- Sync and async streaming requests
- Response validation
- Error handling scenarios
See `tests/unified_google_tests/base_google_test.py` for test implementation examples.
+19
View File
@@ -0,0 +1,19 @@
"""
This allows using Google GenAI model in their native interface.
This module provides generate_content functionality for Google GenAI models.
"""
from .main import (
agenerate_content,
agenerate_content_stream,
generate_content,
generate_content_stream,
)
__all__ = [
"generate_content",
"agenerate_content",
"generate_content_stream",
"agenerate_content_stream",
]
+436
View File
@@ -0,0 +1,436 @@
import asyncio
import contextvars
from functools import partial
from typing import TYPE_CHECKING, Any, Dict, Iterator, Optional, Union
import httpx
from pydantic import BaseModel
import litellm
from litellm.constants import request_timeout
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
from litellm.llms.base_llm.google_genai.transformation import (
BaseGoogleGenAIGenerateContentConfig,
)
from litellm.llms.custom_httpx.llm_http_handler import BaseLLMHTTPHandler
from litellm.types.router import GenericLiteLLMParams
from litellm.utils import ProviderConfigManager, client
if TYPE_CHECKING:
from litellm.types.google_genai.main import (
GenerateContentConfigDict,
GenerateContentContentListUnionDict,
GenerateContentResponse,
)
else:
GenerateContentConfigDict = Any
GenerateContentContentListUnionDict = Any
GenerateContentResponse = Any
####### ENVIRONMENT VARIABLES ###################
# Initialize any necessary instances or variables here
base_llm_http_handler = BaseLLMHTTPHandler()
#################################################
class GenerateContentSetupResult(BaseModel):
"""Internal Type - Result of setting up a generate content call"""
model: str
request_body: Dict[str, Any]
custom_llm_provider: str
generate_content_provider_config: BaseGoogleGenAIGenerateContentConfig
generate_content_config_dict: Dict[str, Any]
litellm_params: GenericLiteLLMParams
litellm_logging_obj: LiteLLMLoggingObj
litellm_call_id: Optional[str]
class Config:
arbitrary_types_allowed = True
class GenerateContentHelper:
"""Helper class for Google GenAI generate content operations"""
@staticmethod
def mock_generate_content_response(
mock_response: str = "This is a mock response from Google GenAI generate_content.",
) -> Dict[str, Any]:
"""Mock response for generate_content for testing purposes"""
return {
"text": mock_response,
"candidates": [
{
"content": {
"parts": [{"text": mock_response}],
"role": "model"
},
"finishReason": "STOP",
"index": 0,
"safetyRatings": []
}
],
"usageMetadata": {
"promptTokenCount": 10,
"candidatesTokenCount": 20,
"totalTokenCount": 30
}
}
@staticmethod
def setup_generate_content_call(
model: str,
contents: GenerateContentContentListUnionDict,
config: Optional[GenerateContentConfigDict] = None,
custom_llm_provider: Optional[str] = None,
stream: bool = False,
**kwargs
) -> GenerateContentSetupResult:
"""
Common setup logic for generate_content calls
Args:
model: The model name
contents: The content to generate from
config: Optional configuration
custom_llm_provider: Optional custom LLM provider
stream: Whether this is a streaming call
local_vars: Local variables from the calling function
**kwargs: Additional keyword arguments
Returns:
GenerateContentSetupResult containing all setup information
"""
litellm_logging_obj: Optional[LiteLLMLoggingObj] = kwargs.get("litellm_logging_obj")
litellm_call_id: Optional[str] = kwargs.get("litellm_call_id", None)
# get llm provider logic
litellm_params = GenericLiteLLMParams(**kwargs)
## MOCK RESPONSE LOGIC (only for non-streaming)
if not stream and litellm_params.mock_response and isinstance(litellm_params.mock_response, str):
raise ValueError("Mock response should be handled by caller")
(
model,
custom_llm_provider,
dynamic_api_key,
dynamic_api_base,
) = litellm.get_llm_provider(
model=model,
custom_llm_provider=custom_llm_provider,
api_base=litellm_params.api_base,
api_key=litellm_params.api_key,
)
# get provider config
generate_content_provider_config: Optional[BaseGoogleGenAIGenerateContentConfig] = (
ProviderConfigManager.get_provider_google_genai_generate_content_config(
model=model,
provider=litellm.LlmProviders(custom_llm_provider),
)
)
if generate_content_provider_config is None:
operation = "streaming" if stream else ""
raise ValueError(
f"Generate content {operation} is not supported for {custom_llm_provider}".strip()
)
#########################################################################################
# Construct request body
#########################################################################################
# Create Google Optional Params Config
generate_content_config_dict = generate_content_provider_config.map_generate_content_optional_params(
generate_content_config_dict=config or {},
model=model,
)
request_body = generate_content_provider_config.transform_generate_content_request(
model=model,
contents=contents,
generate_content_config_dict=generate_content_config_dict,
)
# Pre Call logging
if litellm_logging_obj is None:
raise ValueError("litellm_logging_obj is required, but got None")
litellm_logging_obj.update_environment_variables(
model=model,
optional_params=dict(generate_content_config_dict),
litellm_params={
"litellm_call_id": litellm_call_id,
},
custom_llm_provider=custom_llm_provider,
)
return GenerateContentSetupResult(
model=model,
custom_llm_provider=custom_llm_provider,
request_body=request_body,
generate_content_provider_config=generate_content_provider_config,
generate_content_config_dict=generate_content_config_dict,
litellm_params=litellm_params,
litellm_logging_obj=litellm_logging_obj,
litellm_call_id=litellm_call_id
)
@client
async def agenerate_content(
model: str,
contents: GenerateContentContentListUnionDict,
config: Optional[GenerateContentConfigDict] = None,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Optional[Dict[str, Any]] = None,
extra_query: Optional[Dict[str, Any]] = None,
extra_body: Optional[Dict[str, Any]] = None,
timeout: Optional[Union[float, httpx.Timeout]] = None,
# LiteLLM specific params,
custom_llm_provider: Optional[str] = None,
**kwargs
) -> Any:
"""
Async: Generate content using Google GenAI
"""
local_vars = locals()
try:
loop = asyncio.get_event_loop()
kwargs["agenerate_content"] = True
# get custom llm provider so we can use this for mapping exceptions
if custom_llm_provider is None:
_, custom_llm_provider, _, _ = litellm.get_llm_provider(
model=model,
custom_llm_provider=custom_llm_provider,
)
func = partial(
generate_content,
model=model,
contents=contents,
config=config,
extra_headers=extra_headers,
extra_query=extra_query,
extra_body=extra_body,
timeout=timeout,
custom_llm_provider=custom_llm_provider,
**kwargs,
)
ctx = contextvars.copy_context()
func_with_context = partial(ctx.run, func)
init_response = await loop.run_in_executor(None, func_with_context)
if asyncio.iscoroutine(init_response):
response = await init_response
else:
response = init_response
return response
except Exception as e:
raise litellm.exception_type(
model=model,
custom_llm_provider=custom_llm_provider,
original_exception=e,
completion_kwargs=local_vars,
extra_kwargs=kwargs,
)
@client
def generate_content(
model: str,
contents: GenerateContentContentListUnionDict,
config: Optional[GenerateContentConfigDict] = None,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Optional[Dict[str, Any]] = None,
extra_query: Optional[Dict[str, Any]] = None,
extra_body: Optional[Dict[str, Any]] = None,
timeout: Optional[Union[float, httpx.Timeout]] = None,
# LiteLLM specific params,
custom_llm_provider: Optional[str] = None,
**kwargs,
) -> Any:
"""
Generate content using Google GenAI
"""
local_vars = locals()
try:
_is_async = kwargs.pop("agenerate_content", False) is True
# Check for mock response first
litellm_params = GenericLiteLLMParams(**kwargs)
if litellm_params.mock_response and isinstance(litellm_params.mock_response, str):
return GenerateContentHelper.mock_generate_content_response(
mock_response=litellm_params.mock_response
)
# Setup the call
setup_result = GenerateContentHelper.setup_generate_content_call(
model=model,
contents=contents,
config=config,
custom_llm_provider=custom_llm_provider,
stream=False,
**kwargs
)
# Call the handler
response = base_llm_http_handler.generate_content_handler(
model=setup_result.model,
contents=contents,
generate_content_provider_config=setup_result.generate_content_provider_config,
generate_content_config_dict=setup_result.generate_content_config_dict,
custom_llm_provider=setup_result.custom_llm_provider,
litellm_params=setup_result.litellm_params,
logging_obj=setup_result.litellm_logging_obj,
extra_headers=extra_headers,
extra_body=extra_body,
timeout=timeout or request_timeout,
_is_async=_is_async,
client=kwargs.get("client"),
stream=False,
litellm_metadata=kwargs.get("litellm_metadata", {}),
)
return response
except Exception as e:
raise litellm.exception_type(
model=model,
custom_llm_provider=custom_llm_provider,
original_exception=e,
completion_kwargs=local_vars,
extra_kwargs=kwargs,
)
@client
async def agenerate_content_stream(
model: str,
contents: GenerateContentContentListUnionDict,
config: Optional[GenerateContentConfigDict] = None,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Optional[Dict[str, Any]] = None,
extra_query: Optional[Dict[str, Any]] = None,
extra_body: Optional[Dict[str, Any]] = None,
timeout: Optional[Union[float, httpx.Timeout]] = None,
# LiteLLM specific params,
custom_llm_provider: Optional[str] = None,
**kwargs
) -> Any:
"""
Async: Generate content using Google GenAI with streaming response
"""
local_vars = locals()
try:
kwargs["agenerate_content_stream"] = True
# get custom llm provider so we can use this for mapping exceptions
if custom_llm_provider is None:
_, custom_llm_provider, _, _ = litellm.get_llm_provider(
model=model, api_base=local_vars.get("base_url", None)
)
# Setup the call
setup_result = GenerateContentHelper.setup_generate_content_call(
model=model,
contents=contents,
config=config,
custom_llm_provider=custom_llm_provider,
stream=True,
**kwargs
)
# Call the handler with async enabled and streaming
# Return the coroutine directly for the router to handle
return await base_llm_http_handler.generate_content_handler(
model=setup_result.model,
contents=contents,
generate_content_provider_config=setup_result.generate_content_provider_config,
generate_content_config_dict=setup_result.generate_content_config_dict,
custom_llm_provider=setup_result.custom_llm_provider,
litellm_params=setup_result.litellm_params,
logging_obj=setup_result.litellm_logging_obj,
extra_headers=extra_headers,
extra_body=extra_body,
timeout=timeout or request_timeout,
_is_async=True,
client=kwargs.get("client"),
stream=True,
litellm_metadata=kwargs.get("litellm_metadata", {}),
)
except Exception as e:
raise litellm.exception_type(
model=model,
custom_llm_provider=custom_llm_provider,
original_exception=e,
completion_kwargs=local_vars,
extra_kwargs=kwargs,
)
@client
def generate_content_stream(
model: str,
contents: GenerateContentContentListUnionDict,
config: Optional[GenerateContentConfigDict] = None,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Optional[Dict[str, Any]] = None,
extra_query: Optional[Dict[str, Any]] = None,
extra_body: Optional[Dict[str, Any]] = None,
timeout: Optional[Union[float, httpx.Timeout]] = None,
# LiteLLM specific params,
custom_llm_provider: Optional[str] = None,
**kwargs,
) -> Iterator[Any]:
"""
Generate content using Google GenAI with streaming response
"""
local_vars = locals()
try:
# Remove any async-related flags since this is the sync function
kwargs.pop("agenerate_content_stream", None)
# Setup the call
setup_result = GenerateContentHelper.setup_generate_content_call(
model=model,
contents=contents,
config=config,
custom_llm_provider=custom_llm_provider,
stream=True,
**kwargs
)
# Call the handler with streaming enabled (sync version)
return base_llm_http_handler.generate_content_handler(
model=setup_result.model,
contents=contents,
generate_content_provider_config=setup_result.generate_content_provider_config,
generate_content_config_dict=setup_result.generate_content_config_dict,
custom_llm_provider=setup_result.custom_llm_provider,
litellm_params=setup_result.litellm_params,
logging_obj=setup_result.litellm_logging_obj,
extra_headers=extra_headers,
extra_body=extra_body,
timeout=timeout or request_timeout,
_is_async=False,
client=kwargs.get("client"),
stream=True,
litellm_metadata=kwargs.get("litellm_metadata", {}),
)
except Exception as e:
raise litellm.exception_type(
model=model,
custom_llm_provider=custom_llm_provider,
original_exception=e,
completion_kwargs=local_vars,
extra_kwargs=kwargs,
)
+151
View File
@@ -0,0 +1,151 @@
import asyncio
from datetime import datetime
from typing import TYPE_CHECKING, Any, List, Optional
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
from litellm.proxy.pass_through_endpoints.success_handler import (
PassThroughEndpointLogging,
)
from litellm.types.passthrough_endpoints.pass_through_endpoints import EndpointType
if TYPE_CHECKING:
from litellm.llms.base_llm.google_genai.transformation import (
BaseGoogleGenAIGenerateContentConfig,
)
else:
BaseGoogleGenAIGenerateContentConfig = Any
GLOBAL_PASS_THROUGH_SUCCESS_HANDLER_OBJ = PassThroughEndpointLogging()
class BaseGoogleGenAIGenerateContentStreamingIterator:
"""
Base class for Google GenAI Generate Content streaming iterators that provides common logic
for streaming response handling and logging.
"""
def __init__(
self,
litellm_logging_obj: LiteLLMLoggingObj,
request_body: dict,
model: str,
):
self.litellm_logging_obj = litellm_logging_obj
self.request_body = request_body
self.start_time = datetime.now()
self.collected_chunks: List[bytes] = []
self.model = model
async def _handle_async_streaming_logging(
self,
):
"""Handle the logging after all chunks have been collected."""
from litellm.proxy.pass_through_endpoints.streaming_handler import (
PassThroughStreamingHandler,
)
end_time = datetime.now()
asyncio.create_task(
PassThroughStreamingHandler._route_streaming_logging_to_handler(
litellm_logging_obj=self.litellm_logging_obj,
passthrough_success_handler_obj=GLOBAL_PASS_THROUGH_SUCCESS_HANDLER_OBJ,
url_route="/v1/generateContent",
request_body=self.request_body or {},
endpoint_type=EndpointType.VERTEX_AI,
start_time=self.start_time,
raw_bytes=self.collected_chunks,
end_time=end_time,
model=self.model,
)
)
class GoogleGenAIGenerateContentStreamingIterator(BaseGoogleGenAIGenerateContentStreamingIterator):
"""
Streaming iterator specifically for Google GenAI generate content API.
"""
def __init__(
self,
response,
model: str,
logging_obj: LiteLLMLoggingObj,
generate_content_provider_config: BaseGoogleGenAIGenerateContentConfig,
litellm_metadata: dict,
custom_llm_provider: str,
request_body: Optional[dict] = None,
):
super().__init__(
litellm_logging_obj=logging_obj,
request_body=request_body or {},
model=model,
)
self.response = response
self.model = model
self.generate_content_provider_config = generate_content_provider_config
self.litellm_metadata = litellm_metadata
self.custom_llm_provider = custom_llm_provider
# Store the iterator once to avoid multiple stream consumption
self.stream_iterator = response.iter_bytes()
def __iter__(self):
return self
def __next__(self):
try:
# Get the next chunk from the stored iterator
chunk = next(self.stream_iterator)
self.collected_chunks.append(chunk)
# Just yield raw bytes
return chunk
except StopIteration:
raise StopIteration
def __aiter__(self):
return self
async def __anext__(self):
# This should not be used for sync responses
# If you need async iteration, use AsyncGoogleGenAIGenerateContentStreamingIterator
raise NotImplementedError("Use AsyncGoogleGenAIGenerateContentStreamingIterator for async iteration")
class AsyncGoogleGenAIGenerateContentStreamingIterator(BaseGoogleGenAIGenerateContentStreamingIterator):
"""
Async streaming iterator specifically for Google GenAI generate content API.
"""
def __init__(
self,
response,
model: str,
logging_obj: LiteLLMLoggingObj,
generate_content_provider_config: BaseGoogleGenAIGenerateContentConfig,
litellm_metadata: dict,
custom_llm_provider: str,
request_body: Optional[dict] = None,
):
super().__init__(
litellm_logging_obj=logging_obj,
request_body=request_body or {},
model=model,
)
self.response = response
self.model = model
self.generate_content_provider_config = generate_content_provider_config
self.litellm_metadata = litellm_metadata
self.custom_llm_provider = custom_llm_provider
# Store the async iterator once to avoid multiple stream consumption
self.stream_iterator = response.aiter_bytes()
def __aiter__(self):
return self
async def __anext__(self):
try:
# Get the next chunk from the stored async iterator
chunk = await self.stream_iterator.__anext__()
self.collected_chunks.append(chunk)
# Just yield raw bytes
return chunk
except StopAsyncIteration:
await self._handle_async_streaming_logging()
raise StopAsyncIteration
+50 -9
View File
@@ -1237,7 +1237,12 @@ class Logging(LiteLLMLoggingBaseClass):
return False
# Check for dynamically disabled callbacks via headers
if EnterpriseCallbackControls is not None and EnterpriseCallbackControls.is_callback_disabled_via_headers(callback, litellm_params):
if (
EnterpriseCallbackControls is not None
and EnterpriseCallbackControls.is_callback_disabled_via_headers(
callback, litellm_params
)
):
verbose_logger.debug(
f"Callback {callback} disabled via x-litellm-disable-callbacks header for {event_hook} event"
)
@@ -1270,9 +1275,13 @@ class Logging(LiteLLMLoggingBaseClass):
self.model_call_details["log_event_type"] = "successful_api_call"
self.model_call_details["end_time"] = end_time
self.model_call_details["cache_hit"] = cache_hit
if self.call_type == CallTypes.anthropic_messages.value:
result = self._handle_anthropic_messages_response_logging(result=result)
elif (
self.call_type == CallTypes.generate_content.value or
self.call_type == CallTypes.agenerate_content.value
):
result = self._handle_non_streaming_google_genai_generate_content_response_logging(result=result)
## if model in model cost map - log the response cost
## else set cost to None
@@ -1908,16 +1917,27 @@ class Logging(LiteLLMLoggingBaseClass):
return
## CALCULATE COST FOR BATCH JOBS
if self.call_type == CallTypes.aretrieve_batch.value and isinstance(
result, LiteLLMBatch
if (
self.call_type == CallTypes.aretrieve_batch.value
and isinstance(result, LiteLLMBatch)
and result.status == "completed"
):
response_cost, batch_usage, batch_models = await _handle_completed_batch(
batch=result, custom_llm_provider=self.custom_llm_provider
from litellm.proxy.openai_files_endpoints.common_utils import (
_is_base64_encoded_unified_file_id,
)
result._hidden_params["response_cost"] = response_cost
result._hidden_params["batch_models"] = batch_models
result.usage = batch_usage
# check if file id is a unified file id
is_base64_unified_file_id = _is_base64_encoded_unified_file_id(result.id)
if not is_base64_unified_file_id: # only run for non-unified file ids
response_cost, batch_usage, batch_models = (
await _handle_completed_batch(
batch=result, custom_llm_provider=self.custom_llm_provider
)
)
result._hidden_params["response_cost"] = response_cost
result._hidden_params["batch_models"] = batch_models
result.usage = batch_usage
start_time, end_time, result = self._success_handler_helper_fn(
start_time=start_time,
@@ -2737,6 +2757,27 @@ class Logging(LiteLLMLoggingBaseClass):
json_mode=None,
)
return result
def _handle_non_streaming_google_genai_generate_content_response_logging(self, result: Any) -> ModelResponse:
"""
Handles logging for Google GenAI generate content responses.
"""
import httpx
httpx_response = self.model_call_details.get("httpx_response", None)
if httpx_response is None:
raise ValueError("Google GenAI Generate Content: httpx_response is None")
dict_result = httpx_response.json()
result = litellm.VertexGeminiConfig()._transform_google_generate_content_to_openai_model_response(
completion_response=dict_result,
model_response=litellm.ModelResponse(),
model=self.model,
logging_obj=self,
raw_response=httpx.Response(
status_code=200,
headers={},
),
)
return result
def _get_masked_values(
@@ -18,6 +18,7 @@ from ..chat.transformation import BaseConfig
if TYPE_CHECKING:
from litellm.litellm_core_utils.litellm_logging import Logging as _LiteLLMLoggingObj
from litellm.router import Router as _Router
from litellm.types.llms.openai import HttpxBinaryResponseContent
LiteLLMLoggingObj = _LiteLLMLoggingObj
Span = Any
@@ -154,5 +155,5 @@ class BaseFileEndpoints(ABC):
litellm_parent_otel_span: Optional[Span],
llm_router: Router,
**data: Dict,
) -> str:
) -> "HttpxBinaryResponseContent":
pass
@@ -0,0 +1,204 @@
import types
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union
import httpx
if TYPE_CHECKING:
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
from litellm.types.google_genai.main import (
GenerateContentConfigDict,
GenerateContentContentListUnionDict,
GenerateContentResponse,
)
else:
GenerateContentConfigDict = Any
GenerateContentContentListUnionDict = Any
GenerateContentResponse = Any
LiteLLMLoggingObj = Any
from litellm.types.router import GenericLiteLLMParams
class BaseGoogleGenAIGenerateContentConfig(ABC):
"""Base configuration class for Google GenAI generate_content functionality"""
def __init__(self):
pass
@classmethod
def get_config(cls):
return {
k: v
for k, v in cls.__dict__.items()
if not k.startswith("__")
and not k.startswith("_abc")
and not isinstance(
v,
(
types.FunctionType,
types.BuiltinFunctionType,
classmethod,
staticmethod,
),
)
and v is not None
}
@abstractmethod
def get_supported_generate_content_optional_params(self, model: str) -> List[str]:
"""
Get the list of supported Google GenAI parameters for the model.
Args:
model: The model name
Returns:
List of supported parameter names
"""
raise NotImplementedError("get_supported_generate_content_optional_params is not implemented")
@abstractmethod
def map_generate_content_optional_params(
self,
generate_content_config_dict: GenerateContentConfigDict,
model: str,
) -> Dict[str, Any]:
"""
Map Google GenAI parameters to provider-specific format.
Args:
generate_content_optional_params: Optional parameters for generate content
model: The model name
Returns:
Mapped parameters for the provider
"""
raise NotImplementedError("map_generate_content_optional_params is not implemented")
@abstractmethod
def validate_environment(
self,
api_key: Optional[str],
headers: Optional[dict],
model: str,
litellm_params: Optional[Union[GenericLiteLLMParams, dict]]
) -> dict:
"""
Validate the environment and return headers for the request.
Args:
api_key: API key
headers: Existing headers
model: The model name
litellm_params: LiteLLM parameters
Returns:
Updated headers
"""
raise NotImplementedError("validate_environment is not implemented")
def sync_get_auth_token_and_url(
self,
api_base: Optional[str],
model: str,
litellm_params: dict,
stream: bool,
) -> Tuple[dict, str]:
"""
Sync version of get_auth_token_and_url.
Args:
api_base: Base API URL
model: The model name
litellm_params: LiteLLM parameters
stream: Whether this is a streaming call
Returns:
Tuple of headers and API base
"""
raise NotImplementedError("sync_get_auth_token_and_url is not implemented")
async def get_auth_token_and_url(
self,
api_base: Optional[str],
model: str,
litellm_params: dict,
stream: bool,
) -> Tuple[dict, str]:
"""
Get the complete URL for the request.
Args:
api_base: Base API URL
model: The model name
litellm_params: LiteLLM parameters
Returns:
Tuple of headers and API base
"""
raise NotImplementedError("get_auth_token_and_url is not implemented")
@abstractmethod
def transform_generate_content_request(
self,
model: str,
contents: GenerateContentContentListUnionDict,
generate_content_config_dict: Dict,
) -> dict:
"""
Transform the request parameters for the generate content API.
Args:
model: The model name
contents: Input contents
generate_content_request_params: Request parameters
litellm_params: LiteLLM parameters
headers: Request headers
Returns:
Transformed request data
"""
pass
@abstractmethod
def transform_generate_content_response(
self,
model: str,
raw_response: httpx.Response,
logging_obj: LiteLLMLoggingObj,
) -> GenerateContentResponse:
"""
Transform the raw response from the generate content API.
Args:
model: The model name
raw_response: Raw HTTP response
Returns:
Transformed response data
"""
pass
def get_error_class(
self, error_message: str, status_code: int, headers: Union[dict, httpx.Headers]
) -> Exception:
"""
Get the appropriate exception class for the error.
Args:
error_message: Error message
status_code: HTTP status code
headers: Response headers
Returns:
Exception instance
"""
from litellm.llms.base_llm.chat.transformation import BaseLLMException
return BaseLLMException(
status_code=status_code,
message=error_message,
headers=headers,
)
+10 -1
View File
@@ -580,15 +580,24 @@ class AsyncHTTPHandler:
- True: use default SSL verification (equivalent to ssl.create_default_context())
"""
from litellm.llms.custom_httpx.aiohttp_transport import LiteLLMAiohttpTransport
from litellm.secret_managers.main import str_to_bool
connector_kwargs = AsyncHTTPHandler._get_ssl_connector_kwargs(
ssl_verify=ssl_verify, ssl_context=ssl_context
)
#########################################################
# Check if user enabled aiohttp trust env
# use for HTTP_PROXY, HTTPS_PROXY, etc.
########################################################
trust_env: bool = litellm.aiohttp_trust_env
if str_to_bool(os.getenv("AIOHTTP_TRUST_ENV", "False")) is True:
trust_env = True
verbose_logger.debug("Creating AiohttpTransport...")
return LiteLLMAiohttpTransport(
client=lambda: ClientSession(
connector=TCPConnector(**connector_kwargs)
connector=TCPConnector(**connector_kwargs),
trust_env=trust_env,
),
)
+227 -1
View File
@@ -31,6 +31,9 @@ from litellm.llms.base_llm.base_model_iterator import MockResponseIterator
from litellm.llms.base_llm.chat.transformation import BaseConfig
from litellm.llms.base_llm.embedding.transformation import BaseEmbeddingConfig
from litellm.llms.base_llm.files.transformation import BaseFilesConfig
from litellm.llms.base_llm.google_genai.transformation import (
BaseGoogleGenAIGenerateContentConfig,
)
from litellm.llms.base_llm.image_edit.transformation import BaseImageEditConfig
from litellm.llms.base_llm.realtime.transformation import BaseRealtimeConfig
from litellm.llms.base_llm.rerank.transformation import BaseRerankConfig
@@ -2349,7 +2352,7 @@ class BaseLLMHTTPHandler:
self,
e: Exception,
provider_config: Union[
BaseConfig, BaseRerankConfig, BaseResponsesAPIConfig, BaseImageEditConfig, BaseVectorStoreConfig
BaseConfig, BaseRerankConfig, BaseResponsesAPIConfig, BaseImageEditConfig, BaseVectorStoreConfig, BaseGoogleGenAIGenerateContentConfig
],
):
status_code = getattr(e, "status_code", 500)
@@ -2887,4 +2890,227 @@ class BaseLLMHTTPHandler:
return vector_store_provider_config.transform_create_vector_store_response(
response=response,
)
#####################################################################
################ Google GenAI GENERATE CONTENT HANDLER ###########################
#####################################################################
def generate_content_handler(
self,
model: str,
contents: Any,
generate_content_provider_config: BaseGoogleGenAIGenerateContentConfig,
generate_content_config_dict: Dict,
custom_llm_provider: str,
litellm_params: GenericLiteLLMParams,
logging_obj: LiteLLMLoggingObj,
extra_headers: Optional[Dict[str, Any]] = None,
extra_body: Optional[Dict[str, Any]] = None,
timeout: Optional[Union[float, httpx.Timeout]] = None,
_is_async: bool = False,
client: Optional[Union[HTTPHandler, AsyncHTTPHandler]] = None,
stream: bool = False,
litellm_metadata: Optional[Dict[str, Any]] = None,
) -> Any:
"""
Handles Google GenAI generate content requests.
When _is_async=True, returns a coroutine instead of making the call directly.
"""
from litellm.google_genai.streaming_iterator import (
GoogleGenAIGenerateContentStreamingIterator,
)
if _is_async:
return self.async_generate_content_handler(
model=model,
contents=contents,
generate_content_provider_config=generate_content_provider_config,
generate_content_config_dict=generate_content_config_dict,
custom_llm_provider=custom_llm_provider,
litellm_params=litellm_params,
logging_obj=logging_obj,
extra_headers=extra_headers,
extra_body=extra_body,
timeout=timeout,
client=client if isinstance(client, AsyncHTTPHandler) else None,
stream=stream,
litellm_metadata=litellm_metadata,
)
if client is None or not isinstance(client, HTTPHandler):
sync_httpx_client = _get_httpx_client(
params={"ssl_verify": litellm_params.get("ssl_verify", None)}
)
else:
sync_httpx_client = client
# Get headers and URL from the provider config
headers, api_base = generate_content_provider_config.sync_get_auth_token_and_url(
api_base=litellm_params.api_base,
model=model,
litellm_params=dict(litellm_params),
stream=stream,
)
if extra_headers:
headers.update(extra_headers)
# Get the request body from the provider config
data = generate_content_provider_config.transform_generate_content_request(
model=model,
contents=contents,
generate_content_config_dict=generate_content_config_dict,
)
if extra_body:
data.update(extra_body)
## LOGGING
logging_obj.pre_call(
input=contents,
api_key="",
additional_args={
"complete_input_dict": data,
"api_base": api_base,
"headers": headers,
},
)
try:
if stream:
response = sync_httpx_client.post(
url=api_base,
headers=headers,
json=data,
timeout=timeout,
stream=True,
)
# Return streaming iterator
return GoogleGenAIGenerateContentStreamingIterator(
response=response,
model=model,
logging_obj=logging_obj,
generate_content_provider_config=generate_content_provider_config,
litellm_metadata=litellm_metadata or {},
custom_llm_provider=custom_llm_provider,
request_body=data,
)
else:
response = sync_httpx_client.post(
url=api_base,
headers=headers,
json=data,
timeout=timeout,
)
except Exception as e:
raise self._handle_error(
e=e,
provider_config=generate_content_provider_config,
)
return generate_content_provider_config.transform_generate_content_response(
model=model,
raw_response=response,
logging_obj=logging_obj,
)
async def async_generate_content_handler(
self,
model: str,
contents: Any,
generate_content_provider_config: BaseGoogleGenAIGenerateContentConfig,
generate_content_config_dict: Dict,
custom_llm_provider: str,
litellm_params: GenericLiteLLMParams,
logging_obj: LiteLLMLoggingObj,
extra_headers: Optional[Dict[str, Any]] = None,
extra_body: Optional[Dict[str, Any]] = None,
timeout: Optional[Union[float, httpx.Timeout]] = None,
client: Optional[AsyncHTTPHandler] = None,
stream: bool = False,
litellm_metadata: Optional[Dict[str, Any]] = None,
) -> Any:
"""
Async version of the generate content handler.
Uses async HTTP client to make requests.
"""
from litellm.google_genai.streaming_iterator import (
AsyncGoogleGenAIGenerateContentStreamingIterator,
)
if client is None or not isinstance(client, AsyncHTTPHandler):
async_httpx_client = get_async_httpx_client(
llm_provider=litellm.LlmProviders(custom_llm_provider),
params={"ssl_verify": litellm_params.get("ssl_verify", None)},
)
else:
async_httpx_client = client
# Get headers and URL from the provider config
headers, api_base = await generate_content_provider_config.get_auth_token_and_url(
model=model,
litellm_params=dict(litellm_params),
stream=stream,
api_base=litellm_params.api_base,
)
if extra_headers:
headers.update(extra_headers)
# Get the request body from the provider config
data = generate_content_provider_config.transform_generate_content_request(
model=model,
contents=contents,
generate_content_config_dict=generate_content_config_dict,
)
if extra_body:
data.update(extra_body)
## LOGGING
logging_obj.pre_call(
input=contents,
api_key="",
additional_args={
"complete_input_dict": data,
"api_base": api_base,
"headers": headers,
},
)
try:
if stream:
response = await async_httpx_client.post(
url=api_base,
headers=headers,
json=data,
timeout=timeout,
stream=True,
)
# Return async streaming iterator
return AsyncGoogleGenAIGenerateContentStreamingIterator(
response=response,
model=model,
logging_obj=logging_obj,
generate_content_provider_config=generate_content_provider_config,
litellm_metadata=litellm_metadata or {},
custom_llm_provider=custom_llm_provider,
request_body=data,
)
else:
response = await async_httpx_client.post(
url=api_base,
headers=headers,
json=data,
timeout=timeout,
)
except Exception as e:
raise self._handle_error(
e=e,
provider_config=generate_content_provider_config,
)
return generate_content_provider_config.transform_generate_content_response(
model=model,
raw_response=response,
logging_obj=logging_obj,
)
@@ -0,0 +1,299 @@
"""
Transformation for Calling Google models in their native format.
"""
from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Tuple, Union, cast
import httpx
import litellm
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
from litellm.llms.base_llm.google_genai.transformation import (
BaseGoogleGenAIGenerateContentConfig,
)
from litellm.llms.vertex_ai.gemini.vertex_and_google_ai_studio_gemini import VertexLLM
from litellm.secret_managers.main import get_secret_str
from litellm.types.router import GenericLiteLLMParams
if TYPE_CHECKING:
from litellm.types.google_genai.main import (
GenerateContentConfigDict,
GenerateContentContentListUnionDict,
GenerateContentResponse,
)
else:
GenerateContentConfigDict = Any
GenerateContentContentListUnionDict = Any
GenerateContentResponse = Any
class GoogleGenAIConfig(BaseGoogleGenAIGenerateContentConfig, VertexLLM):
"""
Configuration for calling Google models in their native format.
"""
@property
def custom_llm_provider(self) -> Literal["gemini", "vertex_ai"]:
return "gemini"
def __init__(self):
super().__init__()
VertexLLM.__init__(self)
def get_supported_generate_content_optional_params(self, model: str) -> List[str]:
"""
Get the list of supported Google GenAI parameters for the model.
Args:
model: The model name
Returns:
List of supported parameter names
"""
return [
"http_options",
"system_instruction",
"temperature",
"top_p",
"top_k",
"candidate_count",
"max_output_tokens",
"stop_sequences",
"response_logprobs",
"logprobs",
"presence_penalty",
"frequency_penalty",
"seed",
"response_mime_type",
"response_schema",
"routing_config",
"model_selection_config",
"safety_settings",
"tools",
"tool_config",
"labels",
"cached_content",
"response_modalities",
"media_resolution",
"speech_config",
"audio_timestamp",
"automatic_function_calling",
"thinking_config"
]
def map_generate_content_optional_params(
self,
generate_content_config_dict: GenerateContentConfigDict,
model: str,
) -> Dict[str, Any]:
"""
Map Google GenAI parameters to provider-specific format.
Args:
generate_content_optional_params: Optional parameters for generate content
model: The model name
Returns:
Mapped parameters for the provider
"""
from litellm.types.google_genai.main import GenerateContentConfigDict
_generate_content_config_dict = GenerateContentConfigDict()
supported_google_genai_params = self.get_supported_generate_content_optional_params(model)
for param, value in generate_content_config_dict.items():
if param in supported_google_genai_params:
_generate_content_config_dict[param] = value
return dict(_generate_content_config_dict)
def validate_environment(
self,
api_key: Optional[str],
headers: Optional[dict],
model: str,
litellm_params: Optional[Union[GenericLiteLLMParams, dict]]
) -> dict:
default_headers = {
"Content-Type": "application/json",
}
if api_key is not None:
default_headers["Authorization"] = f"Bearer {api_key}"
if headers is not None:
default_headers.update(headers)
return default_headers
def _get_google_ai_studio_api_key(self, litellm_params: dict) -> Optional[str]:
return (
litellm_params.pop("api_key", None)
or litellm_params.pop("gemini_api_key", None)
or get_secret_str("GEMINI_API_KEY")
or litellm.api_key
)
def _get_common_auth_components(
self,
litellm_params: dict,
) -> Tuple[Any, Optional[str], Optional[str]]:
"""
Get common authentication components used by both sync and async methods.
Returns:
Tuple of (vertex_credentials, vertex_project, vertex_location)
"""
vertex_credentials = self.get_vertex_ai_credentials(litellm_params)
vertex_project = self.get_vertex_ai_project(litellm_params)
vertex_location = self.get_vertex_ai_location(litellm_params)
return vertex_credentials, vertex_project, vertex_location
def _build_final_headers_and_url(
self,
model: str,
auth_header: Optional[str],
vertex_project: Optional[str],
vertex_location: Optional[str],
vertex_credentials: Any,
stream: bool,
api_base: Optional[str],
litellm_params: dict,
) -> Tuple[dict, str]:
"""
Build final headers and API URL from auth components.
"""
gemini_api_key = self._get_google_ai_studio_api_key(litellm_params)
auth_header, api_base = self._get_token_and_url(
model=model,
gemini_api_key=gemini_api_key,
auth_header=auth_header,
vertex_project=vertex_project,
vertex_location=vertex_location,
vertex_credentials=vertex_credentials,
stream=stream,
custom_llm_provider=self.custom_llm_provider,
api_base=api_base,
should_use_v1beta1_features=True,
)
headers = self.validate_environment(
api_key=auth_header,
headers=None,
model=model,
litellm_params=litellm_params,
)
return headers, api_base
def sync_get_auth_token_and_url(
self,
api_base: Optional[str],
model: str,
litellm_params: dict,
stream: bool,
) -> Tuple[dict, str]:
"""
Sync version of get_auth_token_and_url.
"""
vertex_credentials, vertex_project, vertex_location = self._get_common_auth_components(litellm_params)
_auth_header, vertex_project = self._ensure_access_token(
credentials=vertex_credentials,
project_id=vertex_project,
custom_llm_provider=self.custom_llm_provider,
)
return self._build_final_headers_and_url(
model=model,
auth_header=_auth_header,
vertex_project=vertex_project,
vertex_location=vertex_location,
vertex_credentials=vertex_credentials,
stream=stream,
api_base=api_base,
litellm_params=litellm_params,
)
async def get_auth_token_and_url(
self,
api_base: Optional[str],
model: str,
litellm_params: dict,
stream: bool,
) -> Tuple[dict, str]:
"""
Get the complete URL for the request.
Args:
api_base: Base API URL
model: The model name
litellm_params: LiteLLM parameters
Returns:
Tuple of headers and API base
"""
vertex_credentials, vertex_project, vertex_location = self._get_common_auth_components(litellm_params)
_auth_header, vertex_project = await self._ensure_access_token_async(
credentials=vertex_credentials,
project_id=vertex_project,
custom_llm_provider=self.custom_llm_provider,
)
return self._build_final_headers_and_url(
model=model,
auth_header=_auth_header,
vertex_project=vertex_project,
vertex_location=vertex_location,
vertex_credentials=vertex_credentials,
stream=stream,
api_base=api_base,
litellm_params=litellm_params,
)
def transform_generate_content_request(
self,
model: str,
contents: GenerateContentContentListUnionDict,
generate_content_config_dict: Dict,
) -> dict:
from litellm.types.google_genai.main import (
GenerateContentConfigDict,
GenerateContentRequestDict,
)
typed_generate_content_request = GenerateContentRequestDict(
model=model,
contents=contents,
generationConfig=GenerateContentConfigDict(**generate_content_config_dict),
)
request_dict = cast(dict, typed_generate_content_request)
return request_dict
def transform_generate_content_response(
self,
model: str,
raw_response: httpx.Response,
logging_obj: LiteLLMLoggingObj,
) -> GenerateContentResponse:
"""
Transform the raw response from the generate content API.
Args:
model: The model name
raw_response: Raw HTTP response
Returns:
Transformed response data
"""
from litellm.types.google_genai.main import GenerateContentResponse
try:
response = raw_response.json()
except Exception as e:
raise self.get_error_class(
error_message=f"Error transforming generate content response: {e}",
status_code=raw_response.status_code,
headers=raw_response.headers,
)
logging_obj.model_call_details["httpx_response"] = raw_response
return GenerateContentResponse(**response)
@@ -1261,6 +1261,30 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig):
status_code=422,
headers=raw_response.headers,
)
return self._transform_google_generate_content_to_openai_model_response(
completion_response=completion_response,
model_response=model_response,
model=model,
logging_obj=logging_obj,
raw_response=raw_response,
)
def _transform_google_generate_content_to_openai_model_response(
self,
completion_response: Union[GenerateContentResponseBody, dict],
model_response: ModelResponse,
model: str,
logging_obj: LoggingClass,
raw_response: httpx.Response,
) -> ModelResponse:
"""
Transforms a Google GenAI generate content response to an OpenAI model response.
"""
if isinstance(completion_response, dict):
completion_response = GenerateContentResponseBody(**completion_response) # type: ignore
## GET MODEL ##
model_response.model = model
@@ -0,0 +1,16 @@
"""
Transformation for Calling Google models in their native format.
"""
from typing import Literal
from litellm.llms.gemini.google_genai.transformation import GoogleGenAIConfig
class VertexAIGoogleGenAIConfig(GoogleGenAIConfig):
"""
Configuration for calling Google models in their native format.
"""
@property
def custom_llm_provider(self) -> Literal["gemini", "vertex_ai"]:
return "vertex_ai"
@@ -1,10 +1,8 @@
from typing import Any, Dict, List, Optional, Tuple
import litellm
from litellm.llms.anthropic.experimental_pass_through.messages.transformation import (
AnthropicMessagesConfig,
)
from litellm.secret_managers.main import get_secret_str
from litellm.types.llms.vertex_ai import VertexPartnerProvider
from litellm.types.router import GenericLiteLLMParams
@@ -28,25 +26,9 @@ class VertexAIPartnerModelsAnthropicMessagesConfig(AnthropicMessagesConfig, Vert
Validate the environment for the request
"""
if "Authorization" not in headers:
vertex_ai_project = (
litellm_params.pop("vertex_project", None)
or litellm_params.pop("vertex_ai_project", None)
or litellm.vertex_project
or get_secret_str("VERTEXAI_PROJECT")
)
vertex_credentials = (
litellm_params.pop("vertex_credentials", None)
or litellm_params.pop("vertex_ai_credentials", None)
or get_secret_str("VERTEXAI_CREDENTIALS")
)
vertex_ai_location = (
litellm_params.pop("vertex_location", None)
or litellm_params.pop("vertex_ai_location", None)
or litellm.vertex_location
or get_secret_str("VERTEXAI_LOCATION")
or get_secret_str("VERTEX_LOCATION")
)
vertex_ai_project = VertexBase.get_vertex_ai_project(litellm_params)
vertex_credentials = VertexBase.get_vertex_ai_credentials(litellm_params)
vertex_ai_location = VertexBase.get_vertex_ai_location(litellm_params)
access_token, project_id = self._ensure_access_token(
credentials=vertex_credentials,
+32 -5
View File
@@ -8,9 +8,11 @@ import json
import os
from typing import TYPE_CHECKING, Any, Dict, Literal, Optional, Tuple
import litellm
from litellm._logging import verbose_logger
from litellm.litellm_core_utils.asyncify import asyncify
from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler
from litellm.secret_managers.main import get_secret_str
from litellm.types.llms.vertex_ai import VERTEX_CREDENTIALS_TYPES, VertexPartnerProvider
from .common_utils import (
@@ -80,11 +82,9 @@ class VertexBase:
# Check if the JSON object contains Workload Identity Federation configuration
if "type" in json_obj and json_obj["type"] == "external_account":
# If environment_id key contains "aws" value it corresponds to an AWS config file
if (
"credential_source" in json_obj
and "environment_id" in json_obj["credential_source"]
and "aws" in json_obj["credential_source"]["environment_id"]
):
credential_source = json_obj.get("credential_source", {})
environment_id = credential_source.get("environment_id", "") if isinstance(credential_source, dict) else ""
if isinstance(environment_id, str) and "aws" in environment_id:
creds = self._credentials_from_identity_pool_with_aws(json_obj)
else:
creds = self._credentials_from_identity_pool(json_obj)
@@ -490,3 +490,30 @@ class VertexBase:
headers.update(extra_headers)
return headers
@staticmethod
def get_vertex_ai_project(litellm_params: dict) -> Optional[str]:
return (
litellm_params.pop("vertex_project", None)
or litellm_params.pop("vertex_ai_project", None)
or litellm.vertex_project
or get_secret_str("VERTEXAI_PROJECT")
)
@staticmethod
def get_vertex_ai_credentials(litellm_params: dict) -> Optional[str]:
return (
litellm_params.pop("vertex_credentials", None)
or litellm_params.pop("vertex_ai_credentials", None)
or get_secret_str("VERTEXAI_CREDENTIALS")
)
@staticmethod
def get_vertex_ai_location(litellm_params: dict) -> Optional[str]:
return (
litellm_params.pop("vertex_location", None)
or litellm_params.pop("vertex_ai_location", None)
or litellm.vertex_location
or get_secret_str("VERTEXAI_LOCATION")
or get_secret_str("VERTEX_LOCATION")
)
@@ -2165,7 +2165,7 @@
"input_cost_per_token_batches": 1e-05,
"output_cost_per_token_batches": 4e-05,
"litellm_provider": "azure",
"mode": "chat",
"mode": "responses",
"supported_endpoints": [
"/v1/chat/completions",
"/v1/batch",
@@ -2195,7 +2195,7 @@
"input_cost_per_token_batches": 1e-05,
"output_cost_per_token_batches": 4e-05,
"litellm_provider": "azure",
"mode": "chat",
"mode": "responses",
"supported_endpoints": [
"/v1/chat/completions",
"/v1/batch",
+17
View File
@@ -2,3 +2,20 @@ model_list:
- model_name: gemini-2.5-pro
litellm_params:
model: gemini/gemini-2.5-pro
- model_name: azure-batches
litellm_params:
model: azure/gpt-4o
api_key: os.environ/AZURE_API_KEY_HIDDEN
api_base: os.environ/AZURE_API_BASE_HIDDEN
- model_name: openai-gpt-4o
litellm_params:
model: gpt-4o
api_key: os.environ/OPENAI_API_KEY_TEST
model_info:
id: 12345678
- model_name: openai-gpt-4o
litellm_params:
model: gpt-4o
api_key: os.environ/OPENAI_API_KEY_TEST_2
model_info:
id: 12345679
+11
View File
@@ -347,6 +347,17 @@ class LiteLLMRoutes(enum.Enum):
"/mcp/tools/call",
]
google_routes = [
"/v1beta/models/{model_name}:countTokens",
"/v1beta/models/{model_name}:generateContent",
"/v1beta/models/{model_name}:streamGenerateContent",
"/models/{model_name}:countTokens",
"/models/{model_name}:generateContent",
"/models/{model_name}:streamGenerateContent",
]
apply_guardrail_routes = [
"/guardrails/apply_guardrail",
]
+7 -81
View File
@@ -3,16 +3,11 @@ Unified /v1/messages endpoint - (Anthropic Spec)
"""
import asyncio
import json
import time
import traceback
from fastapi import APIRouter, Depends, HTTPException, Request, Response, status
import litellm
from litellm._logging import verbose_proxy_logger
from litellm.constants import STREAM_SSE_DATA_PREFIX
from litellm.litellm_core_utils.safe_json_dumps import safe_dumps
from litellm.proxy._types import *
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
from litellm.proxy.common_request_processing import (
@@ -21,85 +16,14 @@ from litellm.proxy.common_request_processing import (
)
from litellm.proxy.common_utils.http_parsing_utils import _read_request_body
from litellm.proxy.litellm_pre_call_utils import add_litellm_data_to_request
from litellm.proxy.utils import ProxyLogging
router = APIRouter()
def return_anthropic_chunk(chunk: Any) -> str:
"""
Helper function to format streaming chunks for Anthropic API format
Args:
chunk: A string or dictionary to be returned in SSE format
Returns:
str: A properly formatted SSE chunk string
"""
if isinstance(chunk, dict):
# Use safe_dumps for proper JSON serialization with circular reference detection
chunk_str = safe_dumps(chunk)
return f"{STREAM_SSE_DATA_PREFIX}{chunk_str}\n\n"
else:
return chunk
async def async_data_generator_anthropic(
response,
user_api_key_dict: UserAPIKeyAuth,
request_data: dict,
proxy_logging_obj: ProxyLogging,
):
verbose_proxy_logger.debug("inside generator")
try:
time.time()
async for chunk in response:
verbose_proxy_logger.debug(
"async_data_generator: received streaming chunk - {}".format(chunk)
)
### CALL HOOKS ### - modify outgoing data
chunk = await proxy_logging_obj.async_post_call_streaming_hook(
user_api_key_dict=user_api_key_dict, response=chunk
)
# Format chunk using helper function
yield return_anthropic_chunk(chunk)
except Exception as e:
verbose_proxy_logger.exception(
"litellm.proxy.proxy_server.async_data_generator(): Exception occured - {}".format(
str(e)
)
)
await proxy_logging_obj.post_call_failure_hook(
user_api_key_dict=user_api_key_dict,
original_exception=e,
request_data=request_data,
)
verbose_proxy_logger.debug(
f"\033[1;31mAn error occurred: {e}\n\n Debug this by setting `--debug`, e.g. `litellm --model gpt-3.5-turbo --debug`"
)
if isinstance(e, HTTPException):
raise e
else:
error_traceback = traceback.format_exc()
error_msg = f"{str(e)}\n\n{error_traceback}"
proxy_exception = ProxyException(
message=getattr(e, "message", error_msg),
type=getattr(e, "type", "None"),
param=getattr(e, "param", "None"),
code=getattr(e, "status_code", 500),
)
error_returned = json.dumps({"error": proxy_exception.to_dict()})
yield f"{STREAM_SSE_DATA_PREFIX}{error_returned}\n\n"
@router.post(
"/v1/messages",
tags=["[beta] Anthropic `/v1/messages`"],
dependencies=[Depends(user_api_key_auth)],
include_in_schema=False,
)
async def anthropic_response( # noqa: PLR0915
fastapi_response: Response,
@@ -243,11 +167,13 @@ async def anthropic_response( # noqa: PLR0915
if (
"stream" in data and data["stream"] is True
): # use generate_responses to stream responses
selected_data_generator = async_data_generator_anthropic(
response=response,
user_api_key_dict=user_api_key_dict,
request_data=data,
proxy_logging_obj=proxy_logging_obj,
selected_data_generator = (
ProxyBaseLLMRequestProcessing.async_sse_data_generator(
response=response,
user_api_key_dict=user_api_key_dict,
request_data=data,
proxy_logging_obj=proxy_logging_obj,
)
)
return await create_streaming_response(
+29 -12
View File
@@ -368,20 +368,37 @@ async def list_batches(
```
"""
from litellm.proxy.proxy_server import proxy_logging_obj, version
from litellm.proxy.proxy_server import llm_router, proxy_logging_obj, version
verbose_proxy_logger.debug("GET /v1/batches after={} limit={}".format(after, limit))
try:
custom_llm_provider = (
provider
or await get_custom_llm_provider_from_request_body(request=request)
or "openai"
)
response = await litellm.alist_batches(
custom_llm_provider=custom_llm_provider, # type: ignore
after=after,
limit=limit,
)
if llm_router is None:
raise HTTPException(
status_code=500,
detail={"error": CommonProxyErrors.no_llm_router.value},
)
## check for target model names
data = await _read_request_body(request=request)
target_model_names = data.get("target_model_names", None)
if target_model_names:
model = target_model_names.split(",")[0]
response = await llm_router.alist_batches(
model=model,
after=after,
limit=limit,
)
else:
custom_llm_provider = (
provider
or await get_custom_llm_provider_from_request_body(request=request)
or "openai"
)
response = await litellm.alist_batches(
custom_llm_provider=custom_llm_provider, # type: ignore
after=after,
limit=limit,
)
### RESPONSE HEADERS ###
hidden_params = getattr(response, "_hidden_params", {}) or {}
@@ -483,7 +500,7 @@ async def cancel_batch(
_cancel_batch_data = CancelBatchRequest(batch_id=batch_id, **data)
response = await litellm.acancel_batch(
custom_llm_provider=custom_llm_provider, # type: ignore
**_cancel_batch_data
**_cancel_batch_data,
)
### ALERTING ###
+105 -4
View File
@@ -1,5 +1,6 @@
import asyncio
import json
import traceback
import uuid
from datetime import datetime
from typing import (
@@ -20,9 +21,13 @@ from fastapi.responses import Response, StreamingResponse
import litellm
from litellm._logging import verbose_proxy_logger
from litellm.constants import DD_TRACER_STREAMING_CHUNK_YIELD_RESOURCE
from litellm.constants import (
DD_TRACER_STREAMING_CHUNK_YIELD_RESOURCE,
STREAM_SSE_DATA_PREFIX,
)
from litellm.litellm_core_utils.dd_tracing import tracer
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
from litellm.litellm_core_utils.safe_json_dumps import safe_dumps
from litellm.proxy._types import ProxyException, UserAPIKeyAuth
from litellm.proxy.auth.auth_utils import check_response_size_is_safe
from litellm.proxy.common_utils.callback_utils import (
@@ -252,6 +257,8 @@ class ProxyBaseLLMRequestProcessing:
"aretrieve_fine_tuning_job",
"alist_input_items",
"aimage_edit",
"agenerate_content",
"agenerate_content_stream",
],
version: Optional[str] = None,
user_model: Optional[str] = None,
@@ -331,6 +338,8 @@ class ProxyBaseLLMRequestProcessing:
"atext_completion",
"aimage_edit",
"alist_input_items",
"agenerate_content",
"agenerate_content_stream",
],
proxy_logging_obj: ProxyLogging,
general_settings: dict,
@@ -344,6 +353,7 @@ class ProxyBaseLLMRequestProcessing:
user_max_tokens: Optional[int] = None,
user_api_base: Optional[str] = None,
version: Optional[str] = None,
is_streaming_request: Optional[bool] = False,
) -> Any:
"""
Common request processing logic for both chat completions and responses API endpoints
@@ -418,9 +428,7 @@ class ProxyBaseLLMRequestProcessing:
litellm_call_id=self.data.get("litellm_call_id", ""), status="success"
)
)
if (
"stream" in self.data and self.data["stream"] is True
): # use generate_responses to stream responses
if self._is_streaming_request(data=self.data, is_streaming_request=is_streaming_request): # use generate_responses to stream responses
custom_headers = ProxyBaseLLMRequestProcessing.get_custom_headers(
user_api_key_dict=user_api_key_dict,
call_id=logging_obj.litellm_call_id,
@@ -476,6 +484,22 @@ class ProxyBaseLLMRequestProcessing:
return response
def _is_streaming_request(
self, data: dict, is_streaming_request: Optional[bool] = False
) -> bool:
"""
Check if the request is a streaming request.
1. is_streaming_request is a dynamic param passed in
2. if "stream" in data and data["stream"] is True
"""
if is_streaming_request is True:
return True
if "stream" in data and data["stream"] is True:
return True
return False
async def _handle_llm_api_exception(
self,
e: Exception,
@@ -545,3 +569,80 @@ class ProxyBaseLLMRequestProcessing:
return "completion"
elif route_type == "aresponses":
return "responses"
#########################################################
# Proxy Level Streaming Data Generator
#########################################################
@staticmethod
def return_sse_chunk(chunk: Any) -> str:
"""
Helper function to format streaming chunks for Anthropic API format
Args:
chunk: A string or dictionary to be returned in SSE format
Returns:
str: A properly formatted SSE chunk string
"""
if isinstance(chunk, dict):
# Use safe_dumps for proper JSON serialization with circular reference detection
chunk_str = safe_dumps(chunk)
return f"{STREAM_SSE_DATA_PREFIX}{chunk_str}\n\n"
else:
return chunk
@staticmethod
async def async_sse_data_generator(
response,
user_api_key_dict: UserAPIKeyAuth,
request_data: dict,
proxy_logging_obj: ProxyLogging,
):
"""
Anthropic /messages and Google /generateContent streaming data generator require SSE events
"""
verbose_proxy_logger.debug("inside generator")
try:
async for chunk in response:
verbose_proxy_logger.debug(
"async_data_generator: received streaming chunk - {}".format(chunk)
)
### CALL HOOKS ### - modify outgoing data
chunk = await proxy_logging_obj.async_post_call_streaming_hook(
user_api_key_dict=user_api_key_dict, response=chunk
)
# Format chunk using helper function
yield ProxyBaseLLMRequestProcessing.return_sse_chunk(chunk)
except Exception as e:
verbose_proxy_logger.exception(
"litellm.proxy.proxy_server.async_data_generator(): Exception occured - {}".format(
str(e)
)
)
await proxy_logging_obj.post_call_failure_hook(
user_api_key_dict=user_api_key_dict,
original_exception=e,
request_data=request_data,
)
verbose_proxy_logger.debug(
f"\033[1;31mAn error occurred: {e}\n\n Debug this by setting `--debug`, e.g. `litellm --model gpt-3.5-turbo --debug`"
)
if isinstance(e, HTTPException):
raise e
else:
error_traceback = traceback.format_exc()
error_msg = f"{str(e)}\n\n{error_traceback}"
proxy_exception = ProxyException(
message=getattr(e, "message", error_msg),
type=getattr(e, "type", "None"),
param=getattr(e, "param", "None"),
code=getattr(e, "status_code", 500),
)
error_returned = json.dumps({"error": proxy_exception.to_dict()})
yield f"{STREAM_SSE_DATA_PREFIX}{error_returned}\n\n"
+154
View File
@@ -0,0 +1,154 @@
from fastapi import APIRouter, Depends, Request, Response
from litellm.proxy._types import *
from litellm.proxy.auth.user_api_key_auth import UserAPIKeyAuth, user_api_key_auth
from litellm.proxy.common_request_processing import ProxyBaseLLMRequestProcessing
router = APIRouter(
tags=["google genai endpoints"],
)
@router.post("/v1beta/models/{model_name}:generateContent", dependencies=[Depends(user_api_key_auth)])
@router.post("/models/{model_name}:generateContent", dependencies=[Depends(user_api_key_auth)])
async def google_generate_content(
request: Request,
model_name: str,
fastapi_response: Response,
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
):
"""
Not Implemented, this is a placeholder for the google genai generateContent endpoint.
"""
from litellm.proxy.proxy_server import (
_read_request_body,
general_settings,
llm_router,
proxy_config,
proxy_logging_obj,
select_data_generator,
user_api_base,
user_max_tokens,
user_model,
user_request_timeout,
user_temperature,
version,
)
data = await _read_request_body(request=request)
if "model" not in data:
data["model"] = model_name
processor = ProxyBaseLLMRequestProcessing(data=data)
try:
return await processor.base_process_llm_request(
request=request,
fastapi_response=fastapi_response,
user_api_key_dict=user_api_key_dict,
route_type="agenerate_content",
proxy_logging_obj=proxy_logging_obj,
llm_router=llm_router,
general_settings=general_settings,
proxy_config=proxy_config,
select_data_generator=select_data_generator,
model=None,
user_model=user_model,
user_temperature=user_temperature,
user_request_timeout=user_request_timeout,
user_max_tokens=user_max_tokens,
user_api_base=user_api_base,
version=version,
)
except Exception as e:
raise await processor._handle_llm_api_exception(
e=e,
user_api_key_dict=user_api_key_dict,
proxy_logging_obj=proxy_logging_obj,
version=version,
)
class GoogleAIStudioDataGenerator:
"""
Ensures SSE data generator is used for Google AI Studio streaming responses
Thin wrapper around ProxyBaseLLMRequestProcessing.async_sse_data_generator
"""
@staticmethod
def _select_data_generator(response, user_api_key_dict, request_data):
from litellm.proxy.proxy_server import proxy_logging_obj
return ProxyBaseLLMRequestProcessing.async_sse_data_generator(
response=response,
user_api_key_dict=user_api_key_dict,
request_data=request_data,
proxy_logging_obj=proxy_logging_obj,
)
@router.post("/v1beta/models/{model_name}:streamGenerateContent", dependencies=[Depends(user_api_key_auth)])
@router.post("/models/{model_name}:streamGenerateContent", dependencies=[Depends(user_api_key_auth)])
async def google_stream_generate_content(
request: Request,
model_name: str,
fastapi_response: Response,
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
):
"""
Not Implemented, this is a placeholder for the google genai streamGenerateContent endpoint.
"""
from litellm.proxy.proxy_server import (
_read_request_body,
general_settings,
llm_router,
proxy_config,
proxy_logging_obj,
user_api_base,
user_max_tokens,
user_model,
user_request_timeout,
user_temperature,
version,
)
data = await _read_request_body(request=request)
if "model" not in data:
data["model"] = model_name
processor = ProxyBaseLLMRequestProcessing(data=data)
try:
return await processor.base_process_llm_request(
request=request,
fastapi_response=fastapi_response,
user_api_key_dict=user_api_key_dict,
route_type="agenerate_content_stream",
proxy_logging_obj=proxy_logging_obj,
llm_router=llm_router,
general_settings=general_settings,
proxy_config=proxy_config,
select_data_generator=GoogleAIStudioDataGenerator._select_data_generator,
model=None,
user_model=user_model,
user_temperature=user_temperature,
user_request_timeout=user_request_timeout,
user_max_tokens=user_max_tokens,
user_api_base=user_api_base,
version=version,
is_streaming_request=True,
)
except Exception as e:
raise await processor._handle_llm_api_exception(
e=e,
user_api_key_dict=user_api_key_dict,
proxy_logging_obj=proxy_logging_obj,
version=version,
)
@router.post("/v1beta/models/{model_name}:countTokens", dependencies=[Depends(user_api_key_auth)])
@router.post("/models/{model_name}:countTokens", dependencies=[Depends(user_api_key_auth)])
async def google_count_tokens(request: Request, model_name: str):
"""
Not Implemented, this is a placeholder for the google genai countTokens endpoint.
"""
return {}
@@ -1,6 +1,6 @@
import base64
import re
from typing import List, Literal, Union
from typing import List, Literal, Optional, Union
from litellm.types.utils import SpecialEnums
@@ -43,3 +43,24 @@ def get_models_from_unified_file_id(unified_file_id: str) -> List[str]:
return []
except Exception:
return []
def get_model_id_from_unified_batch_id(file_id: str) -> Optional[str]:
"""
Get the model_id from the file_id
Expected format: litellm_proxy;model_id:{};llm_batch_id:{};llm_output_file_id:{}
"""
## use regex to get the model_id from the file_id
try:
return file_id.split("model_id:")[1].split(";")[0]
except Exception:
return None
def get_batch_id_from_unified_batch_id(file_id: str) -> str:
## use regex to get the batch_id from the file_id
if "llm_batch_id" in file_id:
return file_id.split("llm_batch_id:")[1].split(",")[0]
else:
return file_id.split("generic_response_id:")[1].split(",")[0]
@@ -190,6 +190,7 @@ class VertexPassthroughLoggingHandler:
endpoint_type: EndpointType,
start_time: datetime,
all_chunks: List[str],
model: Optional[str],
end_time: datetime,
) -> PassThroughEndpointLoggingTypedDict:
"""
@@ -200,7 +201,7 @@ class VertexPassthroughLoggingHandler:
- Logs in litellm callbacks
"""
kwargs: Dict[str, Any] = {}
model = VertexPassthroughLoggingHandler.extract_model_from_url(url_route)
model = model or VertexPassthroughLoggingHandler.extract_model_from_url(url_route)
complete_streaming_response = (
VertexPassthroughLoggingHandler._build_complete_streaming_response(
all_chunks=all_chunks,
@@ -70,6 +70,7 @@ class PassThroughStreamingHandler:
start_time: datetime,
raw_bytes: List[bytes],
end_time: datetime,
model: Optional[str] = None,
):
"""
Route the logging for the collected chunks to the appropriate handler
@@ -111,6 +112,7 @@ class PassThroughStreamingHandler:
start_time=start_time,
all_chunks=all_chunks,
end_time=end_time,
model=model,
)
)
standard_logging_response_object = (
@@ -122,7 +124,6 @@ class PassThroughStreamingHandler:
standard_logging_response_object = StandardPassThroughResponseObject(
response=f"cannot parse chunks to standard response object. Chunks={all_chunks}"
)
await litellm_logging_obj.async_success_handler(
result=standard_logging_response_object,
start_time=start_time,
+7 -8
View File
@@ -1,12 +1,8 @@
model_list:
- model_name: gemini/*
- model_name: azure_ai/*
litellm_params:
model: gemini/*
api_key: os.environ/GEMINI_API_KEY
- model_name: "anthropic/*"
litellm_params:
model: "anthropic/*"
api_key: os.environ/ANTHROPIC_API_KEY
model: azure_ai/*
mcp_servers:
deepwiki_mcp:
@@ -18,4 +14,7 @@ general_settings:
store_prompts_in_spend_logs: true
litellm_settings:
callbacks: ["langfuse", "datadog"]
callbacks: ["langfuse", "datadog"]
cache: True
cache_params: # set cache params for redis
type: redis
+4
View File
@@ -212,6 +212,7 @@ from litellm.proxy.db.exception_handler import PrismaDBExceptionHandler
from litellm.proxy.discovery_endpoints import ui_discovery_endpoints_router
from litellm.proxy.fine_tuning_endpoints.endpoints import router as fine_tuning_router
from litellm.proxy.fine_tuning_endpoints.endpoints import set_fine_tuning_config
from litellm.proxy.google_endpoints.endpoints import router as google_router
from litellm.proxy.guardrails.guardrail_endpoints import router as guardrails_router
from litellm.proxy.guardrails.init_guardrails import (
init_guardrails_v2,
@@ -2278,6 +2279,8 @@ class ProxyConfig:
model_group=model["model_name"],
litellm_params=model["litellm_params"],
)
else:
model_id = str(model_id)
combined_id_list.append(model_id) # ADD CONFIG MODEL TO COMBINED LIST
router_model_ids = llm_router.get_model_ids()
@@ -8590,6 +8593,7 @@ app.include_router(credential_router)
app.include_router(llm_passthrough_router)
app.include_router(mcp_management_router)
app.include_router(anthropic_router)
app.include_router(google_router)
app.include_router(langfuse_router)
app.include_router(pass_through_router)
app.include_router(health_router)
+2
View File
@@ -73,6 +73,8 @@ async def route_request(
"alist_input_items",
"_arealtime", # private function for realtime API
"aimage_edit",
"agenerate_content",
"agenerate_content_stream",
],
):
"""
+4 -3
View File
@@ -452,8 +452,8 @@ enum JobStatus {
model LiteLLM_ManagedFileTable {
id String @id @default(uuid())
unified_file_id String @unique // The base64 encoded unified file ID
file_object Json // Stores the OpenAIFileObject
model_mappings Json
file_object Json? // Stores the OpenAIFileObject
model_mappings Json
flat_model_file_ids String[] @default([]) // Flat list of model file id's - for faster querying of model id -> unified file id
created_at DateTime @default(now())
created_by String?
@@ -468,7 +468,8 @@ model LiteLLM_ManagedObjectTable { // for batches or finetuning jobs which use t
unified_object_id String @unique // The base64 encoded unified file ID
model_object_id String @unique // the id returned by the backend API provider
file_object Json // Stores the OpenAIFileObject
file_purpose String // either 'batch' or 'fine-tune'
file_purpose String // either 'batch' or 'fine-tune'
status String? // check if batch cost has been tracked
created_at DateTime @default(now())
created_by String?
updated_at DateTime @updatedAt
+53 -4
View File
@@ -743,6 +743,7 @@ class Router:
self.afile_delete = self.factory_function(
litellm.afile_delete, call_type="afile_delete"
)
self.afile_content = self.factory_function(
litellm.afile_content, call_type="afile_content"
)
@@ -781,6 +782,28 @@ class Router:
litellm.allm_passthrough_route, call_type="allm_passthrough_route"
)
#########################################################
# Gemini Native routes
#########################################################
from litellm.google_genai import (
agenerate_content,
agenerate_content_stream,
generate_content,
generate_content_stream,
)
self.agenerate_content = self.factory_function(
agenerate_content, call_type="agenerate_content"
)
self.generate_content = self.factory_function(
generate_content, call_type="generate_content"
)
self.agenerate_content_stream = self.factory_function(
agenerate_content_stream, call_type="agenerate_content_stream"
)
self.generate_content_stream = self.factory_function(
generate_content_stream, call_type="generate_content_stream"
)
def validate_fallbacks(self, fallback_param: Optional[List]):
"""
Validate the fallbacks parameter.
@@ -2458,9 +2481,9 @@ class Router:
self._update_kwargs_before_fallbacks(
model=model,
kwargs=kwargs,
metadata_variable_name = _get_router_metadata_variable_name(
metadata_variable_name=_get_router_metadata_variable_name(
function_name=function_name
)
),
)
try:
verbose_router_logger.debug(
@@ -2790,6 +2813,8 @@ class Router:
**kwargs,
) -> OpenAIFileObject:
try:
from litellm.router_utils.common_utils import add_model_file_id_mappings
verbose_router_logger.debug(
f"Inside _atext_completion()- model: {model}; kwargs: {kwargs}"
)
@@ -2884,6 +2909,7 @@ class Router:
return response
tasks = []
if isinstance(healthy_deployments, dict):
tasks.append(create_file_for_deployment(healthy_deployments))
else:
@@ -2894,7 +2920,15 @@ class Router:
if len(responses) == 0:
raise Exception("No healthy deployments found.")
return responses[0]
model_file_id_mapping = add_model_file_id_mappings(
healthy_deployments=healthy_deployments, responses=responses
)
returned_response = cast(OpenAIFileObject, responses[0])
returned_response._hidden_params["model_file_id_mapping"] = (
model_file_id_mapping
)
return returned_response
except Exception as e:
verbose_router_logger.exception(
f"litellm.acreate_file(model={model}, {kwargs})\033[31m Exception {str(e)}\033[0m"
@@ -3230,6 +3264,10 @@ class Router:
"aimage_edit",
"allm_passthrough_route",
"alist_input_items",
"agenerate_content",
"generate_content",
"agenerate_content_stream",
"generate_content_stream",
] = "assistants",
):
"""
@@ -3240,7 +3278,7 @@ class Router:
- An asynchronous function for asynchronous call types
"""
# Handle synchronous call types
if call_type == "responses":
if call_type in ("responses", "generate_content", "generate_content_stream"):
def sync_wrapper(
custom_llm_provider: Optional[
@@ -3285,6 +3323,8 @@ class Router:
"alist_files",
"aimage_edit",
"allm_passthrough_route",
"agenerate_content",
"agenerate_content_stream",
):
return await self._ageneric_api_call_with_fallbacks(
original_function=original_function,
@@ -6175,6 +6215,8 @@ class Router:
*OR*
- Dict, if specific model chosen
"""
from litellm.router_utils.common_utils import filter_team_based_models
model, healthy_deployments = self._common_checks_available_deployment(
model=model,
messages=messages,
@@ -6182,6 +6224,13 @@ class Router:
specific_deployment=specific_deployment,
) # type: ignore
# IF TEAM ID SPECIFIED ON MODEL, AND REQUEST CONTAINS USER_API_KEY_TEAM_ID, FILTER OUT MODELS THAT ARE NOT IN THE TEAM
## THIS PREVENTS WRITING FILES OF OTHER TEAMS TO MODELS THAT ARE TEAM-ONLY MODELS
healthy_deployments = filter_team_based_models(
healthy_deployments=healthy_deployments,
request_kwargs=request_kwargs,
)
if isinstance(healthy_deployments, dict):
return healthy_deployments
+61
View File
@@ -1,5 +1,9 @@
import hashlib
import json
from typing import TYPE_CHECKING, Dict, List, Optional, Union
if TYPE_CHECKING:
from litellm.types.llms.openai import OpenAIFileObject
from litellm.types.router import CredentialLiteLLMParams
@@ -12,3 +16,60 @@ def get_litellm_params_sensitive_credential_hash(litellm_params: dict) -> str:
return hashlib.sha256(
json.dumps(sensitive_params.model_dump()).encode()
).hexdigest()
def add_model_file_id_mappings(
healthy_deployments: Union[List[Dict], Dict], responses: List["OpenAIFileObject"]
) -> dict:
"""
Create a mapping of model name to file id
{
"model_id": "file_id",
"model_id": "file_id",
}
"""
model_file_id_mapping = {}
if isinstance(healthy_deployments, list):
for deployment, response in zip(healthy_deployments, responses):
model_file_id_mapping[deployment.get("model_info", {}).get("id")] = (
response.id
)
elif isinstance(healthy_deployments, dict):
for model_id, file_id in healthy_deployments.items():
model_file_id_mapping[model_id] = file_id
return model_file_id_mapping
def filter_team_based_models(
healthy_deployments: Union[List[Dict], Dict],
request_kwargs: Optional[Dict] = None,
) -> Union[List[Dict], Dict]:
"""
If a model has a team_id
Only use if request is from that team
"""
if request_kwargs is None:
return healthy_deployments
metadata = request_kwargs.get("metadata") or {}
litellm_metadata = request_kwargs.get("litellm_metadata") or {}
request_team_id = metadata.get("user_api_key_team_id") or litellm_metadata.get(
"user_api_key_team_id"
)
ids_to_remove = []
if isinstance(healthy_deployments, dict):
return healthy_deployments
for deployment in healthy_deployments:
_model_info = deployment.get("model_info") or {}
model_team_id = _model_info.get("team_id")
if model_team_id is None:
continue
if model_team_id != request_team_id:
ids_to_remove.append(deployment.get("model_info", {}).get("id"))
return [
deployment
for deployment in healthy_deployments
if deployment.get("model_info", {}).get("id") not in ids_to_remove
]
+13
View File
@@ -0,0 +1,13 @@
from .main import (
ContentListUnion,
ContentListUnionDict,
GenerateContentConfigOrDict,
GenerateContentResponse,
)
__all__ = [
"ContentListUnion",
"ContentListUnionDict",
"GenerateContentConfigOrDict",
"GenerateContentResponse",
]
+25
View File
@@ -0,0 +1,25 @@
# Import types from the Google GenAI SDK
from typing import TYPE_CHECKING, Any, Optional, TypeAlias, TypedDict
# During static type-checking we can rely on the real google-genai types.
from google.genai import types as _genai_types # type: ignore
from pydantic import BaseModel
from litellm.types.llms.openai import BaseLiteLLMOpenAIResponseObject
ContentListUnion = _genai_types.ContentListUnion
ContentListUnionDict = _genai_types.ContentListUnionDict
GenerateContentConfigOrDict = _genai_types.GenerateContentConfigOrDict
GoogleGenAIGenerateContentResponse = _genai_types.GenerateContentResponse
GenerateContentContentListUnionDict = _genai_types.ContentListUnionDict
GenerateContentConfigDict = _genai_types.GenerateContentConfigDict
GenerateContentRequestParametersDict = _genai_types._GenerateContentParametersDict
class GenerateContentRequestDict(GenerateContentRequestParametersDict): # type: ignore[misc]
generationConfig: Optional[Any]
class GenerateContentResponse(GoogleGenAIGenerateContentResponse, BaseLiteLLMOpenAIResponseObject): # type: ignore[misc]
_hidden_params: dict = {}
pass
+12
View File
@@ -277,6 +277,14 @@ class CallTypes(Enum):
aresponses = "aresponses"
alist_input_items = "alist_input_items"
#########################################################
# Google GenAI Native Call Types
#########################################################
generate_content = "generate_content"
agenerate_content = "agenerate_content"
generate_content_stream = "generate_content_stream"
agenerate_content_stream = "agenerate_content_stream"
CallTypesLiteral = Literal[
"embedding",
@@ -304,6 +312,10 @@ CallTypesLiteral = Literal[
"anthropic_messages",
"aretrieve_batch",
"retrieve_batch",
"generate_content",
"agenerate_content",
"generate_content_stream",
"agenerate_content_stream",
]
+69 -4
View File
@@ -129,6 +129,9 @@ from litellm.litellm_core_utils.redact_messages import (
from litellm.litellm_core_utils.rules import Rules
from litellm.litellm_core_utils.streaming_handler import CustomStreamWrapper
from litellm.litellm_core_utils.token_counter import get_modified_max_tokens
from litellm.llms.base_llm.google_genai.transformation import (
BaseGoogleGenAIGenerateContentConfig,
)
from litellm.llms.bedrock.common_utils import BedrockModelInfo
from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler
from litellm.router_utils.get_retry_from_policy import (
@@ -769,7 +772,12 @@ def function_setup( # noqa: PLR0915
messages = args[0] if len(args) > 0 else kwargs["input"]
else:
messages = "default-message-value"
stream = True if "stream" in kwargs and kwargs["stream"] is True else False
stream = False
if _is_streaming_request(
kwargs=kwargs,
call_type=call_type,
):
stream = True
logging_obj = LiteLLMLogging(
model=model, # type: ignore
messages=messages,
@@ -1022,7 +1030,10 @@ def client(original_function): # noqa: PLR0915
# MODEL CALL
result = original_function(*args, **kwargs)
if "stream" in kwargs and kwargs["stream"] is True:
if _is_streaming_request(
kwargs=kwargs,
call_type=call_type,
):
if (
"complete_response" in kwargs
and kwargs["complete_response"] is True
@@ -1166,7 +1177,10 @@ def client(original_function): # noqa: PLR0915
# MODEL CALL
result = original_function(*args, **kwargs)
end_time = datetime.datetime.now()
if "stream" in kwargs and kwargs["stream"] is True:
if _is_streaming_request(
kwargs=kwargs,
call_type=call_type,
):
if (
"complete_response" in kwargs
and kwargs["complete_response"] is True
@@ -1358,7 +1372,10 @@ def client(original_function): # noqa: PLR0915
# MODEL CALL
result = await original_function(*args, **kwargs)
end_time = datetime.datetime.now()
if "stream" in kwargs and kwargs["stream"] is True:
if _is_streaming_request(
kwargs=kwargs,
call_type=call_type,
):
if (
"complete_response" in kwargs
and kwargs["complete_response"] is True
@@ -1538,6 +1555,35 @@ def _is_async_request(
return False
def _is_streaming_request(
kwargs: Dict[str, Any],
call_type: Union[CallTypes, str],
) -> bool:
"""
Returns True if the call type is a streaming request.
Returns True if:
- if "stream=True" in kwargs (litellm chat completion, litellm text completion, litellm messages)
- if call_type is generate_content_stream or agenerate_content_stream (litellm google genai)
"""
if "stream" in kwargs and kwargs["stream"] is True:
return True
#########################################################
# Check if it's a google genai streaming request
if isinstance(call_type, str):
# check if it can be casted to CallTypes
try:
call_type = CallTypes(call_type)
except ValueError:
return False
if call_type == CallTypes.generate_content_stream or call_type == CallTypes.agenerate_content_stream:
return True
#########################################################
return False
def update_response_metadata(
result: Any,
logging_obj: LiteLLMLoggingObject,
@@ -6981,6 +7027,25 @@ class ProviderConfigManager:
return AzureImageEditConfig()
return None
@staticmethod
def get_provider_google_genai_generate_content_config(
model: str,
provider: LlmProviders,
) -> Optional[BaseGoogleGenAIGenerateContentConfig]:
if litellm.LlmProviders.GEMINI == provider:
from litellm.llms.gemini.google_genai.transformation import (
GoogleGenAIConfig,
)
return GoogleGenAIConfig()
elif litellm.LlmProviders.VERTEX_AI == provider:
from litellm.llms.vertex_ai.google_genai.transformation import (
VertexAIGoogleGenAIConfig,
)
return VertexAIGoogleGenAIConfig()
return None
def get_end_user_id_for_cost_tracking(
+2 -2
View File
@@ -2165,7 +2165,7 @@
"input_cost_per_token_batches": 1e-05,
"output_cost_per_token_batches": 4e-05,
"litellm_provider": "azure",
"mode": "chat",
"mode": "responses",
"supported_endpoints": [
"/v1/chat/completions",
"/v1/batch",
@@ -2195,7 +2195,7 @@
"input_cost_per_token_batches": 1e-05,
"output_cost_per_token_batches": 4e-05,
"litellm_provider": "azure",
"mode": "chat",
"mode": "responses",
"supported_endpoints": [
"/v1/chat/completions",
"/v1/batch",
Generated
+129 -406
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -56,7 +56,7 @@ websockets = {version = "^13.1.0", optional = true}
boto3 = {version = "1.34.34", optional = true}
redisvl = {version = "^0.4.1", optional = true, markers = "python_version >= '3.9' and python_version < '3.14'"}
mcp = {version = "1.9.3", optional = true, python = ">=3.10"}
litellm-proxy-extras = {version = "0.2.5", optional = true}
litellm-proxy-extras = {version = "0.2.6", optional = true}
rich = {version = "13.7.1", optional = true}
litellm-enterprise = {version = "0.1.9", optional = true}
diskcache = {version = "^5.6.1", optional = true}
@@ -141,7 +141,7 @@ requires = ["poetry-core", "wheel"]
build-backend = "poetry.core.masonry.api"
[tool.commitizen]
version = "1.73.2"
version = "1.73.3"
version_files = [
"pyproject.toml:^version"
]
+4 -3
View File
@@ -1,6 +1,6 @@
# LITELLM PROXY DEPENDENCIES #
anyio==4.5.0 # openai + http req.
httpx==0.27.0 # Pin Httpx dependency
anyio==4.8.0 # openai + http req.
httpx==0.28.1
openai==1.81.0 # openai req.
fastapi==0.115.5 # server dep
backoff==2.2.1 # server dep
@@ -14,6 +14,7 @@ prisma==0.11.0 # for db
mangum==0.17.0 # for aws lambda functions
pynacl==1.5.0 # for encrypting keys
google-cloud-aiplatform==1.47.0 # for vertex ai calls
google-genai==1.22.0
anthropic[vertex]==0.54.0
mcp==1.9.3 # for MCP server
google-generativeai==0.5.0 # for vertex ai calls
@@ -37,7 +38,7 @@ sentry_sdk==2.21.0 # for sentry error handling
detect-secrets==1.5.0 # Enterprise - secret detection / masking in LLM requests
cryptography==43.0.1
tzdata==2025.1 # IANA time zone database
litellm-proxy-extras==0.2.5 # for proxy extras - e.g. prisma migrations
litellm-proxy-extras==0.2.6 # for proxy extras - e.g. prisma migrations
### LITELLM PACKAGE DEPENDENCIES
python-dotenv==1.0.0 # for env
tiktoken==0.8.0 # for calculating usage
+43 -42
View File
@@ -16,7 +16,7 @@ model LiteLLM_BudgetTable {
tpm_limit BigInt?
rpm_limit BigInt?
model_max_budget Json?
budget_duration String?
budget_duration String?
budget_reset_at DateTime?
created_at DateTime @default(now()) @map("created_at")
created_by String
@@ -25,8 +25,8 @@ model LiteLLM_BudgetTable {
organization LiteLLM_OrganizationTable[] // multiple orgs can have the same budget
keys LiteLLM_VerificationToken[] // multiple keys can have the same budget
end_users LiteLLM_EndUserTable[] // multiple end-users can have the same budget
team_membership LiteLLM_TeamMembership[] // budgets of Users within a Team
organization_membership LiteLLM_OrganizationMembership[] // budgets of Users within a Organization
team_membership LiteLLM_TeamMembership[] // budgets of Users within a Team
organization_membership LiteLLM_OrganizationMembership[] // budgets of Users within a Organization
}
// Models on proxy
@@ -34,7 +34,7 @@ model LiteLLM_CredentialsTable {
credential_id String @id @default(uuid())
credential_name String @unique
credential_values Json
credential_info Json?
credential_info Json?
created_at DateTime @default(now()) @map("created_at")
created_by String
updated_at DateTime @default(now()) @updatedAt @map("updated_at")
@@ -44,9 +44,9 @@ model LiteLLM_CredentialsTable {
// Models on proxy
model LiteLLM_ProxyModelTable {
model_id String @id @default(uuid())
model_name String
model_name String
litellm_params Json
model_info Json?
model_info Json?
created_at DateTime @default(now()) @map("created_at")
created_by String
updated_at DateTime @default(now()) @updatedAt @map("updated_at")
@@ -67,7 +67,7 @@ model LiteLLM_OrganizationTable {
updated_at DateTime @default(now()) @updatedAt @map("updated_at")
updated_by String
litellm_budget_table LiteLLM_BudgetTable? @relation(fields: [budget_id], references: [budget_id])
teams LiteLLM_TeamTable[]
teams LiteLLM_TeamTable[]
users LiteLLM_UserTable[]
keys LiteLLM_VerificationToken[]
members LiteLLM_OrganizationMembership[] @relation("OrganizationToMembership")
@@ -86,10 +86,10 @@ model LiteLLM_ModelTable {
}
// Assign prod keys to groups, not individuals
// Assign prod keys to groups, not individuals
model LiteLLM_TeamTable {
team_id String @id @default(uuid())
team_alias String?
team_alias String?
organization_id String?
object_permission_id String?
admins String[]
@@ -102,7 +102,7 @@ model LiteLLM_TeamTable {
max_parallel_requests Int?
tpm_limit BigInt?
rpm_limit BigInt?
budget_duration String?
budget_duration String?
budget_reset_at DateTime?
blocked Boolean @default(false)
created_at DateTime @default(now()) @map("created_at")
@@ -119,7 +119,7 @@ model LiteLLM_TeamTable {
// Track spend, rate limit, budget Users
model LiteLLM_UserTable {
user_id String @id
user_alias String?
user_alias String?
team_id String?
sso_user_id String? @unique
organization_id String?
@@ -135,7 +135,7 @@ model LiteLLM_UserTable {
max_parallel_requests Int?
tpm_limit BigInt?
rpm_limit BigInt?
budget_duration String?
budget_duration String?
budget_reset_at DateTime?
allowed_cache_controls String[] @default([])
model_spend Json @default("{}")
@@ -162,7 +162,7 @@ model LiteLLM_ObjectPermissionTable {
users LiteLLM_UserTable[]
}
// Holds the MCP server configuration
// Holds the MCP server configuration
model LiteLLM_MCPServerTable {
server_id String @id @default(uuid())
alias String?
@@ -170,7 +170,7 @@ model LiteLLM_MCPServerTable {
url String
transport String @default("sse")
spec_version String @default("2025-03-26")
auth_type String?
auth_type String?
created_at DateTime? @default(now()) @map("created_at")
created_by String?
updated_at DateTime? @default(now()) @updatedAt @map("updated_at")
@@ -196,8 +196,8 @@ model LiteLLM_VerificationToken {
blocked Boolean?
tpm_limit BigInt?
rpm_limit BigInt?
max_budget Float?
budget_duration String?
max_budget Float?
budget_duration String?
budget_reset_at DateTime?
allowed_cache_controls String[] @default([])
allowed_routes String[] @default([])
@@ -254,7 +254,7 @@ model LiteLLM_SpendLogs {
cache_hit String? @default("")
cache_key String? @default("")
request_tags Json? @default("[]")
team_id String?
team_id String?
end_user String?
requester_ip_address String?
messages Json? @default("{}")
@@ -272,7 +272,7 @@ model LiteLLM_ErrorLogs {
request_id String @id @default(uuid())
startTime DateTime // Assuming start_time is a DateTime field
endTime DateTime // Assuming end_time is a DateTime field
api_base String @default("")
api_base String @default("")
model_group String @default("") // public model_name / model_group
litellm_model_name String @default("") // model passed to litellm
model_id String @default("") // ID of model in ProxyModelTable
@@ -285,7 +285,7 @@ model LiteLLM_ErrorLogs {
// Beta - allow team members to request access to a model
model LiteLLM_UserNotifications {
request_id String @id
user_id String
user_id String
models String[]
justification String
status String // approved, disapproved, pending
@@ -297,7 +297,7 @@ model LiteLLM_TeamMembership {
team_id String
spend Float @default(0.0)
budget_id String?
litellm_budget_table LiteLLM_BudgetTable? @relation(fields: [budget_id], references: [budget_id], onDelete: Cascade, onUpdate: Cascade)
litellm_budget_table LiteLLM_BudgetTable? @relation(fields: [budget_id], references: [budget_id])
@@id([user_id, team_id])
}
@@ -315,8 +315,8 @@ model LiteLLM_OrganizationMembership {
user LiteLLM_UserTable @relation(fields: [user_id], references: [user_id])
organization LiteLLM_OrganizationTable @relation("OrganizationToMembership", fields: [organization_id], references: [organization_id])
litellm_budget_table LiteLLM_BudgetTable? @relation(fields: [budget_id], references: [budget_id])
@@id([user_id, organization_id])
@@unique([user_id, organization_id])
@@ -349,19 +349,19 @@ model LiteLLM_AuditLog {
action String // create, update, delete
table_name String // on of LitellmTableNames.TEAM_TABLE_NAME, LitellmTableNames.USER_TABLE_NAME, LitellmTableNames.PROXY_MODEL_TABLE_NAME,
object_id String // id of the object being audited. This can be the key id, team id, user id, model id
before_value Json? // value of the row
before_value Json? // value of the row
updated_values Json? // value of the row after change
}
// Track daily user spend metrics per model and key
model LiteLLM_DailyUserSpend {
id String @id @default(uuid())
user_id String?
user_id String?
date String
api_key String
model String
model_group String?
custom_llm_provider String?
api_key String
model String
model_group String?
custom_llm_provider String?
prompt_tokens BigInt @default(0)
completion_tokens BigInt @default(0)
cache_read_input_tokens BigInt @default(0)
@@ -385,10 +385,10 @@ model LiteLLM_DailyTeamSpend {
id String @id @default(uuid())
team_id String?
date String
api_key String
model String
model_group String?
custom_llm_provider String?
api_key String
model String
model_group String?
custom_llm_provider String?
prompt_tokens BigInt @default(0)
completion_tokens BigInt @default(0)
cache_read_input_tokens BigInt @default(0)
@@ -410,12 +410,12 @@ model LiteLLM_DailyTeamSpend {
// Track daily team spend metrics per model and key
model LiteLLM_DailyTagSpend {
id String @id @default(uuid())
tag String?
tag String?
date String
api_key String
model String
model_group String?
custom_llm_provider String?
api_key String
model String
model_group String?
custom_llm_provider String?
prompt_tokens BigInt @default(0)
completion_tokens BigInt @default(0)
cache_read_input_tokens BigInt @default(0)
@@ -452,27 +452,28 @@ enum JobStatus {
model LiteLLM_ManagedFileTable {
id String @id @default(uuid())
unified_file_id String @unique // The base64 encoded unified file ID
file_object Json // Stores the OpenAIFileObject
file_object Json? // Stores the OpenAIFileObject
model_mappings Json
flat_model_file_ids String[] @default([]) // Flat list of model file id's - for faster querying of model id -> unified file id
created_at DateTime @default(now())
created_by String?
created_by String?
updated_at DateTime @updatedAt
updated_by String?
@@index([unified_file_id])
}
model LiteLLM_ManagedObjectTable { // for batches or finetuning jobs which use the
model LiteLLM_ManagedObjectTable { // for batches or finetuning jobs which use the
id String @id @default(uuid())
unified_object_id String @unique // The base64 encoded unified file ID
model_object_id String @unique // the id returned by the backend API provider
model_object_id String @unique // the id returned by the backend API provider
file_object Json // Stores the OpenAIFileObject
file_purpose String // either 'batch' or 'fine-tune'
status String? // check if batch cost has been tracked
created_at DateTime @default(now())
created_by String?
updated_at DateTime @updatedAt
updated_by String?
updated_by String?
@@index([unified_object_id])
@@index([model_object_id])
@@ -157,7 +157,7 @@ async def test_batch_cost_calculator(sample_file_content_dict):
so we expect the cost to be 0.5 * 2 = 1.0
"""
with patch("litellm.completion_cost", return_value=0.5):
cost = await _batch_cost_calculator(
cost = _batch_cost_calculator(
file_content_dictionary=sample_file_content_dict,
custom_llm_provider="openai",
)
@@ -1,18 +1,13 @@
import json
import os
import sys
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from fastapi import HTTPException
from fastapi.testclient import TestClient
from litellm_enterprise.proxy.hooks.managed_files import _PROXY_LiteLLMManagedFiles
sys.path.insert(
0, os.path.abspath("../../..")
) # Adds the parent directory to the system path
from unittest.mock import AsyncMock, MagicMock, patch
from enterprise.enterprise_hooks.managed_files import _PROXY_LiteLLMManagedFiles
from litellm.caching import DualCache
from litellm.proxy.openai_files_endpoints.common_utils import (
_is_base64_encoded_unified_file_id,
@@ -269,3 +264,115 @@ async def test_can_user_call_unified_file_id(call_type):
data={"file_id": unified_file_id},
call_type=call_type,
)
@pytest.mark.asyncio
async def test_router_acreate_batch_only_selects_from_file_id_mapping(monkeypatch):
"""
Test that router.acreate_batch only selects model_id from the file_id_mapping
"""
import litellm
prisma_client = AsyncMock()
return_value = MagicMock()
return_value.created_by = "123"
prisma_client.db.litellm_managedobjecttable.find_first.return_value = return_value
proxy_managed_files = _PROXY_LiteLLMManagedFiles(
DualCache(), prisma_client=prisma_client
)
monkeypatch.setattr(
litellm,
"callbacks",
[proxy_managed_files],
)
router = litellm.Router(
model_list=[
{
"model_name": "gpt-3.5-turbo",
"litellm_params": {"model": "gpt-3.5-turbo"},
"model_info": {"id": "1234"},
},
{
"model_name": "gpt-3.5-turbo",
"litellm_params": {"model": "gpt-3.5-turbo"},
"model_info": {"id": "5678"},
},
],
)
file_id = "bGl0ZWxsbV9wcm94eTphcHBsaWNhdGlvbi9vY3RldC1zdHJlYW07dW5pZmllZF9pZCw2YmQ4ZjhhYS02NmEzLTRmY2MtOTIxZS1lMTYwYzIzZWZjNzU7dGFyZ2V0X21vZGVsX25hbWVzLGdwdC00bztsbG1fb3V0cHV0X2ZpbGVfaWQsZmlsZS1MTENVRkI1MnVUTWE5aE5ZanRldzlWO2xsbV9vdXRwdXRfZmlsZV9tb2RlbF9pZCxmMzJlNWQ0OC05YWZmLTQ5YjMtOWE1Ny0zYzJhN2JjN2NjMmE"
model_file_id_mapping = {file_id: {"5678": "file-LLCUFB52uTMa9hNYjtew9V"}}
with patch.object(
litellm, "acreate_batch", return_value=AsyncMock()
) as mock_acreate_batch:
for _ in range(1000):
response = await router.acreate_batch(
model="gpt-3.5-turbo",
input_file_id=file_id,
model_file_id_mapping=model_file_id_mapping,
)
mock_acreate_batch.assert_called()
assert "5678" in json.dumps(mock_acreate_batch.call_args.kwargs)
@pytest.mark.asyncio
async def test_output_file_id_for_batch_retrieve():
"""
Test that the output file id is the same as the input file id
"""
from typing import cast
from openai.types.batch import BatchRequestCounts
from litellm.proxy._types import UserAPIKeyAuth
from litellm.types.utils import LiteLLMBatch
batch = LiteLLMBatch(
id="bGl0ZWxsbV9wcm94eTttb2RlbF9pZDoxMjM0NTY3OTtsbG1fYmF0Y2hfaWQ6YmF0Y2hfNjg1YzVlNWQ2Mzk4ODE5MGI4NWJkYjIxNDdiYTEzMWQ",
completion_window="24h",
created_at=1750883933,
endpoint="/v1/chat/completions",
input_file_id="file-8ci8gux8s7oES7GydYvnMG",
object="batch",
status="completed",
cancelled_at=None,
cancelling_at=None,
completed_at=1750883939,
error_file_id=None,
errors=None,
expired_at=None,
expires_at=1750970333,
failed_at=None,
finalizing_at=1750883938,
in_progress_at=1750883934,
metadata={"description": "nightly eval job"},
output_file_id="file-3BZYhmdJQ3V2oZPAtQsEax",
request_counts=BatchRequestCounts(completed=1, failed=0, total=1),
usage=None,
)
batch._hidden_params = {
"litellm_call_id": "dcd789e0-c0ad-4244-9564-4e611448d650",
"api_base": "https://api.openai.com",
"model_id": "12345679",
"response_cost": 0.0,
"additional_headers": {},
"litellm_model_name": "gpt-4o",
"unified_batch_id": "litellm_proxy;model_id:12345679;llm_batch_id:batch_685c5e5d63988190b85bdb2147ba131d",
}
proxy_managed_files = _PROXY_LiteLLMManagedFiles(
DualCache(), prisma_client=AsyncMock()
)
response = await proxy_managed_files.async_post_call_success_hook(
data={},
user_api_key_dict=MagicMock(),
response=batch,
)
assert not cast(LiteLLMBatch, response).output_file_id.startswith("file-")
@@ -427,6 +427,8 @@ def test_select_azure_base_url_called(setup_mocks):
"afile_list",
"aimage_edit",
"image_edit",
"agenerate_content_stream",
"agenerate_content",
]
],
)
@@ -123,6 +123,34 @@ async def test_ssl_verification_with_aiohttp_transport():
assert transport_connector._ssl == aiohttp_session.connector._ssl
@pytest.mark.asyncio
async def test_aiohttp_transport_trust_env_setting(monkeypatch):
"""Test that trust_env setting is properly configured in aiohttp transport"""
# Test 1: Default trust_env behavior
transport = AsyncHTTPHandler._create_aiohttp_transport()
client_session = transport._get_valid_client_session()
# Default should be False (litellm.aiohttp_trust_env default)
default_trust_env = getattr(litellm, 'aiohttp_trust_env', False)
assert client_session._trust_env == default_trust_env
# Test 2: Environment variable override
monkeypatch.setenv("AIOHTTP_TRUST_ENV", "True")
transport_with_env = AsyncHTTPHandler._create_aiohttp_transport()
client_session_with_env = transport_with_env._get_valid_client_session()
# Should be True when environment variable is set
assert client_session_with_env._trust_env is True
# Test 3: Verify environment variable with False value
monkeypatch.setenv("AIOHTTP_TRUST_ENV", "False")
transport_with_false_env = AsyncHTTPHandler._create_aiohttp_transport()
client_session_with_false_env = transport_with_false_env._get_valid_client_session()
# Should respect the litellm.aiohttp_trust_env setting when env var is False
assert client_session_with_false_env._trust_env == default_trust_env
def test_get_ssl_context():
"""Test that _get_ssl_context() returns a proper SSL context with certifi CA bundle"""
with patch('ssl.create_default_context') as mock_create_context:
@@ -8,7 +8,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from litellm.proxy.anthropic_endpoints.endpoints import async_data_generator_anthropic
from litellm.proxy.common_request_processing import ProxyBaseLLMRequestProcessing
class TestAnthropicEndpoints(unittest.TestCase):
@@ -38,7 +38,7 @@ class TestAnthropicEndpoints(unittest.TestCase):
# Execute
result = [
chunk
async for chunk in async_data_generator_anthropic(
async for chunk in ProxyBaseLLMRequestProcessing.async_sse_data_generator(
response=mock_response,
user_api_key_dict=mock_user_api_key_dict,
request_data=mock_request_data,
@@ -66,7 +66,3 @@ class TestAnthropicEndpoints(unittest.TestCase):
assert (
mock_safe_dumps.call_count == 2
) # Called twice, once for each dict object
if __name__ == "__main__":
unittest.main()
@@ -103,10 +103,12 @@ def test_mock_create_audio_file(mocker: MockerFixture, monkeypatch, llm_router:
"""
Asserts 'create_file' is called with the correct arguments
"""
import litellm
from litellm import Router
from litellm.proxy.utils import ProxyLogging
mock_create_file = mocker.patch("litellm.files.main.create_file")
# Mock create_file as an async function
mock_create_file = mocker.patch("litellm.files.main.create_file", new=mocker.AsyncMock())
proxy_logging_obj = ProxyLogging(
user_api_key_cache=DualCache(default_in_memory_ttl=1)
@@ -114,6 +116,61 @@ def test_mock_create_audio_file(mocker: MockerFixture, monkeypatch, llm_router:
proxy_logging_obj._add_proxy_hooks(llm_router)
# Add managed_files hook to ensure the test reaches the mocked function
from litellm.llms.base_llm.files.transformation import BaseFileEndpoints
class DummyManagedFiles(BaseFileEndpoints):
async def acreate_file(self, llm_router, create_file_request, target_model_names_list, litellm_parent_otel_span, user_api_key_dict):
# Handle both dict and object forms of create_file_request
if isinstance(create_file_request, dict):
file_data = create_file_request.get("file")
purpose_data = create_file_request.get("purpose")
else:
file_data = create_file_request.file
purpose_data = create_file_request.purpose
# Call the mocked litellm.files.main.create_file to ensure asserts work
await litellm.files.main.create_file(
custom_llm_provider="azure",
model="azure/chatgpt-v-2",
api_key="azure_api_key",
file=file_data,
purpose=purpose_data,
)
await litellm.files.main.create_file(
custom_llm_provider="openai",
model="openai/gpt-3.5-turbo",
api_key="openai_api_key",
file=file_data,
purpose=purpose_data,
)
# Return a dummy response object as needed by the test
from litellm.types.llms.openai import OpenAIFileObject
return OpenAIFileObject(
id="dummy-id",
object="file",
bytes=len(file_data[1]) if file_data else 0,
created_at=1234567890,
filename=file_data[0] if file_data else "test.wav",
purpose=purpose_data,
status="uploaded",
)
async def afile_retrieve(self, file_id, litellm_parent_otel_span):
raise NotImplementedError("Not implemented for test")
async def afile_list(self, purpose, litellm_parent_otel_span):
raise NotImplementedError("Not implemented for test")
async def afile_delete(self, file_id, litellm_parent_otel_span):
raise NotImplementedError("Not implemented for test")
async def afile_content(self, file_id, litellm_parent_otel_span):
raise NotImplementedError("Not implemented for test")
# Manually add the hook to the proxy_hook_mapping
proxy_logging_obj.proxy_hook_mapping["managed_files"] = DummyManagedFiles()
monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", llm_router)
monkeypatch.setattr(
"litellm.proxy.proxy_server.proxy_logging_obj", proxy_logging_obj
@@ -133,8 +190,7 @@ def test_mock_create_audio_file(mocker: MockerFixture, monkeypatch, llm_router:
headers={"Authorization": "Bearer test-key"},
)
print(f"response: {response.text}")
# assert response.status_code == 200
assert response.status_code == 200
# Get all calls made to create_file
calls = mock_create_file.call_args_list
@@ -458,3 +458,84 @@ def test_add_team_models_to_all_models():
llm_router=llm_router,
)
assert result == {"gpt-4-model-2": {"team1"}}
@pytest.mark.asyncio
async def test_delete_deployment_type_mismatch():
"""
Test that the _delete_deployment function handles type mismatches correctly.
Specifically test that models 12345678 and 12345679 are NOT deleted when
they exist in both combined_id_list (as integers) and router_model_ids (as strings).
This test reproduces the bug where type mismatch causes valid models to be deleted.
"""
from unittest.mock import MagicMock, patch
from litellm.proxy.proxy_server import ProxyConfig
# Create mock ProxyConfig instance
pc = ProxyConfig()
pc.get_config = MagicMock(
return_value={
"model_list": [
{
"model_name": "openai-gpt-4o",
"litellm_params": {"model": "gpt-4o"},
"model_info": {"id": 12345678},
},
{
"model_name": "openai-gpt-4o",
"litellm_params": {"model": "gpt-4o"},
"model_info": {"id": 12345679},
},
]
}
)
# Mock llm_router with string IDs (this is the source of the type mismatch)
mock_llm_router = MagicMock()
mock_llm_router.get_model_ids.return_value = [
"a96e12e76b36a57cfae57a41288eb41567629cac89b4828c6f7074afc3534695",
"a40186dd0fdb9b7282380277d7f57044d29de95bfbfcd7f4322b3493702d5cd3",
"12345678", # String ID
"12345679", # String ID
]
# Track which deployments were deleted
deleted_ids = []
def mock_delete_deployment(id):
deleted_ids.append(id)
return True # Simulate successful deletion
mock_llm_router.delete_deployment = MagicMock(side_effect=mock_delete_deployment)
# Mock get_config to return empty config (no config models)
async def mock_get_config(config_file_path):
return {}
pc.get_config = MagicMock(side_effect=mock_get_config)
# Patch the global llm_router
with patch("litellm.proxy.proxy_server.llm_router", mock_llm_router), patch(
"litellm.proxy.proxy_server.user_config_file_path", "test_config.yaml"
):
# Call the function under test
deleted_count = await pc._delete_deployment(db_models=[])
# Assertions: Models 12345678 and 12345679 should NOT be deleted
# because they exist in combined_id_list (as integers) even though
# router has them as strings
# The function should delete the other 2 models that are not in combined_id_list
assert deleted_count == 0, f"Expected 0 deletions, got {deleted_count}"
# Verify that 12345678 and 12345679 were NOT deleted
assert (
"12345678" not in deleted_ids
), f"Model 12345678 should NOT be deleted. Deleted IDs: {deleted_ids}"
assert (
"12345679" not in deleted_ids
), f"Model 12345679 should NOT be deleted. Deleted IDs: {deleted_ids}"
@@ -0,0 +1,189 @@
from typing import Dict, List, Optional, Union
from unittest.mock import Mock
import pytest
from litellm.router_utils.common_utils import filter_team_based_models
class TestFilterTeamBasedModels:
"""Test cases for filter_team_based_models function"""
@pytest.fixture
def sample_deployments_with_teams(self) -> List[Dict]:
"""Sample deployments where some have team_id and some don't"""
return [
{"model_info": {"id": "deployment-1", "team_id": "team-a"}},
{"model_info": {"id": "deployment-2", "team_id": "team-b"}},
{
"model_info": {
"id": "deployment-3"
# No team_id - should always be included
}
},
{"model_info": {"id": "deployment-4", "team_id": "team-a"}},
]
@pytest.fixture
def sample_deployments_no_teams(self) -> List[Dict]:
"""Sample deployments with no team_id restrictions"""
return [
{"model_info": {"id": "deployment-1"}},
{"model_info": {"id": "deployment-2"}},
]
def test_filter_team_based_models_none_request_kwargs(
self, sample_deployments_with_teams
):
"""Test that when request_kwargs is None, all deployments are returned unchanged"""
result = filter_team_based_models(sample_deployments_with_teams, None)
assert result == sample_deployments_with_teams
def test_filter_team_based_models_empty_request_kwargs(
self, sample_deployments_with_teams
):
"""Test with empty request_kwargs"""
result = filter_team_based_models(sample_deployments_with_teams, {})
# Should include all deployments since no team_id in request
assert len(result) == 1
def test_filter_team_based_models_no_metadata(self, sample_deployments_with_teams):
"""Test with request_kwargs that has no metadata"""
request_kwargs = {"some_other_key": "value"}
result = filter_team_based_models(sample_deployments_with_teams, request_kwargs)
# Should include only non-team based deployments
assert len(result) == 1
def test_filter_team_based_models_team_match_metadata(
self, sample_deployments_with_teams
):
"""Test filtering when team_id is in metadata"""
request_kwargs = {"metadata": {"user_api_key_team_id": "team-a"}}
result = filter_team_based_models(sample_deployments_with_teams, request_kwargs)
# Should include:
# - deployment-1 (team-a matches)
# - deployment-3 (no team_id restriction)
# - deployment-4 (team-a matches)
# Should exclude:
# - deployment-2 (team-b doesn't match)
expected_ids = ["deployment-1", "deployment-3", "deployment-4"]
result_ids = [d.get("model_info", {}).get("id") for d in result]
assert sorted(result_ids) == sorted(expected_ids)
def test_filter_team_based_models_team_match_litellm_metadata(
self, sample_deployments_with_teams
):
"""Test filtering when team_id is in litellm_metadata"""
request_kwargs = {"litellm_metadata": {"user_api_key_team_id": "team-b"}}
result = filter_team_based_models(sample_deployments_with_teams, request_kwargs)
# Should include:
# - deployment-2 (team-b matches)
# - deployment-3 (no team_id restriction)
# Should exclude:
# - deployment-1 (team-a doesn't match)
# - deployment-4 (team-a doesn't match)
expected_ids = ["deployment-2", "deployment-3"]
result_ids = [d.get("model_info", {}).get("id") for d in result]
assert sorted(result_ids) == sorted(expected_ids)
def test_filter_team_based_models_priority_metadata_over_litellm(
self, sample_deployments_with_teams
):
"""Test that metadata.user_api_key_team_id takes priority over litellm_metadata.user_api_key_team_id"""
request_kwargs = {
"metadata": {
"user_api_key_team_id": "team-a", # This should take priority
"litellm_metadata": {"user_api_key_team_id": "team-b"},
}
}
result = filter_team_based_models(sample_deployments_with_teams, request_kwargs)
# Should filter based on team-a (from metadata, not litellm_metadata)
expected_ids = ["deployment-1", "deployment-3", "deployment-4"]
result_ids = [d.get("model_info", {}).get("id") for d in result]
assert sorted(result_ids) == sorted(expected_ids)
def test_filter_team_based_models_no_matching_team(
self, sample_deployments_with_teams
):
"""Test when request team doesn't match any deployment teams"""
request_kwargs = {"metadata": {"user_api_key_team_id": "team-nonexistent"}}
result = filter_team_based_models(sample_deployments_with_teams, request_kwargs)
# Should only include deployment-3 (no team_id restriction)
expected_ids = ["deployment-3"]
result_ids = [d.get("model_info", {}).get("id") for d in result]
assert result_ids == expected_ids
def test_filter_team_based_models_no_team_restrictions(
self, sample_deployments_no_teams
):
"""Test with deployments that have no team restrictions"""
request_kwargs = {"metadata": {"user_api_key_team_id": "any-team"}}
result = filter_team_based_models(sample_deployments_no_teams, request_kwargs)
# Should include all deployments since none have team_id restrictions
assert result == sample_deployments_no_teams
def test_filter_team_based_models_missing_model_info(self):
"""Test with deployments missing model_info"""
deployments = [
{"model_info": {"id": "deployment-1", "team_id": "team-a"}},
{
# Missing model_info entirely
},
{
"model_info": {
# Missing id
"team_id": "team-b"
}
},
]
request_kwargs = {"metadata": {"user_api_key_team_id": "team-a"}}
result = filter_team_based_models(deployments, request_kwargs)
# Should handle missing model_info gracefully
# deployment-1 should be included (team matches)
# others should be included since they don't have proper team_id setup
assert len(result) >= 1 # At least deployment-1 should be included
def test_filter_team_based_models_dict_input(self):
"""Test with Dict input instead of List[Dict]"""
# Note: Based on the function signature, it accepts Union[List[Dict], Dict]
# But the implementation seems to expect List[Dict] for the filtering logic
# This test documents the current behavior
deployments_dict = {"key1": "value1", "key2": "value2"}
request_kwargs = {"metadata": {"user_api_key_team_id": "team-a"}}
# This should not crash, though the filtering logic won't apply to Dict input
result = filter_team_based_models(deployments_dict, request_kwargs)
# The function will likely return the dict unchanged or handle it differently
assert result is not None
def test_filter_team_based_models_empty_deployments(self):
"""Test with empty deployments list"""
result = filter_team_based_models(
[], {"metadata": {"user_api_key_team_id": "team-a"}}
)
assert result == []
def test_filter_team_based_models_none_team_id_in_deployment(self):
"""Test with explicit None team_id in deployment"""
deployments = [
{"model_info": {"id": "deployment-1", "team_id": None}},
{"model_info": {"id": "deployment-2", "team_id": "team-a"}},
]
request_kwargs = {"metadata": {"user_api_key_team_id": "team-a"}}
result = filter_team_based_models(deployments, request_kwargs)
# Both should be included:
# - deployment-1 (None team_id is treated as no restriction)
# - deployment-2 (team matches)
expected_ids = ["deployment-1", "deployment-2"]
result_ids = [d.get("model_info", {}).get("id") for d in result]
assert sorted(result_ids) == sorted(expected_ids)
+97
View File
@@ -384,3 +384,100 @@ async def test_router_aretrieve_batch():
print(mock_aretrieve_batch.call_args.kwargs)
assert mock_aretrieve_batch.call_args.kwargs["api_key"] == "my-custom-key"
assert mock_aretrieve_batch.call_args.kwargs["api_base"] == "my-custom-base"
@pytest.mark.asyncio
async def test_router_aretrieve_file_content():
"""
Test that router.acreate_file with JSONL file returns the correct response
"""
with patch.object(
litellm, "afile_content", return_value=AsyncMock()
) as mock_afile_content:
router = litellm.Router(
model_list=[
{
"model_name": "gpt-3.5-turbo",
"litellm_params": {
"model": "gpt-3.5-turbo",
"custom_llm_provider": "azure",
"api_key": "my-custom-key",
"api_base": "my-custom-base",
},
}
],
)
try:
response = await router.afile_content(
**{
"model": "gpt-3.5-turbo",
"file_id": "my-unique-file-id",
}
) # type: ignore
except Exception as e:
print(f"Error: {e}")
mock_afile_content.assert_called_once()
print(mock_afile_content.call_args.kwargs)
assert mock_afile_content.call_args.kwargs["api_key"] == "my-custom-key"
assert mock_afile_content.call_args.kwargs["api_base"] == "my-custom-base"
@pytest.mark.asyncio
async def test_router_filter_team_based_models():
"""
Test that router.filter_team_based_models filters out models that are not in the team
"""
from litellm.types.router import Deployment
router = litellm.Router(
model_list=[
{
"model_name": "gpt-3.5-turbo",
"litellm_params": {"model": "gpt-3.5-turbo"},
"model_info": {
"team_id": "test-team",
},
},
],
)
# WORKS
result = await router.acompletion(
model="gpt-3.5-turbo",
messages=[{"role": "user", "content": "Hello, world!"}],
metadata={"user_api_key_team_id": "test-team"},
mock_response="Hello, world!",
)
assert result is not None
# FAILS
with pytest.raises(Exception) as e:
result = await router.acompletion(
model="gpt-3.5-turbo",
messages=[{"role": "user", "content": "Hello, world!"}],
metadata={"user_api_key_team_id": "test-team-2"},
mock_response="Hello, world!",
)
assert "No deployments available" in str(e.value)
## ADD A MODEL THAT IS NOT IN THE TEAM
router.add_deployment(
Deployment(
model_name="gpt-3.5-turbo",
litellm_params={"model": "gpt-3.5-turbo"},
model_info={"tpm": 1000, "rpm": 1000},
)
)
result = await router.acompletion(
model="gpt-3.5-turbo",
messages=[{"role": "user", "content": "Hello, world!"}],
metadata={"user_api_key_team_id": "test-team-2"},
mock_response="Hello, world!",
)
assert result is not None
@@ -0,0 +1,255 @@
import asyncio
import json
import sys
import os
from typing import Any, AsyncIterator, Dict, List, Optional, Union
import pytest
sys.path.insert(
0, os.path.abspath("../../..")
) # Adds the parent directory to the system path
import litellm
from litellm.google_genai import (
generate_content,
agenerate_content,
generate_content_stream,
agenerate_content_stream,
)
from google.genai.types import ContentDict, PartDict, GenerateContentResponse
from litellm.integrations.custom_logger import CustomLogger
from litellm.types.utils import StandardLoggingPayload
class TestCustomLogger(CustomLogger):
def __init__(
self,
):
self.standard_logging_object: Optional[StandardLoggingPayload] = None
async def async_log_success_event(self, kwargs, response_obj, start_time, end_time):
print("in async_log_success_event")
print("kwargs=", json.dumps(kwargs, indent=4, default=str))
self.standard_logging_object = kwargs["standard_logging_object"]
pass
class BaseGoogleGenAITest:
"""Base class for Google GenAI generate content tests to reduce code duplication"""
@property
def model_config(self) -> Dict[str, Any]:
"""Override in subclasses to provide model-specific configuration"""
raise NotImplementedError("Subclasses must implement model_config")
def _validate_non_streaming_response(self, response: Any):
"""Validate non-streaming response structure"""
# Handle type checking - response should be a dict for non-streaming
if isinstance(response, AsyncIterator):
pytest.fail("Expected non-streaming response but got AsyncIterator")
assert isinstance(response, GenerateContentResponse), f"Expected dict response, got {type(response)}"
print(f"Response: {response.model_dump_json(indent=4)}")
# Basic validation - adjust based on actual Google GenAI response structure
# The exact structure may vary, so we'll be flexible here
assert response is not None, "Response should not be None"
def _validate_streaming_response(self, chunks: List[Any]):
"""Validate streaming response chunks"""
assert isinstance(chunks, list), f"Expected list of chunks, got {type(chunks)}"
assert len(chunks) >= 0, "Should have at least 0 chunks"
print(f"Total chunks received: {len(chunks)}")
def _validate_standard_logging_payload(
self, slp: StandardLoggingPayload, response: Any
):
"""
Validate that a StandardLoggingPayload object matches the expected response for Google GenAI
Args:
slp (StandardLoggingPayload): The standard logging payload object to validate
response: The Google GenAI response to compare against
"""
# Validate payload exists
assert slp is not None, "Standard logging payload should not be None"
# Validate basic structure
assert "prompt_tokens" in slp, "Standard logging payload should have prompt_tokens"
assert "completion_tokens" in slp, "Standard logging payload should have completion_tokens"
assert "total_tokens" in slp, "Standard logging payload should have total_tokens"
assert "response_cost" in slp, "Standard logging payload should have response_cost"
# Validate token counts are reasonable (non-negative numbers)
assert slp["prompt_tokens"] >= 0, "Prompt tokens should be non-negative"
assert slp["completion_tokens"] >= 0, "Completion tokens should be non-negative"
assert slp["total_tokens"] >= 0, "Total tokens should be non-negative"
# Validate spend
assert slp["response_cost"] >= 0, "Response cost should be non-negative"
print(f"Standard logging payload validation passed: prompt_tokens={slp['prompt_tokens']}, completion_tokens={slp['completion_tokens']}, total_tokens={slp['total_tokens']}, cost={slp['response_cost']}")
@pytest.mark.parametrize("is_async", [False, True])
@pytest.mark.asyncio
async def test_non_streaming_base(self, is_async: bool):
"""Base test for non-streaming requests (parametrized for sync/async)"""
request_params = self.model_config
contents = ContentDict(
parts=[
PartDict(
text="Hello, can you tell me a short joke?"
)
],
role="user",
)
litellm._turn_on_debug()
print(f"Testing {'async' if is_async else 'sync'} non-streaming with model config: {request_params}")
print(f"Contents: {contents}")
if is_async:
print("\n--- Testing async agenerate_content ---")
response = await agenerate_content(
contents=contents,
**request_params
)
else:
print("\n--- Testing sync generate_content ---")
response = generate_content(
contents=contents,
**request_params
)
print(f"{'Async' if is_async else 'Sync'} response: {json.dumps(response, indent=2, default=str)}")
self._validate_non_streaming_response(response)
return response
@pytest.mark.parametrize("is_async", [False, True])
@pytest.mark.asyncio
async def test_streaming_base(self, is_async: bool):
"""Base test for streaming requests (parametrized for sync/async)"""
request_params = self.model_config
contents = ContentDict(
parts=[
PartDict(
text="Hello, can you tell me a short joke?"
)
],
role="user",
)
print(f"Testing {'async' if is_async else 'sync'} streaming with model config: {request_params}")
print(f"Contents: {contents}")
chunks = []
if is_async:
print("\n--- Testing async agenerate_content_stream ---")
response = await agenerate_content_stream(
contents=contents,
**request_params
)
async for chunk in response:
print(f"Async chunk: {chunk}")
chunks.append(chunk)
else:
print("\n--- Testing sync generate_content_stream ---")
response = generate_content_stream(
contents=contents,
**request_params
)
for chunk in response:
print(f"Sync chunk: {chunk}")
chunks.append(chunk)
self._validate_streaming_response(chunks)
return chunks
@pytest.mark.asyncio
async def test_async_non_streaming_with_logging(self):
"""Test async non-streaming Google GenAI generate content with logging"""
litellm._turn_on_debug()
litellm.logging_callback_manager._reset_all_callbacks()
litellm.set_verbose = True
test_custom_logger = TestCustomLogger()
litellm.callbacks = [test_custom_logger]
request_params = self.model_config
contents = ContentDict(
parts=[
PartDict(
text="Hello, can you tell me a short joke?"
)
],
role="user",
)
print("\n--- Testing async agenerate_content with logging ---")
response = await agenerate_content(
contents=contents,
**request_params
)
print("Google GenAI response=", json.dumps(response, indent=4, default=str))
print("sleeping for 5 seconds...")
await asyncio.sleep(5)
print(
"standard logging payload=",
json.dumps(test_custom_logger.standard_logging_object, indent=4, default=str),
)
assert response is not None
assert test_custom_logger.standard_logging_object is not None
self._validate_standard_logging_payload(
test_custom_logger.standard_logging_object, response
)
@pytest.mark.asyncio
async def test_async_streaming_with_logging(self):
"""Test async streaming Google GenAI generate content with logging"""
litellm._turn_on_debug()
litellm.set_verbose = True
litellm.logging_callback_manager._reset_all_callbacks()
test_custom_logger = TestCustomLogger()
litellm.callbacks = [test_custom_logger]
request_params = self.model_config
contents = ContentDict(
parts=[
PartDict(
text="Hello, can you tell me a short joke?"
)
],
role="user",
)
print("\n--- Testing async agenerate_content_stream with logging ---")
response = await agenerate_content_stream(
contents=contents,
**request_params
)
chunks = []
async for chunk in response:
print(f"Google GenAI chunk: {chunk}")
chunks.append(chunk)
print("sleeping for 5 seconds...")
await asyncio.sleep(5)
print(
"standard logging payload=",
json.dumps(test_custom_logger.standard_logging_object, indent=4, default=str),
)
assert len(chunks) >= 0
assert test_custom_logger.standard_logging_object is not None
self._validate_standard_logging_payload(
test_custom_logger.standard_logging_object, chunks
)
@@ -0,0 +1,10 @@
from base_google_test import BaseGoogleGenAITest
class TestGoogleGenAIStudio(BaseGoogleGenAITest):
"""Test Google GenAI Studio"""
@property
def model_config(self):
return {
"model": "gemini/gemini-1.5-flash",
}
@@ -0,0 +1,10 @@
from base_google_test import BaseGoogleGenAITest
class TestVertexAIGenerateContent(BaseGoogleGenAITest):
"""Test Vertex AI"""
@property
def model_config(self):
return {
"model": "vertex_ai/gemini-1.5-flash",
}
@@ -225,23 +225,31 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
}
// If only one page, just set the data
if (firstPageData.metadata.total_pages === 1) {
if (firstPageData.metadata.total_pages <= 1) {
setUserSpendData(firstPageData);
return;
}
// Fetch all pages
const allResults = [...firstPageData.results];
const aggregatedMetadata = { ...firstPageData.metadata };
for (let page = 2; page <= firstPageData.metadata.total_pages; page++) {
const pageData = await userDailyActivityCall(accessToken, startTime, endTime, page);
allResults.push(...pageData.results);
if (pageData.metadata) {
aggregatedMetadata.total_spend += pageData.metadata.total_spend || 0;
aggregatedMetadata.total_api_requests += pageData.metadata.total_api_requests || 0;
aggregatedMetadata.total_successful_requests += pageData.metadata.total_successful_requests || 0;
aggregatedMetadata.total_failed_requests += pageData.metadata.total_failed_requests || 0;
aggregatedMetadata.total_tokens += pageData.metadata.total_tokens || 0;
}
}
// Combine all results with the first page's metadata
setUserSpendData({
results: allResults,
metadata: firstPageData.metadata
metadata: aggregatedMetadata
});
} catch (error) {
console.error("Error fetching user spend data:", error);