Merge remote-tracking branch 'origin/main' into litellm_org_usage

This commit is contained in:
yuneng-jiang
2025-11-14 18:05:50 -08:00
114 changed files with 5755 additions and 3005 deletions
+24 -16
View File
@@ -563,8 +563,9 @@ jobs:
- run:
name: Install Docker CLI (In case it's not already installed)
command: |
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
docker version
- run:
name: Install Python 3.13
command: |
@@ -1770,8 +1771,9 @@ jobs:
- run:
name: Install Docker CLI (In case it's not already installed)
command: |
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
docker version
- run:
name: Install Python 3.9
command: |
@@ -1908,8 +1910,9 @@ jobs:
- run:
name: Install Docker CLI (In case it's not already installed)
command: |
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
docker version
- run:
name: Install Python 3.9
command: |
@@ -2050,8 +2053,9 @@ jobs:
- run:
name: Install Docker CLI (In case it's not already installed)
command: |
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
docker version
- run:
name: Install Python 3.9
command: |
@@ -2234,8 +2238,9 @@ jobs:
- run:
name: Install Docker CLI (In case it's not already installed)
command: |
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
docker version
- run:
name: Install Python 3.9
command: |
@@ -2342,8 +2347,9 @@ jobs:
- run:
name: Install Docker CLI (In case it's not already installed)
command: |
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
docker version
- run:
name: Install Python 3.9
command: |
@@ -2475,8 +2481,9 @@ jobs:
- run:
name: Install Docker CLI (In case it's not already installed)
command: |
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
docker version
sudo systemctl restart docker
- run:
name: Install Python 3.9
@@ -2684,8 +2691,9 @@ jobs:
- run:
name: Install Docker CLI (In case it's not already installed)
command: |
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
docker version
- run:
name: Install Python 3.9
command: |
@@ -0,0 +1,184 @@
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
# Adding a New Guardrail Integration
You're going to create a class that checks text before it goes to the LLM or after it comes back. If it violates your rules, you block it.
## How It Works
Request with guardrail:
```bash
curl --location 'http://localhost:4000/chat/completions' \
--header 'Authorization: Bearer sk-1234' \
--header 'Content-Type: application/json' \
--data '{
"model": "gpt-4",
"messages": [{"role": "user", "content": "How do I hack a system?"}],
"guardrails": ["my-guardrail"]
}'
```
Your guardrail checks input, then output. If something's wrong, raise an exception.
## Build Your Guardrail
### Create Your Directory
```bash
mkdir -p litellm/proxy/guardrails/guardrail_hooks/my_guardrail
cd litellm/proxy/guardrails/guardrail_hooks/my_guardrail
```
Two files: `my_guardrail.py` (main class) and `__init__.py` (initialization).
### Write the Main Class
`my_guardrail.py`:
```python
import os
from typing import Optional, List
from fastapi import HTTPException
from litellm.integrations.custom_guardrail import CustomGuardrail
from litellm.types.guardrails import PiiEntityType
from litellm._logging import verbose_proxy_logger
from litellm.llms.custom_httpx.http_handler import (
get_async_httpx_client,
httpxSpecialProvider,
)
class MyGuardrail(CustomGuardrail):
def __init__(self, api_key: Optional[str] = None, api_base: Optional[str] = None, **kwargs):
self.api_key = api_key or os.getenv("MY_GUARDRAIL_API_KEY")
self.api_base = api_base or os.getenv("MY_GUARDRAIL_API_BASE", "https://api.myguardrail.com")
super().__init__(default_on=True)
async def apply_guardrail(
self,
text: str,
language: Optional[str] = None,
entities: Optional[List[PiiEntityType]] = None,
request_data: Optional[dict] = None,
) -> str:
result = await self._check_with_api(text, request_data)
if result.get("action") == "BLOCK":
raise Exception(f"Content blocked: {result.get('reason', 'Policy violation')}")
return text
async def _check_with_api(self, text: str, request_data: Optional[dict]) -> dict:
async_client = get_async_httpx_client(llm_provider=httpxSpecialProvider.LoggingCallback)
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {self.api_key}",
}
response = await async_client.post(
f"{self.api_base}/check",
headers=headers,
json={"text": text},
timeout=5,
)
response.raise_for_status()
return response.json()
```
### Create the Init File
`__init__.py`:
```python
from typing import TYPE_CHECKING
from litellm.types.guardrails import SupportedGuardrailIntegrations
from .my_guardrail import MyGuardrail
if TYPE_CHECKING:
from litellm.types.guardrails import Guardrail, LitellmParams
def initialize_guardrail(litellm_params: "LitellmParams", guardrail: "Guardrail"):
import litellm
_my_guardrail_callback = MyGuardrail(
api_base=litellm_params.api_base,
api_key=litellm_params.api_key,
guardrail_name=guardrail.get("guardrail_name", ""),
event_hook=litellm_params.mode,
default_on=litellm_params.default_on,
)
litellm.logging_callback_manager.add_litellm_callback(_my_guardrail_callback)
return _my_guardrail_callback
guardrail_initializer_registry = {
SupportedGuardrailIntegrations.MY_GUARDRAIL.value: initialize_guardrail,
}
guardrail_class_registry = {
SupportedGuardrailIntegrations.MY_GUARDRAIL.value: MyGuardrail,
}
```
### Register Your Guardrail Type
Add to `litellm/types/guardrails.py`:
```python
class SupportedGuardrailIntegrations(str, Enum):
LAKERA = "lakera_prompt_injection"
APORIA = "aporia"
BEDROCK = "bedrock_guardrails"
PRESIDIO = "presidio"
ZSCALER_AI_GUARD = "zscaler_ai_guard"
MY_GUARDRAIL = "my_guardrail"
```
## Usage
### Config File
```yaml
model_list:
- model_name: gpt-4
litellm_params:
model: gpt-4
api_key: os.environ/OPENAI_API_KEY
litellm_settings:
guardrails:
- guardrail_name: my_guardrail
litellm_params:
guardrail: my_guardrail
mode: during_call
api_key: os.environ/MY_GUARDRAIL_API_KEY
api_base: https://api.myguardrail.com
```
### Per-Request
```bash
curl --location 'http://localhost:4000/chat/completions' \
--header 'Authorization: Bearer sk-1234' \
--header 'Content-Type: application/json' \
--data '{
"model": "gpt-4",
"messages": [{"role": "user", "content": "Test message"}],
"guardrails": ["my_guardrail"]
}'
```
## Testing
Add unit tests inside `test_litellm/` folder.
@@ -40,6 +40,8 @@ model_list:
s3_access_key_id: os.environ/AWS_ACCESS_KEY_ID
s3_secret_access_key: os.environ/AWS_SECRET_ACCESS_KEY
aws_batch_role_arn: arn:aws:iam::888602223428:role/service-role/AmazonBedrockExecutionRoleForAgents_BB9HNW6V4CV
# Optional: Custom KMS encryption key for S3 output
# s3_encryption_key_id: arn:aws:kms:us-west-2:123456789012:key/12345678-1234-1234-1234-123456789012
model_info:
mode: batch # 👈 SPECIFY MODE AS BATCH, to tell user this is a batch model
```
@@ -55,6 +57,12 @@ model_list:
| `aws_batch_role_arn` | IAM role ARN for Bedrock batch operations. Bedrock Batch APIs require an IAM role ARN to be set. |
| `mode: batch` | Indicates to LiteLLM this is a batch model |
**Optional Parameters:**
| Parameter | Description |
|-----------|-------------|
| `s3_encryption_key_id` | Custom KMS encryption key ID for S3 output data. If not specified, Bedrock uses AWS managed encryption keys. |
### 2. Create Virtual Key
```bash showLineNumbers title="create_virtual_key.sh"
@@ -174,6 +182,29 @@ When a `target_model_names` is specified, the file is written to the S3 bucket c
LiteLLM only supports Bedrock Anthropic Models for Batch API. If you want other bedrock models file an issue [here](https://github.com/BerriAI/litellm/issues/new/choose).
### How do I use a custom KMS encryption key?
If your S3 bucket requires a custom KMS encryption key, you can specify it in your configuration using `s3_encryption_key_id`. This is useful for enterprise customers with specific encryption requirements.
You can set the encryption key in 2 ways:
1. **In config.yaml** (recommended):
```yaml
model_list:
- model_name: "bedrock-batch-claude"
litellm_params:
model: bedrock/us.anthropic.claude-3-5-sonnet-20240620-v1:0
s3_encryption_key_id: arn:aws:kms:us-west-2:123456789012:key/12345678-1234-1234-1234-123456789012
# ... other params
```
2. **As an environment variable**:
```bash
export AWS_S3_ENCRYPTION_KEY_ID=arn:aws:kms:us-west-2:123456789012:key/12345678-1234-1234-1234-123456789012
```
## Further Reading
- [AWS Bedrock Batch Inference Documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/batch-inference.html)
+1
View File
@@ -31,6 +31,7 @@ Get your API key from [fal.ai](https://fal.ai/).
| Model Name | Description | Documentation |
|------------|-------------|---------------|
| `fal_ai/flux/schnell` | Flux Schnell - Low-latency generation with `image_size` support | [Docs ↗](https://fal.ai/models/fal-ai/flux/schnell) |
| `fal_ai/fal-ai/flux-pro/v1.1-ultra` | FLUX Pro v1.1 Ultra - High-quality image generation | [Docs ↗](https://fal.ai/models/fal-ai/flux-pro/v1.1-ultra) |
| `fal_ai/fal-ai/imagen4/preview` | Google's Imagen 4 - Highest quality model | [Docs ↗](https://fal.ai/models/fal-ai/imagen4/preview) |
| `fal_ai/fal-ai/recraft/v3/text-to-image` | Recraft v3 - Multiple style options | [Docs ↗](https://fal.ai/models/fal-ai/recraft/v3/text-to-image) |
+8 -3
View File
@@ -412,7 +412,7 @@ Expected Response:
### Advanced: Using `reasoning_effort` with `summary` field
By default, `reasoning_effort` accepts a string value (`"low"`, `"medium"`, `"high"`, `"minimal"`) and only sets the effort level without including a reasoning summary.
By default, `reasoning_effort` accepts a string value (`"none"`, `"minimal"`, `"low"`, `"medium"`, `"high"`) and only sets the effort level without including a reasoning summary.
To opt-in to the `summary` feature, you can pass `reasoning_effort` as a dictionary. **Note:** The `summary` field requires your OpenAI organization to have verification status. Using `summary` without verification will result in a 400 error from OpenAI.
@@ -472,12 +472,17 @@ curl -X POST 'http://0.0.0.0:4000/chat/completions' \
| Model | Default (when not set) | Supported Values |
|-------|----------------------|------------------|
| `gpt-5.1` | `none` | `none`, `low`, `medium`, `high` |
| `gpt-5` | `medium` | `minimal`, `low`, `medium`, `high` |
| `gpt-5-mini` | `medium` | `minimal`, `low`, `medium`, `high` |
| `gpt-5-mini` | `medium` | `none`, `minimal`, `low`, `medium`, `high` |
| `gpt-5-nano` | `none` | `none`, `low`, `medium`, `high` |
| `gpt-5-codex` | `adaptive` | `low`, `medium`, `high` (no `minimal`) |
| `gpt-5-pro` | `high` | `high` only |
**Note:** `gpt-5-pro` only accepts `reasoning_effort="high"`. Other values will return an error. When `reasoning_effort` is not set (None), OpenAI defaults to the value shown in the "Default" column.
**Note:**
- GPT-5.1 introduced a new `reasoning_effort="none"` setting for faster, lower-latency responses. This replaces the `"minimal"` setting from GPT-5.
- `gpt-5-pro` only accepts `reasoning_effort="high"`. Other values will return an error.
- When `reasoning_effort` is not set (None), OpenAI defaults to the value shown in the "Default" column.
See [OpenAI Reasoning documentation](https://platform.openai.com/docs/guides/reasoning) for more details on organization verification requirements.
+509 -47
View File
@@ -1604,53 +1604,6 @@ litellm.vertex_location = "us-central1 # Your Location
| gemini-2.5-flash-preview-09-2025 | `completion('gemini-2.5-flash-preview-09-2025', messages)`, `completion('vertex_ai/gemini-2.5-flash-preview-09-2025', messages)` |
| gemini-2.5-flash-lite-preview-09-2025 | `completion('gemini-2.5-flash-lite-preview-09-2025', messages)`, `completion('vertex_ai/gemini-2.5-flash-lite-preview-09-2025', messages)` |
## Private Service Connect (PSC) Endpoints
LiteLLM supports Vertex AI models deployed to Private Service Connect (PSC) endpoints, allowing you to use custom `api_base` URLs for private deployments.
### Usage
```python
from litellm import completion
# Use PSC endpoint with custom api_base
response = completion(
model="vertex_ai/1234567890", # Numeric endpoint ID
messages=[{"role": "user", "content": "Hello!"}],
api_base="http://10.96.32.8", # Your PSC endpoint
vertex_project="my-project-id",
vertex_location="us-central1"
)
```
**Key Features:**
- Supports both numeric endpoint IDs and custom model names
- Works with both completion and embedding endpoints
- Automatically constructs full PSC URL: `{api_base}/v1/projects/{project}/locations/{location}/endpoints/{model}:{endpoint}`
- Compatible with streaming requests
### Configuration
Add PSC endpoints to your `config.yaml`:
```yaml
model_list:
- model_name: psc-gemini
litellm_params:
model: vertex_ai/1234567890 # Numeric endpoint ID
api_base: "http://10.96.32.8" # Your PSC endpoint
vertex_project: "my-project-id"
vertex_location: "us-central1"
vertex_credentials: "/path/to/service_account.json"
- model_name: psc-embedding
litellm_params:
model: vertex_ai/text-embedding-004
api_base: "http://10.96.32.8" # Your PSC endpoint
vertex_project: "my-project-id"
vertex_location: "us-central1"
vertex_credentials: "/path/to/service_account.json"
```
## Fine-tuned Models
You can call fine-tuned Vertex AI Gemini models through LiteLLM
@@ -2089,6 +2042,515 @@ curl http://0.0.0.0:4000/v1/chat/completions \
| code-gecko@latest| `completion('code-gecko@latest', messages)` |
## **Embedding Models**
#### Usage - Embedding
<Tabs>
<TabItem value="sdk" label="SDK">
```python
import litellm
from litellm import embedding
litellm.vertex_project = "hardy-device-38811" # Your Project ID
litellm.vertex_location = "us-central1" # proj location
response = embedding(
model="vertex_ai/textembedding-gecko",
input=["good morning from litellm"],
)
print(response)
```
</TabItem>
<TabItem value="proxy" label="LiteLLM PROXY">
1. Add model to config.yaml
```yaml
model_list:
- model_name: snowflake-arctic-embed-m-long-1731622468876
litellm_params:
model: vertex_ai/<your-model-id>
vertex_project: "adroit-crow-413218"
vertex_location: "us-central1"
vertex_credentials: adroit-crow-413218-a956eef1a2a8.json
litellm_settings:
drop_params: True
```
2. Start Proxy
```
$ litellm --config /path/to/config.yaml
```
3. Make Request using OpenAI Python SDK, Langchain Python SDK
```python
import openai
client = openai.OpenAI(api_key="sk-1234", base_url="http://0.0.0.0:4000")
response = client.embeddings.create(
model="snowflake-arctic-embed-m-long-1731622468876",
input = ["good morning from litellm", "this is another item"],
)
print(response)
```
</TabItem>
</Tabs>
#### Supported Embedding Models
All models listed [here](https://github.com/BerriAI/litellm/blob/57f37f743886a0249f630a6792d49dffc2c5d9b7/model_prices_and_context_window.json#L835) are supported
| Model Name | Function Call |
|--------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| text-embedding-004 | `embedding(model="vertex_ai/text-embedding-004", input)` |
| text-multilingual-embedding-002 | `embedding(model="vertex_ai/text-multilingual-embedding-002", input)` |
| textembedding-gecko | `embedding(model="vertex_ai/textembedding-gecko", input)` |
| textembedding-gecko-multilingual | `embedding(model="vertex_ai/textembedding-gecko-multilingual", input)` |
| textembedding-gecko-multilingual@001 | `embedding(model="vertex_ai/textembedding-gecko-multilingual@001", input)` |
| textembedding-gecko@001 | `embedding(model="vertex_ai/textembedding-gecko@001", input)` |
| textembedding-gecko@003 | `embedding(model="vertex_ai/textembedding-gecko@003", input)` |
| text-embedding-preview-0409 | `embedding(model="vertex_ai/text-embedding-preview-0409", input)` |
| text-multilingual-embedding-preview-0409 | `embedding(model="vertex_ai/text-multilingual-embedding-preview-0409", input)` |
| Fine-tuned OR Custom Embedding models | `embedding(model="vertex_ai/<your-model-id>", input)` |
### Supported OpenAI (Unified) Params
| [param](../embedding/supported_embedding.md#input-params-for-litellmembedding) | type | [vertex equivalent](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/text-embeddings-api) |
|-------|-------------|--------------------|
| `input` | **string or List[string]** | `instances` |
| `dimensions` | **int** | `output_dimensionality` |
| `input_type` | **Literal["RETRIEVAL_QUERY","RETRIEVAL_DOCUMENT", "SEMANTIC_SIMILARITY", "CLASSIFICATION", "CLUSTERING", "QUESTION_ANSWERING", "FACT_VERIFICATION"]** | `task_type` |
#### Usage with OpenAI (Unified) Params
<Tabs>
<TabItem value="sdk" label="SDK">
```python
response = litellm.embedding(
model="vertex_ai/text-embedding-004",
input=["good morning from litellm", "gm"]
input_type = "RETRIEVAL_DOCUMENT",
dimensions=1,
)
```
</TabItem>
<TabItem value="proxy" label="LiteLLM PROXY">
```python
import openai
client = openai.OpenAI(api_key="sk-1234", base_url="http://0.0.0.0:4000")
response = client.embeddings.create(
model="text-embedding-004",
input = ["good morning from litellm", "gm"],
dimensions=1,
extra_body = {
"input_type": "RETRIEVAL_QUERY",
}
)
print(response)
```
</TabItem>
</Tabs>
### Supported Vertex Specific Params
| param | type |
|-------|-------------|
| `auto_truncate` | **bool** |
| `task_type` | **Literal["RETRIEVAL_QUERY","RETRIEVAL_DOCUMENT", "SEMANTIC_SIMILARITY", "CLASSIFICATION", "CLUSTERING", "QUESTION_ANSWERING", "FACT_VERIFICATION"]** |
| `title` | **str** |
#### Usage with Vertex Specific Params (Use `task_type` and `title`)
You can pass any vertex specific params to the embedding model. Just pass them to the embedding function like this:
[Relevant Vertex AI doc with all embedding params](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/text-embeddings-api#request_body)
<Tabs>
<TabItem value="sdk" label="SDK">
```python
response = litellm.embedding(
model="vertex_ai/text-embedding-004",
input=["good morning from litellm", "gm"]
task_type = "RETRIEVAL_DOCUMENT",
title = "test",
dimensions=1,
auto_truncate=True,
)
```
</TabItem>
<TabItem value="proxy" label="LiteLLM PROXY">
```python
import openai
client = openai.OpenAI(api_key="sk-1234", base_url="http://0.0.0.0:4000")
response = client.embeddings.create(
model="text-embedding-004",
input = ["good morning from litellm", "gm"],
dimensions=1,
extra_body = {
"task_type": "RETRIEVAL_QUERY",
"auto_truncate": True,
"title": "test",
}
)
print(response)
```
</TabItem>
</Tabs>
## **Multi-Modal Embeddings**
Known Limitations:
- Only supports 1 image / video / image per request
- Only supports GCS or base64 encoded images / videos
### Usage
<Tabs>
<TabItem value="sdk" label="SDK">
Using GCS Images
```python
response = await litellm.aembedding(
model="vertex_ai/multimodalembedding@001",
input="gs://cloud-samples-data/vertex-ai/llm/prompts/landmark1.png" # will be sent as a gcs image
)
```
Using base 64 encoded images
```python
response = await litellm.aembedding(
model="vertex_ai/multimodalembedding@001",
input="data:image/jpeg;base64,..." # will be sent as a base64 encoded image
)
```
</TabItem>
<TabItem value="proxy" label="LiteLLM PROXY (Unified Endpoint)">
1. Add model to config.yaml
```yaml
model_list:
- model_name: multimodalembedding@001
litellm_params:
model: vertex_ai/multimodalembedding@001
vertex_project: "adroit-crow-413218"
vertex_location: "us-central1"
vertex_credentials: adroit-crow-413218-a956eef1a2a8.json
litellm_settings:
drop_params: True
```
2. Start Proxy
```
$ litellm --config /path/to/config.yaml
```
3. Make Request use OpenAI Python SDK, Langchain Python SDK
<Tabs>
<TabItem value="OpenAI SDK" label="OpenAI SDK">
Requests with GCS Image / Video URI
```python
import openai
client = openai.OpenAI(api_key="sk-1234", base_url="http://0.0.0.0:4000")
# # request sent to model set on litellm proxy, `litellm --model`
response = client.embeddings.create(
model="multimodalembedding@001",
input = "gs://cloud-samples-data/vertex-ai/llm/prompts/landmark1.png",
)
print(response)
```
Requests with base64 encoded images
```python
import openai
client = openai.OpenAI(api_key="sk-1234", base_url="http://0.0.0.0:4000")
# # request sent to model set on litellm proxy, `litellm --model`
response = client.embeddings.create(
model="multimodalembedding@001",
input = "data:image/jpeg;base64,...",
)
print(response)
```
</TabItem>
<TabItem value="langchain" label="Langchain">
Requests with GCS Image / Video URI
```python
from langchain_openai import OpenAIEmbeddings
embeddings_models = "multimodalembedding@001"
embeddings = OpenAIEmbeddings(
model="multimodalembedding@001",
base_url="http://0.0.0.0:4000",
api_key="sk-1234", # type: ignore
)
query_result = embeddings.embed_query(
"gs://cloud-samples-data/vertex-ai/llm/prompts/landmark1.png"
)
print(query_result)
```
Requests with base64 encoded images
```python
from langchain_openai import OpenAIEmbeddings
embeddings_models = "multimodalembedding@001"
embeddings = OpenAIEmbeddings(
model="multimodalembedding@001",
base_url="http://0.0.0.0:4000",
api_key="sk-1234", # type: ignore
)
query_result = embeddings.embed_query(
"data:image/jpeg;base64,..."
)
print(query_result)
```
</TabItem>
</Tabs>
</TabItem>
<TabItem value="proxy-vtx" label="LiteLLM PROXY (Vertex SDK)">
1. Add model to config.yaml
```yaml
default_vertex_config:
vertex_project: "adroit-crow-413218"
vertex_location: "us-central1"
vertex_credentials: adroit-crow-413218-a956eef1a2a8.json
```
2. Start Proxy
```
$ litellm --config /path/to/config.yaml
```
3. Make Request use OpenAI Python SDK
```python
import vertexai
from vertexai.vision_models import Image, MultiModalEmbeddingModel, Video
from vertexai.vision_models import VideoSegmentConfig
from google.auth.credentials import Credentials
LITELLM_PROXY_API_KEY = "sk-1234"
LITELLM_PROXY_BASE = "http://0.0.0.0:4000/vertex-ai"
import datetime
class CredentialsWrapper(Credentials):
def __init__(self, token=None):
super().__init__()
self.token = token
self.expiry = None # or set to a future date if needed
def refresh(self, request):
pass
def apply(self, headers, token=None):
headers['Authorization'] = f'Bearer {self.token}'
@property
def expired(self):
return False # Always consider the token as non-expired
@property
def valid(self):
return True # Always consider the credentials as valid
credentials = CredentialsWrapper(token=LITELLM_PROXY_API_KEY)
vertexai.init(
project="adroit-crow-413218",
location="us-central1",
api_endpoint=LITELLM_PROXY_BASE,
credentials = credentials,
api_transport="rest",
)
model = MultiModalEmbeddingModel.from_pretrained("multimodalembedding")
image = Image.load_from_file(
"gs://cloud-samples-data/vertex-ai/llm/prompts/landmark1.png"
)
embeddings = model.get_embeddings(
image=image,
contextual_text="Colosseum",
dimension=1408,
)
print(f"Image Embedding: {embeddings.image_embedding}")
print(f"Text Embedding: {embeddings.text_embedding}")
```
</TabItem>
</Tabs>
### Text + Image + Video Embeddings
<Tabs>
<TabItem value="sdk" label="SDK">
Text + Image
```python
response = await litellm.aembedding(
model="vertex_ai/multimodalembedding@001",
input=["hey", "gs://cloud-samples-data/vertex-ai/llm/prompts/landmark1.png"] # will be sent as a gcs image
)
```
Text + Video
```python
response = await litellm.aembedding(
model="vertex_ai/multimodalembedding@001",
input=["hey", "gs://my-bucket/embeddings/supermarket-video.mp4"] # will be sent as a gcs image
)
```
Image + Video
```python
response = await litellm.aembedding(
model="vertex_ai/multimodalembedding@001",
input=["gs://cloud-samples-data/vertex-ai/llm/prompts/landmark1.png", "gs://my-bucket/embeddings/supermarket-video.mp4"] # will be sent as a gcs image
)
```
</TabItem>
<TabItem value="proxy" label="LiteLLM PROXY (Unified Endpoint)">
1. Add model to config.yaml
```yaml
model_list:
- model_name: multimodalembedding@001
litellm_params:
model: vertex_ai/multimodalembedding@001
vertex_project: "adroit-crow-413218"
vertex_location: "us-central1"
vertex_credentials: adroit-crow-413218-a956eef1a2a8.json
litellm_settings:
drop_params: True
```
2. Start Proxy
```
$ litellm --config /path/to/config.yaml
```
3. Make Request use OpenAI Python SDK, Langchain Python SDK
Text + Image
```python
import openai
client = openai.OpenAI(api_key="sk-1234", base_url="http://0.0.0.0:4000")
# # request sent to model set on litellm proxy, `litellm --model`
response = client.embeddings.create(
model="multimodalembedding@001",
input = ["hey", "gs://cloud-samples-data/vertex-ai/llm/prompts/landmark1.png"],
)
print(response)
```
Text + Video
```python
import openai
client = openai.OpenAI(api_key="sk-1234", base_url="http://0.0.0.0:4000")
# # request sent to model set on litellm proxy, `litellm --model`
response = client.embeddings.create(
model="multimodalembedding@001",
input = ["hey", "gs://my-bucket/embeddings/supermarket-video.mp4"],
)
print(response)
```
Image + Video
```python
import openai
client = openai.OpenAI(api_key="sk-1234", base_url="http://0.0.0.0:4000")
# # request sent to model set on litellm proxy, `litellm --model`
response = client.embeddings.create(
model="multimodalembedding@001",
input = ["gs://cloud-samples-data/vertex-ai/llm/prompts/landmark1.png", "gs://my-bucket/embeddings/supermarket-video.mp4"],
)
print(response)
```
</TabItem>
</Tabs>
## **Gemini TTS (Text-to-Speech) Audio Output**
:::info
@@ -1,587 +0,0 @@
import Image from '@theme/IdealImage';
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
# Vertex AI Embedding
## Usage - Embedding
<Tabs>
<TabItem value="sdk" label="SDK">
```python
import litellm
from litellm import embedding
litellm.vertex_project = "hardy-device-38811" # Your Project ID
litellm.vertex_location = "us-central1" # proj location
response = embedding(
model="vertex_ai/textembedding-gecko",
input=["good morning from litellm"],
)
print(response)
```
</TabItem>
<TabItem value="proxy" label="LiteLLM PROXY">
1. Add model to config.yaml
```yaml
model_list:
- model_name: snowflake-arctic-embed-m-long-1731622468876
litellm_params:
model: vertex_ai/<your-model-id>
vertex_project: "adroit-crow-413218"
vertex_location: "us-central1"
vertex_credentials: adroit-crow-413218-a956eef1a2a8.json
litellm_settings:
drop_params: True
```
2. Start Proxy
```
$ litellm --config /path/to/config.yaml
```
3. Make Request using OpenAI Python SDK, Langchain Python SDK
```python
import openai
client = openai.OpenAI(api_key="sk-1234", base_url="http://0.0.0.0:4000")
response = client.embeddings.create(
model="snowflake-arctic-embed-m-long-1731622468876",
input = ["good morning from litellm", "this is another item"],
)
print(response)
```
</TabItem>
</Tabs>
#### Supported Embedding Models
All models listed [here](https://github.com/BerriAI/litellm/blob/57f37f743886a0249f630a6792d49dffc2c5d9b7/model_prices_and_context_window.json#L835) are supported
| Model Name | Function Call |
|--------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| text-embedding-004 | `embedding(model="vertex_ai/text-embedding-004", input)` |
| text-multilingual-embedding-002 | `embedding(model="vertex_ai/text-multilingual-embedding-002", input)` |
| textembedding-gecko | `embedding(model="vertex_ai/textembedding-gecko", input)` |
| textembedding-gecko-multilingual | `embedding(model="vertex_ai/textembedding-gecko-multilingual", input)` |
| textembedding-gecko-multilingual@001 | `embedding(model="vertex_ai/textembedding-gecko-multilingual@001", input)` |
| textembedding-gecko@001 | `embedding(model="vertex_ai/textembedding-gecko@001", input)` |
| textembedding-gecko@003 | `embedding(model="vertex_ai/textembedding-gecko@003", input)` |
| text-embedding-preview-0409 | `embedding(model="vertex_ai/text-embedding-preview-0409", input)` |
| text-multilingual-embedding-preview-0409 | `embedding(model="vertex_ai/text-multilingual-embedding-preview-0409", input)` |
| Fine-tuned OR Custom Embedding models | `embedding(model="vertex_ai/<your-model-id>", input)` |
### Supported OpenAI (Unified) Params
| [param](../embedding/supported_embedding.md#input-params-for-litellmembedding) | type | [vertex equivalent](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/text-embeddings-api) |
|-------|-------------|--------------------|
| `input` | **string or List[string]** | `instances` |
| `dimensions` | **int** | `output_dimensionality` |
| `input_type` | **Literal["RETRIEVAL_QUERY","RETRIEVAL_DOCUMENT", "SEMANTIC_SIMILARITY", "CLASSIFICATION", "CLUSTERING", "QUESTION_ANSWERING", "FACT_VERIFICATION"]** | `task_type` |
#### Usage with OpenAI (Unified) Params
<Tabs>
<TabItem value="sdk" label="SDK">
```python
response = litellm.embedding(
model="vertex_ai/text-embedding-004",
input=["good morning from litellm", "gm"]
input_type = "RETRIEVAL_DOCUMENT",
dimensions=1,
)
```
</TabItem>
<TabItem value="proxy" label="LiteLLM PROXY">
```python
import openai
client = openai.OpenAI(api_key="sk-1234", base_url="http://0.0.0.0:4000")
response = client.embeddings.create(
model="text-embedding-004",
input = ["good morning from litellm", "gm"],
dimensions=1,
extra_body = {
"input_type": "RETRIEVAL_QUERY",
}
)
print(response)
```
</TabItem>
</Tabs>
### Supported Vertex Specific Params
| param | type |
|-------|-------------|
| `auto_truncate` | **bool** |
| `task_type` | **Literal["RETRIEVAL_QUERY","RETRIEVAL_DOCUMENT", "SEMANTIC_SIMILARITY", "CLASSIFICATION", "CLUSTERING", "QUESTION_ANSWERING", "FACT_VERIFICATION"]** |
| `title` | **str** |
#### Usage with Vertex Specific Params (Use `task_type` and `title`)
You can pass any vertex specific params to the embedding model. Just pass them to the embedding function like this:
[Relevant Vertex AI doc with all embedding params](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/text-embeddings-api#request_body)
<Tabs>
<TabItem value="sdk" label="SDK">
```python
response = litellm.embedding(
model="vertex_ai/text-embedding-004",
input=["good morning from litellm", "gm"]
task_type = "RETRIEVAL_DOCUMENT",
title = "test",
dimensions=1,
auto_truncate=True,
)
```
</TabItem>
<TabItem value="proxy" label="LiteLLM PROXY">
```python
import openai
client = openai.OpenAI(api_key="sk-1234", base_url="http://0.0.0.0:4000")
response = client.embeddings.create(
model="text-embedding-004",
input = ["good morning from litellm", "gm"],
dimensions=1,
extra_body = {
"task_type": "RETRIEVAL_QUERY",
"auto_truncate": True,
"title": "test",
}
)
print(response)
```
</TabItem>
</Tabs>
## **BGE Embeddings**
Use BGE (Baidu General Embedding) models deployed on Vertex AI.
### Usage
<Tabs>
<TabItem value="sdk" label="SDK">
```python showLineNumbers title="Using BGE on Vertex AI"
import litellm
response = litellm.embedding(
model="vertex_ai/bge/<your-endpoint-id>",
input=["Hello", "World"],
vertex_project="your-project-id",
vertex_location="your-location"
)
print(response)
```
</TabItem>
<TabItem value="proxy" label="LiteLLM PROXY">
1. Add model to config.yaml
```yaml showLineNumbers title="config.yaml"
model_list:
- model_name: bge-embedding
litellm_params:
model: vertex_ai/bge/<your-endpoint-id>
vertex_project: "your-project-id"
vertex_location: "us-central1"
vertex_credentials: your-credentials.json
litellm_settings:
drop_params: True
```
2. Start Proxy
```bash
$ litellm --config /path/to/config.yaml
```
3. Make Request using OpenAI Python SDK
```python showLineNumbers title="Making requests to BGE"
import openai
client = openai.OpenAI(api_key="sk-1234", base_url="http://0.0.0.0:4000")
response = client.embeddings.create(
model="bge-embedding",
input=["good morning from litellm", "this is another item"]
)
print(response)
```
Using a Private Service Connect (PSC) endpoint
```yaml showLineNumbers title="config.yaml (PSC)"
model_list:
- model_name: bge-small-en-v1.5
litellm_params:
model: vertex_ai/bge/1234567890
api_base: http://10.96.32.8 # Your PSC IP
vertex_project: my-project-id #optional
vertex_location: us-central1 #optional
```
</TabItem>
</Tabs>
## **Multi-Modal Embeddings**
Known Limitations:
- Only supports 1 image / video / image per request
- Only supports GCS or base64 encoded images / videos
### Usage
<Tabs>
<TabItem value="sdk" label="SDK">
Using GCS Images
```python
response = await litellm.aembedding(
model="vertex_ai/multimodalembedding@001",
input="gs://cloud-samples-data/vertex-ai/llm/prompts/landmark1.png" # will be sent as a gcs image
)
```
Using base 64 encoded images
```python
response = await litellm.aembedding(
model="vertex_ai/multimodalembedding@001",
input="data:image/jpeg;base64,..." # will be sent as a base64 encoded image
)
```
</TabItem>
<TabItem value="proxy" label="LiteLLM PROXY (Unified Endpoint)">
1. Add model to config.yaml
```yaml
model_list:
- model_name: multimodalembedding@001
litellm_params:
model: vertex_ai/multimodalembedding@001
vertex_project: "adroit-crow-413218"
vertex_location: "us-central1"
vertex_credentials: adroit-crow-413218-a956eef1a2a8.json
litellm_settings:
drop_params: True
```
2. Start Proxy
```
$ litellm --config /path/to/config.yaml
```
3. Make Request use OpenAI Python SDK, Langchain Python SDK
<Tabs>
<TabItem value="OpenAI SDK" label="OpenAI SDK">
Requests with GCS Image / Video URI
```python
import openai
client = openai.OpenAI(api_key="sk-1234", base_url="http://0.0.0.0:4000")
# # request sent to model set on litellm proxy, `litellm --model`
response = client.embeddings.create(
model="multimodalembedding@001",
input = "gs://cloud-samples-data/vertex-ai/llm/prompts/landmark1.png",
)
print(response)
```
Requests with base64 encoded images
```python
import openai
client = openai.OpenAI(api_key="sk-1234", base_url="http://0.0.0.0:4000")
# # request sent to model set on litellm proxy, `litellm --model`
response = client.embeddings.create(
model="multimodalembedding@001",
input = "data:image/jpeg;base64,...",
)
print(response)
```
</TabItem>
<TabItem value="langchain" label="Langchain">
Requests with GCS Image / Video URI
```python
from langchain_openai import OpenAIEmbeddings
embeddings_models = "multimodalembedding@001"
embeddings = OpenAIEmbeddings(
model="multimodalembedding@001",
base_url="http://0.0.0.0:4000",
api_key="sk-1234", # type: ignore
)
query_result = embeddings.embed_query(
"gs://cloud-samples-data/vertex-ai/llm/prompts/landmark1.png"
)
print(query_result)
```
Requests with base64 encoded images
```python
from langchain_openai import OpenAIEmbeddings
embeddings_models = "multimodalembedding@001"
embeddings = OpenAIEmbeddings(
model="multimodalembedding@001",
base_url="http://0.0.0.0:4000",
api_key="sk-1234", # type: ignore
)
query_result = embeddings.embed_query(
"data:image/jpeg;base64,..."
)
print(query_result)
```
</TabItem>
</Tabs>
</TabItem>
<TabItem value="proxy-vtx" label="LiteLLM PROXY (Vertex SDK)">
1. Add model to config.yaml
```yaml
default_vertex_config:
vertex_project: "adroit-crow-413218"
vertex_location: "us-central1"
vertex_credentials: adroit-crow-413218-a956eef1a2a8.json
```
2. Start Proxy
```
$ litellm --config /path/to/config.yaml
```
3. Make Request use OpenAI Python SDK
```python
import vertexai
from vertexai.vision_models import Image, MultiModalEmbeddingModel, Video
from vertexai.vision_models import VideoSegmentConfig
from google.auth.credentials import Credentials
LITELLM_PROXY_API_KEY = "sk-1234"
LITELLM_PROXY_BASE = "http://0.0.0.0:4000/vertex-ai"
import datetime
class CredentialsWrapper(Credentials):
def __init__(self, token=None):
super().__init__()
self.token = token
self.expiry = None # or set to a future date if needed
def refresh(self, request):
pass
def apply(self, headers, token=None):
headers['Authorization'] = f'Bearer {self.token}'
@property
def expired(self):
return False # Always consider the token as non-expired
@property
def valid(self):
return True # Always consider the credentials as valid
credentials = CredentialsWrapper(token=LITELLM_PROXY_API_KEY)
vertexai.init(
project="adroit-crow-413218",
location="us-central1",
api_endpoint=LITELLM_PROXY_BASE,
credentials = credentials,
api_transport="rest",
)
model = MultiModalEmbeddingModel.from_pretrained("multimodalembedding")
image = Image.load_from_file(
"gs://cloud-samples-data/vertex-ai/llm/prompts/landmark1.png"
)
embeddings = model.get_embeddings(
image=image,
contextual_text="Colosseum",
dimension=1408,
)
print(f"Image Embedding: {embeddings.image_embedding}")
print(f"Text Embedding: {embeddings.text_embedding}")
```
</TabItem>
</Tabs>
### Text + Image + Video Embeddings
<Tabs>
<TabItem value="sdk" label="SDK">
Text + Image
```python
response = await litellm.aembedding(
model="vertex_ai/multimodalembedding@001",
input=["hey", "gs://cloud-samples-data/vertex-ai/llm/prompts/landmark1.png"] # will be sent as a gcs image
)
```
Text + Video
```python
response = await litellm.aembedding(
model="vertex_ai/multimodalembedding@001",
input=["hey", "gs://my-bucket/embeddings/supermarket-video.mp4"] # will be sent as a gcs image
)
```
Image + Video
```python
response = await litellm.aembedding(
model="vertex_ai/multimodalembedding@001",
input=["gs://cloud-samples-data/vertex-ai/llm/prompts/landmark1.png", "gs://my-bucket/embeddings/supermarket-video.mp4"] # will be sent as a gcs image
)
```
</TabItem>
<TabItem value="proxy" label="LiteLLM PROXY (Unified Endpoint)">
1. Add model to config.yaml
```yaml
model_list:
- model_name: multimodalembedding@001
litellm_params:
model: vertex_ai/multimodalembedding@001
vertex_project: "adroit-crow-413218"
vertex_location: "us-central1"
vertex_credentials: adroit-crow-413218-a956eef1a2a8.json
litellm_settings:
drop_params: True
```
2. Start Proxy
```
$ litellm --config /path/to/config.yaml
```
3. Make Request use OpenAI Python SDK, Langchain Python SDK
Text + Image
```python
import openai
client = openai.OpenAI(api_key="sk-1234", base_url="http://0.0.0.0:4000")
# # request sent to model set on litellm proxy, `litellm --model`
response = client.embeddings.create(
model="multimodalembedding@001",
input = ["hey", "gs://cloud-samples-data/vertex-ai/llm/prompts/landmark1.png"],
)
print(response)
```
Text + Video
```python
import openai
client = openai.OpenAI(api_key="sk-1234", base_url="http://0.0.0.0:4000")
# # request sent to model set on litellm proxy, `litellm --model`
response = client.embeddings.create(
model="multimodalembedding@001",
input = ["hey", "gs://my-bucket/embeddings/supermarket-video.mp4"],
)
print(response)
```
Image + Video
```python
import openai
client = openai.OpenAI(api_key="sk-1234", base_url="http://0.0.0.0:4000")
# # request sent to model set on litellm proxy, `litellm --model`
response = client.embeddings.create(
model="multimodalembedding@001",
input = ["gs://cloud-samples-data/vertex-ai/llm/prompts/landmark1.png", "gs://my-bucket/embeddings/supermarket-video.mp4"],
)
print(response)
```
</TabItem>
</Tabs>
+111 -3
View File
@@ -14,12 +14,41 @@ import os
os.environ['VOYAGE_API_KEY'] = ""
response = embedding(
model="voyage/voyage-3-large",
model="voyage/voyage-3.5",
input=["good morning from litellm"],
)
print(response)
```
## Supported Parameters
VoyageAI embeddings support the following optional parameters:
- `input_type`: Specifies the type of input for retrieval optimization
- `"query"`: Use for search queries
- `"document"`: Use for documents being indexed
- `dimensions`: Output embedding dimensions (256, 512, 1024, or 2048)
- `encoding_format`: Output format (`"float"`, `"int8"`, `"uint8"`, `"binary"`, `"ubinary"`)
- `truncation`: Whether to truncate inputs exceeding max tokens (default: `True`)
### Example with Parameters
```python
from litellm import embedding
import os
os.environ['VOYAGE_API_KEY'] = "your-api-key"
# Embedding with custom dimensions and input type
response = embedding(
model="voyage/voyage-3.5",
input=["Your text here"],
dimensions=512,
input_type="document"
)
print(f"Embedding dimensions: {len(response.data[0]['embedding'])}")
```
## Supported Models
All models listed here https://docs.voyageai.com/embeddings/#models-and-specifics are supported
@@ -40,5 +69,84 @@ All models listed here https://docs.voyageai.com/embeddings/#models-and-specific
| voyage-2 | `embedding(model="voyage/voyage-2", input)` |
| voyage-lite-02-instruct | `embedding(model="voyage/voyage-lite-02-instruct", input)` |
| voyage-01 | `embedding(model="voyage/voyage-01", input)` |
| voyage-lite-01 | `embedding(model="voyage/voyage-lite-01", input)` |
| voyage-lite-01-instruct | `embedding(model="voyage/voyage-lite-01-instruct", input)` |
| voyage-lite-01 | `embedding(model="voyage/voyage-lite-01", input)` |
| voyage-lite-01-instruct | `embedding(model="voyage/voyage-lite-01-instruct", input)` |
## Contextual Embeddings (voyage-context-3)
VoyageAI's `voyage-context-3` model provides contextualized chunk embeddings, where each chunk is embedded with awareness of its surrounding document context. This significantly improves retrieval quality compared to standard context-agnostic embeddings.
### Key Benefits
- Chunks understand their position and role within the full document
- Improved retrieval accuracy for long documents (outperforms competitors by 7-23%)
- Better handling of ambiguous references and cross-chunk dependencies
- Seamless drop-in replacement for standard embeddings in RAG pipelines
### Usage
Contextual embeddings require a **nested input format** where each inner list represents chunks from a single document:
```python
from litellm import embedding
import os
os.environ['VOYAGE_API_KEY'] = "your-api-key"
# Single document with multiple chunks
response = embedding(
model="voyage/voyage-context-3",
input=[
[
"Chapter 1: Introduction to AI",
"This chapter covers the basics of artificial intelligence.",
"We will explore machine learning and deep learning."
]
]
)
print(f"Number of chunk groups: {len(response.data)}")
# Multiple documents
response = embedding(
model="voyage/voyage-context-3",
input=[
["Paris is the capital of France.", "It is known for the Eiffel Tower."],
["Tokyo is the capital of Japan.", "It is a major economic hub."]
]
)
print(f"Processed {len(response.data)} documents")
```
### Specifications
- Model: `voyage-context-3`
- Context length: 32,000 tokens per document
- Output dimensions: 256, 512, 1024 (default), or 2048
- Max inputs: 1,000 per request
- Max total tokens: 120,000
- Max chunks: 16,000
- Pricing: $0.18 per million tokens
### When to Use Contextual Embeddings
**Use `voyage-context-3` when:**
- Processing long documents split into chunks
- Document structure and flow are important
- References between sections matter
- You need to preserve document hierarchy
**Use standard models (voyage-3.5, voyage-3-large) when:**
- Embedding independent pieces of text
- Processing short queries
- Document context is not relevant
- You need faster/cheaper processing
## Model Selection Guide
| Model | Best For | Context Length | Price/M Tokens |
|-------|----------|----------------|----------------|
| voyage-3.5 | General-purpose, multilingual | 32K | $0.06 |
| voyage-3.5-lite | Latency-sensitive applications | 32K | $0.02 |
| voyage-3-large | Best overall quality | 32K | $0.18 |
| voyage-code-3 | Code retrieval and search | 32K | $0.18 |
| voyage-finance-2 | Financial documents | 32K | $0.12 |
| voyage-law-2 | Legal documents | 16K | $0.12 |
| voyage-context-3 | Contextual document embeddings | 32K | $0.18 |
+5 -233
View File
@@ -1,7 +1,7 @@
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
# Control Model Access
# Restrict Model Access
## **Restrict models by Virtual Key**
@@ -114,238 +114,6 @@ curl --location 'http://0.0.0.0:4000/chat/completions' \
### [API Reference](https://litellm-api.up.railway.app/#/team%20management/new_team_team_new_post)
## **Model Access Groups**
Use model access groups to give users access to select models, and add new ones to it over time (e.g. mistral, llama-2, etc.)
**Step 1. Assign model, access group in config.yaml**
```yaml
model_list:
- model_name: gpt-4
litellm_params:
model: openai/fake
api_key: fake-key
api_base: https://exampleopenaiendpoint-production.up.railway.app/
model_info:
access_groups: ["beta-models"] # 👈 Model Access Group
- model_name: fireworks-llama-v3-70b-instruct
litellm_params:
model: fireworks_ai/accounts/fireworks/models/llama-v3-70b-instruct
api_key: "os.environ/FIREWORKS"
model_info:
access_groups: ["beta-models"] # 👈 Model Access Group
```
<Tabs>
<TabItem value="key" label="Key Access Groups">
**Create key with access group**
```bash
curl --location 'http://localhost:4000/key/generate' \
-H 'Authorization: Bearer <your-master-key>' \
-H 'Content-Type: application/json' \
-d '{"models": ["beta-models"], # 👈 Model Access Group
"max_budget": 0,}'
```
Test Key
<Tabs>
<TabItem label="Allowed Access" value = "allowed">
```shell
curl -i http://localhost:4000/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk-<key-from-previous-step>" \
-d '{
"model": "gpt-4",
"messages": [
{"role": "user", "content": "Hello"}
]
}'
```
</TabItem>
<TabItem label="Disallowed Access" value = "not-allowed">
:::info
Expect this to fail since gpt-4o is not in the `beta-models` access group
:::
```shell
curl -i http://localhost:4000/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk-<key-from-previous-step>" \
-d '{
"model": "gpt-4o",
"messages": [
{"role": "user", "content": "Hello"}
]
}'
```
</TabItem>
</Tabs>
</TabItem>
<TabItem value="team" label="Team Access Groups">
Create Team
```shell
curl --location 'http://localhost:4000/team/new' \
-H 'Authorization: Bearer sk-<key-from-previous-step>' \
-H 'Content-Type: application/json' \
-d '{"models": ["beta-models"]}'
```
Create Key for Team
```shell
curl --location 'http://0.0.0.0:4000/key/generate' \
--header 'Authorization: Bearer sk-<key-from-previous-step>' \
--header 'Content-Type: application/json' \
--data '{"team_id": "0ac97648-c194-4c90-8cd6-40af7b0d2d2a"}
```
Test Key
<Tabs>
<TabItem label="Allowed Access" value = "allowed">
```shell
curl -i http://localhost:4000/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk-<key-from-previous-step>" \
-d '{
"model": "gpt-4",
"messages": [
{"role": "user", "content": "Hello"}
]
}'
```
</TabItem>
<TabItem label="Disallowed Access" value = "not-allowed">
:::info
Expect this to fail since gpt-4o is not in the `beta-models` access group
:::
```shell
curl -i http://localhost:4000/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk-<key-from-previous-step>" \
-d '{
"model": "gpt-4o",
"messages": [
{"role": "user", "content": "Hello"}
]
}'
```
</TabItem>
</Tabs>
</TabItem>
</Tabs>
### ✨ Control Access on Wildcard Models
Control access to all models with a specific prefix (e.g. `openai/*`).
Use this to also give users access to all models, except for a few that you don't want them to use (e.g. `openai/o1-*`).
:::info
Setting model access groups on wildcard models is an Enterprise feature.
See pricing [here](https://litellm.ai/#pricing)
Get a trial key [here](https://litellm.ai/#trial)
:::
1. Setup config.yaml
```yaml
model_list:
- model_name: openai/*
litellm_params:
model: openai/*
api_key: os.environ/OPENAI_API_KEY
model_info:
access_groups: ["default-models"]
- model_name: openai/o1-*
litellm_params:
model: openai/o1-*
api_key: os.environ/OPENAI_API_KEY
model_info:
access_groups: ["restricted-models"]
```
2. Generate a key with access to `default-models`
```bash
curl -L -X POST 'http://0.0.0.0:4000/key/generate' \
-H 'Authorization: Bearer sk-1234' \
-H 'Content-Type: application/json' \
-d '{
"models": ["default-models"],
}'
```
3. Test the key
<Tabs>
<TabItem label="Successful Request" value = "success">
```bash
curl -i http://localhost:4000/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk-<key-from-previous-step>" \
-d '{
"model": "openai/gpt-4",
"messages": [
{"role": "user", "content": "Hello"}
]
}'
```
</TabItem>
<TabItem value="bad-request" label="Rejected Request">
```bash
curl -i http://localhost:4000/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk-<key-from-previous-step>" \
-d '{
"model": "openai/o1-mini",
"messages": [
{"role": "user", "content": "Hello"}
]
}'
```
</TabItem>
</Tabs>
## **View Available Fallback Models**
Use the `/v1/models` endpoint to discover available fallback models for a given model. This helps you understand which backup models are available when your primary model is unavailable or restricted.
@@ -451,4 +219,8 @@ When `include_metadata=true` is specified, the response includes fallback inform
| `include_metadata` | boolean | Include additional model metadata including fallbacks |
| `fallback_type` | string | Filter fallbacks by type: `general`, `context_window`, or `content_policy` |
## Advanced: Model Access Groups
For advanced use cases, use [Model Access Groups](./model_access_groups) to dynamically group multiple models and manage access without restarting the proxy.
## [Role Based Access Control (RBAC)](./jwt_auth_arch)
@@ -0,0 +1,503 @@
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
# Model Access Groups
### Overview
Group multiple models under a single name, then grant keys or teams access to the entire group. Add or remove models from a group without updating individual keys.
Use cases:
- Separate production and development models
- Restrict expensive models to specific teams
- Organize models by provider or capability
- Control access to model families with wildcards (e.g., `openai/*`)
### How It Works
```mermaid
graph LR
subgraph AG1["Access Group: 'prod-models'"]
M1["gpt-4o"]
M2["claude-opus"]
end
subgraph AG2["Access Group: 'dev-models'"]
M3["gpt-4o-mini"]
M4["claude-haiku"]
end
K1["Production API Key"] --> AG1
K2["Development API Key"] --> AG2
style AG1 fill:#e3f2fd
style AG2 fill:#fff8e1
```
**Key Concept:** Group models together → Attach group to key → Key gets access to all models in group
**Step 1. Assign model, access group in config.yaml**
```yaml showLineNumbers title="config.yaml"
model_list:
- model_name: gpt-4
litellm_params:
model: openai/fake
api_key: fake-key
api_base: https://exampleopenaiendpoint-production.up.railway.app/
model_info:
access_groups: ["beta-models"] # 👈 Model Access Group
- model_name: fireworks-llama-v3-70b-instruct
litellm_params:
model: fireworks_ai/accounts/fireworks/models/llama-v3-70b-instruct
api_key: "os.environ/FIREWORKS"
model_info:
access_groups: ["beta-models"] # 👈 Model Access Group
```
<Tabs>
<TabItem value="key" label="Key Access Groups">
**Create key with access group**
```bash showLineNumbers title="Create Key with Access Group"
curl --location 'http://localhost:4000/key/generate' \
-H 'Authorization: Bearer <your-master-key>' \
-H 'Content-Type: application/json' \
-d '{"models": ["beta-models"], # 👈 Model Access Group
"max_budget": 0,}'
```
Test Key
<Tabs>
<TabItem label="Allowed Access" value = "allowed">
```bash showLineNumbers title="Test Key - Allowed Access"
curl -i http://localhost:4000/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk-<key-from-previous-step>" \
-d '{
"model": "gpt-4",
"messages": [
{"role": "user", "content": "Hello"}
]
}'
```
</TabItem>
<TabItem label="Disallowed Access" value = "not-allowed">
:::info
Expect this to fail since gpt-4o is not in the `beta-models` access group
:::
```bash showLineNumbers title="Test Key - Disallowed Access"
curl -i http://localhost:4000/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk-<key-from-previous-step>" \
-d '{
"model": "gpt-4o",
"messages": [
{"role": "user", "content": "Hello"}
]
}'
```
</TabItem>
</Tabs>
</TabItem>
<TabItem value="team" label="Team Access Groups">
Create Team
```bash showLineNumbers title="Create Team"
curl --location 'http://localhost:4000/team/new' \
-H 'Authorization: Bearer sk-<key-from-previous-step>' \
-H 'Content-Type: application/json' \
-d '{"models": ["beta-models"]}'
```
Create Key for Team
```bash showLineNumbers title="Create Key for Team"
curl --location 'http://0.0.0.0:4000/key/generate' \
--header 'Authorization: Bearer sk-<key-from-previous-step>' \
--header 'Content-Type: application/json' \
--data '{"team_id": "0ac97648-c194-4c90-8cd6-40af7b0d2d2a"}
```
Test Key
<Tabs>
<TabItem label="Allowed Access" value = "allowed">
```bash showLineNumbers title="Test Team Key - Allowed Access"
curl -i http://localhost:4000/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk-<key-from-previous-step>" \
-d '{
"model": "gpt-4",
"messages": [
{"role": "user", "content": "Hello"}
]
}'
```
</TabItem>
<TabItem label="Disallowed Access" value = "not-allowed">
:::info
Expect this to fail since gpt-4o is not in the `beta-models` access group
:::
```bash showLineNumbers title="Test Team Key - Disallowed Access"
curl -i http://localhost:4000/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk-<key-from-previous-step>" \
-d '{
"model": "gpt-4o",
"messages": [
{"role": "user", "content": "Hello"}
]
}'
```
</TabItem>
</Tabs>
</TabItem>
</Tabs>
### ✨ Control Access on Wildcard Models
Control access to all models with a specific prefix (e.g. `openai/*`).
Use this to also give users access to all models, except for a few that you don't want them to use (e.g. `openai/o1-*`).
:::info
Setting model access groups on wildcard models is an Enterprise feature.
See pricing [here](https://litellm.ai/#pricing)
Get a trial key [here](https://litellm.ai/#trial)
:::
1. Setup config.yaml
```yaml showLineNumbers title="config.yaml - Wildcard Models"
model_list:
- model_name: openai/*
litellm_params:
model: openai/*
api_key: os.environ/OPENAI_API_KEY
model_info:
access_groups: ["default-models"]
- model_name: openai/o1-*
litellm_params:
model: openai/o1-*
api_key: os.environ/OPENAI_API_KEY
model_info:
access_groups: ["restricted-models"]
```
2. Generate a key with access to `default-models`
```bash showLineNumbers title="Generate Key for Wildcard Access Group"
curl -L -X POST 'http://0.0.0.0:4000/key/generate' \
-H 'Authorization: Bearer sk-1234' \
-H 'Content-Type: application/json' \
-d '{
"models": ["default-models"],
}'
```
3. Test the key
<Tabs>
<TabItem label="Successful Request" value = "success">
```bash showLineNumbers title="Test Wildcard Access - Allowed"
curl -i http://localhost:4000/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk-<key-from-previous-step>" \
-d '{
"model": "openai/gpt-4",
"messages": [
{"role": "user", "content": "Hello"}
]
}'
```
</TabItem>
<TabItem value="bad-request" label="Rejected Request">
```bash showLineNumbers title="Test Wildcard Access - Rejected"
curl -i http://localhost:4000/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk-<key-from-previous-step>" \
-d '{
"model": "openai/o1-mini",
"messages": [
{"role": "user", "content": "Hello"}
]
}'
```
</TabItem>
</Tabs>
## Managing Access Groups via API
:::warning Database Models Only
Access group management APIs only work with models stored in the database (added via `/model/new`).
Models defined in `config.yaml` cannot be managed through these APIs and must be configured directly in the config file.
:::
Use the access group management endpoints to dynamically create, update, and delete access groups without restarting the proxy.
### Tutorial: Complete Access Group Workflow
This tutorial shows how to create an access group, view its details, attach it to a key, and update the models in the group.
**Prerequisites:**
- Models must be added to the database first (not just in config.yaml)
- You need your master key for authorization
#### Step 1: Add Models to Database
First, add some models to the database:
```bash showLineNumbers title="Add Models to Database"
# Add GPT-4 to database
curl -X POST 'http://localhost:4000/model/new' \
-H 'Authorization: Bearer sk-1234' \
-H 'Content-Type: application/json' \
-d '{
"model_name": "gpt-4",
"litellm_params": {
"model": "gpt-4",
"api_key": "os.environ/OPENAI_API_KEY"
}
}'
# Add Claude to database
curl -X POST 'http://localhost:4000/model/new' \
-H 'Authorization: Bearer sk-1234' \
-H 'Content-Type: application/json' \
-d '{
"model_name": "claude-3-opus",
"litellm_params": {
"model": "claude-3-opus-20240229",
"api_key": "os.environ/ANTHROPIC_API_KEY"
}
}'
```
#### Step 2: Create Access Group
Create an access group containing multiple models:
```bash showLineNumbers title="Create Access Group"
curl -X POST 'http://localhost:4000/access_group/new' \
-H 'Authorization: Bearer sk-1234' \
-H 'Content-Type: application/json' \
-d '{
"access_group": "production-models",
"model_names": ["gpt-4", "claude-3-opus"]
}'
```
**Response:**
```json showLineNumbers title="Response"
{
"access_group": "production-models",
"model_names": ["gpt-4", "claude-3-opus"],
"models_updated": 2
}
```
#### Step 3: View Access Group Info
Check the access group details:
```bash showLineNumbers title="Get Access Group Info"
curl -X GET 'http://localhost:4000/access_group/production-models/info' \
-H 'Authorization: Bearer sk-1234'
```
**Response:**
```json showLineNumbers title="Response"
{
"access_group": "production-models",
"model_names": ["gpt-4", "claude-3-opus"],
"deployment_count": 2
}
```
#### Step 4: Create Key with Access Group
Create an API key that can access all models in the group:
```bash showLineNumbers title="Create Key with Access Group"
curl -X POST 'http://localhost:4000/key/generate' \
-H 'Authorization: Bearer sk-1234' \
-H 'Content-Type: application/json' \
-d '{
"models": ["production-models"],
"max_budget": 100
}'
```
**Response:**
```json showLineNumbers title="Response"
{
"key": "sk-...",
"models": ["production-models"]
}
```
**Test the key:**
```bash showLineNumbers title="Test Key Access"
# This succeeds - gpt-4 is in production-models
curl -X POST 'http://localhost:4000/v1/chat/completions' \
-H 'Authorization: Bearer sk-...' \
-H 'Content-Type: application/json' \
-d '{
"model": "gpt-4",
"messages": [{"role": "user", "content": "Hello"}]
}'
# This succeeds - claude-3-opus is in production-models
curl -X POST 'http://localhost:4000/v1/chat/completions' \
-H 'Authorization: Bearer sk-...' \
-H 'Content-Type: application/json' \
-d '{
"model": "claude-3-opus",
"messages": [{"role": "user", "content": "Hello"}]
}'
```
#### Step 5: Update Access Group
Add or remove models from the access group:
```bash showLineNumbers title="Update Access Group"
curl -X PUT 'http://localhost:4000/access_group/production-models/update' \
-H 'Authorization: Bearer sk-1234' \
-H 'Content-Type: application/json' \
-d '{
"model_names": ["gpt-4", "claude-3-opus", "gemini-pro"]
}'
```
**Response:**
```json showLineNumbers title="Response"
{
"access_group": "production-models",
"model_names": ["gpt-4", "claude-3-opus", "gemini-pro"],
"models_updated": 3
}
```
The API key from Step 4 now automatically has access to `gemini-pro` without any changes to the key itself.
### API Reference - Access Group Management
For complete API documentation including all endpoints, parameters, and response schemas, see the [Access Group Management API Reference](https://litellm-api.up.railway.app/#/model%20management/create_model_group_access_group_new_post).
## Managing Access Groups via UI
You can also manage access groups through the LiteLLM Admin UI.
### Step 1: Add Model to Access Group
When adding a model to the database, assign it to an access group using the "Model Access Group" field:
![Add Model with Access Group](../../img/add_model_access.png)
In this example, `gpt-4` is added to the `production-models` access group.
### Step 2: Create Key with Access Group
When creating an API key, specify the access group in the "Models" field:
![Create Key with Access Group](../../img/add_model_key.png)
The key will have access to all models in the `production-models` group.
### Step 3: Test the Key
Use the generated key to make requests:
```bash showLineNumbers title="Test Key with Access Group"
# This succeeds - gpt-4 is in production-models
curl -X POST 'http://localhost:4000/v1/chat/completions' \
-H 'Authorization: Bearer sk-...' \
-H 'Content-Type: application/json' \
-d '{
"model": "gpt-4",
"messages": [{"role": "user", "content": "Hello"}]
}'
```
**Response:**
```json showLineNumbers title="Success Response"
{
"id": "chatcmpl-...",
"object": "chat.completion",
"created": 1234567890,
"model": "gpt-4",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "Hello! How can I help you today?"
},
"finish_reason": "stop"
}
]
}
```
If you try to access a model not in the access group, the request will be rejected:
```bash showLineNumbers title="Test Rejected Request"
# This fails - gpt-4o is not in production-models
curl -X POST 'http://localhost:4000/v1/chat/completions' \
-H 'Authorization: Bearer sk-...' \
-H 'Content-Type: application/json' \
-d '{
"model": "gpt-4o",
"messages": [{"role": "user", "content": "Hello"}]
}'
```
**Response:**
```json showLineNumbers title="Error Response"
{
"error": {
"message": "Invalid model for key",
"type": "invalid_request_error"
}
}
```
@@ -85,4 +85,9 @@ litellm_settings:
fallbacks: [{"my-custom-model": ["my-other-model"]}]
```
Fallbacks are done sequentially, so the first model group in the list will be tried first. If it fails, the next model group will be tried.
Fallbacks are done sequentially, so the first model group in the list will be tried first. If it fails, the next model group will be tried.
## Advanced: Model Access Groups
For advanced use cases, use [Model Access Groups](./model_access_groups) to dynamically group multiple models and manage access without restarting the proxy.
@@ -1,8 +1,21 @@
# Syncing Models to GitHub model_context_window
# Auto Sync New Models (Day-0 Launches)
Sync model pricing data from GitHub's `model_prices_and_context_window.json` file outside of the LiteLLM UI.
Automatically keep your model pricing and context window data up to date without restarting your service. **This allows you to add day-0 support for new models without restarting your service.**
> **📹 Video Tutorial**: [Watch how to sync models via the Admin UI](https://www.loom.com/share/ba41acc1882d41b284bbddbb0e9c27ce?sid=bdae351e-2026-4e39-932b-fcb185ff612c)
## Overview
When providers like OpenAI or Anthropic release new models (e.g., GPT-5, Claude 4), you typically need to restart your LiteLLM service to get the latest pricing and context window data.
With auto-sync, LiteLLM automatically pulls the latest model data from GitHub's [`model_prices_and_context_window.json`](https://github.com/BerriAI/litellm/blob/main/model_prices_and_context_window.json) without requiring a restart. This means:
- **Zero downtime** when new models are released
- **Always accurate pricing** for cost tracking and budgets
- **Automatic updates** - set it once and forget it
<iframe width="840" height="500" src="https://www.loom.com/embed/ba41acc1882d41b284bbddbb0e9c27ce?sid=bdae351e-2026-4e39-932b-fcb185ff612c" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>
<br/>
<br/>
## Quick Start
Binary file not shown.

After

Width:  |  Height:  |  Size: 576 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 514 KiB

+4 -40
View File
@@ -10296,18 +10296,6 @@
"node": ">=8.0.0"
}
},
"node_modules/esprima": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
"bin": {
"esparse": "bin/esparse.js",
"esvalidate": "bin/esvalidate.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/esrecurse": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
@@ -11092,26 +11080,6 @@
"node": ">=6.0"
}
},
"node_modules/gray-matter/node_modules/argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"dependencies": {
"sprintf-js": "~1.0.2"
}
},
"node_modules/gray-matter/node_modules/js-yaml": {
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
"dependencies": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
},
"bin": {
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/hachure-fill": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz",
@@ -12148,9 +12116,10 @@
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
},
"node_modules/js-yaml": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
},
@@ -19035,11 +19004,6 @@
"node": ">= 6"
}
},
"node_modules/sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="
},
"node_modules/srcset": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/srcset/-/srcset-4.0.0.tgz",
+2 -1
View File
@@ -50,6 +50,7 @@
"overrides": {
"webpack-dev-server": ">=5.2.1",
"form-data": ">=4.0.4",
"mermaid": ">=11.10.0"
"mermaid": ">=11.10.0",
"js-yaml": ">=4.1.1"
}
}
+10 -2
View File
@@ -31,9 +31,16 @@ const sidebars = {
label: "Guardrails",
items: [
"proxy/guardrails/quick_start",
{
type: "category",
"label": "Contributing to Guardrails",
items: [
"adding_provider/simple_guardrail_tutorial",
"adding_provider/adding_guardrail_support",
]
},
"proxy/guardrails/test_playground",
...[
"adding_provider/adding_guardrail_support",
"proxy/guardrails/aim_security",
"proxy/guardrails/aporia_api",
"proxy/guardrails/azure_content_guardrail",
@@ -252,6 +259,7 @@ const sidebars = {
items: [
"proxy/model_access_guide",
"proxy/model_access",
"proxy/model_access_groups",
"proxy/team_model_add"
]
},
@@ -277,6 +285,7 @@ const sidebars = {
items: [
"proxy/cost_tracking",
"proxy/custom_pricing",
"proxy/sync_models_github",
"proxy/billing",
],
},
@@ -488,7 +497,6 @@ const sidebars = {
"providers/vertex_ai/videos",
"providers/vertex_partner",
"providers/vertex_self_deployed",
"providers/vertex_embedding",
"providers/vertex_image",
"providers/vertex_batch",
"providers/vertex_ocr",
Binary file not shown.
@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "LiteLLM_DailyTagSpend" ADD COLUMN "request_id" TEXT;
+2 -2
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "litellm-proxy-extras"
version = "0.4.3"
version = "0.4.4"
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.4.3"
version = "0.4.4"
version_files = [
"pyproject.toml:version",
"../requirements.txt:litellm-proxy-extras==",
+11 -1
View File
@@ -485,6 +485,7 @@ vertex_ai_ai21_models: Set = set()
vertex_mistral_models: Set = set()
vertex_openai_models: Set = set()
vertex_minimax_models: Set = set()
vertex_moonshot_models: Set = set()
ai21_models: Set = set()
ai21_chat_models: Set = set()
nlp_cloud_models: Set = set()
@@ -500,6 +501,7 @@ watsonx_models: Set = set()
gemini_models: Set = set()
xai_models: Set = set()
deepseek_models: Set = set()
runwayml_models: Set = set()
azure_ai_models: Set = set()
jina_ai_models: Set = set()
voyage_models: Set = set()
@@ -648,6 +650,9 @@ def add_known_models():
elif value.get("litellm_provider") == "vertex_ai-minimax_models":
key = key.replace("vertex_ai/", "")
vertex_minimax_models.add(key)
elif value.get("litellm_provider") == "vertex_ai-moonshot_models":
key = key.replace("vertex_ai/", "")
vertex_moonshot_models.add(key)
elif value.get("litellm_provider") == "ai21":
if value.get("mode") == "chat":
ai21_chat_models.add(key)
@@ -687,6 +692,8 @@ def add_known_models():
fal_ai_models.add(key)
elif value.get("litellm_provider") == "deepseek":
deepseek_models.add(key)
elif value.get("litellm_provider") == "runwayml":
runwayml_models.add(key)
elif value.get("litellm_provider") == "meta_llama":
llama_models.add(key)
elif value.get("litellm_provider") == "nscale":
@@ -830,6 +837,7 @@ model_list = list(
| deepinfra_models
| perplexity_models
| set(maritalk_models)
| runwayml_models
| vertex_language_models
| watsonx_models
| gemini_models
@@ -904,7 +912,8 @@ models_by_provider: dict = {
| vertex_vision_models
| vertex_language_models
| vertex_deepseek_models
| vertex_minimax_models,
| vertex_minimax_models
| vertex_moonshot_models,
"ai21": ai21_models,
"bedrock": bedrock_models | bedrock_converse_models,
"petals": petals_models,
@@ -921,6 +930,7 @@ models_by_provider: dict = {
"xai": xai_models,
"fal_ai": fal_ai_models,
"deepseek": deepseek_models,
"runwayml": runwayml_models,
"mistral": mistral_chat_models,
"azure_ai": azure_ai_models,
"voyage": voyage_models,
@@ -544,7 +544,9 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
return Reasoning(**reasoning_effort) # type: ignore[typeddict-item]
# If string is passed, map without summary (default)
if reasoning_effort == "high":
if reasoning_effort == "none":
return Reasoning(effort="none") # type: ignore
elif reasoning_effort == "high":
return Reasoning(effort="high")
elif reasoning_effort == "medium":
return Reasoning(effort="medium")
+3
View File
@@ -400,6 +400,8 @@ def image_generation( # noqa: PLR0915
or custom_llm_provider == LlmProviders.LITELLM_PROXY.value
or custom_llm_provider in litellm.openai_compatible_providers
):
# Forward OpenAI organization if present (set by proxy pre-call utils)
organization: Optional[str] = kwargs.get("organization", None)
model_response = openai_chat_completions.image_generation(
model=model,
prompt=prompt,
@@ -409,6 +411,7 @@ def image_generation( # noqa: PLR0915
logging_obj=litellm_logging_obj,
optional_params=optional_params,
model_response=model_response,
organization=organization,
aimg_generation=aimg_generation,
client=client,
)
@@ -152,32 +152,27 @@ class LiteLLMMessagesToCompletionTransformationHandler:
)
)
try:
completion_response = await litellm.acompletion(**completion_kwargs)
completion_response = await litellm.acompletion(**completion_kwargs)
if stream:
transformed_stream = (
ANTHROPIC_ADAPTER.translate_completion_output_params_streaming(
completion_response,
model=model,
)
if stream:
transformed_stream = (
ANTHROPIC_ADAPTER.translate_completion_output_params_streaming(
completion_response,
model=model,
)
if transformed_stream is not None:
return transformed_stream
raise ValueError("Failed to transform streaming response")
else:
anthropic_response = (
ANTHROPIC_ADAPTER.translate_completion_output_params(
cast(ModelResponse, completion_response)
)
)
if anthropic_response is not None:
return anthropic_response
raise ValueError("Failed to transform response to Anthropic format")
except Exception as e: # noqa: BLE001
raise ValueError(
f"Error calling litellm.acompletion for non-Anthropic model: {str(e)}"
)
if transformed_stream is not None:
return transformed_stream
raise ValueError("Failed to transform streaming response")
else:
anthropic_response = (
ANTHROPIC_ADAPTER.translate_completion_output_params(
cast(ModelResponse, completion_response)
)
)
if anthropic_response is not None:
return anthropic_response
raise ValueError("Failed to transform response to Anthropic format")
@staticmethod
def anthropic_messages_handler(
@@ -239,29 +234,24 @@ class LiteLLMMessagesToCompletionTransformationHandler:
)
)
try:
completion_response = litellm.completion(**completion_kwargs)
completion_response = litellm.completion(**completion_kwargs)
if stream:
transformed_stream = (
ANTHROPIC_ADAPTER.translate_completion_output_params_streaming(
completion_response,
model=model,
)
if stream:
transformed_stream = (
ANTHROPIC_ADAPTER.translate_completion_output_params_streaming(
completion_response,
model=model,
)
if transformed_stream is not None:
return transformed_stream
raise ValueError("Failed to transform streaming response")
else:
anthropic_response = (
ANTHROPIC_ADAPTER.translate_completion_output_params(
cast(ModelResponse, completion_response)
)
)
if anthropic_response is not None:
return anthropic_response
raise ValueError("Failed to transform response to Anthropic format")
except Exception as e: # noqa: BLE001
raise ValueError(
f"Error calling litellm.completion for non-Anthropic model: {str(e)}"
)
if transformed_stream is not None:
return transformed_stream
raise ValueError("Failed to transform streaming response")
else:
anthropic_response = (
ANTHROPIC_ADAPTER.translate_completion_output_params(
cast(ModelResponse, completion_response)
)
)
if anthropic_response is not None:
return anthropic_response
raise ValueError("Failed to transform response to Anthropic format")
+14 -3
View File
@@ -6,6 +6,7 @@ from httpx import Headers, Response
from litellm.llms.base_llm.batches.transformation import BaseBatchesConfig
from litellm.llms.base_llm.chat.transformation import BaseLLMException
from litellm.secret_managers.main import get_secret_str
from litellm.types.llms.bedrock import (
BedrockCreateBatchRequest,
BedrockCreateBatchResponse,
@@ -140,10 +141,20 @@ class BedrockBatchesConfig(BaseAWSLLM, BaseBatchesConfig):
}
# Build output data config
s3_output_config: BedrockS3OutputDataConfig = BedrockS3OutputDataConfig(
s3Uri=f"s3://{output_bucket}/{output_key}"
)
# Add optional KMS encryption key ID if provided
s3_encryption_key_id = (
litellm_params.get("s3_encryption_key_id")
or get_secret_str("AWS_S3_ENCRYPTION_KEY_ID")
)
if s3_encryption_key_id:
s3_output_config["s3EncryptionKeyId"] = s3_encryption_key_id
output_data_config: BedrockOutputDataConfig = {
"s3OutputDataConfig": BedrockS3OutputDataConfig(
s3Uri=f"s3://{output_bucket}/{output_key}"
)
"s3OutputDataConfig": s3_output_config
}
# Create Bedrock batch request with proper typing
@@ -19,21 +19,151 @@ if TYPE_CHECKING:
class AgentCoreSSEStreamIterator:
"""Async iterator for AgentCore SSE streaming responses."""
"""Iterator for AgentCore SSE streaming responses. Supports both sync and async iteration."""
def __init__(self, response: httpx.Response, model: str):
self.response = response
self.model = model
self.finished = False
self.line_iterator = self.response.aiter_lines()
self.line_iterator = None
self.async_line_iterator = None
def __aiter__(self):
def __iter__(self):
"""Initialize sync iteration."""
self.line_iterator = self.response.iter_lines()
return self
async def __anext__(self) -> ModelResponse:
"""Parse SSE events and yield ModelResponse chunks."""
def __aiter__(self):
"""Initialize async iteration."""
self.async_line_iterator = self.response.aiter_lines()
return self
def __next__(self) -> ModelResponse:
"""Sync iteration - parse SSE events and yield ModelResponse chunks."""
try:
async for line in self.line_iterator:
if self.line_iterator is None:
raise StopIteration
for line in self.line_iterator:
line = line.strip()
if not line or not line.startswith('data:'):
continue
# Extract JSON from SSE line
json_str = line[5:].strip()
if not json_str:
continue
try:
data = json.loads(json_str)
# Skip non-dict data
if not isinstance(data, dict):
continue
# Process content delta events
if "event" in data and isinstance(data["event"], dict):
event_payload = data["event"]
content_block_delta = event_payload.get("contentBlockDelta")
if content_block_delta:
delta = content_block_delta.get("delta", {})
text = delta.get("text", "")
if text:
# Yield chunk with text
chunk = ModelResponse(
id=f"chatcmpl-{uuid.uuid4()}",
created=0,
model=self.model,
object="chat.completion.chunk",
)
chunk.choices = [
StreamingChoices(
finish_reason=None,
index=0,
delta=Delta(content=text, role="assistant"),
)
]
return chunk
# Check for metadata/usage
metadata = event_payload.get("metadata")
if metadata and "usage" in metadata:
# This is the final chunk with usage
chunk = ModelResponse(
id=f"chatcmpl-{uuid.uuid4()}",
created=0,
model=self.model,
object="chat.completion.chunk",
)
chunk.choices = [
StreamingChoices(
finish_reason="stop",
index=0,
delta=Delta(),
)
]
usage_data: AgentCoreUsage = metadata["usage"] # type: ignore
setattr(chunk, "usage", Usage(
prompt_tokens=usage_data.get("inputTokens", 0),
completion_tokens=usage_data.get("outputTokens", 0),
total_tokens=usage_data.get("totalTokens", 0),
))
self.finished = True
return chunk
# Check for final message (alternative finish signal)
if "message" in data and isinstance(data["message"], dict):
if not self.finished:
chunk = ModelResponse(
id=f"chatcmpl-{uuid.uuid4()}",
created=0,
model=self.model,
object="chat.completion.chunk",
)
chunk.choices = [
StreamingChoices(
finish_reason="stop",
index=0,
delta=Delta(),
)
]
self.finished = True
return chunk
except json.JSONDecodeError:
verbose_logger.debug(f"Skipping non-JSON SSE line: {line[:100]}")
continue
# Stream ended naturally
raise StopIteration
except StopIteration:
raise
except httpx.StreamConsumed:
# This is expected when the stream has been fully consumed
raise StopIteration
except httpx.StreamClosed:
# This is expected when the stream is closed
raise StopIteration
except Exception as e:
verbose_logger.error(f"Error in AgentCore SSE stream: {str(e)}")
raise StopIteration
async def __anext__(self) -> ModelResponse:
"""Async iteration - parse SSE events and yield ModelResponse chunks."""
try:
if self.async_line_iterator is None:
raise StopAsyncIteration
async for line in self.async_line_iterator:
line = line.strip()
if not line or not line.startswith('data:'):
+2
View File
@@ -3,6 +3,7 @@ from .image_generation import (
FalAIBaseConfig,
FalAIBriaConfig,
FalAIFluxProV11UltraConfig,
FalAIFluxSchnellConfig,
FalAIImageGenerationConfig,
FalAIImagen4Config,
FalAIRecraftV3Config,
@@ -18,6 +19,7 @@ __all__ = [
"FalAIRecraftV3Config",
"FalAIBriaConfig",
"FalAIFluxProV11UltraConfig",
"FalAIFluxSchnellConfig",
"FalAIStableDiffusionConfig",
"get_fal_ai_image_generation_config",
]
@@ -4,6 +4,7 @@ from litellm.llms.base_llm.image_generation.transformation import (
from .bria_transformation import FalAIBriaConfig
from .flux_pro_v11_ultra_transformation import FalAIFluxProV11UltraConfig
from .flux_schnell_transformation import FalAIFluxSchnellConfig
from .imagen4_transformation import FalAIImagen4Config
from .recraft_v3_transformation import FalAIRecraftV3Config
from .stable_diffusion_transformation import FalAIStableDiffusionConfig
@@ -16,6 +17,7 @@ __all__ = [
"FalAIRecraftV3Config",
"FalAIBriaConfig",
"FalAIFluxProV11UltraConfig",
"FalAIFluxSchnellConfig",
"FalAIStableDiffusionConfig",
]
@@ -41,6 +43,8 @@ def get_fal_ai_image_generation_config(model: str) -> BaseImageGenerationConfig:
return FalAIBriaConfig()
elif "flux-pro" in model_lower and "ultra" in model_lower:
return FalAIFluxProV11UltraConfig()
elif "flux/schnell" in model_lower or "flux-schnell" in model_lower or "schnell" in model_lower:
return FalAIFluxSchnellConfig()
elif "stable-diffusion" in model_lower:
return FalAIStableDiffusionConfig()
@@ -0,0 +1,88 @@
from typing import Any
from .flux_pro_v11_ultra_transformation import FalAIFluxProV11UltraConfig
class FalAIFluxSchnellConfig(FalAIFluxProV11UltraConfig):
"""
Configuration for Fal AI Flux Schnell model.
Flux Schnell shares the same response format as Flux Pro models but expects
the OpenAI `size` parameter to be translated into Fal AI's `image_size`
enum/object.
Model endpoint: fal-ai/flux/schnell
Documentation: https://fal.ai/models/fal-ai/flux/schnell
"""
IMAGE_GENERATION_ENDPOINT: str = "fal-ai/flux/schnell"
_OPENAI_SIZE_TO_IMAGE_SIZE = {
"1024x1024": "square_hd",
"512x512": "square",
"1792x1024": "landscape_16_9",
"1024x1792": "portrait_16_9",
"1024x768": "landscape_4_3",
"768x1024": "portrait_4_3",
}
def map_openai_params(
self,
non_default_params: dict,
optional_params: dict,
model: str,
drop_params: bool,
) -> dict:
supported_params = self.get_supported_openai_params(model)
param_mapping = {
"n": "num_images",
"response_format": "output_format",
"size": "image_size",
}
for k in non_default_params.keys():
if k not in optional_params.keys():
if k in supported_params:
mapped_key = param_mapping.get(k, k)
mapped_value = non_default_params[k]
if k == "response_format":
if mapped_value in ["b64_json", "url"]:
mapped_value = "jpeg"
elif k == "size":
mapped_value = self._map_image_size(mapped_value)
optional_params[mapped_key] = mapped_value
elif drop_params:
continue
else:
raise ValueError(
f"Parameter {k} is not supported for model {model}. "
f"Supported parameters are {supported_params}. "
"Set drop_params=True to drop unsupported parameters."
)
return optional_params
def _map_image_size(self, size: Any) -> Any:
if isinstance(size, dict):
return size
if not isinstance(size, str):
return size
if size in self._OPENAI_SIZE_TO_IMAGE_SIZE:
return self._OPENAI_SIZE_TO_IMAGE_SIZE[size]
if "x" in size:
try:
width_str, height_str = size.split("x")
width = int(width_str)
height = int(height_str)
return {"width": width, "height": height}
except (ValueError, AttributeError, ZeroDivisionError):
pass
return "landscape_4_3"
@@ -23,7 +23,7 @@ class FalAIImagen4Config(FalAIBaseConfig):
Model variants:
- fal-ai/imagen4/preview (Standard): $0.05 per image
- fal-ai/imagen4/preview/fast (Fast): $0.04 per image
- fal-ai/imagen4/preview/fast (Fast): $0.02 per image
- fal-ai/imagen4/preview/ultra (Ultra): $0.06 per image
Documentation: https://fal.ai/models/fal-ai/imagen4/preview
+53 -8
View File
@@ -1,9 +1,27 @@
"""
Translate from OpenAI's `/v1/chat/completions` to Groq's `/v1/chat/completions`
"""
from typing import Any, Coroutine, List, Literal, Optional, Tuple, Union, cast, overload
from typing import (
Any,
Coroutine,
List,
Literal,
Optional,
Tuple,
Union,
cast,
overload,
Iterator,
AsyncIterator,
)
import httpx
from litellm.llms.openai.chat.gpt_transformation import (
OpenAIChatCompletionStreamingHandler,
)
from litellm.llms.openai.common_utils import OpenAIError
from pydantic import BaseModel
import litellm
@@ -16,7 +34,7 @@ from litellm.types.llms.openai import (
ChatCompletionToolParam,
ChatCompletionToolParamFunctionChunk,
)
from litellm.types.utils import ModelResponse
from litellm.types.utils import ModelResponse, ModelResponseStream
from ...openai_like.chat.transformation import OpenAILikeChatConfig
@@ -65,6 +83,18 @@ class GroqChatConfig(OpenAILikeChatConfig):
def get_config(cls):
return super().get_config()
def get_model_response_iterator(
self,
streaming_response: Union[Iterator[str], AsyncIterator[str], ModelResponse],
sync_stream: bool,
json_mode: Optional[bool] = False,
) -> Any:
return GroqChatCompletionStreamingHandler(
streaming_response=streaming_response,
sync_stream=sync_stream,
json_mode=json_mode,
)
def get_supported_openai_params(self, model: str) -> list:
base_params = super().get_supported_openai_params(model)
try:
@@ -209,7 +239,6 @@ class GroqChatConfig(OpenAILikeChatConfig):
)
return optional_params
def transform_response(
self,
@@ -239,12 +268,17 @@ class GroqChatConfig(OpenAILikeChatConfig):
json_mode=json_mode,
)
mapped_service_tier: Literal["auto", "default", "flex"] = self._map_groq_service_tier(original_service_tier=getattr(model_response, "service_tier"))
mapped_service_tier: Literal[
"auto", "default", "flex"
] = self._map_groq_service_tier(
original_service_tier=getattr(model_response, "service_tier")
)
setattr(model_response, "service_tier", mapped_service_tier)
return model_response
def _map_groq_service_tier(self, original_service_tier: Optional[str]) -> Literal["auto", "default", "flex"]:
def _map_groq_service_tier(
self, original_service_tier: Optional[str]
) -> Literal["auto", "default", "flex"]:
"""
Ensure groq service tier is OpenAI compatible.
"""
@@ -252,5 +286,16 @@ class GroqChatConfig(OpenAILikeChatConfig):
return "auto"
if original_service_tier not in ["auto", "default", "flex"]:
return "auto"
return cast(Literal["auto", "default", "flex"], original_service_tier)
return cast(Literal["auto", "default", "flex"], original_service_tier)
class GroqChatCompletionStreamingHandler(OpenAIChatCompletionStreamingHandler):
def chunk_parser(self, chunk: dict) -> ModelResponseStream:
error = chunk.get("error")
if error:
raise OpenAIError(
status_code=error.get("code"), message=error.get("message"), body=error
)
return super().chunk_parser(chunk)
+5 -1
View File
@@ -1285,6 +1285,7 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM):
api_base: Optional[str] = None,
client=None,
max_retries=None,
organization: Optional[str] = None,
):
response = None
try:
@@ -1294,6 +1295,7 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM):
api_base=api_base,
timeout=timeout,
max_retries=max_retries,
organization=organization,
client=client,
)
@@ -1328,6 +1330,7 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM):
model_response: Optional[ImageResponse] = None,
client=None,
aimg_generation=None,
organization: Optional[str] = None,
) -> ImageResponse:
data = {}
try:
@@ -1337,7 +1340,7 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM):
raise OpenAIError(status_code=422, message="max retries must be an int")
if aimg_generation is True:
return self.aimage_generation(data=data, prompt=prompt, logging_obj=logging_obj, model_response=model_response, api_base=api_base, api_key=api_key, timeout=timeout, client=client, max_retries=max_retries) # type: ignore
return self.aimage_generation(data=data, prompt=prompt, logging_obj=logging_obj, model_response=model_response, api_base=api_base, api_key=api_key, timeout=timeout, client=client, max_retries=max_retries, organization=organization) # type: ignore
openai_client: OpenAI = self._get_openai_client( # type: ignore
is_async=False,
@@ -1345,6 +1348,7 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM):
api_base=api_base,
timeout=timeout,
max_retries=max_retries,
organization=organization,
client=client,
)
+32 -1
View File
@@ -4,9 +4,13 @@ Sambanova Chat Completions API
this is OpenAI compatible - no translation needed / occurs
"""
from typing import Optional, Union
from typing import Any, Coroutine, List, Literal, Optional, Union, overload
from litellm.litellm_core_utils.prompt_templates.common_utils import (
handle_messages_with_content_list_to_str_conversion,
)
from litellm.llms.openai.chat.gpt_transformation import OpenAIGPTConfig
from litellm.types.llms.openai import AllMessageValues
class SambanovaConfig(OpenAIGPTConfig):
@@ -92,3 +96,30 @@ class SambanovaConfig(OpenAIGPTConfig):
elif param in supported_openai_params:
optional_params[param] = value
return optional_params
@overload
def _transform_messages(
self, messages: List[AllMessageValues], model: str, is_async: Literal[True]
) -> Coroutine[Any, Any, List[AllMessageValues]]:
...
@overload
def _transform_messages(
self,
messages: List[AllMessageValues],
model: str,
is_async: Literal[False] = False,
) -> List[AllMessageValues]:
...
def _transform_messages(
self, messages: List[AllMessageValues], model: str, is_async: bool = False
) -> Union[List[AllMessageValues], Coroutine[Any, Any, List[AllMessageValues]]]:
"""
Transform messages to handle content list conversion.
SambaNova API doesn't support content as a list - only string content.
This converts content lists like [{"type": "text", "text": "..."}] to strings.
"""
messages = handle_messages_with_content_list_to_str_conversion(messages)
return messages
@@ -61,10 +61,6 @@ class VertexAIBatchPrediction(VertexLLM):
stream=None,
auth_header=None,
url=default_api_base,
model=None,
vertex_project=vertex_project or project_id,
vertex_location=vertex_location or "us-central1",
vertex_api_version="v1",
)
headers = {
@@ -170,10 +166,6 @@ class VertexAIBatchPrediction(VertexLLM):
stream=None,
auth_header=None,
url=default_api_base,
model=None,
vertex_project=vertex_project or project_id,
vertex_location=vertex_location or "us-central1",
vertex_api_version="v1",
)
headers = {
+7 -88
View File
@@ -31,11 +31,9 @@ class VertexAIModelRoute(str, Enum):
PARTNER_MODELS = "partner_models"
GEMINI = "gemini"
GEMMA = "gemma"
BGE = "bge"
MODEL_GARDEN = "model_garden"
NON_GEMINI = "non_gemini"
VERTEX_AI_MODEL_ROUTES = [f"{route.value}/" for route in VertexAIModelRoute]
def get_vertex_ai_model_route(
model: str, litellm_params: Optional[dict] = None
@@ -62,9 +60,6 @@ def get_vertex_ai_model_route(
>>> get_vertex_ai_model_route("openai/gpt-oss-120b")
VertexAIModelRoute.MODEL_GARDEN
>>> get_vertex_ai_model_route("1234567890", {"api_base": "http://10.96.32.8"})
VertexAIModelRoute.GEMINI # Numeric endpoints with api_base use HTTP path
"""
from litellm.llms.vertex_ai.vertex_ai_partner_models.main import (
VertexAIPartnerModels,
@@ -74,20 +69,11 @@ def get_vertex_ai_model_route(
if litellm_params and litellm_params.get("base_model") is not None:
if "gemini" in litellm_params["base_model"]:
return VertexAIModelRoute.GEMINI
# Check if numeric endpoint ID with custom api_base (PSC endpoint)
# Route to GEMINI (HTTP path) to support PSC endpoints properly
if model.isdigit() and litellm_params and litellm_params.get("api_base"):
return VertexAIModelRoute.GEMINI
# Check for partner models (llama, mistral, claude, etc.)
if VertexAIPartnerModels.is_vertex_partner_model(model=model):
return VertexAIModelRoute.PARTNER_MODELS
# Check for BGE models
if "bge/" in model or "bge" in model.lower():
return VertexAIModelRoute.BGE
# Check for gemma models
if "gemma/" in model:
return VertexAIModelRoute.GEMMA
@@ -150,71 +136,6 @@ all_gemini_url_modes = Literal[
]
def get_vertex_base_model_name(model: str) -> str:
"""
Strip routing prefixes from model name for PSC/endpoint URL construction.
Patterns like "bge/", "gemma/", "openai/" are used for internal routing but
should not appear in the actual endpoint URL. Routing prefixes are derived
from VertexAIModelRoute enum values.
Args:
model: The model name with potential prefix (e.g., "bge/123456", "gemma/gemma-3-12b-it")
Returns:
str: The model name without routing prefix (e.g., "123456", "gemma-3-12b-it")
Examples:
>>> get_vertex_base_model_name("bge/378943383978115072")
"378943383978115072"
>>> get_vertex_base_model_name("gemma/gemma-3-12b-it")
"gemma-3-12b-it"
>>> get_vertex_base_model_name("openai/gpt-oss-120b")
"gpt-oss-120b"
>>> get_vertex_base_model_name("1234567890")
"1234567890"
"""
# Derive routing prefixes from VertexAIModelRoute enum
# Map specific routes to their prefixes (some routes like PARTNER_MODELS, GEMINI don't have prefixes)
for route in VERTEX_AI_MODEL_ROUTES:
if model.startswith(route):
return model.replace(route, "", 1)
return model
def _get_embedding_url(
model: str,
vertex_project: Optional[str],
vertex_location: Optional[str],
vertex_api_version: Literal["v1", "v1beta1"],
) -> Tuple[str, str]:
"""
Get URL for embedding models.
Handles special patterns:
- bge/endpoint_id -> strips to endpoint_id for endpoints/ routing
- numeric model -> routes to endpoints/
- regular model -> routes to publishers/google/models/
"""
endpoint = "predict"
# Strip routing prefixes (bge/, gemma/, etc.) for endpoint URL construction
model = get_vertex_base_model_name(model=model)
url = f"https://{vertex_location}-aiplatform.googleapis.com/v1/projects/{vertex_project}/locations/{vertex_location}/publishers/google/models/{model}:{endpoint}"
if model.isdigit():
# https://us-central1-aiplatform.googleapis.com/v1/projects/$PROJECT_ID/locations/us-central1/endpoints/$ENDPOINT_ID:predict
url = f"https://{vertex_location}-aiplatform.googleapis.com/{vertex_api_version}/projects/{vertex_project}/locations/{vertex_location}/endpoints/{model}:{endpoint}"
return url, endpoint
def _get_vertex_url(
mode: all_gemini_url_modes,
model: str,
@@ -227,7 +148,6 @@ def _get_vertex_url(
endpoint: Optional[str] = None
model = litellm.VertexGeminiConfig.get_model_for_vertex_ai_url(model=model)
if mode == "chat":
### SET RUNTIME ENDPOINT ###
endpoint = "generateContent"
@@ -252,12 +172,11 @@ def _get_vertex_url(
if stream is True:
url += "?alt=sse"
elif mode == "embedding":
return _get_embedding_url(
model=model,
vertex_project=vertex_project,
vertex_location=vertex_location,
vertex_api_version=vertex_api_version,
)
endpoint = "predict"
url = f"https://{vertex_location}-aiplatform.googleapis.com/v1/projects/{vertex_project}/locations/{vertex_location}/publishers/google/models/{model}:{endpoint}"
if model.isdigit():
# https://us-central1-aiplatform.googleapis.com/v1/projects/$PROJECT_ID/locations/us-central1/endpoints/$ENDPOINT_ID:predict
url = f"https://{vertex_location}-aiplatform.googleapis.com/{vertex_api_version}/projects/{vertex_project}/locations/{vertex_location}/endpoints/{model}:{endpoint}"
elif mode == "image_generation":
endpoint = "predict"
url = f"https://{vertex_location}-aiplatform.googleapis.com/v1/projects/{vertex_project}/locations/{vertex_location}/publishers/google/models/{model}:{endpoint}"
@@ -79,10 +79,6 @@ class ContextCachingEndpoints(VertexBase):
stream=None,
auth_header=auth_header,
url=url,
model=None,
vertex_project=vertex_project,
vertex_location=vertex_location,
vertex_api_version="v1beta1" if custom_llm_provider == "vertex_ai_beta" else "v1",
)
def check_cache(
@@ -39,6 +39,7 @@ class PartnerModelPrefixes(str, Enum):
QWEN_PREFIX = "qwen"
GPT_OSS_PREFIX = "openai/gpt-oss-"
MINIMAX_PREFIX = "minimaxai/"
MOONSHOT_PREFIX = "moonshotai/"
class VertexAIPartnerModels(VertexBase):
@@ -64,6 +65,7 @@ class VertexAIPartnerModels(VertexBase):
or model.startswith(PartnerModelPrefixes.QWEN_PREFIX)
or model.startswith(PartnerModelPrefixes.GPT_OSS_PREFIX)
or model.startswith(PartnerModelPrefixes.MINIMAX_PREFIX)
or model.startswith(PartnerModelPrefixes.MOONSHOT_PREFIX)
):
return True
return False
@@ -76,6 +78,7 @@ class VertexAIPartnerModels(VertexBase):
PartnerModelPrefixes.QWEN_PREFIX,
PartnerModelPrefixes.GPT_OSS_PREFIX,
PartnerModelPrefixes.MINIMAX_PREFIX,
PartnerModelPrefixes.MOONSHOT_PREFIX,
]
if any(provider in model for provider in OPENAI_LIKE_VERTEX_PROVIDERS):
return True
@@ -1,182 +0,0 @@
"""
Vertex AI BGE (BAAI General Embedding) Configuration
BGE models deployed on Vertex AI require different input/output format:
- Request: Use "prompt" instead of "content" as the input field
- Response: Embeddings are returned directly as arrays, not wrapped in objects
Model name handling:
- Model names like "bge/endpoint_id" are automatically transformed in common_utils._get_vertex_url()
- This module focuses on request/response transformation only
"""
from typing import List, Optional, Union
from litellm.types.utils import EmbeddingResponse, Usage
from .types import (
EmbeddingParameters,
TaskType,
TextEmbeddingBGEInput,
VertexEmbeddingRequest,
)
class VertexBGEConfig:
"""
Configuration and transformation logic for BGE models on Vertex AI.
BGE (BAAI General Embedding) models use a different request format
where the input field is named "prompt" instead of "content".
Supported model patterns (after provider split in main.py):
- "bge-small-en-v1.5" (model name)
- "bge/204379420394258432" (endpoint ID pattern)
Note: Model name transformation (bge/ -> numeric ID) is handled automatically
in common_utils._get_vertex_url(). This class focuses on request/response format only.
"""
@staticmethod
def is_bge_model(model: str) -> bool:
"""
Check if the model is a BGE (BAAI General Embedding) model.
After provider split in main.py, supports:
- "bge-small-en-v1.5" (model name)
- "bge/204379420394258432" (endpoint ID pattern)
Args:
model: The model name after provider split
Returns:
bool: True if the model is a BGE model
"""
model_lower = model.lower()
# Check for "bge/" prefix (endpoint pattern) or "bge" in model name
return model_lower.startswith("bge/") or "bge" in model_lower
@staticmethod
def transform_request(
input: Union[list, str], optional_params: dict, model: str
) -> VertexEmbeddingRequest:
"""
Transforms an OpenAI request to a Vertex BGE embedding request.
BGE models use "prompt" instead of "content" as the input field.
Args:
input: The input text(s) to embed
optional_params: Optional parameters for the request
model: The model name
Returns:
VertexEmbeddingRequest: The transformed request
"""
vertex_request: VertexEmbeddingRequest = VertexEmbeddingRequest()
vertex_text_embedding_input_list: List[TextEmbeddingBGEInput] = []
task_type: Optional[TaskType] = optional_params.get("task_type")
title = optional_params.get("title")
if isinstance(input, str):
input = [input]
for text in input:
embedding_input = VertexBGEConfig._create_embedding_input(
prompt=text, task_type=task_type, title=title
)
vertex_text_embedding_input_list.append(embedding_input)
vertex_request["instances"] = vertex_text_embedding_input_list
vertex_request["parameters"] = EmbeddingParameters(**optional_params)
return vertex_request
@staticmethod
def _create_embedding_input(
prompt: str,
task_type: Optional[TaskType] = None,
title: Optional[str] = None,
) -> TextEmbeddingBGEInput:
"""
Creates a TextEmbeddingBGEInput object for BGE models.
BGE models use "prompt" instead of "content" as the input field.
Args:
prompt: The prompt to be embedded
task_type: The type of task to be performed
title: The title of the document to be embedded
Returns:
TextEmbeddingBGEInput: A TextEmbeddingBGEInput object
"""
text_embedding_input = TextEmbeddingBGEInput(prompt=prompt)
if task_type is not None:
text_embedding_input["task_type"] = task_type
if title is not None:
text_embedding_input["title"] = title
return text_embedding_input
@staticmethod
def transform_response(
response: dict, model: str, model_response: EmbeddingResponse
) -> EmbeddingResponse:
"""
Transforms a Vertex BGE embedding response to OpenAI format.
BGE models return embeddings directly as arrays in predictions:
{
"predictions": [
[0.002, 0.021, ...],
[0.003, 0.022, ...]
]
}
Args:
response: The raw response from Vertex AI
model: The model name
model_response: The EmbeddingResponse object to populate
Returns:
EmbeddingResponse: The transformed response in OpenAI format
Raises:
KeyError: If response doesn't contain 'predictions'
ValueError: If predictions is not a list or contains invalid data
"""
if "predictions" not in response:
raise KeyError("Response missing 'predictions' field")
_predictions = response["predictions"]
if not isinstance(_predictions, list):
raise ValueError(f"Expected 'predictions' to be a list, got {type(_predictions)}")
embedding_response = []
# BGE models don't return token counts, so we estimate or set to 0
input_tokens = 0
for idx, embedding_values in enumerate(_predictions):
if not isinstance(embedding_values, list):
raise ValueError(
f"Expected embedding at index {idx} to be a list, got {type(embedding_values)}"
)
embedding_response.append(
{
"object": "embedding",
"index": idx,
"embedding": embedding_values,
}
)
model_response.object = "list"
model_response.data = embedding_response
model_response.model = model
usage = Usage(
prompt_tokens=input_tokens, completion_tokens=0, total_tokens=input_tokens
)
setattr(model_response, "usage", usage)
return model_response
@@ -105,16 +105,10 @@ class VertexAITextEmbeddingConfig(BaseModel):
"""
Transforms an openai request to a vertex embedding request.
"""
# Import here to avoid circular import issues with litellm.__init__
from litellm.llms.vertex_ai.vertex_embeddings.bge import VertexBGEConfig
if model.isdigit():
return self._transform_openai_request_to_fine_tuned_embedding_request(
input, optional_params, model
)
if VertexBGEConfig.is_bge_model(model):
return VertexBGEConfig.transform_request(
input=input, optional_params=optional_params, model=model
)
vertex_request: VertexEmbeddingRequest = VertexEmbeddingRequest()
vertex_text_embedding_input_list: List[TextEmbeddingInput] = []
@@ -173,9 +167,6 @@ class VertexAITextEmbeddingConfig(BaseModel):
vertex_request["parameters"] = TextEmbeddingFineTunedParameters(
**optional_params
)
# Remove 'shared_session' from parameters if present
if vertex_request["parameters"] is not None and "shared_session" in vertex_request["parameters"]:
del vertex_request["parameters"]["shared_session"] # type: ignore[typeddict-item]
return vertex_request
@@ -192,8 +183,8 @@ class VertexAITextEmbeddingConfig(BaseModel):
Args:
content (str): The content to be embedded.
task_type (Optional[TaskType]): The type of task to be performed.
title (Optional[str]): The title of the document to be embedded.
task_type (Optional[TaskType]): The type of task to be performed".
title (Optional[str]): The title of the document to be embedded
Returns:
TextEmbeddingInput: A TextEmbeddingInput object.
@@ -215,14 +206,6 @@ class VertexAITextEmbeddingConfig(BaseModel):
return self._transform_vertex_response_to_openai_for_fine_tuned_models(
response, model, model_response
)
# Import here to avoid circular import issues with litellm.__init__
from litellm.llms.vertex_ai.vertex_embeddings.bge import VertexBGEConfig
if VertexBGEConfig.is_bge_model(model):
return VertexBGEConfig.transform_response(
response=response, model=model, model_response=model_response
)
_predictions = response["predictions"]
@@ -25,12 +25,6 @@ class TextEmbeddingInput(TypedDict, total=False):
title: Optional[str]
class TextEmbeddingBGEInput(TypedDict, total=False):
prompt: str
task_type: Optional[TaskType]
title: Optional[str]
# Fine-tuned models require a different input format
# Ref: https://console.cloud.google.com/vertex-ai/model-garden?hl=en&project=adroit-crow-413218&pageState=(%22galleryStateKey%22:(%22f%22:(%22g%22:%5B%5D,%22o%22:%5B%5D),%22s%22:%22%22))
class TextEmbeddingFineTunedInput(TypedDict, total=False):
@@ -50,7 +44,7 @@ class EmbeddingParameters(TypedDict, total=False):
class VertexEmbeddingRequest(TypedDict, total=False):
instances: Union[List[TextEmbeddingInput], List[TextEmbeddingBGEInput], List[TextEmbeddingFineTunedInput]]
instances: Union[List[TextEmbeddingInput], List[TextEmbeddingFineTunedInput]]
parameters: Optional[Union[EmbeddingParameters, TextEmbeddingFineTunedParameters]]
@@ -25,7 +25,7 @@ import httpx # type: ignore
from litellm.utils import ModelResponse
from ..common_utils import VertexAIError, get_vertex_base_model_name
from ..common_utils import VertexAIError
from ..vertex_llm_base import VertexBase
@@ -82,8 +82,7 @@ class VertexAIGemmaModels(VertexBase):
message="""Upgrade vertex ai. Run `pip install "google-cloud-aiplatform>=1.38"`""",
)
try:
model = get_vertex_base_model_name(model=model)
model = model.replace("gemma/", "")
vertex_httpx_logic = VertexLLM()
access_token, project_id = vertex_httpx_logic._ensure_access_token(
+3 -49
View File
@@ -19,7 +19,6 @@ from .common_utils import (
_get_gemini_url,
_get_vertex_url,
all_gemini_url_modes,
get_vertex_base_model_name,
is_global_only_vertex_model,
)
@@ -242,9 +241,6 @@ class VertexBase:
auth_header=None,
url=default_api_base,
model=model,
vertex_project=vertex_project or project_id,
vertex_location=vertex_location or "us-central1",
vertex_api_version="v1", # Partner models typically use v1
)
return api_base
@@ -293,18 +289,9 @@ class VertexBase:
auth_header: Optional[str],
url: str,
model: Optional[str] = None,
vertex_project: Optional[str] = None,
vertex_location: Optional[str] = None,
vertex_api_version: Optional[Literal["v1", "v1beta1"]] = None,
) -> Tuple[Optional[str], str]:
"""
for cloudflare ai gateway - https://github.com/BerriAI/litellm/issues/4317
Handles custom api_base for:
1. Gemini (Google AI Studio) - constructs /models/{model}:{endpoint}
2. Vertex AI with standard proxies - constructs {api_base}:{endpoint}
3. Vertex AI with PSC endpoints - constructs full path structure
{api_base}/v1/projects/{project}/locations/{location}/endpoints/{model}:{endpoint}
## Returns
- (auth_header, url) - Tuple[Optional[str], str]
@@ -324,37 +311,8 @@ class VertexBase:
if gemini_api_key is not None:
auth_header = {"x-goog-api-key": gemini_api_key} # type: ignore[assignment]
else:
# For Vertex AI
# Check if this is a PSC endpoint or custom deployment
# PSC/custom endpoints need the full path structure
if vertex_project and vertex_location and model:
# Strip routing prefixes (bge/, gemma/, etc.) for endpoint URL construction
model_for_url = get_vertex_base_model_name(model=model)
# Check if model is numeric (endpoint ID) or if api_base doesn't contain googleapis.com
# These are indicators of PSC/custom endpoints
is_psc_or_custom = (
"googleapis.com" not in api_base.lower() or model_for_url.isdigit()
)
if is_psc_or_custom:
# Construct full PSC/custom endpoint URL
# Format: {api_base}/v1/projects/{project}/locations/{location}/endpoints/{model}:{endpoint}
version = vertex_api_version or "v1"
url = "{}/{}/projects/{}/locations/{}/endpoints/{}:{}".format(
api_base.rstrip("/"),
version,
vertex_project,
vertex_location,
model_for_url,
endpoint,
)
else:
# Standard proxy - just append endpoint
url = "{}:{}".format(api_base, endpoint)
else:
# Fallback to simple format if we don't have all parameters
url = "{}:{}".format(api_base, endpoint)
url = "{}:{}".format(api_base, endpoint)
if stream is True:
url = url + "?alt=sse"
return auth_header, url
@@ -381,7 +339,6 @@ class VertexBase:
Returns
token, url
"""
version: Optional[Literal["v1beta1", "v1"]] = None
if custom_llm_provider == "gemini":
url, endpoint = _get_gemini_url(
mode=mode,
@@ -397,7 +354,7 @@ class VertexBase:
)
### SET RUNTIME ENDPOINT ###
version = (
version: Literal["v1beta1", "v1"] = (
"v1beta1" if should_use_v1beta1_features is True else "v1"
)
url, endpoint = _get_vertex_url(
@@ -418,9 +375,6 @@ class VertexBase:
stream=stream,
url=url,
model=model,
vertex_project=vertex_project,
vertex_location=vertex_location,
vertex_api_version=version,
)
def _handle_reauthentication(
@@ -22,7 +22,7 @@ import httpx # type: ignore
from litellm.utils import ModelResponse
from ..common_utils import VertexAIError, get_vertex_base_model_name
from ..common_utils import VertexAIError
from ..vertex_llm_base import VertexBase
@@ -89,7 +89,7 @@ class VertexAIModelGardenModels(VertexBase):
message="""Upgrade vertex ai. Run `pip install "google-cloud-aiplatform>=1.38"`""",
)
try:
model = get_vertex_base_model_name(model=model)
model = model.replace("openai/", "")
vertex_httpx_logic = VertexLLM()
access_token, project_id = vertex_httpx_logic._ensure_access_token(
@@ -123,10 +123,6 @@ class VertexAIModelGardenModels(VertexBase):
stream=stream,
auth_header=None,
url=default_api_base,
model=model,
vertex_project=vertex_project or project_id,
vertex_location=vertex_location or "us-central1",
vertex_api_version="v1beta1",
)
model = ""
return openai_like_chat_completions.completion(
@@ -8523,6 +8523,14 @@
"/v1/images/generations"
]
},
"fal_ai/fal-ai/flux/schnell": {
"litellm_provider": "fal_ai",
"mode": "image_generation",
"output_cost_per_image": 0.003,
"supported_endpoints": [
"/v1/images/generations"
]
},
"fal_ai/fal-ai/imagen4/preview": {
"litellm_provider": "fal_ai",
"mode": "image_generation",
@@ -8531,6 +8539,22 @@
"/v1/images/generations"
]
},
"fal_ai/fal-ai/imagen4/preview/fast": {
"litellm_provider": "fal_ai",
"mode": "image_generation",
"output_cost_per_image": 0.02,
"supported_endpoints": [
"/v1/images/generations"
]
},
"fal_ai/fal-ai/imagen4/preview/ultra": {
"litellm_provider": "fal_ai",
"mode": "image_generation",
"output_cost_per_image": 0.06,
"supported_endpoints": [
"/v1/images/generations"
]
},
"fal_ai/fal-ai/recraft/v3/text-to-image": {
"litellm_provider": "fal_ai",
"mode": "image_generation",
@@ -23408,6 +23432,19 @@
"supports_function_calling": true,
"supports_tool_choice": true
},
"vertex_ai/moonshotai/kimi-k2-thinking-maas": {
"input_cost_per_token": 6e-07,
"litellm_provider": "vertex_ai-moonshot_models",
"max_input_tokens": 256000,
"max_output_tokens": 256000,
"max_tokens": 256000,
"mode": "chat",
"output_cost_per_token": 2.5e-06,
"source": "https://cloud.google.com/vertex-ai/generative-ai/pricing#partner-models",
"supports_function_calling": true,
"supports_tool_choice": true,
"supports_web_search": true
},
"vertex_ai/mistral-medium-3": {
"input_cost_per_token": 4e-07,
"litellm_provider": "vertex_ai-mistral_models",
@@ -23744,6 +23781,22 @@
"mode": "embedding",
"output_cost_per_token": 0.0
},
"voyage/voyage-3.5": {
"input_cost_per_token": 6e-08,
"litellm_provider": "voyage",
"max_input_tokens": 32000,
"max_tokens": 32000,
"mode": "embedding",
"output_cost_per_token": 0.0
},
"voyage/voyage-3.5-lite": {
"input_cost_per_token": 2e-08,
"litellm_provider": "voyage",
"max_input_tokens": 32000,
"max_tokens": 32000,
"mode": "embedding",
"output_cost_per_token": 0.0
},
"voyage/voyage-code-2": {
"input_cost_per_token": 1.2e-07,
"litellm_provider": "voyage",
@@ -24789,7 +24842,9 @@
"1280x720",
"720x1280"
],
"comment": "5 credits per second @ $0.01 per credit = $0.05 per second"
"metadata": {
"comment": "5 credits per second @ $0.01 per credit = $0.05 per second"
}
},
"runwayml/gen4_aleph": {
"litellm_provider": "runwayml",
@@ -24807,7 +24862,9 @@
"1280x720",
"720x1280"
],
"comment": "15 credits per second @ $0.01 per credit = $0.15 per second"
"metadata": {
"comment": "15 credits per second @ $0.01 per credit = $0.15 per second"
}
},
"runwayml/gen3a_turbo": {
"litellm_provider": "runwayml",
@@ -24825,7 +24882,9 @@
"1280x720",
"720x1280"
],
"comment": "5 credits per second @ $0.01 per credit = $0.05 per second"
"metadata": {
"comment": "5 credits per second @ $0.01 per credit = $0.05 per second"
}
},
"runwayml/gen4_image": {
"litellm_provider": "runwayml",
@@ -24844,7 +24903,9 @@
"1280x720",
"1920x1080"
],
"comment": "5 credits per 720p image or 8 credits per 1080p image @ $0.01 per credit. Using 5 credits ($0.05) as base cost"
"metadata": {
"comment": "5 credits per 720p image or 8 credits per 1080p image @ $0.01 per credit. Using 5 credits ($0.05) as base cost"
}
},
"runwayml/gen4_image_turbo": {
"litellm_provider": "runwayml",
@@ -24863,6 +24924,17 @@
"1280x720",
"1920x1080"
],
"comment": "2 credits per image (any resolution) @ $0.01 per credit = $0.02 per image"
"metadata": {
"comment": "2 credits per image (any resolution) @ $0.01 per credit = $0.02 per image"
}
},
"runwayml/eleven_multilingual_v2": {
"litellm_provider": "runwayml",
"mode": "audio_speech",
"input_cost_per_character": 3e-07,
"source": "https://docs.dev.runwayml.com/guides/pricing/",
"metadata": {
"comment": "Estimated cost based on standard TTS pricing. RunwayML uses ElevenLabs models."
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

+6 -2
View File
@@ -292,6 +292,7 @@ class ProxyBaseLLMRequestProcessing:
proxy_config: ProxyConfig,
route_type: Literal[
"acompletion",
"aembedding",
"aresponses",
"_arealtime",
"aget_responses",
@@ -403,6 +404,7 @@ class ProxyBaseLLMRequestProcessing:
user_api_key_dict: UserAPIKeyAuth,
route_type: Literal[
"acompletion",
"aembedding",
"aresponses",
"_arealtime",
"aget_responses",
@@ -772,10 +774,12 @@ class ProxyBaseLLMRequestProcessing:
@staticmethod
def _get_pre_call_type(
route_type: Literal["acompletion", "aresponses"],
) -> Literal["completion", "responses"]:
route_type: Literal["acompletion", "aembedding", "aresponses"],
) -> Literal["completion", "embeddings", "responses"]:
if route_type == "acompletion":
return "completion"
elif route_type == "aembedding":
return "embeddings"
elif route_type == "aresponses":
return "responses"
+1 -1
View File
@@ -1158,7 +1158,7 @@ def _enforced_params_check(
)
if enforced_params is None:
return True
if enforced_params is not None and premium_user is not True:
if enforced_params and premium_user is not True:
raise ValueError(
f"Enforced Params is an Enterprise feature. Enforced Params: {enforced_params}. {CommonProxyErrors.not_premium_user.value}"
)
@@ -68,7 +68,9 @@ if MCP_AVAILABLE:
except AttributeError:
redacted_server = mcp_server.copy(deep=True) # type: ignore[attr-defined]
redacted_server.credentials = None
if hasattr(redacted_server, "credentials"):
setattr(redacted_server, "credentials", None)
return redacted_server
def _redact_mcp_credentials_list(
@@ -0,0 +1,688 @@
"""
Allow proxy admin to manage model access groups
Endpoints here:
- POST /model_group/new - Create a new access group with multiple model names
"""
import json
from typing import Any, Dict, List, Tuple
from fastapi import APIRouter, Depends, HTTPException
from litellm._logging import verbose_proxy_logger
from litellm.proxy._types import UserAPIKeyAuth
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
# Clear cache and reload models to pick up the access group changes
from litellm.proxy.management_endpoints.model_management_endpoints import (
clear_cache,
)
from litellm.proxy.utils import PrismaClient
from litellm.types.proxy.management_endpoints.model_management_endpoints import (
AccessGroupInfo,
DeleteModelGroupResponse,
ListAccessGroupsResponse,
NewModelGroupRequest,
NewModelGroupResponse,
UpdateModelGroupRequest,
)
router = APIRouter()
def validate_models_exist(
model_names: List[str], llm_router
) -> Tuple[bool, List[str]]:
"""
Validate that all requested model names exist in the router.
Checks only exact model name matches.
Returns:
Tuple[bool, List[str]]: (all_valid, missing_models)
"""
if llm_router is None:
return False, model_names
router_model_names = set(llm_router.get_model_names())
missing = [m for m in model_names if m not in router_model_names]
return (len(missing) == 0, missing)
def add_access_group_to_deployment(
model_info: Dict[str, Any], access_group: str
) -> Tuple[Dict[str, Any], bool]:
"""
Add an access group to a deployment's model_info.
Args:
model_info: The model_info dictionary from the deployment
access_group: The access group name to add
Returns:
Tuple[Dict[str, Any], bool]: (updated_model_info, was_modified)
"""
access_groups = model_info.get("access_groups", [])
# Check if access group already exists
if access_group in access_groups:
return model_info, False
# Add the access group
access_groups.append(access_group)
model_info["access_groups"] = access_groups
return model_info, True
async def update_deployments_with_access_group(
model_names: List[str],
access_group: str,
prisma_client: PrismaClient,
) -> int:
"""
Update all deployments for the given model names to include the access group.
Args:
model_names: List of model names whose deployments should be updated
access_group: The access group name to add
prisma_client: Database client
Returns:
int: Number of deployments updated
"""
models_updated = 0
for model_name in model_names:
verbose_proxy_logger.debug(
f"Updating deployments for model_name: {model_name}"
)
# Get all deployments with this model_name
deployments = await prisma_client.db.litellm_proxymodeltable.find_many(
where={"model_name": model_name}
)
verbose_proxy_logger.debug(
f"Found {len(deployments)} deployments for model_name: {model_name}"
)
# If no deployments found, this is a config model (not in DB)
if len(deployments) == 0:
raise HTTPException(
status_code=400,
detail={
"error": f"Can't find model '{model_name}' in Database. Access group management is only supported for database models."
},
)
# Update each deployment
for deployment in deployments:
model_info = deployment.model_info or {}
# Add access group using helper
updated_model_info, was_modified = add_access_group_to_deployment(
model_info=model_info,
access_group=access_group,
)
# Only update in DB if modified
if was_modified:
await prisma_client.db.litellm_proxymodeltable.update(
where={"model_id": deployment.model_id},
data={"model_info": json.dumps(updated_model_info)},
)
models_updated += 1
verbose_proxy_logger.debug(
f"Updated deployment {deployment.model_id} with access group: {access_group}"
)
return models_updated
def remove_access_group_from_deployment(
model_info: Dict[str, Any], access_group: str
) -> Tuple[Dict[str, Any], bool]:
"""
Remove an access group from a deployment's model_info.
Args:
model_info: The model_info dictionary from the deployment
access_group: The access group name to remove
Returns:
Tuple[Dict[str, Any], bool]: (updated_model_info, was_modified)
"""
access_groups = model_info.get("access_groups", [])
# Check if access group exists
if access_group not in access_groups:
return model_info, False
# Remove the access group
access_groups.remove(access_group)
model_info["access_groups"] = access_groups
return model_info, True
async def get_all_access_groups_from_db(
prisma_client: PrismaClient,
) -> Dict[str, AccessGroupInfo]:
"""
Get all access groups from the database.
Returns:
Dict[str, AccessGroupInfo]: Dictionary mapping access_group name to info
"""
# Get all deployments
deployments = await prisma_client.db.litellm_proxymodeltable.find_many()
# Build access group map
access_group_map: Dict[str, Dict[str, Any]] = {}
for deployment in deployments:
model_info = deployment.model_info or {}
access_groups = model_info.get("access_groups", [])
model_name = deployment.model_name
for access_group in access_groups:
if access_group not in access_group_map:
access_group_map[access_group] = {
"model_names": set(),
"deployment_count": 0,
}
access_group_map[access_group]["model_names"].add(model_name)
access_group_map[access_group]["deployment_count"] += 1
# Convert to AccessGroupInfo objects
result = {}
for access_group, data in access_group_map.items():
result[access_group] = AccessGroupInfo(
access_group=access_group,
model_names=sorted(list(data["model_names"])),
deployment_count=data["deployment_count"],
)
return result
@router.post(
"/access_group/new",
tags=["model management"],
dependencies=[Depends(user_api_key_auth)],
response_model=NewModelGroupResponse,
)
async def create_model_group(
data: NewModelGroupRequest,
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
):
"""
Create a new access group containing multiple model names.
An access group is a named collection of model groups that can be referenced
by teams/keys for simplified access control.
Example:
```bash
curl -X POST 'http://localhost:4000/access_group/new' \\
-H 'Authorization: Bearer sk-1234' \\
-H 'Content-Type: application/json' \\
-d '{
"access_group": "production-models",
"model_names": ["gpt-4", "claude-3-opus", "gemini-pro"]
}'
```
Parameters:
- access_group: str - The access group name (e.g., "production-models")
- model_names: List[str] - List of existing model groups to include
Returns:
- NewModelGroupResponse with the created access group details
Raises:
- HTTPException 400: If any model names don't exist
- HTTPException 500: If database operations fail
"""
from litellm.proxy.proxy_server import (
llm_router,
prisma_client,
)
verbose_proxy_logger.debug(
f"Creating access group: {data.access_group} with models: {data.model_names}"
)
# Validation: Check if access_group is provided
if not data.access_group or not data.access_group.strip():
raise HTTPException(
status_code=400,
detail={"error": "access_group is required and cannot be empty"},
)
# Validation: Check if model_names list is provided and not empty
if not data.model_names or len(data.model_names) == 0:
raise HTTPException(
status_code=400,
detail={"error": "model_names list is required and cannot be empty"},
)
# Validation: Check if all models exist in the router
all_valid, missing_models = validate_models_exist(
model_names=data.model_names,
llm_router=llm_router,
)
if not all_valid:
raise HTTPException(
status_code=400,
detail={"error": f"Model(s) not found: {', '.join(missing_models)}"},
)
# Check if database is connected
if prisma_client is None:
raise HTTPException(
status_code=500,
detail={"error": "Database not connected. Cannot create access group."},
)
try:
# Check if access group already exists
existing_access_groups = await get_all_access_groups_from_db(
prisma_client=prisma_client
)
if data.access_group in existing_access_groups:
raise HTTPException(
status_code=409,
detail={"error": f"Access group '{data.access_group}' already exists. Use PUT /access_group/{data.access_group}/update to modify it."},
)
# Update deployments using helper function
models_updated = await update_deployments_with_access_group(
model_names=data.model_names,
access_group=data.access_group,
prisma_client=prisma_client,
)
await clear_cache()
verbose_proxy_logger.info(
f"Successfully created access group '{data.access_group}' with {models_updated} models updated"
)
return NewModelGroupResponse(
access_group=data.access_group,
model_names=data.model_names,
models_updated=models_updated,
)
except HTTPException:
raise
except Exception as e:
verbose_proxy_logger.exception(
f"Error creating access group '{data.access_group}': {str(e)}"
)
raise HTTPException(
status_code=500,
detail={"error": f"Failed to create access group: {str(e)}"},
)
@router.get(
"/access_group/list",
tags=["model management"],
dependencies=[Depends(user_api_key_auth)],
response_model=ListAccessGroupsResponse,
)
async def list_access_groups(
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
):
"""
List all access groups.
Returns a list of all access groups with their model names and deployment counts.
Example:
```bash
curl -X GET 'http://localhost:4000/access_group/list' \\
-H 'Authorization: Bearer sk-1234'
```
Returns:
- ListAccessGroupsResponse with all access groups
"""
from litellm.proxy.proxy_server import prisma_client
if prisma_client is None:
raise HTTPException(
status_code=500,
detail={"error": "Database not connected."},
)
try:
access_groups_map = await get_all_access_groups_from_db(
prisma_client=prisma_client
)
# Sort by access group name
access_groups_list = sorted(
access_groups_map.values(),
key=lambda x: x.access_group,
)
return ListAccessGroupsResponse(access_groups=access_groups_list)
except Exception as e:
verbose_proxy_logger.exception(f"Error listing access groups: {str(e)}")
raise HTTPException(
status_code=500,
detail={"error": f"Failed to list access groups: {str(e)}"},
)
@router.get(
"/access_group/{access_group}/info",
tags=["model management"],
dependencies=[Depends(user_api_key_auth)],
response_model=AccessGroupInfo,
)
async def get_access_group_info(
access_group: str,
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
):
"""
Get information about a specific access group.
Example:
```bash
curl -X GET 'http://localhost:4000/access_group/production-models/info' \\
-H 'Authorization: Bearer sk-1234'
```
Parameters:
- access_group: str - The access group name (URL path parameter)
Returns:
- AccessGroupInfo with the access group details
Raises:
- HTTPException 404: If access group not found
"""
from litellm.proxy.proxy_server import prisma_client
if prisma_client is None:
raise HTTPException(
status_code=500,
detail={"error": "Database not connected."},
)
try:
access_groups_map = await get_all_access_groups_from_db(
prisma_client=prisma_client
)
if access_group not in access_groups_map:
raise HTTPException(
status_code=404,
detail={"error": f"Access group '{access_group}' not found"},
)
return access_groups_map[access_group]
except HTTPException:
raise
except Exception as e:
verbose_proxy_logger.exception(
f"Error getting access group info for '{access_group}': {str(e)}"
)
raise HTTPException(
status_code=500,
detail={"error": f"Failed to get access group info: {str(e)}"},
)
@router.put(
"/access_group/{access_group}/update",
tags=["model management"],
dependencies=[Depends(user_api_key_auth)],
response_model=NewModelGroupResponse,
)
async def update_access_group(
access_group: str,
data: UpdateModelGroupRequest,
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
):
"""
Update an access group's model names.
This will:
1. Remove the access group from all current deployments
2. Add the access group to all deployments for the new model_names list
Example:
```bash
curl -X PUT 'http://localhost:4000/access_group/production-models/update' \\
-H 'Authorization: Bearer sk-1234' \\
-H 'Content-Type: application/json' \\
-d '{
"model_names": ["gpt-4", "claude-3-sonnet"]
}'
```
Parameters:
- access_group: str - The access group name (URL path parameter)
- model_names: List[str] - New list of model groups to include
Returns:
- NewModelGroupResponse with the updated access group details
Raises:
- HTTPException 400: If any model names don't exist
- HTTPException 404: If access group not found
"""
from litellm.proxy.proxy_server import llm_router, prisma_client
if prisma_client is None:
raise HTTPException(
status_code=500,
detail={"error": "Database not connected."},
)
verbose_proxy_logger.debug(
f"Updating access group: {access_group} with models: {data.model_names}"
)
# Validation: Check if model_names list is provided and not empty
if not data.model_names or len(data.model_names) == 0:
raise HTTPException(
status_code=400,
detail={"error": "model_names list is required and cannot be empty"},
)
# Validation: Check if access group exists
try:
access_groups_map = await get_all_access_groups_from_db(
prisma_client=prisma_client
)
if access_group not in access_groups_map:
raise HTTPException(
status_code=404,
detail={"error": f"Access group '{access_group}' not found"},
)
except HTTPException:
raise
except Exception as e:
raise HTTPException(
status_code=500,
detail={"error": f"Failed to check access group existence: {str(e)}"},
)
# Validation: Check if all new models exist
all_valid, missing_models = validate_models_exist(
model_names=data.model_names,
llm_router=llm_router,
)
if not all_valid:
raise HTTPException(
status_code=400,
detail={"error": f"Model(s) not found: {', '.join(missing_models)}"},
)
try:
# Step 1: Remove access group from ALL DB deployments (skip config models)
all_deployments = await prisma_client.db.litellm_proxymodeltable.find_many()
for deployment in all_deployments:
model_info = deployment.model_info or {}
updated_model_info, was_modified = remove_access_group_from_deployment(
model_info=model_info,
access_group=access_group,
)
if was_modified:
await prisma_client.db.litellm_proxymodeltable.update(
where={"model_id": deployment.model_id},
data={"model_info": json.dumps(updated_model_info)},
)
# Step 2: Add access group to new model_names
models_updated = await update_deployments_with_access_group(
model_names=data.model_names,
access_group=access_group,
prisma_client=prisma_client,
)
# Clear cache and reload models to pick up the access group changes
await clear_cache()
verbose_proxy_logger.info(
f"Successfully updated access group '{access_group}' with {models_updated} models updated"
)
return NewModelGroupResponse(
access_group=access_group,
model_names=data.model_names,
models_updated=models_updated,
)
except HTTPException:
raise
except Exception as e:
verbose_proxy_logger.exception(
f"Error updating access group '{access_group}': {str(e)}"
)
raise HTTPException(
status_code=500,
detail={"error": f"Failed to update access group: {str(e)}"},
)
@router.delete(
"/access_group/{access_group}/delete",
tags=["model management"],
dependencies=[Depends(user_api_key_auth)],
response_model=DeleteModelGroupResponse,
)
async def delete_access_group(
access_group: str,
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
):
"""
Delete an access group.
Removes the access group from all deployments that have it.
Example:
```bash
curl -X DELETE 'http://localhost:4000/access_group/production-models/delete' \\
-H 'Authorization: Bearer sk-1234'
```
Parameters:
- access_group: str - The access group name (URL path parameter)
Returns:
- DeleteModelGroupResponse with deletion details
Raises:
- HTTPException 404: If access group not found
"""
from litellm.proxy.proxy_server import prisma_client
if prisma_client is None:
raise HTTPException(
status_code=500,
detail={"error": "Database not connected."},
)
verbose_proxy_logger.debug(f"Deleting access group: {access_group}")
# Validation: Check if access group exists
try:
access_groups_map = await get_all_access_groups_from_db(
prisma_client=prisma_client
)
if access_group not in access_groups_map:
raise HTTPException(
status_code=404,
detail={"error": f"Access group '{access_group}' not found"},
)
except HTTPException:
raise
except Exception as e:
raise HTTPException(
status_code=500,
detail={"error": f"Failed to check access group existence: {str(e)}"},
)
try:
# Remove access group from all DB deployments (skip config models)
all_deployments = await prisma_client.db.litellm_proxymodeltable.find_many()
models_updated = 0
for deployment in all_deployments:
model_info = deployment.model_info or {}
updated_model_info, was_modified = remove_access_group_from_deployment(
model_info=model_info,
access_group=access_group,
)
if was_modified:
await prisma_client.db.litellm_proxymodeltable.update(
where={"model_id": deployment.model_id},
data={"model_info": json.dumps(updated_model_info)},
)
models_updated += 1
# Clear cache and reload models to pick up the access group changes
await clear_cache()
verbose_proxy_logger.info(
f"Successfully deleted access group '{access_group}' from {models_updated} deployments"
)
return DeleteModelGroupResponse(
access_group=access_group,
models_updated=models_updated,
message=f"Access group '{access_group}' deleted successfully",
)
except HTTPException:
raise
except Exception as e:
verbose_proxy_logger.exception(
f"Error deleting access group '{access_group}': {str(e)}"
)
raise HTTPException(
status_code=500,
detail={"error": f"Failed to delete access group: {str(e)}"},
)
+53 -143
View File
@@ -292,6 +292,9 @@ from litellm.proxy.management_endpoints.model_management_endpoints import (
from litellm.proxy.management_endpoints.model_management_endpoints import (
router as model_management_router,
)
from litellm.proxy.management_endpoints.model_access_group_management_endpoints import (
router as model_access_group_management_router,
)
from litellm.proxy.management_endpoints.organization_endpoints import (
router as organization_router,
)
@@ -5011,40 +5014,11 @@ async def embeddings( # noqa: PLR0915
global proxy_logging_obj
data: Any = {}
try:
# Use orjson to parse JSON data, orjson speeds up requests significantly
body = await request.body()
data = orjson.loads(body)
verbose_proxy_logger.debug(
"Request received by LiteLLM:\n%s",
json.dumps(data, indent=4),
)
# Include original request and headers in the data
data = await add_litellm_data_to_request(
data=data,
request=request,
general_settings=general_settings,
user_api_key_dict=user_api_key_dict,
version=version,
proxy_config=proxy_config,
)
data["model"] = (
general_settings.get("embedding_model", None) # server default
or user_model # model name passed via cli args
or model # for azure deployments
or data.get("model", None) # default passed in http request
)
if user_model:
data["model"] = user_model
### MODEL ALIAS MAPPING ###
# check if model name in model alias map
# get the actual model name
if data["model"] in litellm.model_alias_map:
data["model"] = litellm.model_alias_map[data["model"]]
# Use shared request body reading helper (same as chat/completions)
data = await _read_request_body(request=request)
### HANDLE TOKEN ARRAY INPUT DECODING ###
# This must happen BEFORE base_process_llm_request() since it modifies the input
router_model_names = llm_router.model_names if llm_router is not None else []
if (
"input" in data
@@ -5054,126 +5028,61 @@ async def embeddings( # noqa: PLR0915
and isinstance(data["input"][0][0], int)
): # check if array of tokens passed in
# check if provider accept list of tokens as input - e.g. for langchain integration
if llm_model_list is not None and data["model"] in router_model_names:
for m in llm_model_list:
if m["model_name"] == data["model"]:
if m["litellm_params"][
"model"
] in litellm.open_ai_embedding_models or any(
m["litellm_params"]["model"].startswith(provider)
if llm_router is not None and data.get("model") in router_model_names:
# Use router's O(1) lookup instead of O(N) iteration through llm_model_list
deployment = llm_router.get_deployment(model_id=data["model"])
if deployment is not None:
litellm_model = deployment.get("litellm_params", {}).get("model", "")
# Check if this provider supports token arrays
supports_token_arrays = (
litellm_model in litellm.open_ai_embedding_models
or any(
litellm_model.startswith(provider)
for provider in LITELLM_EMBEDDING_PROVIDERS_SUPPORTING_INPUT_ARRAY_OF_TOKENS
):
pass
else:
# non-openai/azure embedding model called with token input
input_list = []
for i in data["input"]:
input_list.append(
litellm.decode(model="gpt-3.5-turbo", tokens=i)
)
data["input"] = input_list
break
)
)
if not supports_token_arrays:
# non-openai/azure embedding model called with token input - decode tokens
input_list = []
for i in data["input"]:
input_list.append(
litellm.decode(model="gpt-3.5-turbo", tokens=i)
)
data["input"] = input_list
### CALL HOOKS ### - modify incoming data / reject request before calling the model
data = await proxy_logging_obj.pre_call_hook(
# Use unified request processor (same as chat/completions and responses)
base_llm_response_processor = ProxyBaseLLMRequestProcessing(data=data)
# Process the request with all optimizations (shared sessions, network tuning, etc.)
response = await base_llm_response_processor.base_process_llm_request(
request=request,
fastapi_response=fastapi_response,
user_api_key_dict=user_api_key_dict,
data=data,
call_type=CallTypes.aembedding.value,
)
tasks = []
tasks.append(
proxy_logging_obj.during_call_hook(
data=data,
user_api_key_dict=user_api_key_dict,
call_type="aembedding",
)
)
## ROUTE TO CORRECT ENDPOINT ##
llm_call = await route_request(
data=data,
route_type="aembedding",
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=model,
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,
)
tasks.append(llm_call)
# wait for call to end
llm_responses = asyncio.gather(
*tasks
) # run the moderation check in parallel to the actual llm api call
responses = await llm_responses
response = responses[1]
### ALERTING ###
asyncio.create_task(
proxy_logging_obj.update_request_status(
litellm_call_id=data.get("litellm_call_id", ""), status="success"
)
)
### RESPONSE HEADERS ###
hidden_params = getattr(response, "_hidden_params", {}) or {}
model_id = hidden_params.get("model_id", None) or ""
cache_key = hidden_params.get("cache_key", None) or ""
api_base = hidden_params.get("api_base", None) or ""
response_cost = hidden_params.get("response_cost", None) or ""
litellm_call_id = hidden_params.get("litellm_call_id", None) or ""
additional_headers: dict = hidden_params.get("additional_headers", {}) or {}
fastapi_response.headers.update(
ProxyBaseLLMRequestProcessing.get_custom_headers(
user_api_key_dict=user_api_key_dict,
model_id=model_id,
cache_key=cache_key,
api_base=api_base,
version=version,
response_cost=response_cost,
model_region=getattr(user_api_key_dict, "allowed_model_region", ""),
call_id=litellm_call_id,
request_data=data,
hidden_params=hidden_params,
**additional_headers,
)
)
await check_response_size_is_safe(response=response)
return response
except Exception as e:
await proxy_logging_obj.post_call_failure_hook(
user_api_key_dict=user_api_key_dict, original_exception=e, request_data=data
# Use unified error handler
base_llm_response_processor = ProxyBaseLLMRequestProcessing(data=data)
raise await base_llm_response_processor._handle_llm_api_exception(
e=e,
user_api_key_dict=user_api_key_dict,
proxy_logging_obj=proxy_logging_obj,
version=version,
)
litellm_debug_info = getattr(e, "litellm_debug_info", "")
verbose_proxy_logger.debug(
"\033[1;31mAn error occurred: %s %s\n\n Debug this by setting `--debug`, e.g. `litellm --model gpt-3.5-turbo --debug`",
e,
litellm_debug_info,
)
verbose_proxy_logger.exception(
"litellm.proxy.proxy_server.embeddings(): Exception occured - {}".format(
str(e)
)
)
if isinstance(e, HTTPException):
message = get_error_message_str(e)
raise ProxyException(
message=message,
type=getattr(e, "type", "None"),
param=getattr(e, "param", "None"),
code=getattr(e, "status_code", status.HTTP_400_BAD_REQUEST),
)
else:
error_msg = f"{str(e)}"
raise ProxyException(
message=getattr(e, "message", error_msg),
type=getattr(e, "type", "None"),
param=getattr(e, "param", "None"),
openai_code=getattr(e, "code", None),
code=getattr(e, "status_code", 500),
)
@router.post(
@@ -10180,6 +10089,7 @@ app.include_router(openai_files_router)
app.include_router(team_callback_router)
app.include_router(budget_management_router)
app.include_router(model_management_router)
app.include_router(model_access_group_management_router)
app.include_router(tag_management_router)
app.include_router(cost_tracking_settings_router)
app.include_router(router_settings_router)
@@ -0,0 +1,769 @@
from __future__ import annotations
from typing import Any, Dict, List
from litellm.types.proxy.public_endpoints.public_endpoints import (
ProviderCreateInfo,
ProviderCredentialField,
)
from litellm.types.utils import LlmProviders
DEFAULT_MODEL_PLACEHOLDER = "gpt-3.5-turbo"
_FALLBACK_FIELDS: List[Dict[str, Any]] = [
{
"key": "api_base",
"label": "API Base",
"field_type": "text",
"required": False,
},
{
"key": "api_key",
"label": "API Key",
"field_type": "password",
"required": False,
},
]
PROVIDER_BASE_INFO: Dict[str, Dict[str, Any]] = {
"AIML": {
"provider_display_name": "AI/ML API",
"litellm_provider": "aiml",
"default_model_placeholder": "aiml/flux-pro/v1.1",
},
"Anthropic": {
"provider_display_name": "Anthropic",
"litellm_provider": "anthropic",
"default_model_placeholder": "claude-3-opus",
},
"AssemblyAI": {
"provider_display_name": "AssemblyAI",
"litellm_provider": "assemblyai",
},
"Azure": {
"provider_display_name": "Azure",
"litellm_provider": "azure",
"default_model_placeholder": "azure/my-deployment",
},
"Azure_AI_Studio": {
"provider_display_name": "Azure AI Foundry (Studio)",
"litellm_provider": "azure_ai",
"default_model_placeholder": "azure_ai/command-r-plus",
},
"Bedrock": {
"provider_display_name": "Amazon Bedrock",
"litellm_provider": "bedrock",
"default_model_placeholder": "claude-3-opus",
},
"Cerebras": {
"provider_display_name": "Cerebras",
"litellm_provider": "cerebras",
},
"Cohere": {
"provider_display_name": "Cohere",
"litellm_provider": "cohere",
},
"Dashscope": {
"provider_display_name": "Dashscope",
"litellm_provider": "dashscope",
},
"Databricks": {
"provider_display_name": "Databricks (Qwen API)",
"litellm_provider": "databricks",
},
"DeepInfra": {
"provider_display_name": "DeepInfra",
"litellm_provider": "deepinfra",
"default_model_placeholder": "deepinfra/<any-model-on-deepinfra>",
},
"Deepgram": {
"provider_display_name": "Deepgram",
"litellm_provider": "deepgram",
},
"Deepseek": {
"provider_display_name": "Deepseek",
"litellm_provider": "deepseek",
},
"ElevenLabs": {
"provider_display_name": "ElevenLabs",
"litellm_provider": "elevenlabs",
},
"FalAI": {
"provider_display_name": "Fal AI",
"litellm_provider": "fal_ai",
"default_model_placeholder": "fal_ai/fal-ai/flux-pro/v1.1-ultra",
},
"FireworksAI": {
"provider_display_name": "Fireworks AI",
"litellm_provider": "fireworks_ai",
},
"Google_AI_Studio": {
"provider_display_name": "Google AI Studio",
"litellm_provider": "gemini",
"default_model_placeholder": "gemini-pro",
},
"GradientAI": {
"provider_display_name": "GradientAI",
"litellm_provider": "gradient_ai",
},
"Groq": {
"provider_display_name": "Groq",
"litellm_provider": "groq",
},
"Hosted_Vllm": {
"provider_display_name": "vllm",
"litellm_provider": "hosted_vllm",
},
"Infinity": {
"provider_display_name": "Infinity",
"litellm_provider": "infinity",
},
"JinaAI": {
"provider_display_name": "Jina AI",
"litellm_provider": "jina_ai",
"default_model_placeholder": "jina_ai/",
},
"MistralAI": {
"provider_display_name": "Mistral AI",
"litellm_provider": "mistral",
},
"Ollama": {
"provider_display_name": "Ollama",
"litellm_provider": "ollama",
},
"OpenAI": {
"provider_display_name": "OpenAI",
"litellm_provider": "openai",
},
"OpenAI_Compatible": {
"provider_display_name": "OpenAI-Compatible Endpoints (Together AI, etc.)",
"litellm_provider": "openai",
},
"OpenAI_Text": {
"provider_display_name": "OpenAI Text Completion",
"litellm_provider": "text-completion-openai",
},
"OpenAI_Text_Compatible": {
"provider_display_name": "OpenAI-Compatible Text Completion Models (Together AI, etc.)",
"litellm_provider": "text-completion-openai",
},
"Openrouter": {
"provider_display_name": "Openrouter",
"litellm_provider": "openrouter",
},
"Oracle": {
"provider_display_name": "Oracle Cloud Infrastructure (OCI)",
"litellm_provider": "oci",
"default_model_placeholder": "oci/xai.grok-4",
},
"Perplexity": {
"provider_display_name": "Perplexity",
"litellm_provider": "perplexity",
},
"SageMaker": {
"provider_display_name": "AWS SageMaker",
"litellm_provider": "sagemaker_chat",
"default_model_placeholder": "sagemaker/jumpstart-dft-meta-textgeneration-llama-2-7b",
},
"Sambanova": {
"provider_display_name": "Sambanova",
"litellm_provider": "sambanova",
},
"Snowflake": {
"provider_display_name": "Snowflake",
"litellm_provider": "snowflake",
"default_model_placeholder": "snowflake/mistral-7b",
},
"TogetherAI": {
"provider_display_name": "TogetherAI",
"litellm_provider": "together_ai",
},
"Triton": {
"provider_display_name": "Triton",
"litellm_provider": "triton",
},
"Vertex_AI": {
"provider_display_name": "Vertex AI (Anthropic, Gemini, etc.)",
"litellm_provider": "vertex_ai",
"default_model_placeholder": "gemini-pro",
},
"VolcEngine": {
"provider_display_name": "VolcEngine",
"litellm_provider": "volcengine",
"default_model_placeholder": "volcengine/<any-model-on-volcengine>",
},
"Voyage": {
"provider_display_name": "Voyage AI",
"litellm_provider": "voyage",
"default_model_placeholder": "voyage/",
},
"xAI": {
"provider_display_name": "xAI",
"litellm_provider": "xai",
},
}
PROVIDER_CREDENTIAL_FIELDS: Dict[str, List[Dict[str, Any]]] = {
"OpenAI": [
{
"key": "api_base",
"label": "API Base",
"field_type": "text",
"placeholder": "https://api.openai.com/v1",
"tooltip": "Common endpoints: https://api.openai.com/v1, https://eu.api.openai.com, https://us.api.openai.com",
"default_value": "https://api.openai.com/v1",
},
{
"key": "organization",
"label": "OpenAI Organization ID",
"placeholder": "[OPTIONAL] my-unique-org",
},
{
"key": "api_key",
"label": "OpenAI API Key",
"field_type": "password",
"required": True,
},
],
"OpenAI_Text": [
{
"key": "api_base",
"label": "API Base",
"field_type": "text",
"placeholder": "https://api.openai.com/v1",
"tooltip": "Common endpoints: https://api.openai.com/v1, https://eu.api.openai.com, https://us.api.openai.com",
"default_value": "https://api.openai.com/v1",
},
{
"key": "organization",
"label": "OpenAI Organization ID",
"placeholder": "[OPTIONAL] my-unique-org",
},
{
"key": "api_key",
"label": "OpenAI API Key",
"field_type": "password",
"required": True,
},
],
"Vertex_AI": [
{
"key": "vertex_project",
"label": "Vertex Project",
"placeholder": "adroit-cadet-1234..",
"required": True,
},
{
"key": "vertex_location",
"label": "Vertex Location",
"placeholder": "us-east-1",
"required": True,
},
{
"key": "vertex_credentials",
"label": "Vertex Credentials",
"field_type": "upload",
"required": True,
},
],
"AssemblyAI": [
{
"key": "api_base",
"label": "API Base",
"field_type": "select",
"required": True,
"options": [
"https://api.assemblyai.com",
"https://api.eu.assemblyai.com",
],
},
{
"key": "api_key",
"label": "AssemblyAI API Key",
"field_type": "password",
"required": True,
},
],
"Azure": [
{
"key": "api_base",
"label": "API Base",
"placeholder": "https://...",
"required": True,
},
{
"key": "api_version",
"label": "API Version",
"placeholder": "2023-07-01-preview",
"tooltip": "By default litellm will use the latest version. If you want to use a different version, you can specify it here",
},
{
"key": "base_model",
"label": "Base Model",
"placeholder": "azure/gpt-3.5-turbo",
},
{
"key": "api_key",
"label": "Azure API Key",
"field_type": "password",
"placeholder": "Enter your Azure API Key",
},
{
"key": "azure_ad_token",
"label": "Azure AD Token",
"field_type": "password",
"placeholder": "Enter your Azure AD Token",
},
],
"Azure_AI_Studio": [
{
"key": "api_base",
"label": "API Base",
"placeholder": "https://<test>.openai.azure.com/openai/deployments/gpt-4o/chat/completions?api-version=2024-10-21",
"tooltip": "Enter your full Target URI from Azure Foundry here. Example: https://litellm8397336933.openai.azure.com/openai/deployments/gpt-4o/chat/completions?api-version=2024-10-21",
"required": True,
},
{
"key": "api_key",
"label": "Azure API Key",
"field_type": "password",
"required": True,
},
],
"OpenAI_Compatible": [
{
"key": "api_base",
"label": "API Base",
"placeholder": "https://...",
"required": True,
},
{
"key": "api_key",
"label": "OpenAI API Key",
"field_type": "password",
"required": True,
},
],
"Dashscope": [
{
"key": "api_key",
"label": "Dashscope API Key",
"field_type": "password",
"required": True,
},
{
"key": "api_base",
"label": "API Base",
"placeholder": "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
"default_value": "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
"required": True,
"tooltip": "The base URL for your Dashscope server. Defaults to https://dashscope-intl.aliyuncs.com/compatible-mode/v1 if not specified.",
},
],
"OpenAI_Text_Compatible": [
{
"key": "api_base",
"label": "API Base",
"placeholder": "https://...",
"required": True,
},
{
"key": "api_key",
"label": "OpenAI API Key",
"field_type": "password",
"required": True,
},
],
"Bedrock": [
{
"key": "aws_access_key_id",
"label": "AWS Access Key ID",
"field_type": "password",
"tooltip": "You can provide the raw key or the environment variable (e.g. `os.environ/MY_SECRET_KEY`).",
},
{
"key": "aws_secret_access_key",
"label": "AWS Secret Access Key",
"field_type": "password",
"tooltip": "You can provide the raw key or the environment variable (e.g. `os.environ/MY_SECRET_KEY`).",
},
{
"key": "aws_session_token",
"label": "AWS Session Token",
"field_type": "password",
"tooltip": "Temporary credentials session token. You can provide the raw token or the environment variable (e.g. `os.environ/MY_SESSION_TOKEN`).",
},
{
"key": "aws_region_name",
"label": "AWS Region Name",
"placeholder": "us-east-1",
"tooltip": "You can provide the raw key or the environment variable (e.g. `os.environ/MY_SECRET_KEY`).",
},
{
"key": "aws_session_name",
"label": "AWS Session Name",
"placeholder": "my-session",
"tooltip": "Name for the AWS session. You can provide the raw value or the environment variable (e.g. `os.environ/MY_SESSION_NAME`).",
},
{
"key": "aws_profile_name",
"label": "AWS Profile Name",
"placeholder": "default",
"tooltip": "AWS profile name to use for authentication. You can provide the raw value or the environment variable (e.g. `os.environ/MY_PROFILE_NAME`).",
},
{
"key": "aws_role_name",
"label": "AWS Role Name",
"placeholder": "MyRole",
"tooltip": "AWS IAM role name to assume. You can provide the raw value or the environment variable (e.g. `os.environ/MY_ROLE_NAME`).",
},
{
"key": "aws_web_identity_token",
"label": "AWS Web Identity Token",
"field_type": "password",
"tooltip": "Web identity token for OIDC authentication. You can provide the raw token or the environment variable (e.g. `os.environ/MY_WEB_IDENTITY_TOKEN`).",
},
{
"key": "aws_bedrock_runtime_endpoint",
"label": "AWS Bedrock Runtime Endpoint",
"placeholder": "https://bedrock-runtime.us-east-1.amazonaws.com",
"tooltip": "Custom Bedrock runtime endpoint URL. You can provide the raw value or the environment variable (e.g. `os.environ/MY_BEDROCK_ENDPOINT`).",
},
],
"SageMaker": [
{
"key": "aws_access_key_id",
"label": "AWS Access Key ID",
"field_type": "password",
"tooltip": "You can provide the raw key or the environment variable (e.g. `os.environ/MY_SECRET_KEY`).",
},
{
"key": "aws_secret_access_key",
"label": "AWS Secret Access Key",
"field_type": "password",
"tooltip": "You can provide the raw key or the environment variable (e.g. `os.environ/MY_SECRET_KEY`).",
},
{
"key": "aws_region_name",
"label": "AWS Region Name",
"placeholder": "us-east-1",
"tooltip": "You can provide the raw key or the environment variable (e.g. `os.environ/MY_SECRET_KEY`).",
},
],
"Ollama": [
{
"key": "api_base",
"label": "API Base",
"placeholder": "http://localhost:11434",
"default_value": "http://localhost:11434",
"tooltip": "The base URL for your Ollama server. Defaults to http://localhost:11434 if not specified.",
},
],
"Anthropic": [
{
"key": "api_key",
"label": "API Key",
"placeholder": "sk-",
"field_type": "password",
"required": True,
},
],
"Deepgram": [
{
"key": "api_key",
"label": "API Key",
"field_type": "password",
"required": True,
},
],
"ElevenLabs": [
{
"key": "api_key",
"label": "API Key",
"field_type": "password",
"required": True,
},
],
"Google_AI_Studio": [
{
"key": "api_key",
"label": "API Key",
"placeholder": "aig-",
"field_type": "password",
"required": True,
},
],
"Groq": [
{
"key": "api_key",
"label": "API Key",
"field_type": "password",
"required": True,
},
],
"MistralAI": [
{
"key": "api_key",
"label": "API Key",
"field_type": "password",
"required": True,
},
],
"Deepseek": [
{
"key": "api_key",
"label": "API Key",
"field_type": "password",
"required": True,
},
],
"Cohere": [
{
"key": "api_key",
"label": "API Key",
"field_type": "password",
"required": True,
},
],
"Databricks": [
{
"key": "api_key",
"label": "API Key",
"field_type": "password",
"required": True,
},
],
"xAI": [
{
"key": "api_key",
"label": "API Key",
"field_type": "password",
"required": True,
},
],
"AIML": [
{
"key": "api_key",
"label": "API Key",
"field_type": "password",
"required": True,
},
],
"Cerebras": [
{
"key": "api_key",
"label": "API Key",
"field_type": "password",
"required": True,
},
],
"Sambanova": [
{
"key": "api_key",
"label": "API Key",
"field_type": "password",
"required": True,
},
],
"Perplexity": [
{
"key": "api_key",
"label": "API Key",
"field_type": "password",
"required": True,
},
],
"TogetherAI": [
{
"key": "api_key",
"label": "API Key",
"field_type": "password",
"required": True,
},
],
"Openrouter": [
{
"key": "api_key",
"label": "API Key",
"field_type": "password",
"required": True,
},
],
"FireworksAI": [
{
"key": "api_key",
"label": "API Key",
"field_type": "password",
"required": True,
},
],
"GradientAI": [
{
"key": "api_base",
"label": "GradientAI Endpoint",
"placeholder": "https://...",
},
{
"key": "api_key",
"label": "GradientAI API Key",
"field_type": "password",
"required": True,
},
],
"Triton": [
{
"key": "api_key",
"label": "API Key",
"field_type": "password",
},
{
"key": "api_base",
"label": "API Base",
"placeholder": "http://localhost:8000/generate",
},
],
"Hosted_Vllm": [
{
"key": "api_base",
"label": "API Base",
"placeholder": "https://...",
"required": True,
},
{
"key": "api_key",
"label": "vLLM API Key",
"field_type": "password",
},
],
"Voyage": [
{
"key": "api_key",
"label": "API Key",
"field_type": "password",
"required": True,
},
],
"JinaAI": [
{
"key": "api_key",
"label": "API Key",
"field_type": "password",
"required": True,
},
],
"VolcEngine": [
{
"key": "api_key",
"label": "API Key",
"field_type": "password",
"required": True,
},
],
"DeepInfra": [
{
"key": "api_key",
"label": "API Key",
"field_type": "password",
"required": True,
},
],
"Oracle": [
{
"key": "api_key",
"label": "API Key",
"field_type": "password",
"required": True,
},
],
"Snowflake": [
{
"key": "api_key",
"label": "Snowflake API Key / JWT Key for Authentication",
"field_type": "password",
"required": True,
},
{
"key": "api_base",
"label": "Snowflake API Endpoint",
"placeholder": "https://1234567890.snowflakecomputing.com/api/v2/cortex/inference:complete",
"tooltip": "Enter the full endpoint with path here. Example: https://1234567890.snowflakecomputing.com/api/v2/cortex/inference:complete",
"required": True,
},
],
"Infinity": [
{
"key": "api_base",
"label": "API Base",
"placeholder": "http://localhost:7997",
},
],
"FalAI": [
{
"key": "api_key",
"label": "API Key",
"field_type": "password",
"required": True,
},
],
}
def _normalize_field(field: Dict[str, Any]) -> ProviderCredentialField:
return ProviderCredentialField(
key=field["key"],
label=field["label"],
placeholder=field.get("placeholder"),
tooltip=field.get("tooltip"),
required=field.get("required", False),
field_type=field.get("field_type", "text"),
options=field.get("options"),
default_value=field.get("default_value"),
)
def get_provider_create_metadata() -> List[ProviderCreateInfo]:
providers: List[ProviderCreateInfo] = []
for provider_key, base_info in PROVIDER_BASE_INFO.items():
raw_fields = PROVIDER_CREDENTIAL_FIELDS.get(provider_key, _FALLBACK_FIELDS)
normalized_fields = [_normalize_field(field) for field in raw_fields]
providers.append(
ProviderCreateInfo(
provider=provider_key,
provider_display_name=base_info["provider_display_name"],
litellm_provider=base_info["litellm_provider"],
default_model_placeholder=base_info.get(
"default_model_placeholder", DEFAULT_MODEL_PLACEHOLDER
),
credential_fields=normalized_fields,
)
)
# Ensure we have metadata entries for all providers defined in LlmProviders.
# If a provider enum value is not already present in the litellm_provider
# field of any entry, create a default entry for it using the fallback
# credential fields (api_key + api_base) and a generated display name.
existing_litellm_providers = {p.litellm_provider for p in providers}
for provider_enum in LlmProviders:
litellm_provider_value = provider_enum.value
if litellm_provider_value in existing_litellm_providers:
continue
normalized_fields = [_normalize_field(field) for field in _FALLBACK_FIELDS]
provider_display_name = provider_enum.value.replace("_", " ").title()
providers.append(
ProviderCreateInfo(
provider=provider_enum.name,
provider_display_name=provider_display_name,
litellm_provider=litellm_provider_value,
default_model_placeholder=DEFAULT_MODEL_PLACEHOLDER,
credential_fields=normalized_fields,
)
)
providers.sort(key=lambda item: item.provider_display_name.lower())
return providers
@@ -3,11 +3,17 @@ from typing import List
from fastapi import APIRouter, Depends, HTTPException
from litellm.proxy._types import CommonProxyErrors
from litellm.proxy.public_endpoints.provider_create_metadata import (
get_provider_create_metadata,
)
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
from litellm.types.proxy.management_endpoints.model_management_endpoints import (
ModelGroupInfoProxy,
)
from litellm.types.proxy.public_endpoints.public_endpoints import PublicModelHubInfo
from litellm.types.proxy.public_endpoints.public_endpoints import (
PublicModelHubInfo,
ProviderCreateInfo,
)
from litellm.types.utils import LlmProviders
router = APIRouter()
@@ -74,3 +80,16 @@ async def get_supported_providers() -> List[str]:
"""
return sorted(provider.value for provider in LlmProviders)
@router.get(
"/public/providers/fields",
tags=["public", "providers"],
response_model=List[ProviderCreateInfo],
)
async def get_provider_fields() -> List[ProviderCreateInfo]:
"""
Return provider metadata required by the dashboard create-model flow.
"""
return get_provider_create_metadata()
@@ -2968,10 +2968,30 @@ async def ui_view_session_spend_logs(
session_id: str = fastapi.Query(
description="Get all spend logs for a particular session",
),
page: int = fastapi.Query(
default=1,
ge=1,
description="Page number for pagination",
),
page_size: int = fastapi.Query(
default=50,
ge=1,
le=100,
description="Number of items per page",
),
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
):
"""
Get all spend logs for a particular session
Get paginated spend logs for a particular session.
Returns:
{
"data": List[LiteLLM_SpendLogs],
"total": int,
"page": int,
"page_size": int,
"total_pages": int,
}
"""
from litellm.proxy.proxy_server import prisma_client
@@ -2984,11 +3004,32 @@ async def ui_view_session_spend_logs(
# Build query conditions
where_conditions = {"session_id": session_id}
# Query the database
result = await prisma_client.db.litellm_spendlogs.find_many(
where=where_conditions, order={"startTime": "asc"}
# Calculate pagination offsets
skip = (page - 1) * page_size
# Get total count for pagination metadata
total_records = await prisma_client.db.litellm_spendlogs.count(
where=where_conditions
)
return result
# Query the database with pagination
result = await prisma_client.db.litellm_spendlogs.find_many(
where=where_conditions,
order={"startTime": "asc"},
skip=skip,
take=page_size,
)
total_pages = (total_records + page_size - 1) // page_size
return {
"data": result,
"total": total_records,
"page": page,
"page_size": page_size,
"total_pages": total_pages,
}
except Exception as e:
if isinstance(e, HTTPException):
raise e
@@ -526,8 +526,10 @@ class LiteLLM_Proxy_MCP_Handler:
else:
assistant_message_content.append(content)
# Add assistant message with content and function calls
if assistant_message_content or function_calls:
# Add assistant message only if there's actual content (not empty)
# For example, gemini requires that function call turns come immediately after user turns,
# so we should not add empty assistant messages
if assistant_message_content:
follow_up_input.append(
{
"type": "message",
@@ -536,9 +538,9 @@ class LiteLLM_Proxy_MCP_Handler:
}
)
# Add function calls after assistant message
for function_call in function_calls:
follow_up_input.append(function_call)
# Add function calls (these can come directly after user message for LLM)
for function_call in function_calls:
follow_up_input.append(function_call)
# Add tool results (function call outputs)
for tool_result in tool_results:
+2 -1
View File
@@ -679,10 +679,11 @@ class BedrockInputDataConfig(TypedDict):
s3InputDataConfig: BedrockS3InputDataConfig
class BedrockS3OutputDataConfig(TypedDict):
class BedrockS3OutputDataConfig(TypedDict, total=False):
"""S3 output data configuration for Bedrock batch jobs."""
s3Uri: str
s3EncryptionKeyId: Optional[str]
class BedrockOutputDataConfig(TypedDict):
+1 -1
View File
@@ -1475,7 +1475,7 @@ ResponsesAPIStreamingResponse = Annotated[
]
REASONING_EFFORT = Literal["minimal", "low", "medium", "high"]
REASONING_EFFORT = Literal["none", "minimal", "low", "medium", "high"]
class OpenAIRealtimeStreamSession(TypedDict, total=False):
@@ -1,4 +1,4 @@
from typing import Dict
from typing import Dict, List
from pydantic import BaseModel, Field
@@ -11,3 +11,34 @@ class ModelGroupInfoProxy(ModelGroupInfo):
class UpdateUsefulLinksRequest(BaseModel):
useful_links: Dict[str, str]
class NewModelGroupRequest(BaseModel):
access_group: str # The access group name (e.g., "production-models")
model_names: List[str] # Existing model groups to include (e.g., ["gpt-4", "claude-3"])
class NewModelGroupResponse(BaseModel):
access_group: str
model_names: List[str]
models_updated: int # Number of models updated
class UpdateModelGroupRequest(BaseModel):
model_names: List[str] # Updated list of model groups to include
class DeleteModelGroupResponse(BaseModel):
access_group: str
models_updated: int # Number of deployments where the access group was removed
message: str
class AccessGroupInfo(BaseModel):
access_group: str
model_names: List[str] # List of model names in this access group
deployment_count: int # Total number of deployments with this access group
class ListAccessGroupsResponse(BaseModel):
access_groups: List[AccessGroupInfo]
@@ -1,4 +1,4 @@
from typing import Dict, Optional
from typing import Dict, List, Literal, Optional
from pydantic import BaseModel
@@ -8,3 +8,22 @@ class PublicModelHubInfo(BaseModel):
custom_docs_description: Optional[str]
litellm_version: str
useful_links: Optional[Dict[str, str]]
class ProviderCredentialField(BaseModel):
key: str
label: str
placeholder: Optional[str] = None
tooltip: Optional[str] = None
required: bool = False
field_type: Literal["text", "password", "select", "upload"] = "text"
options: Optional[List[str]] = None
default_value: Optional[str] = None
class ProviderCreateInfo(BaseModel):
provider: str
provider_display_name: str
litellm_provider: str
credential_fields: List[ProviderCredentialField]
default_model_placeholder: Optional[str] = None
+2
View File
@@ -205,6 +205,7 @@ class GenericLiteLLMParams(CredentialLiteLLMParams, CustomPricingLiteLLMParams):
# Batch/File API Params
s3_bucket_name: Optional[str] = None
s3_encryption_key_id: Optional[str] = None
gcs_bucket_name: Optional[str] = None
# Vector Store Params
@@ -262,6 +263,7 @@ class GenericLiteLLMParams(CredentialLiteLLMParams, CustomPricingLiteLLMParams):
auto_router_embedding_model: Optional[str] = None,
# Batch/File API Params
s3_bucket_name: Optional[str] = None,
s3_encryption_key_id: Optional[str] = None,
gcs_bucket_name: Optional[str] = None,
**params,
):
+77 -5
View File
@@ -8523,6 +8523,14 @@
"/v1/images/generations"
]
},
"fal_ai/fal-ai/flux/schnell": {
"litellm_provider": "fal_ai",
"mode": "image_generation",
"output_cost_per_image": 0.003,
"supported_endpoints": [
"/v1/images/generations"
]
},
"fal_ai/fal-ai/imagen4/preview": {
"litellm_provider": "fal_ai",
"mode": "image_generation",
@@ -8531,6 +8539,22 @@
"/v1/images/generations"
]
},
"fal_ai/fal-ai/imagen4/preview/fast": {
"litellm_provider": "fal_ai",
"mode": "image_generation",
"output_cost_per_image": 0.02,
"supported_endpoints": [
"/v1/images/generations"
]
},
"fal_ai/fal-ai/imagen4/preview/ultra": {
"litellm_provider": "fal_ai",
"mode": "image_generation",
"output_cost_per_image": 0.06,
"supported_endpoints": [
"/v1/images/generations"
]
},
"fal_ai/fal-ai/recraft/v3/text-to-image": {
"litellm_provider": "fal_ai",
"mode": "image_generation",
@@ -23408,6 +23432,19 @@
"supports_function_calling": true,
"supports_tool_choice": true
},
"vertex_ai/moonshotai/kimi-k2-thinking-maas": {
"input_cost_per_token": 6e-07,
"litellm_provider": "vertex_ai-moonshot_models",
"max_input_tokens": 256000,
"max_output_tokens": 256000,
"max_tokens": 256000,
"mode": "chat",
"output_cost_per_token": 2.5e-06,
"source": "https://cloud.google.com/vertex-ai/generative-ai/pricing#partner-models",
"supports_function_calling": true,
"supports_tool_choice": true,
"supports_web_search": true
},
"vertex_ai/mistral-medium-3": {
"input_cost_per_token": 4e-07,
"litellm_provider": "vertex_ai-mistral_models",
@@ -23744,6 +23781,22 @@
"mode": "embedding",
"output_cost_per_token": 0.0
},
"voyage/voyage-3.5": {
"input_cost_per_token": 6e-08,
"litellm_provider": "voyage",
"max_input_tokens": 32000,
"max_tokens": 32000,
"mode": "embedding",
"output_cost_per_token": 0.0
},
"voyage/voyage-3.5-lite": {
"input_cost_per_token": 2e-08,
"litellm_provider": "voyage",
"max_input_tokens": 32000,
"max_tokens": 32000,
"mode": "embedding",
"output_cost_per_token": 0.0
},
"voyage/voyage-code-2": {
"input_cost_per_token": 1.2e-07,
"litellm_provider": "voyage",
@@ -24789,7 +24842,9 @@
"1280x720",
"720x1280"
],
"comment": "5 credits per second @ $0.01 per credit = $0.05 per second"
"metadata": {
"comment": "5 credits per second @ $0.01 per credit = $0.05 per second"
}
},
"runwayml/gen4_aleph": {
"litellm_provider": "runwayml",
@@ -24807,7 +24862,9 @@
"1280x720",
"720x1280"
],
"comment": "15 credits per second @ $0.01 per credit = $0.15 per second"
"metadata": {
"comment": "15 credits per second @ $0.01 per credit = $0.15 per second"
}
},
"runwayml/gen3a_turbo": {
"litellm_provider": "runwayml",
@@ -24825,7 +24882,9 @@
"1280x720",
"720x1280"
],
"comment": "5 credits per second @ $0.01 per credit = $0.05 per second"
"metadata": {
"comment": "5 credits per second @ $0.01 per credit = $0.05 per second"
}
},
"runwayml/gen4_image": {
"litellm_provider": "runwayml",
@@ -24844,7 +24903,9 @@
"1280x720",
"1920x1080"
],
"comment": "5 credits per 720p image or 8 credits per 1080p image @ $0.01 per credit. Using 5 credits ($0.05) as base cost"
"metadata": {
"comment": "5 credits per 720p image or 8 credits per 1080p image @ $0.01 per credit. Using 5 credits ($0.05) as base cost"
}
},
"runwayml/gen4_image_turbo": {
"litellm_provider": "runwayml",
@@ -24863,6 +24924,17 @@
"1280x720",
"1920x1080"
],
"comment": "2 credits per image (any resolution) @ $0.01 per credit = $0.02 per image"
"metadata": {
"comment": "2 credits per image (any resolution) @ $0.01 per credit = $0.02 per image"
}
},
"runwayml/eleven_multilingual_v2": {
"litellm_provider": "runwayml",
"mode": "audio_speech",
"input_cost_per_character": 3e-07,
"source": "https://docs.dev.runwayml.com/guides/pricing/",
"metadata": {
"comment": "Estimated cost based on standard TTS pricing. RunwayML uses ElevenLabs models."
}
}
}
Generated
+641 -110
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "litellm"
version = "1.79.3"
version = "1.79.4"
description = "Library to easily interface with LLM API providers"
authors = ["BerriAI"]
license = "MIT"
@@ -59,7 +59,7 @@ websockets = {version = "^13.1.0", optional = true}
boto3 = {version = "1.36.0", optional = true}
redisvl = {version = "^0.4.1", optional = true, markers = "python_version >= '3.9' and python_version < '3.14'"}
mcp = {version = "^1.10.0", optional = true, python = ">=3.10"}
litellm-proxy-extras = {version = "0.4.3", optional = true}
litellm-proxy-extras = {version = "0.4.4", optional = true}
rich = {version = "13.7.1", optional = true}
litellm-enterprise = {version = "0.1.20", optional = true}
diskcache = {version = "^5.6.1", optional = true}
@@ -159,7 +159,7 @@ requires = ["poetry-core", "wheel"]
build-backend = "poetry.core.masonry.api"
[tool.commitizen]
version = "1.79.3"
version = "1.79.4"
version_files = [
"pyproject.toml:^version"
]
+1 -1
View File
@@ -43,7 +43,7 @@ sentry_sdk==2.21.0 # for sentry error handling
detect-secrets==1.5.0 # Enterprise - secret detection / masking in LLM requests
cryptography==44.0.1
tzdata==2025.1 # IANA time zone database
litellm-proxy-extras==0.4.3 # for proxy extras - e.g. prisma migrations
litellm-proxy-extras==0.4.4 # for proxy extras - e.g. prisma migrations
### LITELLM PACKAGE DEPENDENCIES
python-dotenv==1.0.1 # for env
tiktoken==0.8.0 # for calculating usage
Binary file not shown.
+1 -1
View File
@@ -426,7 +426,7 @@ async def test_runwayml_tts_async():
assert speech_file_path.exists()
assert speech_file_path.stat().st_size > 0
print(f"Azure TTS audio saved to: {speech_file_path}")
print(f"RunwayML TTS audio saved to: {speech_file_path}")
# assert response cost is greater than 0
print("Response cost: ", response._hidden_params["response_cost"])
@@ -193,3 +193,53 @@ async def test_bedrock_retrieve_batch():
assert batch_response.input_file_id == "s3://test-bucket/input/test-input.jsonl"
assert batch_response.output_file_id == "s3://test-bucket/output/"
def test_bedrock_batch_with_encryption_key_in_post_request():
"""
Test that s3_encryption_key_id is included in the AWS POST request payload.
"""
import json
import litellm
test_kms_key_id = "arn:aws:kms:us-west-2:123456789012:key/12345678-1234-1234-1234-123456789012"
captured_request_body = None
def mock_post(*args, **kwargs):
nonlocal captured_request_body
if "data" in kwargs:
captured_request_body = kwargs["data"]
mock_response = MagicMock()
mock_response.json.return_value = {
"jobArn": "arn:aws:bedrock:us-west-2:123456789012:model-invocation-job/test-job",
"jobName": "test-job",
"status": "Submitted"
}
mock_response.status_code = 200
mock_response.raise_for_status.return_value = None
return mock_response
with patch("litellm.llms.custom_httpx.http_handler.HTTPHandler.post", side_effect=mock_post):
response = litellm.create_batch(
completion_window="24h",
endpoint="/v1/chat/completions",
input_file_id="s3://test-bucket/input/test.jsonl",
custom_llm_provider="bedrock",
model="us.anthropic.claude-3-5-sonnet-20240620-v1:0",
s3_encryption_key_id=test_kms_key_id,
aws_batch_role_arn="arn:aws:iam::123456789012:role/test-role"
)
assert captured_request_body is not None, "Request body was not captured"
request_data = json.loads(captured_request_body)
print("REQUEST DATA to bedrock batch creation", json.dumps(request_data, indent=4))
assert "outputDataConfig" in request_data
assert "s3OutputDataConfig" in request_data["outputDataConfig"]
assert "s3EncryptionKeyId" in request_data["outputDataConfig"]["s3OutputDataConfig"]
assert request_data["outputDataConfig"]["s3OutputDataConfig"]["s3EncryptionKeyId"] == test_kms_key_id
print("SUCCESS: s3_encryption_key_id properly included in AWS POST request")
@@ -14,6 +14,7 @@ from litellm import aimage_generation
"model",
[
"fal_ai/fal-ai/flux-pro/v1.1-ultra",
"fal_ai/fal-ai/flux/schnell",
"fal_ai/fal-ai/recraft/v3/text-to-image",
"fal_ai/bria/text-to-image/3.2",
"fal_ai/fal-ai/stable-diffusion-v35-medium"
+42
View File
@@ -337,6 +337,48 @@ def test_openai_max_retries_0(mock_get_openai_client):
assert mock_get_openai_client.call_args.kwargs["max_retries"] == 0
@patch("litellm.main.openai_chat_completions._get_openai_client")
def test_openai_image_generation_forwards_organization(mock_get_openai_client):
"""Ensure organization flows to OpenAI client for image generation."""
class _DummyImages:
def generate(self, **kwargs): # type: ignore
class _Resp:
def model_dump(self_inner): # minimal OpenAI ImagesResponse shape
return {
"created": 123,
"data": [{"url": "http://example.com/image.png"}],
"usage": {"input_tokens": 0, "output_tokens": 0, "total_tokens": 0},
}
return _Resp()
class _DummyClient:
def __init__(self):
self.api_key = "sk-test"
class _BaseURL:
_uri_reference = "https://api.openai.com/v1"
self._base_url = _BaseURL()
self.images = _DummyImages()
mock_get_openai_client.return_value = _DummyClient()
org = "org_test_123"
resp = litellm.image_generation(
model="gpt-image-1",
prompt="A cute baby sea otter",
organization=org,
)
# Assert organization forwarded into OpenAI client factory
assert mock_get_openai_client.call_args.kwargs.get("organization") == org
# Basic sanity on response shape
assert hasattr(resp, "data") and len(resp.data) == 1
@pytest.mark.parametrize("model", ["o1", "o3-mini"])
def test_o1_parallel_tool_calls(model):
litellm.completion(
@@ -0,0 +1,127 @@
"""
Unit tests for SambaNova chat message transformation
"""
import pytest
from litellm.llms.sambanova.chat import SambanovaConfig
class TestSambanovaContentListHandling:
"""
Test that SambaNova properly transforms content lists to strings
"""
def test_content_list_to_string_transformation(self):
"""
Test content list with text objects is converted to string.
SambaNova API doesn't support content as a list - only string content.
"""
config = SambanovaConfig()
messages = [
{
"role": "user",
"content": [
{"type": "text", "text": "Hello, how are you?"}
]
}
]
transformed_messages = config._transform_messages(
messages=messages,
model="sambanova/gpt-oss-120b",
is_async=False
)
assert len(transformed_messages) == 1
assert transformed_messages[0]["role"] == "user"
assert isinstance(transformed_messages[0]["content"], str)
assert transformed_messages[0]["content"] == "Hello, how are you?"
def test_content_list_multiple_text_blocks(self):
"""
Test content list with multiple text blocks is converted to concatenated string.
"""
config = SambanovaConfig()
messages = [
{
"role": "user",
"content": [
{"type": "text", "text": "Hello, "},
{"type": "text", "text": "how are you?"}
]
}
]
transformed_messages = config._transform_messages(
messages=messages,
model="sambanova/gpt-oss-120b",
is_async=False
)
assert transformed_messages[0]["content"] == "Hello, how are you?"
def test_string_content_unchanged(self):
"""
Test that string content is passed through unchanged.
"""
config = SambanovaConfig()
messages = [
{
"role": "user",
"content": "Hello, how are you?"
}
]
transformed_messages = config._transform_messages(
messages=messages,
model="sambanova/gpt-oss-120b",
is_async=False
)
assert transformed_messages[0]["content"] == "Hello, how are you?"
def test_multiple_messages_transformation(self):
"""
Test transformation of multiple messages with mixed content types.
"""
config = SambanovaConfig()
messages = [
{
"role": "system",
"content": "You are a helpful assistant."
},
{
"role": "user",
"content": [
{"type": "text", "text": "What is the weather?"}
]
},
{
"role": "assistant",
"content": "I need your location."
},
{
"role": "user",
"content": [
{"type": "text", "text": "I'm in "},
{"type": "text", "text": "San Francisco"}
]
}
]
transformed_messages = config._transform_messages(
messages=messages,
model="sambanova/gpt-oss-120b",
is_async=False
)
assert len(transformed_messages) == 4
assert transformed_messages[0]["content"] == "You are a helpful assistant."
assert transformed_messages[1]["content"] == "What is the weather?"
assert transformed_messages[2]["content"] == "I need your location."
assert transformed_messages[3]["content"] == "I'm in San Francisco"
@@ -132,6 +132,11 @@ def prisma_client():
### add connection pool + pool timeout args
params = {"connection_limit": 100, "pool_timeout": 60}
database_url = os.getenv("DATABASE_URL")
# If DATABASE_URL is not set, use a default test database URL
if not database_url:
database_url = "postgresql://postgres:postgres@localhost:5432/circle_test"
modified_url = append_query_params(database_url, params)
os.environ["DATABASE_URL"] = modified_url
@@ -666,7 +671,8 @@ def test_call_with_end_user_over_budget(prisma_client):
except Exception as e:
print(f"raised error: {e}, traceback: {traceback.format_exc()}")
error_detail = e.message
assert "Budget has been exceeded! Current" in error_detail
assert "ExceededBudget: End User=" in error_detail
assert "over budget" in error_detail
assert isinstance(e, ProxyException)
assert e.type == ProxyErrorTypes.budget_exceeded
print(vars(e))
@@ -157,6 +157,9 @@ def test_embedding_auth_exception_azure(mock_aembedding, client):
metadata=mock.ANY,
proxy_server_request=mock.ANY,
secret_fields=mock.ANY,
request_timeout=mock.ANY,
litellm_call_id=mock.ANY,
litellm_logging_obj=mock.ANY,
)
print("Response from proxy=", response)
+25 -10
View File
@@ -545,17 +545,22 @@ def test_embedding(mock_aembedding, client_no_auth):
"input": ["good morning from litellm"],
}
pre_call_return_value = {
**test_data,
"metadata": {"source": "unit-test"},
"proxy_server_request": {"path": "/v1/embeddings"},
"secret_fields": [],
}
async def _pre_call_hook_side_effect(**kwargs):
data = kwargs["data"]
metadata = {**(data.get("metadata") or {}), "source": "unit-test"}
data["metadata"] = metadata
proxy_request = {**(data.get("proxy_server_request") or {})}
proxy_request["path"] = "/v1/embeddings"
data["proxy_server_request"] = proxy_request
return data
async def _post_call_success_side_effect(**kwargs):
return kwargs["response"]
with patch.object(
litellm.proxy.proxy_server.proxy_logging_obj,
"pre_call_hook",
new=AsyncMock(return_value=pre_call_return_value),
new=AsyncMock(side_effect=_pre_call_hook_side_effect),
) as mock_pre_call_hook, patch.object(
litellm.proxy.proxy_server.proxy_logging_obj,
"during_call_hook",
@@ -563,7 +568,7 @@ def test_embedding(mock_aembedding, client_no_auth):
) as mock_during_hook, patch.object(
litellm.proxy.proxy_server.proxy_logging_obj,
"post_call_success_hook",
new=AsyncMock(return_value=None),
new=AsyncMock(side_effect=_post_call_success_side_effect),
):
response = client_no_auth.post("/v1/embeddings", json=test_data)
@@ -571,6 +576,9 @@ def test_embedding(mock_aembedding, client_no_auth):
model="azure/text-embedding-ada-002",
input=["good morning from litellm"],
specific_deployment=True,
litellm_call_id=mock.ANY,
litellm_logging_obj=mock.ANY,
request_timeout=mock.ANY,
metadata=mock.ANY,
proxy_server_request=mock.ANY,
secret_fields=mock.ANY,
@@ -580,6 +588,9 @@ def test_embedding(mock_aembedding, client_no_auth):
print(len(result["data"][0]["embedding"]))
assert len(result["data"][0]["embedding"]) > 10 # this usually has len==1536 so
call_metadata = mock_aembedding.call_args.kwargs["metadata"]
assert call_metadata.get("source") == "unit-test"
pre_call_kwargs = mock_pre_call_hook.await_args_list[0].kwargs
assert (
pre_call_kwargs.get("call_type") == "aembedding"
@@ -587,8 +598,8 @@ def test_embedding(mock_aembedding, client_no_auth):
during_call_kwargs = mock_during_hook.await_args_list[0].kwargs
assert (
during_call_kwargs.get("call_type") == "aembedding"
), f"expected during_call_hook to receive call_type='aembedding', got {during_call_kwargs.get('call_type')}"
during_call_kwargs.get("call_type") == "embeddings"
), f"expected during_call_hook to receive call_type='embeddings', got {during_call_kwargs.get('call_type')}"
except Exception as e:
pytest.fail(f"LiteLLM Proxy test failed. Exception - {str(e)}")
@@ -609,11 +620,15 @@ def test_bedrock_embedding(mock_aembedding, client_no_auth):
mock_aembedding.assert_called_once_with(
model="amazon-embeddings",
input=["good morning from litellm"],
litellm_call_id=mock.ANY,
litellm_logging_obj=mock.ANY,
request_timeout=mock.ANY,
metadata=mock.ANY,
proxy_server_request=mock.ANY,
secret_fields=mock.ANY,
)
assert response.status_code == 200
print(response.status_code, response.text)
result = response.json()
print(len(result["data"][0]["embedding"]))
assert len(result["data"][0]["embedding"]) > 10 # this usually has len==1536 so
@@ -153,3 +153,33 @@ def test_gpt5_codex_supports_function_calling(config: OpenAIConfig):
assert "functions" in supported_params
assert "function_call" in supported_params
assert "tools" in supported_params
def test_gpt5_1_reasoning_effort_none(config: OpenAIConfig):
"""Test that GPT-5.1 supports reasoning_effort='none' parameter.
Related issue: https://github.com/BerriAI/litellm/issues/16633
GPT-5.1 introduced 'none' as the new default reasoning effort setting
for faster, lower-latency responses.
"""
# Test that reasoning_effort is a supported parameter
assert "reasoning_effort" in config.get_supported_openai_params(model="gpt-5.1")
# Test that reasoning_effort="none" passes through correctly
params = config.map_openai_params(
non_default_params={"reasoning_effort": "none"},
optional_params={},
model="gpt-5.1",
drop_params=False,
)
assert params["reasoning_effort"] == "none"
# Test with other valid values for GPT-5.1
for effort in ["low", "medium", "high"]:
params = config.map_openai_params(
non_default_params={"reasoning_effort": effort},
optional_params={},
model="gpt-5.1",
drop_params=False,
)
assert params["reasoning_effort"] == effort
@@ -1,251 +0,0 @@
"""
Test BGE embeddings with Vertex AI using custom api_base.
This test ensures that BGE embeddings work correctly with Vertex AI
and that the request body is properly formatted.
"""
import json
import os
import sys
from unittest.mock import MagicMock, patch
sys.path.insert(
0, os.path.abspath("../../../..")
)
import pytest
import litellm
from litellm.llms.custom_httpx.http_handler import HTTPHandler
def test_vertex_ai_bge_embedding_with_custom_api_base():
"""
Test Vertex AI BGE embeddings with custom api_base.
This test verifies that when using a BGE model with Vertex AI and
a custom api_base, the request is properly formatted and sent to
the correct endpoint.
"""
client = HTTPHandler()
def mock_auth_token(*args, **kwargs):
return "fake-token", "fake-project"
with patch.object(client, "post") as mock_post, patch(
"litellm.llms.vertex_ai.vertex_embeddings.embedding_handler.VertexEmbedding._ensure_access_token",
side_effect=mock_auth_token
):
mock_response = MagicMock()
mock_response.status_code = 200
# BGE models return embeddings directly as arrays, not wrapped in objects
mock_response.json.return_value = {
"predictions": [
[0.1, 0.2, 0.3, 0.4, 0.5],
[0.6, 0.7, 0.8, 0.9, 1.0]
],
"deployedModelId": "849506872875548672",
"model": "projects/1060139831167/locations/us-central1/models/baai_bge-small-en-v1.5",
"modelDisplayName": "baai_bge-small-en-v1.5",
"modelVersionId": "1"
}
mock_post.return_value = mock_response
response = litellm.embedding(
model="vertex_ai/bge-small-en-v1.5",
input=["Hello", "World"],
api_base="http://10.96.32.8",
client=client
)
mock_post.assert_called_once()
call_args = mock_post.call_args
kwargs = call_args.kwargs if hasattr(call_args, 'kwargs') else call_args[1]
if "url" in kwargs:
api_url_called = kwargs["url"]
elif len(call_args[0]) > 0:
api_url_called = call_args[0][0]
else:
api_url_called = "Unknown"
# Vertex AI may use 'json' or 'data' parameter
if "json" in kwargs:
request_data = kwargs["json"]
elif "data" in kwargs:
request_data = json.loads(kwargs["data"])
else:
request_data = {}
print("\n" + "="*50)
print("Mock Request Body Received:")
print("="*50)
print(json.dumps(request_data, indent=2))
print("="*50)
print(f"API Base: {api_url_called}")
print("="*50 + "\n")
assert "instances" in request_data
assert len(request_data["instances"]) == 2
# BGE models should use "prompt" instead of "content"
assert "prompt" in request_data["instances"][0]
assert request_data["instances"][0]["prompt"] == "Hello"
assert "prompt" in request_data["instances"][1]
assert request_data["instances"][1]["prompt"] == "World"
assert isinstance(response.data, list)
assert len(response.data) == 2
assert "embedding" in response.data[0]
def test_vertex_ai_bge_with_endpoint_id_pattern():
"""
Test BGE with vertex_ai/bge/endpoint_id pattern.
This test verifies that the pattern vertex_ai/bge/204379420394258432
correctly triggers BGE transformations and routes to the endpoint.
"""
client = HTTPHandler()
def mock_auth_token(*args, **kwargs):
return "fake-token", "fake-project"
with patch.object(client, "post") as mock_post, patch(
"litellm.llms.vertex_ai.vertex_embeddings.embedding_handler.VertexEmbedding._ensure_access_token",
side_effect=mock_auth_token
):
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {
"predictions": [
[0.1, 0.2, 0.3, 0.4, 0.5],
[0.6, 0.7, 0.8, 0.9, 1.0]
],
"deployedModelId": "204379420394258432",
"model": "projects/1060139831167/locations/europe-west4/models/baai_bge-base-en",
"modelDisplayName": "baai_bge-base-en",
"modelVersionId": "1"
}
mock_post.return_value = mock_response
response = litellm.embedding(
model="vertex_ai/bge/204379420394258432",
input=["Hello", "World"],
vertex_project="1060139831167",
vertex_location="europe-west4",
client=client
)
mock_post.assert_called_once()
call_args = mock_post.call_args
kwargs = call_args.kwargs if hasattr(call_args, 'kwargs') else call_args[1]
if "url" in kwargs:
api_url_called = kwargs["url"]
elif len(call_args[0]) > 0:
api_url_called = call_args[0][0]
else:
api_url_called = "Unknown"
# Vertex AI may use 'json' or 'data' parameter
if "json" in kwargs:
request_data = kwargs["json"]
elif "data" in kwargs:
request_data = json.loads(kwargs["data"])
else:
request_data = {}
print("\n" + "="*50)
print("BGE Endpoint Pattern Test:")
print("="*50)
print(f"Model: vertex_ai/bge/204379420394258432")
print(f"API URL: {api_url_called}")
print("Request Body:")
print(json.dumps(request_data, indent=2))
print("="*50 + "\n")
# Verify URL contains the endpoint ID and uses endpoints/ path
assert "204379420394258432" in api_url_called, f"Endpoint ID not in URL: {api_url_called}"
assert "endpoints" in api_url_called, f"Expected 'endpoints' in URL, got: {api_url_called}"
# Verify BGE-specific request format (uses "prompt" not "content")
assert "instances" in request_data
assert "prompt" in request_data["instances"][0]
assert request_data["instances"][0]["prompt"] == "Hello"
# Verify response
assert isinstance(response.data, list)
assert len(response.data) == 2
def test_vertex_ai_bge_psc_endpoint_url_construction():
"""
Test that BGE models with PSC endpoints construct correct URL without bge/ prefix.
Verifies that vertex_ai/bge/378943383978115072 with api_base http://10.128.16.2
constructs URL: http://10.128.16.2/v1/projects/{project}/locations/{location}/endpoints/378943383978115072:predict
The bge/ prefix should be stripped from the endpoint URL.
"""
client = HTTPHandler()
def mock_auth_token(*args, **kwargs):
return "fake-token", "gen-lang-client-0682925754"
with patch.object(client, "post") as mock_post, patch(
"litellm.llms.vertex_ai.vertex_embeddings.embedding_handler.VertexEmbedding._ensure_access_token",
side_effect=mock_auth_token
):
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {
"predictions": [
[0.1, 0.2, 0.3, 0.4, 0.5]
]
}
mock_post.return_value = mock_response
response = litellm.embedding(
model="vertex_ai/bge/378943383978115072",
input=["The food was delicious and the waiter.."],
api_base="http://10.128.16.2",
vertex_project="gen-lang-client-0682925754",
vertex_location="us-central1",
client=client
)
mock_post.assert_called_once()
call_args = mock_post.call_args
kwargs = call_args.kwargs if hasattr(call_args, 'kwargs') else call_args[1]
if "url" in kwargs:
api_url_called = kwargs["url"]
elif len(call_args[0]) > 0:
api_url_called = call_args[0][0]
else:
api_url_called = "Unknown"
print("\n" + "="*50)
print("PSC Endpoint URL Construction Test:")
print("="*50)
print(f"Model: vertex_ai/bge/378943383978115072")
print(f"API Base: http://10.128.16.2")
print(f"Constructed URL: {api_url_called}")
print("="*50 + "\n")
# Verify the URL is constructed correctly
expected_url = "http://10.128.16.2/v1/projects/gen-lang-client-0682925754/locations/us-central1/endpoints/378943383978115072:predict"
assert api_url_called == expected_url, f"Expected URL: {expected_url}, Got: {api_url_called}"
# Verify bge/ prefix is NOT in the URL
assert "bge/" not in api_url_called, f"URL should not contain 'bge/' prefix: {api_url_called}"
# Verify response works
assert isinstance(response.data, list)
assert len(response.data) == 1
@@ -1,111 +0,0 @@
"""
Test BGE response transformation validation.
This test verifies that the BGE response transformer properly validates
and handles different response formats.
"""
import os
import sys
sys.path.insert(
0, os.path.abspath("../../../..")
)
import pytest
from litellm.llms.vertex_ai.vertex_embeddings.bge import VertexBGEConfig
from litellm.types.utils import EmbeddingResponse
def test_is_bge_model_detection():
"""
Test BGE model detection for post-provider-split patterns.
After main.py splits the provider, model strings are passed without the provider prefix.
Model name transformation (bge/ -> numeric ID) is handled in common_utils._get_vertex_url().
"""
# Should detect BGE models (after provider split)
assert VertexBGEConfig.is_bge_model("bge-small-en-v1.5") is True
assert VertexBGEConfig.is_bge_model("bge/204379420394258432") is True
assert VertexBGEConfig.is_bge_model("BGE-large-en-v1.5") is True # case insensitive
# Should not detect non-BGE models
assert VertexBGEConfig.is_bge_model("textembedding-gecko") is False
assert VertexBGEConfig.is_bge_model("gemma") is False
assert VertexBGEConfig.is_bge_model("123456789") is False
def test_bge_response_transformation_success():
"""
Test successful BGE response transformation.
Verifies that a valid BGE response is properly transformed
to OpenAI format.
"""
response = {
"predictions": [
[0.1, 0.2, 0.3],
[0.4, 0.5, 0.6]
],
"deployedModelId": "123456",
"model": "projects/test/models/bge-base"
}
model_response = EmbeddingResponse()
result = VertexBGEConfig.transform_response(
response=response,
model="bge-small-en-v1.5",
model_response=model_response
)
assert result.object == "list"
assert len(result.data) == 2
assert result.data[0]["embedding"] == [0.1, 0.2, 0.3]
assert result.data[1]["embedding"] == [0.4, 0.5, 0.6]
assert result.data[0]["index"] == 0
assert result.data[1]["index"] == 1
assert result.model == "bge-small-en-v1.5"
def test_bge_response_missing_predictions():
"""
Test BGE response transformation with missing predictions field.
Verifies that a KeyError is raised when the response doesn't
contain the required 'predictions' field.
"""
response = {
"deployedModelId": "123456",
"model": "projects/test/models/bge-base"
}
model_response = EmbeddingResponse()
with pytest.raises(KeyError, match="Response missing 'predictions' field"):
VertexBGEConfig.transform_response(
response=response,
model="bge-small-en-v1.5",
model_response=model_response
)
def test_bge_response_invalid_predictions_type():
"""
Test BGE response transformation with invalid predictions type.
Verifies that a ValueError is raised when predictions is not a list.
"""
response = {
"predictions": "not-a-list"
}
model_response = EmbeddingResponse()
with pytest.raises(ValueError, match="Expected 'predictions' to be a list"):
VertexBGEConfig.transform_response(
response=response,
model="bge-small-en-v1.5",
model_response=model_response
)
@@ -967,6 +967,10 @@ async def test_vertex_ai_partner_model_detection():
assert VertexAIPartnerModels.is_vertex_partner_model("meta/llama-3.1-405b")
# Test Minimax models
assert VertexAIPartnerModels.is_vertex_partner_model("minimaxai/minimax-m2-maas")
# Test Moonshot models
assert VertexAIPartnerModels.is_vertex_partner_model(
"moonshotai/kimi-k2-thinking-maas"
)
# Test Gemini models (should NOT be detected as partner model)
assert not VertexAIPartnerModels.is_vertex_partner_model("gemini-1.5-pro")
@@ -989,3 +993,16 @@ def test_vertex_ai_minimax_uses_openai_handler():
assert VertexAIPartnerModels.should_use_openai_handler(
"minimaxai/minimax-m2-maas"
)
def test_vertex_ai_moonshot_uses_openai_handler():
"""
Ensure Moonshot partner models re-use the OpenAI-format handler.
"""
from litellm.llms.vertex_ai.vertex_ai_partner_models.main import (
VertexAIPartnerModels,
)
assert VertexAIPartnerModels.should_use_openai_handler(
"moonshotai/kimi-k2-thinking-maas"
)
@@ -1,258 +0,0 @@
"""
Unit tests for Vertex AI Private Service Connect (PSC) endpoint support
Tests that LiteLLM properly constructs URLs when using custom api_base
for PSC endpoints.
"""
import pytest
import sys
import os
# Add the litellm package to the path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "../../../.."))
from litellm.llms.vertex_ai.vertex_llm_base import VertexBase
class TestVertexAIPSCEndpointSupport:
"""Test cases for PSC endpoint URL construction"""
def test_psc_endpoint_url_construction_basic(self):
"""Test basic PSC endpoint URL construction for predict endpoint"""
vertex_base = VertexBase()
psc_api_base = "http://10.96.32.8"
endpoint_id = "1234567890"
project_id = "test-project"
location = "us-central1"
auth_header, url = vertex_base._check_custom_proxy(
api_base=psc_api_base,
custom_llm_provider="vertex_ai",
gemini_api_key=None,
endpoint="predict",
stream=False,
auth_header="test-token",
url="", # This will be replaced
model=endpoint_id,
vertex_project=project_id,
vertex_location=location,
vertex_api_version="v1",
)
expected_url = f"{psc_api_base}/v1/projects/{project_id}/locations/{location}/endpoints/{endpoint_id}:predict"
assert (
url == expected_url
), f"Expected {expected_url}, but got {url}"
def test_psc_endpoint_url_construction_with_streaming(self):
"""Test PSC endpoint URL construction with streaming enabled"""
vertex_base = VertexBase()
psc_api_base = "http://10.96.32.8"
endpoint_id = "1234567890"
project_id = "test-project"
location = "us-central1"
auth_header, url = vertex_base._check_custom_proxy(
api_base=psc_api_base,
custom_llm_provider="vertex_ai",
gemini_api_key=None,
endpoint="streamGenerateContent",
stream=True,
auth_header="test-token",
url="",
model=endpoint_id,
vertex_project=project_id,
vertex_location=location,
vertex_api_version="v1",
)
expected_url = f"{psc_api_base}/v1/projects/{project_id}/locations/{location}/endpoints/{endpoint_id}:streamGenerateContent?alt=sse"
assert (
url == expected_url
), f"Expected {expected_url}, but got {url}"
def test_psc_endpoint_url_construction_v1beta1(self):
"""Test PSC endpoint URL construction with v1beta1 API version"""
vertex_base = VertexBase()
psc_api_base = "http://10.96.32.8"
endpoint_id = "1234567890"
project_id = "test-project"
location = "us-central1"
auth_header, url = vertex_base._check_custom_proxy(
api_base=psc_api_base,
custom_llm_provider="vertex_ai",
gemini_api_key=None,
endpoint="predict",
stream=False,
auth_header="test-token",
url="",
model=endpoint_id,
vertex_project=project_id,
vertex_location=location,
vertex_api_version="v1beta1",
)
expected_url = f"{psc_api_base}/v1beta1/projects/{project_id}/locations/{location}/endpoints/{endpoint_id}:predict"
assert (
url == expected_url
), f"Expected {expected_url}, but got {url}"
def test_psc_endpoint_url_with_https(self):
"""Test PSC endpoint URL construction with HTTPS"""
vertex_base = VertexBase()
psc_api_base = "https://10.96.32.8"
endpoint_id = "1234567890"
project_id = "test-project"
location = "us-central1"
auth_header, url = vertex_base._check_custom_proxy(
api_base=psc_api_base,
custom_llm_provider="vertex_ai",
gemini_api_key=None,
endpoint="predict",
stream=False,
auth_header="test-token",
url="",
model=endpoint_id,
vertex_project=project_id,
vertex_location=location,
vertex_api_version="v1",
)
expected_url = f"{psc_api_base}/v1/projects/{project_id}/locations/{location}/endpoints/{endpoint_id}:predict"
assert (
url == expected_url
), f"Expected {expected_url}, but got {url}"
def test_psc_endpoint_with_trailing_slash(self):
"""Test that trailing slashes in api_base are handled correctly"""
vertex_base = VertexBase()
psc_api_base = "http://10.96.32.8/"
endpoint_id = "1234567890"
project_id = "test-project"
location = "us-central1"
auth_header, url = vertex_base._check_custom_proxy(
api_base=psc_api_base,
custom_llm_provider="vertex_ai",
gemini_api_key=None,
endpoint="predict",
stream=False,
auth_header="test-token",
url="",
model=endpoint_id,
vertex_project=project_id,
vertex_location=location,
vertex_api_version="v1",
)
# rstrip('/') should remove the trailing slash
expected_url = f"{psc_api_base.rstrip('/')}/v1/projects/{project_id}/locations/{location}/endpoints/{endpoint_id}:predict"
assert (
url == expected_url
), f"Expected {expected_url}, but got {url}"
def test_standard_proxy_with_googleapis(self):
"""Test that standard proxies with googleapis.com in URL use simple format"""
vertex_base = VertexBase()
proxy_api_base = "https://my-proxy.googleapis.com"
endpoint_id = "gemini-pro" # Not numeric
project_id = "test-project"
location = "us-central1"
auth_header, url = vertex_base._check_custom_proxy(
api_base=proxy_api_base,
custom_llm_provider="vertex_ai",
gemini_api_key=None,
endpoint="generateContent",
stream=False,
auth_header="test-token",
url="",
model=endpoint_id,
vertex_project=project_id,
vertex_location=location,
vertex_api_version="v1",
)
# Should use simple format: api_base:endpoint
expected_url = f"{proxy_api_base}:generateContent"
assert (
url == expected_url
), f"Expected {expected_url}, but got {url}"
def test_custom_proxy_with_numeric_model(self):
"""Test that numeric model IDs trigger PSC-style URL construction"""
vertex_base = VertexBase()
proxy_api_base = "https://my-custom-proxy.example.com"
endpoint_id = "9876543210" # Numeric endpoint ID
project_id = "test-project"
location = "us-central1"
auth_header, url = vertex_base._check_custom_proxy(
api_base=proxy_api_base,
custom_llm_provider="vertex_ai",
gemini_api_key=None,
endpoint="predict",
stream=False,
auth_header="test-token",
url="",
model=endpoint_id,
vertex_project=project_id,
vertex_location=location,
vertex_api_version="v1",
)
# Numeric model should trigger full path construction
expected_url = f"{proxy_api_base}/v1/projects/{project_id}/locations/{location}/endpoints/{endpoint_id}:predict"
assert (
url == expected_url
), f"Expected {expected_url}, but got {url}"
def test_no_api_base_returns_original_url(self):
"""Test that when api_base is None, the original URL is returned"""
vertex_base = VertexBase()
original_url = "https://us-central1-aiplatform.googleapis.com/v1/projects/test/locations/us-central1/publishers/google/models/gemini-pro:generateContent"
auth_header, url = vertex_base._check_custom_proxy(
api_base=None,
custom_llm_provider="vertex_ai",
gemini_api_key=None,
endpoint="generateContent",
stream=False,
auth_header="test-token",
url=original_url,
model="gemini-pro",
vertex_project="test-project",
vertex_location="us-central1",
vertex_api_version="v1",
)
# When api_base is None, original URL should be returned unchanged
assert url == original_url, f"Expected {original_url}, but got {url}"
def test_auth_header_preserved(self):
"""Test that auth_header is properly preserved"""
vertex_base = VertexBase()
psc_api_base = "http://10.96.32.8"
test_auth_header = "Bearer test-token-12345"
auth_header, url = vertex_base._check_custom_proxy(
api_base=psc_api_base,
custom_llm_provider="vertex_ai",
gemini_api_key=None,
endpoint="predict",
stream=False,
auth_header=test_auth_header,
url="",
model="1234567890",
vertex_project="test-project",
vertex_location="us-central1",
vertex_api_version="v1",
)
assert (
auth_header == test_auth_header
), f"Auth header should be preserved, got {auth_header}"
@@ -0,0 +1,80 @@
"""
Test access group management endpoints
"""
import os
import sys
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
sys.path.insert(
0, os.path.abspath("../../../..")
) # Adds the parent directory to the system path
from litellm import Router
@pytest.mark.asyncio
async def test_create_duplicate_access_group_fails():
"""
Test that creating an access group with a name that already exists returns 409 error.
Scenario: User creates "production-models" access group, then tries to create it again.
Should fail with 409 Conflict.
"""
from fastapi import HTTPException
from litellm.proxy._types import LitellmUserRoles, UserAPIKeyAuth
from litellm.proxy.management_endpoints.model_access_group_management_endpoints import (
create_model_group,
)
from litellm.types.proxy.management_endpoints.model_management_endpoints import (
NewModelGroupRequest,
)
# Mock dependencies - use exact model name (not wildcard)
mock_router = Router(
model_list=[
{
"model_name": "gpt-4", # Exact model name
"litellm_params": {
"model": "gpt-4",
"api_key": "fake-key",
},
}
]
)
mock_prisma = MagicMock()
mock_prisma.db.litellm_proxymodeltable.find_many = AsyncMock(
return_value=[
MagicMock(
model_id="1",
model_name="gpt-4",
model_info={"access_groups": ["production-models"]}, # Already exists
)
]
)
mock_user = UserAPIKeyAuth(
user_id="test_admin",
user_role=LitellmUserRoles.PROXY_ADMIN,
)
request_data = NewModelGroupRequest(
access_group="production-models",
model_names=["gpt-4"],
)
# Mock the imported dependencies from proxy_server (where they're actually imported from)
with patch("litellm.proxy.proxy_server.llm_router", mock_router), \
patch("litellm.proxy.proxy_server.prisma_client", mock_prisma):
# Should raise 409 Conflict
with pytest.raises(HTTPException) as exc_info:
await create_model_group(data=request_data, user_api_key_dict=mock_user)
assert exc_info.value.status_code == 409
assert "already exists" in str(exc_info.value.detail)
@@ -520,6 +520,51 @@ class TestListMCPServers:
assert mock_server.credentials == {"auth_value": "top-secret"}
assert result.status == "healthy"
@pytest.mark.asyncio
async def test_fetch_single_mcp_server_handles_missing_credentials_field(self):
mock_server = generate_mock_mcp_server_db_record(
server_id="server-2", alias="Server 2"
)
# Simulate ORM object without credentials attribute (e.g., older schema)
delattr(mock_server, "credentials")
mock_prisma_client = MagicMock()
mock_health_result = {
"status": "healthy",
"last_health_check": datetime.now().isoformat(),
"error": None,
}
mock_user_auth = generate_mock_user_api_key_auth(
user_role=LitellmUserRoles.PROXY_ADMIN
)
with patch(
"litellm.proxy.management_endpoints.mcp_management_endpoints.get_prisma_client_or_throw",
return_value=mock_prisma_client,
), patch(
"litellm.proxy.management_endpoints.mcp_management_endpoints.get_mcp_server",
AsyncMock(return_value=mock_server),
), patch(
"litellm.proxy.management_endpoints.mcp_management_endpoints.global_mcp_server_manager.health_check_server",
AsyncMock(return_value=mock_health_result),
), patch(
"litellm.proxy.management_endpoints.mcp_management_endpoints._user_has_admin_view",
return_value=True,
):
from litellm.proxy.management_endpoints.mcp_management_endpoints import (
fetch_mcp_server,
)
result = await fetch_mcp_server(
server_id="server-2", user_api_key_dict=mock_user_auth
)
assert result.server_id == "server-2"
# credentials attribute should still be absent and no exception raised
assert not hasattr(result, "credentials")
assert result.status == "healthy"
class TestMCPHealthCheckEndpoints:
"""Test MCP health check endpoints"""
@@ -0,0 +1,55 @@
import os
import sys
from copy import deepcopy
import pytest
sys.path.insert(0, os.path.abspath("../../.."))
import litellm.proxy.public_endpoints.provider_create_metadata as pcm # noqa: E402
from litellm.proxy.public_endpoints.provider_create_metadata import ( # noqa: E402
_normalize_field,
get_provider_create_metadata,
)
def test_get_provider_create_metadata_includes_openai_fields():
metadata = get_provider_create_metadata()
openai_info = next(item for item in metadata if item.provider == "OpenAI")
assert openai_info.provider_display_name == "OpenAI"
assert openai_info.litellm_provider == "openai"
keys = {field.key for field in openai_info.credential_fields}
assert {"api_base", "api_key"}.issubset(keys)
def test_get_provider_create_metadata_returns_sorted_display_names():
metadata = get_provider_create_metadata()
display_names = [item.provider_display_name for item in metadata]
assert display_names == sorted(display_names, key=str.lower)
def test_get_provider_create_metadata_uses_fallback_fields(monkeypatch):
overridden_fields = deepcopy(pcm.PROVIDER_CREDENTIAL_FIELDS)
overridden_fields.pop("Azure", None)
monkeypatch.setattr(pcm, "PROVIDER_CREDENTIAL_FIELDS", overridden_fields)
metadata = get_provider_create_metadata()
azure_info = next(item for item in metadata if item.provider == "Azure")
fallback_keys = [field.key for field in azure_info.credential_fields]
assert fallback_keys == ["api_base", "api_key"]
assert all(field.required is False for field in azure_info.credential_fields)
def test_normalize_field_applies_defaults():
normalized = _normalize_field({"key": "api_key", "label": "API Key"})
assert normalized.key == "api_key"
assert normalized.label == "API Key"
assert normalized.field_type == "text"
assert normalized.required is False
assert normalized.placeholder is None
assert normalized.options is None
@@ -23,3 +23,44 @@ def test_get_supported_providers_returns_enum_values():
expected_providers = sorted(provider.value for provider in LlmProviders)
assert response.json() == expected_providers
def test_get_provider_fields_returns_metadata():
app = FastAPI()
app.include_router(router)
client = TestClient(app)
response = client.get("/public/providers/fields")
assert response.status_code == 200
payload = response.json()
assert isinstance(payload, list)
provider_lookup = {item["provider"]: item for item in payload}
assert "OpenAI" in provider_lookup
openai_fields = provider_lookup["OpenAI"]
assert openai_fields["provider_display_name"] == "OpenAI"
assert openai_fields["litellm_provider"] == "openai"
credential_keys = {field["key"] for field in openai_fields["credential_fields"]}
assert {"api_base", "api_key"}.issubset(credential_keys)
# Every provider exposed by `/public/providers` (i.e. every LlmProviders value)
# should have a corresponding entry in `/public/providers/fields`.
expected_litellm_providers = {provider.value for provider in LlmProviders}
actual_litellm_providers = {item["litellm_provider"] for item in payload}
assert expected_litellm_providers.issubset(actual_litellm_providers)
# Sanity check for runwayml specifically it should be present and use the
# default API base + API key credential fields at minimum.
runway_entries = [
item for item in payload if item["litellm_provider"] == "runwayml"
]
assert (
len(runway_entries) >= 1
), "Expected runwayml provider metadata in /public/providers/fields"
runway_credential_keys = {
field["key"] for field in runway_entries[0]["credential_fields"]
}
assert {"api_base", "api_key"}.issubset(runway_credential_keys)
@@ -628,6 +628,59 @@ async def test_ui_view_spend_logs_pagination(client, monkeypatch):
assert data["page"] == 2
@pytest.mark.asyncio
async def test_ui_view_session_spend_logs_pagination(client, monkeypatch):
mock_spend_logs = [
{
"id": "log1",
"request_id": "req1",
"session_id": "session-123",
"startTime": "2024-01-01T00:00:00Z",
},
{
"id": "log2",
"request_id": "req2",
"session_id": "session-123",
"startTime": "2024-01-02T00:00:00Z",
},
]
class MockDB:
async def count(self, *args, **kwargs):
assert kwargs.get("where") == {"session_id": "session-123"}
return len(mock_spend_logs)
async def find_many(self, *args, **kwargs):
assert kwargs.get("where") == {"session_id": "session-123"}
assert kwargs.get("order") == {"startTime": "asc"}
assert kwargs.get("skip") == 1 # page=2, page_size=1
assert kwargs.get("take") == 1
return [mock_spend_logs[1]]
class MockPrismaClient:
def __init__(self):
self.db = MockDB()
self.db.litellm_spendlogs = self.db
mock_prisma_client = MockPrismaClient()
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", mock_prisma_client)
response = client.get(
"/spend/logs/session/ui",
params={"session_id": "session-123", "page": 2, "page_size": 1},
headers={"Authorization": "Bearer sk-test"},
)
assert response.status_code == 200
data = response.json()
assert data["total"] == 2
assert data["page"] == 2
assert data["page_size"] == 1
assert data["total_pages"] == 2
assert len(data["data"]) == 1
assert data["data"][0]["request_id"] == "req2"
@pytest.mark.asyncio
async def test_ui_view_spend_logs_date_range_filter(client, monkeypatch):
# Create mock data with different dates
+14 -7
View File
@@ -325,13 +325,20 @@ def test_embedding_input_array_of_tokens(mock_aembedding, client_no_auth):
response = client_no_auth.post("/v1/embeddings", json=test_data)
mock_aembedding.assert_called_once_with(
model="vllm_embed_model",
input=[[2046, 13269, 158208]],
metadata=mock.ANY,
proxy_server_request=mock.ANY,
secret_fields=mock.ANY,
)
# DEPRECATED - mock_aembedding.assert_called_once_with is too strict, and will fail when new kwargs are added to embeddings
# mock_aembedding.assert_called_once_with(
# model="vllm_embed_model",
# input=[[2046, 13269, 158208]],
# metadata=mock.ANY,
# proxy_server_request=mock.ANY,
# secret_fields=mock.ANY,
# )
# Assert that aembedding was called, and that input was not modified
mock_aembedding.assert_called_once()
call_args, call_kwargs = mock_aembedding.call_args
assert call_kwargs["model"] == "vllm_embed_model"
assert call_kwargs["input"] == [[2046, 13269, 158208]]
assert response.status_code == 200
result = response.json()
print(len(result["data"][0]["embedding"]))
@@ -115,14 +115,17 @@ class TestEncryptResponseId:
"litellm.proxy.hooks.responses_id_security.encrypt_value_helper"
) as mock_encrypt:
mock_encrypt.return_value = "encrypted_base64_value"
with patch.object(
responses_id_security, "_get_signing_key", return_value="test-key"
):
result = responses_id_security._encrypt_response_id(
mock_response, mock_user_api_key_dict
)
result = responses_id_security._encrypt_response_id(
mock_response, mock_user_api_key_dict
)
assert result.id == "resp_encrypted_base64_value"
assert result.id.startswith("resp_")
mock_encrypt.assert_called_once()
assert result.id == "resp_encrypted_base64_value"
assert result.id.startswith("resp_")
mock_encrypt.assert_called_once()
def test_encrypt_response_id_maintains_prefix(
self, responses_id_security, mock_user_api_key_dict
@@ -136,12 +139,15 @@ class TestEncryptResponseId:
"litellm.proxy.hooks.responses_id_security.encrypt_value_helper"
) as mock_encrypt:
mock_encrypt.return_value = "encrypted_value_456"
with patch.object(
responses_id_security, "_get_signing_key", return_value="test-key"
):
result = responses_id_security._encrypt_response_id(
mock_response, mock_user_api_key_dict
)
result = responses_id_security._encrypt_response_id(
mock_response, mock_user_api_key_dict
)
assert result.id.startswith("resp_")
assert result.id.startswith("resp_")
class TestCheckUserAccessToResponseId:
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

@@ -1,14 +1,4 @@
import {
Button,
Icon,
Table,
TableBody,
TableCell,
TableHead,
TableHeaderCell,
TableRow,
Text,
} from "@tremor/react";
import { Button, Icon, Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow, Text } from "@tremor/react";
import { Tooltip } from "antd";
import { formatNumberWithCommas } from "@/utils/dataUtils";
import { PencilAltIcon, TrashIcon } from "@heroicons/react/outline";
@@ -755,6 +755,7 @@ const Teams: React.FC<TeamProps> = ({
<TableHeaderCell>Models</TableHeaderCell>
<TableHeaderCell>Organization</TableHeaderCell>
<TableHeaderCell>Info</TableHeaderCell>
<TableHeaderCell>Actions</TableHeaderCell>
</TableRow>
</TableHead>
@@ -937,20 +938,28 @@ const Teams: React.FC<TeamProps> = ({
<TableCell>
{userRole == "Admin" ? (
<>
<Icon
icon={PencilAltIcon}
size="sm"
onClick={() => {
setSelectedTeamId(team.team_id);
setEditTeam(true);
}}
/>
<Icon
onClick={() => handleDelete(team.team_id)}
icon={TrashIcon}
size="sm"
data-testid="delete-team-button"
/>
<Tooltip title="Edit team">
{" "}
<Icon
icon={PencilAltIcon}
size="sm"
className="cursor-pointer hover:text-blue-600"
onClick={() => {
setSelectedTeamId(team.team_id);
setEditTeam(true);
}}
/>
</Tooltip>
<Tooltip title="Delete team">
{" "}
<Icon
onClick={() => handleDelete(team.team_id)}
icon={TrashIcon}
size="sm"
className="cursor-pointer hover:text-red-600"
data-testid="delete-team-button"
/>
</Tooltip>
</>
) : null}
</TableCell>
@@ -19,6 +19,15 @@ vi.mock("../networking", async () => {
modelAvailableCall: vi.fn().mockResolvedValue({
data: [{ id: "model-group-1" }, { id: "model-group-2" }],
}),
getProviderCreateMetadata: vi.fn().mockResolvedValue([
{
provider: "OpenAI",
provider_display_name: "OpenAI",
litellm_provider: "openai",
default_model_placeholder: "gpt-3.5-turbo",
credential_fields: [],
},
]),
};
});
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react";
import React, { useEffect, useMemo, useState } from "react";
import { Card, Form, Button, Tooltip, Typography, Select as AntdSelect, Modal } from "antd";
import type { FormInstance } from "antd";
import type { UploadProps } from "antd/es/upload";
@@ -9,7 +9,14 @@ import ProviderSpecificFields from "./provider_specific_fields";
import AdvancedSettings from "./advanced_settings";
import { Providers, providerLogoMap } from "../provider_info_helpers";
import type { Team } from "../key_team_helpers/key_list";
import { CredentialItem, getGuardrailsList, modelAvailableCall, tagListCall } from "../networking";
import {
type CredentialItem,
type ProviderCreateInfo,
getGuardrailsList,
getProviderCreateMetadata,
modelAvailableCall,
tagListCall,
} from "../networking";
import ConnectionErrorDisplay from "./model_connection_test";
import { TEST_MODES } from "./add_model_modes";
import { Row, Col } from "antd";
@@ -68,6 +75,11 @@ const AddModelTab: React.FC<AddModelTabProps> = ({
// Using a unique ID to force the ConnectionErrorDisplay to remount and run a fresh test
const [connectionTestId, setConnectionTestId] = useState<string>("");
// Provider metadata for driving the provider select from backend config
const [providerMetadata, setProviderMetadata] = useState<ProviderCreateInfo[] | null>(null);
const [isProviderMetadataLoading, setIsProviderMetadataLoading] = useState<boolean>(false);
const [providerMetadataError, setProviderMetadataError] = useState<string | null>(null);
useEffect(() => {
const fetchGuardrails = async () => {
try {
@@ -95,6 +107,37 @@ const AddModelTab: React.FC<AddModelTabProps> = ({
fetchTags();
}, [accessToken]);
useEffect(() => {
let isMounted = true;
const fetchProviderMetadata = async () => {
setIsProviderMetadataLoading(true);
setProviderMetadataError(null);
try {
const metadata = await getProviderCreateMetadata();
if (!isMounted) {
return;
}
setProviderMetadata(metadata);
} catch (error) {
console.error("Failed to fetch provider metadata:", error);
if (isMounted) {
setProviderMetadataError("Failed to load providers");
}
} finally {
if (isMounted) {
setIsProviderMetadataLoading(false);
}
}
};
fetchProviderMetadata();
return () => {
isMounted = false;
};
}, []);
// Test connection when button is clicked
const handleTestConnection = async () => {
setIsTestingConnection(true);
@@ -118,6 +161,13 @@ const AddModelTab: React.FC<AddModelTabProps> = ({
fetchModelAccessGroups();
}, [accessToken]);
const sortedProviderMetadata: ProviderCreateInfo[] = useMemo(() => {
if (!providerMetadata) {
return [];
}
return [...providerMetadata].sort((a, b) => a.provider_display_name.localeCompare(b.provider_display_name));
}, [providerMetadata]);
const isAdmin = all_admin_roles.includes(userRole);
const handleAutoRouterOk = () => {
@@ -166,41 +216,68 @@ const AddModelTab: React.FC<AddModelTabProps> = ({
labelAlign="left"
>
<AntdSelect
showSearch={true}
value={selectedProvider}
showSearch
loading={isProviderMetadataLoading}
placeholder={isProviderMetadataLoading ? "Loading providers..." : "Select a provider"}
optionFilterProp="data-label"
onChange={(value) => {
setSelectedProvider(value);
setProviderModelsFn(value);
setSelectedProvider(value as Providers);
setProviderModelsFn(value as Providers);
form.setFieldsValue({
custom_llm_provider: value,
});
form.setFieldsValue({
model: [],
model_name: undefined,
});
}}
>
{Object.entries(Providers).map(([providerEnum, providerDisplayName]) => (
<AntdSelect.Option key={providerEnum} value={providerEnum}>
<div className="flex items-center space-x-2">
<img
src={providerLogoMap[providerDisplayName]}
alt={`${providerEnum} logo`}
className="w-5 h-5"
onError={(e) => {
// Create a div with provider initial as fallback
const target = e.target as HTMLImageElement;
const parent = target.parentElement;
if (parent) {
const fallbackDiv = document.createElement("div");
fallbackDiv.className =
"w-5 h-5 rounded-full bg-gray-200 flex items-center justify-center text-xs";
fallbackDiv.textContent = providerDisplayName.charAt(0);
parent.replaceChild(fallbackDiv, target);
}
}}
/>
<span>{providerDisplayName}</span>
</div>
{providerMetadataError && sortedProviderMetadata.length === 0 && (
<AntdSelect.Option key="__error" value="">
{providerMetadataError}
</AntdSelect.Option>
))}
)}
{sortedProviderMetadata.map((providerInfo) => {
const displayName = providerInfo.provider_display_name;
const providerKey = providerInfo.provider;
const logoSrc = providerLogoMap[displayName] ?? "";
return (
<AntdSelect.Option key={providerKey} value={providerKey} data-label={displayName}>
<div className="flex items-center space-x-2">
{logoSrc ? (
<img
src={logoSrc}
alt={`${displayName} logo`}
className="w-5 h-5"
onError={(e) => {
const target = e.currentTarget as HTMLImageElement;
const parent = target.parentElement;
if (!parent || !parent.contains(target)) {
return;
}
try {
const fallbackDiv = document.createElement("div");
fallbackDiv.className =
"w-5 h-5 rounded-full bg-gray-200 flex items-center justify-center text-xs";
fallbackDiv.textContent = displayName.charAt(0);
parent.replaceChild(fallbackDiv, target);
} catch (error) {
console.error("Failed to replace provider logo fallback:", error);
}
}}
/>
) : (
<div className="w-5 h-5 rounded-full bg-gray-200 flex items-center justify-center text-xs">
{displayName.charAt(0)}
</div>
)}
<span>{displayName}</span>
</div>
</AntdSelect.Option>
);
})}
</AntdSelect>
</Form.Item>
<LiteLLMModelNameField
@@ -0,0 +1,58 @@
import { describe, expect, it, vi } from "vitest";
import { prepareModelAddRequest } from "./handle_add_model_submit";
vi.mock("../molecules/notifications_manager", () => ({
default: {
fromBackend: vi.fn(),
},
}));
describe("prepareModelAddRequest", () => {
it("returns deployment data for the most basic form", async () => {
const formValues = {
model_mappings: [
{
public_name: "Public Model",
litellm_model: "litellm/public",
},
],
model_name: "custom-model-name",
base_model: "gpt-4",
team_id: "team-123",
model_access_group: ["group-1"],
input_cost_per_token: "2000000",
output_cost_per_token: "1000000",
};
const deployments = await prepareModelAddRequest({ ...formValues }, "token", null);
expect(deployments).toHaveLength(1);
const [deployment] = deployments!;
expect(deployment.modelName).toBe("Public Model");
expect(deployment.litellmParamsObj.model).toBe("custom-model-name");
expect(deployment.litellmParamsObj.input_cost_per_token).toBe(2);
expect(deployment.litellmParamsObj.output_cost_per_token).toBe(1);
expect(deployment.modelInfoObj.base_model).toBe("gpt-4");
expect(deployment.modelInfoObj.access_groups).toEqual(["group-1"]);
expect(deployment.modelInfoObj.team_id).toBe("team-123");
});
it("uses a lowercase fallback for unrecognized custom providers", async () => {
const fallbackValues = {
model_mappings: [
{
public_name: "Petals Model",
litellm_model: "petals/model",
},
],
model_name: "petals/model",
custom_llm_provider: "Petals",
};
const deployments = await prepareModelAddRequest({ ...fallbackValues }, "token", null);
expect(deployments).toHaveLength(1);
const [deployment] = deployments!;
expect(deployment.litellmParamsObj.custom_llm_provider).toBe("petals");
});
});
@@ -1,6 +1,6 @@
import { provider_map, Providers } from "../provider_info_helpers";
import { modelCreateCall, Model } from "../networking";
import NotificationManager from "../molecules/notifications_manager";
import { Model, modelCreateCall } from "../networking";
import { provider_map } from "../provider_info_helpers";
export const prepareModelAddRequest = async (formValues: Record<string, any>, accessToken: string, form: any) => {
try {
@@ -14,8 +14,10 @@ export const prepareModelAddRequest = async (formValues: Record<string, any>, ac
// Handle wildcard case
if (formValues["model"] && formValues["model"].includes("all-wildcard")) {
const customProvider: Providers = formValues["custom_llm_provider"];
const litellm_custom_provider = provider_map[customProvider as keyof typeof Providers];
const customProviderKey = formValues["custom_llm_provider"] as string;
const mappedProvider =
provider_map[customProviderKey as keyof typeof provider_map] ?? customProviderKey.toLowerCase();
const litellm_custom_provider = mappedProvider;
const wildcardModel = litellm_custom_provider + "/*";
formValues["model_name"] = wildcardModel;
modelMappings.push({
@@ -59,7 +61,8 @@ export const prepareModelAddRequest = async (formValues: Record<string, any>, ac
litellmParamsObj["model"] = value;
} else if (key == "custom_llm_provider") {
console.log("custom_llm_provider:", value);
const mappingResult = provider_map[value]; // Get the corresponding value from the mapping
const providerKey = value as string;
const mappingResult = provider_map[providerKey as keyof typeof provider_map] ?? providerKey.toLowerCase();
litellmParamsObj["custom_llm_provider"] = mappingResult;
console.log("custom_llm_provider mappingResult:", mappingResult);
} else if (key == "model") {
@@ -1,9 +1,102 @@
import { render, waitFor } from "@testing-library/react";
import { describe, it, expect, beforeAll } from "vitest";
import { describe, it, expect, beforeAll, vi } from "vitest";
import { Form } from "antd";
import { Providers } from "../provider_info_helpers";
import ProviderSpecificFields from "./provider_specific_fields";
vi.mock("../networking", async () => {
const actual = await vi.importActual("../networking");
return {
...actual,
getProviderCreateMetadata: vi.fn().mockResolvedValue([
{
provider: "OpenAI",
provider_display_name: Providers.OpenAI,
litellm_provider: "openai",
default_model_placeholder: "gpt-3.5-turbo",
credential_fields: [
{
key: "api_base",
label: "API Base",
field_type: "text",
placeholder: "https://api.openai.com/v1",
tooltip:
"Common endpoints: https://api.openai.com/v1, https://eu.api.openai.com, https://us.api.openai.com",
default_value: "https://api.openai.com/v1",
},
{
key: "organization",
label: "OpenAI Organization ID",
placeholder: "[OPTIONAL] my-unique-org",
},
{
key: "api_key",
label: "OpenAI API Key",
field_type: "password",
required: true,
},
],
},
{
provider: "Hosted_Vllm",
provider_display_name: Providers.Hosted_Vllm,
litellm_provider: "hosted_vllm",
default_model_placeholder: "vllm/any-model",
credential_fields: [
{
key: "api_base",
label: "API Base",
placeholder: "https://...",
},
{
key: "api_key",
label: "vLLM API Key",
field_type: "password",
},
],
},
{
provider: "Azure",
provider_display_name: Providers.Azure,
litellm_provider: "azure",
default_model_placeholder: "azure/my-deployment",
credential_fields: [
{
key: "api_base",
label: "API Base",
placeholder: "https://...",
required: true,
},
{
key: "api_version",
label: "API Version",
placeholder: "2023-07-01-preview",
tooltip:
"By default litellm will use the latest version. If you want to use a different version, you can specify it here",
},
{
key: "base_model",
label: "Base Model",
placeholder: "azure/gpt-3.5-turbo",
},
{
key: "api_key",
label: "Azure API Key",
field_type: "password",
placeholder: "Enter your Azure API Key",
},
{
key: "azure_ad_token",
label: "Azure AD Token",
field_type: "password",
placeholder: "Enter your Azure AD Token",
},
],
},
]),
};
});
// Mock window.matchMedia for Ant Design components
beforeAll(() => {
Object.defineProperty(window, "matchMedia", {
@@ -4,7 +4,12 @@ import { TextInput, Text } from "@tremor/react";
import { Row, Col, Typography, Button as Button2, Upload, UploadProps } from "antd";
import { UploadOutlined } from "@ant-design/icons";
import { provider_map, Providers } from "../provider_info_helpers";
import { CredentialItem } from "../networking";
import {
CredentialItem,
ProviderCreateInfo,
ProviderCredentialFieldMetadata,
getProviderCreateMetadata,
} from "../networking";
const { Link } = Typography;
interface ProviderSpecificFieldsProps {
@@ -28,6 +33,33 @@ export interface CredentialValues {
value: string;
}
const mapFieldMetadataToUiField = (field: ProviderCredentialFieldMetadata): ProviderCredentialField => {
const type: ProviderCredentialField["type"] =
field.field_type === "password"
? "password"
: field.field_type === "select"
? "select"
: field.field_type === "upload"
? "upload"
: "text";
return {
key: field.key,
label: field.label,
placeholder: field.placeholder ?? undefined,
tooltip: field.tooltip ?? undefined,
required: field.required ?? false,
type,
options: field.options ?? undefined,
defaultValue: field.default_value ?? undefined,
};
};
// In-memory cache of provider credential fields keyed by provider display name.
// This lets us reuse the data across multiple mounts and also supports
// non-React helpers like createCredentialFromModel.
const providerFieldsByDisplayName: Record<string, ProviderCredentialField[]> = {};
export const createCredentialFromModel = (provider: string, modelData: any): CredentialItem => {
console.log("provider", provider);
console.log("modelData", modelData);
@@ -35,8 +67,8 @@ export const createCredentialFromModel = (provider: string, modelData: any): Cre
if (!enumKey) {
throw new Error(`Provider ${provider} not found in provider_map`);
}
const providerEnum = Providers[enumKey as keyof typeof Providers];
const providerFields = PROVIDER_CREDENTIAL_FIELDS[providerEnum] || [];
const providerDisplayName = Providers[enumKey as keyof typeof Providers];
const providerFields = providerFieldsByDisplayName[providerDisplayName] || [];
const credentialValues: object = {};
console.log("providerFields", providerFields);
@@ -63,544 +95,103 @@ export const createCredentialFromModel = (provider: string, modelData: any): Cre
return credential;
};
const PROVIDER_CREDENTIAL_FIELDS: Record<Providers, ProviderCredentialField[]> = {
[Providers.OpenAI]: [
{
key: "api_base",
label: "API Base",
type: "text",
placeholder: "https://api.openai.com/v1",
tooltip: "Common endpoints: https://api.openai.com/v1, https://eu.api.openai.com, https://us.api.openai.com",
defaultValue: "https://api.openai.com/v1",
},
{
key: "organization",
label: "OpenAI Organization ID",
placeholder: "[OPTIONAL] my-unique-org",
},
{
key: "api_key",
label: "OpenAI API Key",
type: "password",
required: true,
},
],
[Providers.OpenAI_Text]: [
{
key: "api_base",
label: "API Base",
type: "text",
placeholder: "https://api.openai.com/v1",
tooltip: "Common endpoints: https://api.openai.com/v1, https://eu.api.openai.com, https://us.api.openai.com",
defaultValue: "https://api.openai.com/v1",
},
{
key: "organization",
label: "OpenAI Organization ID",
placeholder: "[OPTIONAL] my-unique-org",
},
{
key: "api_key",
label: "OpenAI API Key",
type: "password",
required: true,
},
],
[Providers.Vertex_AI]: [
{
key: "vertex_project",
label: "Vertex Project",
placeholder: "adroit-cadet-1234..",
required: true,
},
{
key: "vertex_location",
label: "Vertex Location",
placeholder: "us-east-1",
required: true,
},
{
key: "vertex_credentials",
label: "Vertex Credentials",
required: true,
type: "upload",
},
],
[Providers.AssemblyAI]: [
{
key: "api_base",
label: "API Base",
type: "select",
required: true,
options: ["https://api.assemblyai.com", "https://api.eu.assemblyai.com"],
},
{
key: "api_key",
label: "AssemblyAI API Key",
type: "password",
required: true,
},
],
[Providers.Azure]: [
{
key: "api_base",
label: "API Base",
placeholder: "https://...",
required: true,
},
{
key: "api_version",
label: "API Version",
placeholder: "2023-07-01-preview",
tooltip:
"By default litellm will use the latest version. If you want to use a different version, you can specify it here",
},
{
key: "base_model",
label: "Base Model",
placeholder: "azure/gpt-3.5-turbo",
},
{
key: "api_key",
label: "Azure API Key",
type: "password",
placeholder: "Enter your Azure API Key",
required: false,
},
{
key: "azure_ad_token",
label: "Azure AD Token",
type: "password",
placeholder: "Enter your Azure AD Token",
required: false,
},
],
[Providers.Azure_AI_Studio]: [
{
key: "api_base",
label: "API Base",
placeholder: "https://<test>.openai.azure.com/openai/deployments/gpt-4o/chat/completions?api-version=2024-10-21",
tooltip:
"Enter your full Target URI from Azure Foundry here. Example: https://litellm8397336933.openai.azure.com/openai/deployments/gpt-4o/chat/completions?api-version=2024-10-21",
required: true,
},
{
key: "api_key",
label: "Azure API Key",
type: "password",
required: true,
},
],
[Providers.OpenAI_Compatible]: [
{
key: "api_base",
label: "API Base",
placeholder: "https://...",
required: true,
},
{
key: "api_key",
label: "OpenAI API Key",
type: "password",
required: true,
},
],
[Providers.Dashscope]: [
{
key: "api_key",
label: "Dashscope API Key",
type: "password",
required: true,
},
{
key: "api_base",
label: "API Base",
placeholder: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
defaultValue: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
required: true,
tooltip:
"The base URL for your Dashscope server. Defaults to https://dashscope-intl.aliyuncs.com/compatible-mode/v1 if not specified.",
},
],
[Providers.OpenAI_Text_Compatible]: [
{
key: "api_base",
label: "API Base",
placeholder: "https://...",
required: true,
},
{
key: "api_key",
label: "OpenAI API Key",
type: "password",
required: true,
},
],
[Providers.Bedrock]: [
{
key: "aws_access_key_id",
label: "AWS Access Key ID",
type: "password",
required: false,
tooltip: "You can provide the raw key or the environment variable (e.g. `os.environ/MY_SECRET_KEY`).",
},
{
key: "aws_secret_access_key",
label: "AWS Secret Access Key",
type: "password",
required: false,
tooltip: "You can provide the raw key or the environment variable (e.g. `os.environ/MY_SECRET_KEY`).",
},
{
key: "aws_session_token",
label: "AWS Session Token",
type: "password",
required: false,
tooltip:
"Temporary credentials session token. You can provide the raw token or the environment variable (e.g. `os.environ/MY_SESSION_TOKEN`).",
},
{
key: "aws_region_name",
label: "AWS Region Name",
placeholder: "us-east-1",
required: false,
tooltip: "You can provide the raw key or the environment variable (e.g. `os.environ/MY_SECRET_KEY`).",
},
{
key: "aws_session_name",
label: "AWS Session Name",
placeholder: "my-session",
required: false,
tooltip:
"Name for the AWS session. You can provide the raw value or the environment variable (e.g. `os.environ/MY_SESSION_NAME`).",
},
{
key: "aws_profile_name",
label: "AWS Profile Name",
placeholder: "default",
required: false,
tooltip:
"AWS profile name to use for authentication. You can provide the raw value or the environment variable (e.g. `os.environ/MY_PROFILE_NAME`).",
},
{
key: "aws_role_name",
label: "AWS Role Name",
placeholder: "MyRole",
required: false,
tooltip:
"AWS IAM role name to assume. You can provide the raw value or the environment variable (e.g. `os.environ/MY_ROLE_NAME`).",
},
{
key: "aws_web_identity_token",
label: "AWS Web Identity Token",
type: "password",
required: false,
tooltip:
"Web identity token for OIDC authentication. You can provide the raw token or the environment variable (e.g. `os.environ/MY_WEB_IDENTITY_TOKEN`).",
},
{
key: "aws_bedrock_runtime_endpoint",
label: "AWS Bedrock Runtime Endpoint",
placeholder: "https://bedrock-runtime.us-east-1.amazonaws.com",
required: false,
tooltip:
"Custom Bedrock runtime endpoint URL. You can provide the raw value or the environment variable (e.g. `os.environ/MY_BEDROCK_ENDPOINT`).",
},
],
[Providers.SageMaker]: [
{
key: "aws_access_key_id",
label: "AWS Access Key ID",
type: "password",
required: false,
tooltip: "You can provide the raw key or the environment variable (e.g. `os.environ/MY_SECRET_KEY`).",
},
{
key: "aws_secret_access_key",
label: "AWS Secret Access Key",
type: "password",
required: false,
tooltip: "You can provide the raw key or the environment variable (e.g. `os.environ/MY_SECRET_KEY`).",
},
{
key: "aws_region_name",
label: "AWS Region Name",
placeholder: "us-east-1",
required: false,
tooltip: "You can provide the raw key or the environment variable (e.g. `os.environ/MY_SECRET_KEY`).",
},
],
[Providers.Ollama]: [
{
key: "api_base",
label: "API Base",
placeholder: "http://localhost:11434",
defaultValue: "http://localhost:11434",
required: false,
tooltip: "The base URL for your Ollama server. Defaults to http://localhost:11434 if not specified.",
},
],
[Providers.Anthropic]: [
{
key: "api_key",
label: "API Key",
placeholder: "sk-",
type: "password",
required: true,
},
],
[Providers.Deepgram]: [
{
key: "api_key",
label: "API Key",
type: "password",
required: true,
},
],
[Providers.ElevenLabs]: [
{
key: "api_key",
label: "API Key",
type: "password",
required: true,
},
],
[Providers.Google_AI_Studio]: [
{
key: "api_key",
label: "API Key",
placeholder: "aig-",
type: "password",
required: true,
},
],
[Providers.Groq]: [
{
key: "api_key",
label: "API Key",
type: "password",
required: true,
},
],
[Providers.MistralAI]: [
{
key: "api_key",
label: "API Key",
type: "password",
required: true,
},
],
[Providers.Deepseek]: [
{
key: "api_key",
label: "API Key",
type: "password",
required: true,
},
],
[Providers.Cohere]: [
{
key: "api_key",
label: "API Key",
type: "password",
required: true,
},
],
[Providers.Databricks]: [
{
key: "api_key",
label: "API Key",
type: "password",
required: true,
},
],
[Providers.xAI]: [
{
key: "api_key",
label: "API Key",
type: "password",
required: true,
},
],
[Providers.AIML]: [
{
key: "api_key",
label: "API Key",
type: "password",
required: true,
},
],
[Providers.Cerebras]: [
{
key: "api_key",
label: "API Key",
type: "password",
required: true,
},
],
[Providers.Sambanova]: [
{
key: "api_key",
label: "API Key",
type: "password",
required: true,
},
],
[Providers.Perplexity]: [
{
key: "api_key",
label: "API Key",
type: "password",
required: true,
},
],
[Providers.TogetherAI]: [
{
key: "api_key",
label: "API Key",
type: "password",
required: true,
},
],
[Providers.Openrouter]: [
{
key: "api_key",
label: "API Key",
type: "password",
required: true,
},
],
[Providers.FireworksAI]: [
{
key: "api_key",
label: "API Key",
type: "password",
required: true,
},
],
[Providers.GradientAI]: [
{
key: "api_base",
label: "GradientAI Endpoint",
placeholder: "https://...",
required: false,
},
{
key: "api_key",
label: "GradientAI API Key",
type: "password",
required: true,
},
],
[Providers.Triton]: [
{
key: "api_key",
label: "API Key",
type: "password",
required: false,
},
{
key: "api_base",
label: "API Base",
placeholder: "http://localhost:8000/generate",
required: false,
},
],
[Providers.Hosted_Vllm]: [
{
key: "api_base",
label: "API Base",
placeholder: "https://...",
required: true,
},
{
key: "api_key",
label: "vLLM API Key",
type: "password",
required: false,
},
],
[Providers.Voyage]: [
{
key: "api_key",
label: "API Key",
type: "password",
required: true,
},
],
[Providers.JinaAI]: [
{
key: "api_key",
label: "API Key",
type: "password",
required: true,
},
],
[Providers.VolcEngine]: [
{
key: "api_key",
label: "API Key",
type: "password",
required: true,
},
],
[Providers.DeepInfra]: [
{
key: "api_key",
label: "API Key",
type: "password",
required: true,
},
],
[Providers.Oracle]: [
{
key: "api_key",
label: "API Key",
type: "password",
required: true,
},
],
[Providers.Snowflake]: [
{
key: "api_key",
label: "Snowflake API Key / JWT Key for Authentication",
type: "password",
required: true,
},
{
key: "api_base",
label: "Snowflake API Endpoint",
placeholder: "https://1234567890.snowflakecomputing.com/api/v2/cortex/inference:complete",
tooltip:
"Enter the full endpoint with path here. Example: https://1234567890.snowflakecomputing.com/api/v2/cortex/inference:complete",
required: true,
},
],
[Providers.Infinity]: [
{
key: "api_base",
label: "API Base",
placeholder: "http://localhost:7997",
},
],
[Providers.FalAI]: [
{
key: "api_key",
label: "API Key",
type: "password",
required: true,
},
],
};
const ProviderSpecificFields: React.FC<ProviderSpecificFieldsProps> = ({ selectedProvider, uploadProps }) => {
const selectedProviderEnum = Providers[selectedProvider as keyof typeof Providers] as Providers;
const form = Form.useFormInstance(); // Get form instance from context
// Simply use the fields as defined in PROVIDER_CREDENTIAL_FIELDS
const [providerMetadata, setProviderMetadata] = React.useState<ProviderCreateInfo[] | null>(null);
const [isLoading, setIsLoading] = React.useState<boolean>(false);
const [loadError, setLoadError] = React.useState<string | null>(null);
React.useEffect(() => {
const hasCachedFields = Object.keys(providerFieldsByDisplayName).length > 0;
if (hasCachedFields) {
// We already have fields cached globally; no need to refetch.
// This is important so we can reuse credential field definitions
// across mounts and in non-React helpers.
return;
}
let isMounted = true;
const fetchProviderFields = async () => {
setIsLoading(true);
setLoadError(null);
try {
const metadata = await getProviderCreateMetadata();
if (!isMounted) {
return;
}
setProviderMetadata(metadata);
// Populate cache keyed by provider display name and identifiers
metadata.forEach((providerInfo) => {
const displayName = providerInfo.provider_display_name;
const mappedFields = providerInfo.credential_fields.map(mapFieldMetadataToUiField);
// Primary key: human-readable display name
providerFieldsByDisplayName[displayName] = mappedFields;
// Also cache by backend identifiers so lookups by provider slug work
if (providerInfo.provider) {
providerFieldsByDisplayName[providerInfo.provider] = mappedFields;
}
if (providerInfo.litellm_provider) {
providerFieldsByDisplayName[providerInfo.litellm_provider] = mappedFields;
}
});
} catch (error) {
console.error("Failed to load provider credential fields:", error);
if (isMounted) {
setLoadError("Failed to load provider credential fields");
}
} finally {
if (isMounted) {
setIsLoading(false);
}
}
};
fetchProviderFields();
return () => {
isMounted = false;
};
}, []);
const allFields = React.useMemo(() => {
return PROVIDER_CREDENTIAL_FIELDS[selectedProviderEnum] || [];
}, [selectedProviderEnum]);
// First try to resolve from the in-memory cache. We support both the
// enum/display-name form and the raw provider slug (e.g. "petals").
const cachedFields =
providerFieldsByDisplayName[selectedProviderEnum] ?? providerFieldsByDisplayName[selectedProvider];
if (cachedFields) {
return cachedFields;
}
if (!providerMetadata) {
return [];
}
const providerInfo = providerMetadata.find(
(p) =>
p.provider_display_name === selectedProviderEnum ||
p.provider === selectedProvider ||
p.litellm_provider === selectedProvider,
);
if (!providerInfo) {
return [];
}
const mapped = providerInfo.credential_fields.map(mapFieldMetadataToUiField);
providerFieldsByDisplayName[providerInfo.provider_display_name] = mapped;
if (providerInfo.provider) {
providerFieldsByDisplayName[providerInfo.provider] = mapped;
}
if (providerInfo.litellm_provider) {
providerFieldsByDisplayName[providerInfo.litellm_provider] = mapped;
}
return mapped;
}, [selectedProviderEnum, selectedProvider, providerMetadata]);
const handleUpload = {
name: "file",
@@ -633,6 +224,20 @@ const ProviderSpecificFields: React.FC<ProviderSpecificFieldsProps> = ({ selecte
return (
<>
{isLoading && allFields.length === 0 && (
<Row>
<Col span={24}>
<Text className="mb-2">Loading provider fields...</Text>
</Col>
</Row>
)}
{loadError && allFields.length === 0 && (
<Row>
<Col span={24}>
<Text className="mb-2 text-red-500">{loadError}</Text>
</Col>
</Row>
)}
{allFields.map((field) => (
<React.Fragment key={field.key}>
<Form.Item
@@ -159,7 +159,7 @@ const GuardrailTable: React.FC<GuardrailTableProps> = ({
},
{
id: "actions",
header: "",
header: "Actions",
cell: ({ row }) => {
const guardrail = row.original;
const isConfigGuardrail = guardrail.guardrail_definition_location === GuardrailDefinitionLocation.CONFIG;
@@ -177,16 +177,17 @@ const GuardrailTable: React.FC<GuardrailTableProps> = ({
/>
</Tooltip>
) : (
<Icon
icon={TrashIcon}
size="sm"
onClick={() =>
guardrail.guardrail_id &&
onDeleteClick(guardrail.guardrail_id, guardrail.guardrail_name || "Unnamed Guardrail")
}
className="cursor-pointer hover:text-red-500"
tooltip="Delete guardrail"
/>
<Tooltip title="Delete guardrail">
<Icon
icon={TrashIcon}
size="sm"
onClick={() =>
guardrail.guardrail_id &&
onDeleteClick(guardrail.guardrail_id, guardrail.guardrail_name || "Unnamed Guardrail")
}
className="cursor-pointer hover:text-red-500"
/>
</Tooltip>
)}
</div>
);
@@ -0,0 +1,50 @@
import { CredentialItem } from "@/components/networking";
import { render, waitFor } from "@testing-library/react";
import { UploadProps } from "antd/es/upload";
import { describe, expect, it, vi } from "vitest";
import CredentialsPanel from "./credentials";
const DEFAULT_UPLOAD_PROPS = {} as UploadProps;
describe("CredentialsPanel", () => {
it("renders without crashing and fetches credentials when token exists", async () => {
const fetchCredentials = vi.fn(() => Promise.resolve());
const { getByRole, getByText } = render(
<CredentialsPanel
accessToken="test-token"
uploadProps={DEFAULT_UPLOAD_PROPS}
credentialList={[]}
fetchCredentials={fetchCredentials}
/>,
);
await waitFor(() => {
expect(getByRole("button", { name: /add credential/i })).toBeInTheDocument();
expect(getByText("Credential Name")).toBeInTheDocument();
expect(getByText("Provider")).toBeInTheDocument();
});
});
it("displays provided credentials and still calls the fetch helper", async () => {
const fetchCredentials = vi.fn(() => Promise.resolve());
const credentials: CredentialItem[] = [
{
credential_name: "openai-key",
credential_values: {},
credential_info: { custom_llm_provider: "openai" },
},
];
const { getByText } = render(
<CredentialsPanel
accessToken="another-token"
uploadProps={DEFAULT_UPLOAD_PROPS}
credentialList={credentials}
fetchCredentials={fetchCredentials}
/>,
);
await waitFor(() => expect(getByText("openai-key")).toBeInTheDocument());
});
});
@@ -143,7 +143,6 @@ const CredentialsPanel: React.FC<CredentialsPanelProps> = ({
<TableRow>
<TableHeaderCell>Credential Name</TableHeaderCell>
<TableHeaderCell>Provider</TableHeaderCell>
<TableHeaderCell>Description</TableHeaderCell>
</TableRow>
</TableHead>
<TableBody>
@@ -160,7 +159,6 @@ const CredentialsPanel: React.FC<CredentialsPanelProps> = ({
<TableCell>
{renderProviderBadge((credential.credential_info?.custom_llm_provider as string) || "-")}
</TableCell>
<TableCell>{credential.credential_info?.description || "-"}</TableCell>
<TableCell>
<Button
icon={PencilAltIcon}
@@ -419,15 +419,20 @@ export default function ModelInfoView({
alt={`${modelData.provider} logo`}
className="w-4 h-4"
onError={(e) => {
// Create a div with provider initial as fallback
const target = e.target as HTMLImageElement;
const target = e.currentTarget as HTMLImageElement;
const parent = target.parentElement;
if (parent) {
if (!parent || !parent.contains(target)) {
return;
}
try {
const fallbackDiv = document.createElement("div");
fallbackDiv.className =
"w-4 h-4 rounded-full bg-gray-200 flex items-center justify-center text-xs";
fallbackDiv.textContent = modelData.provider?.charAt(0) || "-";
parent.replaceChild(fallbackDiv, target);
} catch (error) {
console.error("Failed to replace provider logo fallback:", error);
}
}}
/>
@@ -493,22 +498,14 @@ export default function ModelInfoView({
<Title>Model Settings</Title>
<div className="flex gap-2">
{isAutoRouter && canEditModel && !isEditing && (
<TremorButton
variant="primary"
onClick={() => setIsAutoRouterModalOpen(true)}
className="flex items-center"
>
<TremorButton onClick={() => setIsAutoRouterModalOpen(true)} className="flex items-center">
Edit Auto Router
</TremorButton>
)}
{canEditModel ? (
!isEditing && (
<TremorButton
variant="secondary"
onClick={() => setIsEditing(true)}
className="flex items-center"
>
Edit Model
<TremorButton onClick={() => setIsEditing(true)} className="flex items-center">
Edit Settings
</TremorButton>
)
) : (

Some files were not shown because too many files have changed in this diff Show More