mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-14 00:23:05 +00:00
Merge branch 'BerriAI:main' into fix-today-selector-date-mutation-bug
This commit is contained in:
@@ -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
|
||||
@@ -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`
|
||||
|
||||
@@ -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 |
@@ -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": [
|
||||
|
||||
@@ -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",
|
||||
{
|
||||
|
||||
@@ -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
|
||||
+97
-58
@@ -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)
|
||||
|
||||
+9
@@ -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;
|
||||
|
||||
+3
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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}")
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
+3
-21
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 ###
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
+2
-1
@@ -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,
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -73,6 +73,8 @@ async def route_request(
|
||||
"alist_input_items",
|
||||
"_arealtime", # private function for realtime API
|
||||
"aimage_edit",
|
||||
"agenerate_content",
|
||||
"agenerate_content_stream",
|
||||
],
|
||||
):
|
||||
"""
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
]
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
from .main import (
|
||||
ContentListUnion,
|
||||
ContentListUnionDict,
|
||||
GenerateContentConfigOrDict,
|
||||
GenerateContentResponse,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"ContentListUnion",
|
||||
"ContentListUnionDict",
|
||||
"GenerateContentConfigOrDict",
|
||||
"GenerateContentResponse",
|
||||
]
|
||||
@@ -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
|
||||
@@ -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
@@ -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(
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -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
@@ -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
@@ -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",
|
||||
)
|
||||
|
||||
+114
-7
@@ -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)
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user